OXYGENHUB

Settings › Tokens: cards, and Mark as agent on each token 00ef7b83b340

The token list and the New token form are cards of one width with a gap
between them. Each token is a row with its name, an agent pill, scope
chips and dates, and Mark as agent / Not an agent and Delete beside it,
so an existing token can be marked as an agent's.

Closes #185
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-10-14 12:39 UTC
Signature
✔ signed by Andy McCall
Hash
00ef7b83b340a7fd53544c0a8a4e63a1f79c0b0a4a7d6f2ec554722988ff46d1
Depends on
18e381ea88c2 19bb31ff1f5c 60f11b71a740 6ae3fa0af4f6 b45384c909c1 bff9106edcba d23464c0ac60
Depended on by
8597fb5ca16c ffb67a81188c
In bookmarks
api-unknown-fields cancel-landed-runs diffusion-pin-bookmark draft-ready main oh-comment-body-file proposal-list-fast
In tags
v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
Edits
6 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified crates/hub/tests/issues.rs +2 −1
…
1134 unchanged lines
1135 1135
    assert_eq!(agent.2, "andy (agent token Claude)");
1136 1136
    // The tokens page marks and unmarks one.
1137 1137
    let page = get(app, "/settings/tokens", Some(&andy)).await;
1138 -
    assert!(page.body.contains("agent ✓") && page.body.contains("mark as agent"), "{}", page.body);
1138 +
    assert!(page.body.contains("Not an agent") && page.body.contains("Mark as agent"), "{}", page.body);
1139 +
    assert!(page.body.contains(r#"<span class="pill agent-pill""#), "the agent's token says so");
1139 1140
}
modified templates/settings_tokens.html +41 −28
…
6 unchanged lines
7 7
{% if let Some(n) = notice %}<p class="note">{{ n }}</p>{% endif %}
8 8
{% if let Some(e) = error %}<p class="error">{{ e }}</p>{% endif %}
9 9
{% if let Some(f) = fresh %}
10 -
<div class="panel"><h2>Your new token</h2><p class="secret">{{ f }}</p>
11 -
<p class="small">In a checkout: <code>oxy config remote.&lt;name&gt;.token {{ f }}</code>, or set <code>OXY_TOKEN</code>.</p></div>
10 +
<section class="card-box token-fresh">
11 +
  <div class="card-head"><h2>Your new token</h2><span class="small mute more">shown once: copy it now</span></div>
12 +
  <div class="pad"><p class="secret">{{ f }}</p>
13 +
  <p class="small mute">In a checkout: <code>oxy config remote.&lt;name&gt;.token {{ f }}</code>, or set <code>OXY_TOKEN</code>.</p></div>
14 +
</section>
12 15
{% endif %}
13 -
{% if tokens.is_empty() %}<p class="empty">No tokens yet.</p>{% else %}
14 -
<div class="table"><table>
15 -
<tr><th>Name</th><th>Scopes</th><th>Repositories</th><th>Created</th><th>Last used</th><th>Expires</th><th title="What an agent does with this token is told to you">Agent</th><th></th></tr>
16 -
{% for t in tokens %}
17 -
<tr><td>{{ t.name }}</td><td>{{ t.scopes }}</td><td>{% if t.repos.is_empty() %}<span class="mute">all</span>{% else %}{{ t.repos }}{% endif %}</td>
18 -
<td><time title="{{ t.created_ms|when_i }}">{{ t.created_ms|ago_i }}</time></td>
19 -
<td>{% if let Some(u) = t.last_used_ms %}<time title="{{ u|when_i }}">{{ u|ago_i }}</time>{% else %}<span class="mute">never</span>{% endif %}</td>
20 -
<td>{% if let Some(x) = t.expires_ms %}{{ x|when_i }}{% if t.renew_ms.is_some() %} <span class="mute small">renewed on use</span>{% endif %}{% else %}<span class="mute">never</span>{% endif %}</td>
21 -
<td><form class="tiny" method="post" action="/settings/tokens/{{ t.id }}/agent"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button title="{% if t.agent %}Stop telling me what is done with it{% else %}An agent uses this token: tell me what it does{% endif %}">{% if t.agent %}agent ✓{% else %}mark as agent{% endif %}</button></form></td>
22 -
<td><form class="tiny" method="post" action="/settings/tokens/{{ t.id }}/delete"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button>delete</button></form></td></tr>
23 -
{% endfor %}
24 -
</table></div>
25 -
{% endif %}
26 -
<form class="card wide" method="post" action="/settings/tokens">
27 -
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
28 -
  <h2>New token</h2>
29 -
  <label for="name">Name</label><input type="text" id="name" name="name" required placeholder="laptop, CI, …">
30 -
  <label>Scopes</label>
31 -
  <div class="scopes">
32 -
    {% for (name, what) in scopes %}<label><input type="checkbox" name="scopes" value="{{ name }}"{% if *name == "repo:read" || *name == "repo:write" %} checked{% endif %}> <code>{{ name }}</code> <span class="small mute">{{ what }}</span></label>{% endfor %}
33 -
  </div>
34 -
  <label for="repos">Only these repositories (comma-separated owner/name; empty for all)</label><input type="text" id="repos" name="repos">
35 -
  <label for="expires_days">Expires after (days; empty for never)</label><input type="number" id="expires_days" name="expires_days" min="1">
36 -
  <label class="choice"><input type="checkbox" name="agent" value="1"> An agent uses this token: tell me what it does <span class="small mute">(proposals it opens, comments it makes and the rest reach your inbox and email like anyone else's would)</span></label>
37 -
  <button type="submit">CREATE TOKEN</button>
38 -
</form>
16 +
<section class="card-box">
17 +
  <div class="card-head"><h2>Tokens</h2><span class="count-chip">{{ tokens.len() }}</span></div>
18 +
  {% if tokens.is_empty() %}<p class="empty small pad">No tokens yet.</p>{% else %}
19 +
  <ul class="rows token-rows">
20 +
  {% for t in tokens %}
21 +
    <li>
22 +
      <div class="token-main">
23 +
        <div class="token-name"><strong>{{ t.name }}</strong>{% if t.agent %} <span class="pill agent-pill" title="What is done with this token is told to you">agent</span>{% endif %}</div>
24 +
        <div class="token-scopes">{% for sc in t.scopes.split(',') %}<code class="chip">{{ sc }}</code>{% endfor %}{% if !t.repos.is_empty() %} <span class="small mute">only {{ t.repos }}</span>{% endif %}</div>
25 +
        <div class="small mute">created <time title="{{ t.created_ms|when_i }}">{{ t.created_ms|ago_i }}</time> · last used {% if let Some(u) = t.last_used_ms %}<time title="{{ u|when_i }}">{{ u|ago_i }}</time>{% else %}never{% endif %} · {% if let Some(x) = t.expires_ms %}expires {{ x|when_i }}{% if t.renew_ms.is_some() %} (renewed on use){% endif %}{% else %}never expires{% endif %}</div>
26 +
      </div>
27 +
      <div class="token-actions">
28 +
        <form class="inline" method="post" action="/settings/tokens/{{ t.id }}/agent"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button class="small" title="{% if t.agent %}Stop telling me what is done with it{% else %}An agent uses this token: tell me what it does{% endif %}">{% if t.agent %}Not an agent{% else %}Mark as agent{% endif %}</button></form>
29 +
        <form class="inline" method="post" action="/settings/tokens/{{ t.id }}/delete"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button class="small danger">Delete</button></form>
30 +
      </div>
31 +
    </li>
32 +
  {% endfor %}
33 +
  </ul>
34 +
  {% endif %}
35 +
</section>
36 +
<section class="card-box">
37 +
  <div class="card-head"><h2>New token</h2></div>
38 +
  <form class="pad token-form" method="post" action="/settings/tokens">
39 +
    <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
40 +
    <label for="name">Name</label><input type="text" id="name" name="name" required placeholder="laptop, CI, …">
41 +
    <label>Scopes</label>
42 +
    <div class="scopes">
43 +
      {% for (name, what) in scopes %}<label><input type="checkbox" name="scopes" value="{{ name }}"{% if *name == "repo:read" || *name == "repo:write" %} checked{% endif %}> <code>{{ name }}</code> <span class="small mute">{{ what }}</span></label>{% endfor %}
44 +
    </div>
45 +
    <label for="repos">Only these repositories (comma-separated owner/name; empty for all)</label><input type="text" id="repos" name="repos">
46 +
    <label for="expires_days">Expires after (days; empty for never)</label><input type="number" id="expires_days" name="expires_days" min="1">
47 +
    <label class="choice token-agent"><input type="checkbox" name="agent" value="1"> An agent uses this token: tell me what it does</label>
48 +
    <p class="small mute token-hint">Proposals it opens, comments it makes and the rest reach your inbox and email like anyone else's would.</p>
49 +
    <button type="submit">CREATE TOKEN</button>
50 +
  </form>
51 +
</section>
39 52
{% endblock %}
modified templates/style.css +15 −0
…
1173 unchanged lines
1174 1174
   inline code wrap where they must. */
1175 1175
.tl-event, .tl-event code { overflow-wrap: anywhere; }
1176 1176
article.doc :not(pre) > code, .comment :not(pre) > code, .tl-card :not(pre) > code { overflow-wrap: anywhere; }
1177 +
/* Settings › Tokens (oxygenhub#185): one card per token, actions beside it. */
1178 +
.card-box > .pad, .card-box .pad { padding: 14px 16px; }
1179 +
.token-rows li { display: flex; align-items: flex-start; gap: 16px; padding: 12px 16px; }
1180 +
.token-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
1181 +
.token-scopes { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
1182 +
.token-scopes .chip { font-size: 11.5px; padding: 1px 6px; border: 1px solid var(--line); border-radius: 10px; background: var(--soft); }
1183 +
.token-actions { display: flex; gap: 6px; flex-shrink: 0; }
1184 +
button.danger { color: var(--bad); }
1185 +
.agent-pill { font-size: 11px; padding: 1px 7px; border-radius: 10px; background: color-mix(in srgb, var(--acc) 18%, transparent); color: var(--ink); }
1186 +
.token-form { display: flex; flex-direction: column; gap: 6px; max-width: 720px; }
1187 +
.token-form .scopes { display: flex; flex-direction: column; gap: 2px; }
1188 +
.token-form button[type=submit] { align-self: flex-start; margin-top: 8px; }
1189 +
.token-form .token-agent { margin-top: 8px; }
1190 +
.token-form .token-hint { margin: 0 0 0 24px; }
1191 +
@media (max-width: 700px) { .token-rows li { flex-direction: column; } }