@songsid/agend 2.3.3-beta.7 → 2.3.3-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -916,15 +916,19 @@ img.amb-p { object-fit: contain; }
916
916
  /* A guest's note and the public link's sentence are kept whole: on a narrow screen they wrap rather than lose words. */
917
917
  .composer-row .guest-note, .composer-row .webauth-row[data-state="public"] { margin: 0; text-align: left; white-space: normal; flex-shrink: 1; }
918
918
  .webauth-row { display: inline-flex; align-items: center; gap: var(--s-1); }
919
- .webauth-row .webauth-ok, .webauth-row .webauth-ask { font: inherit; padding: 0 2px; border: 0; background: none; cursor: pointer; border-radius: var(--r-sm); }
919
+ .webauth-row .webauth-ok, .webauth-row .webauth-ask, .webauth-row .webauth-why { font: inherit; padding: 0 2px; border: 0; background: none; cursor: pointer; border-radius: var(--r-sm); }
920
920
  .webauth-row .webauth-ok { color: var(--ok); }
921
921
  .webauth-row .webauth-ask { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
922
- .webauth-row .webauth-ok:focus-visible, .webauth-row .webauth-ask:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
922
+ /* #2889: "Not verified" where it can't be authorized: the row's grey words, a dotted underline saying a tap tells why. */
923
+ .webauth-row .webauth-why { color: inherit; text-decoration: underline dotted; text-underline-offset: 2px; }
924
+ .webauth-row .webauth-ok:focus-visible, .webauth-row .webauth-ask:focus-visible, .webauth-row .webauth-why:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
923
925
  /* What pressing Authorize answered is said in the row, in the hint's place: nothing is laid over the composer. */
924
926
  .webauth-row .webauth-notice { white-space: normal; color: var(--text-2); }
925
927
  .webauth-row .webauth-notice::before { content: "·"; margin-right: var(--s-2); color: var(--text-3); }
926
928
  .composer-row:has(.webauth-notice) > .hint { display: none; }
927
- /* When "✓ Verified" ends, on a tap (touch has no hover): a bubble above the row while it has focus. */
929
+ /* When "✓ Verified" ends, or why it can't be authorized (#2889), on a tap (touch has no hover): a bubble above the
930
+ composer (web-authorize.js lifts it by the composer's height: never over Attach or the text, #2216) until the next tap,
931
+ a tap or key elsewhere, or Escape. */
928
932
  .webauth-tip { position: absolute; left: 0; bottom: calc(100% + 4px); z-index: 5; max-width: min(420px, calc(100vw - 32px)); white-space: normal;
929
933
  padding: var(--s-1) var(--s-2); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface); color: var(--text);
930
934
  box-shadow: var(--shadow-2); }
@@ -1,7 +1,7 @@
1
1
  // #1939 S2b (design §0.1): "Authorize this browser". The app shows this browser's state — verified (until when), a
2
2
  // Confirm card waiting in the account's Discord or Telegram DM (with its countdown), or why it can't be authorized here —
3
3
  // and asks for the card. The card itself is the gate: nothing here can authorize anything.
4
- import { html, useEffect, useRef, useState } from "./app-html.js";
4
+ import { html, useEffect, useLayoutEffect, useRef, useState } from "./app-html.js";
5
5
  import { register, t } from "./app-i18n.js";
6
6
  import { guestPage } from "./app-store.js";
7
7
 
@@ -31,6 +31,7 @@ register("webauth", {
31
31
  verifiedShort: "Verified",
32
32
  authorizeShort: "Authorize",
33
33
  authorizeWhy: "Not verified: authorize this browser to speak as you",
34
+ notVerifiedWhy: "Not verified: {0}",
34
35
  waitingShort: "Waiting for your Confirm ({0})",
35
36
  },
36
37
  "zh-TW": {
@@ -58,6 +59,7 @@ register("webauth", {
58
59
  verifiedShort: "已驗證",
59
60
  authorizeShort: "授權",
60
61
  authorizeWhy: "未驗證:授權這個瀏覽器以你的身分發言",
62
+ notVerifiedWhy: "未驗證:{0}",
61
63
  waitingShort: "等待確認({0})",
62
64
  },
63
65
  });
@@ -155,26 +157,46 @@ export function AuthorizeChip() {
155
157
  * The composer's row (#2215, the user's request, 2026-10-19): the same states as AuthorizeChip, each kept to a few words so
156
158
  * they share one row with the key hint. What a state would have said at length is in its tooltip and its accessible
157
159
  * name: when "Verified" ends, why it can't be authorized, the countdown's ask. A tap on "✓ Verified" shows when it
158
- * ends (touch has no hover) in a bubble above the row, until the next tap, a tap elsewhere or Escape. What pressing
159
- * Authorize answered (sent, refused) is said in the row itself, in the hint's place (#2216 review: a bubble left over the
160
- * composer covered Attach and what was being typed, with nothing to close it).
160
+ * ends, and a tap on "Not verified" why it can't be authorized here (#2889; touch has no hover), in a bubble above the
161
+ * composer, until the next tap, a tap or key elsewhere, or Escape. What pressing Authorize answered (sent, refused) is
162
+ * said in the row itself, in the hint's place (#2216 review: a bubble left over the composer covered Attach and what was
163
+ * being typed, with nothing to close it).
161
164
  */
162
165
  export function ComposerAuth() {
163
166
  const a = useWebAuthorization();
164
167
  const [open, setOpen] = useState(false);
165
- const tip = useRef("wa-tip-" + Math.random().toString(36).slice(2, 8));
168
+ const tip = useRef("wa-tip-" + Math.random().toString(36).slice(2, 8)), opener = useRef(null);
166
169
  useEffect(() => { setOpen(false); }, [a.st && a.st.state]); // a new state closes the bubble
170
+ // #2889: a tap, focus or key (Escape too) anywhere else closes it, even where a tapped button never takes focus (Safari);
171
+ // on the button itself, its own handlers decide.
172
+ useEffect(() => {
173
+ if (!open) return undefined;
174
+ const doc = globalThis.document, kinds = ["pointerdown", "focusin", "keydown"];
175
+ const away = (e) => { if (!opener.current || !opener.current.contains(e.target)) setOpen(false); };
176
+ for (const k of kinds) doc.addEventListener(k, away, true);
177
+ return () => { for (const k of kinds) doc.removeEventListener(k, away, true); };
178
+ }, [open]);
179
+ // Above the whole composer, not over it (#2216): lifted by the composer's height, measured when it opens.
180
+ const bubble = useRef(null);
181
+ useLayoutEffect(() => {
182
+ const el = bubble.current, row = el && el.parentElement && el.parentElement.closest(".composer-row");
183
+ const composer = row && row.parentElement ? row.parentElement.querySelector(".composer") : null;
184
+ if (!el || !open || !composer) { if (el) el.style.removeProperty("bottom"); return; }
185
+ const lift = Math.max(0, Math.round(row.getBoundingClientRect().top - composer.getBoundingClientRect().top));
186
+ el.style.setProperty("bottom", `calc(100% + ${lift + 4}px)`);
187
+ }, [open]);
167
188
  if (guestPage()) return null;
168
189
  if (!a.st) return null;
169
190
  const st = a.st, id = tip.current;
170
191
  const said = a.notice ? html`<span class="webauth-notice" role="status">${a.notice}</span>` : null;
192
+ // The words in the row, a button whose tap shows `text` in the bubble; `title` keeps the hover on a desk.
193
+ const tipped = (cls, label, name, text) => html`<button type="button" class=${cls} ref=${opener} title=${text} aria-label=${name}
194
+ aria-expanded=${open ? "true" : "false"} aria-controls=${id} onClick=${() => setOpen((v) => !v)} onBlur=${() => setOpen(false)}
195
+ onKeyDown=${(e) => { if (e.key === "Escape" && open) { e.stopPropagation(); setOpen(false); } }}>${label}</button><span
196
+ id=${id} ref=${bubble} class="webauth-tip" role="tooltip" hidden=${!open}>${text}</span>`;
171
197
  if (st.state === "verified") {
172
198
  const until = tn("verified", clock(st.until));
173
- return html`<span class="webauth-row" data-state="verified">
174
- <button type="button" class="webauth-ok" title=${until} aria-label=${`✓ ${until}`} aria-expanded=${open ? "true" : "false"}
175
- aria-controls=${id} onClick=${() => setOpen((v) => !v)} onBlur=${() => setOpen(false)}
176
- onKeyDown=${(e) => { if (e.key === "Escape" && open) { e.stopPropagation(); setOpen(false); } }}>✓ ${tn("verifiedShort")}</button><span
177
- id=${id} class="webauth-tip" role="tooltip" hidden=${!open}>${until}</span>${said}</span>`;
199
+ return html`<span class="webauth-row" data-state="verified">${tipped("webauth-ok", `✓ ${tn("verifiedShort")}`, `✓ ${until}`, until)}${said}</span>`;
178
200
  }
179
201
  if (st.state === "pending") {
180
202
  const asks = tn("waiting", mmss(a.left));
@@ -185,7 +207,7 @@ export function ComposerAuth() {
185
207
  title=${tn("authorizeWhy")} aria-label=${tn("authorizeWhy")} onClick=${a.ask}>${tn("authorizeShort")}</button>${said}</span>`;
186
208
  }
187
209
  const why = REASONS.includes(st.reason) ? tn(`reason_${st.reason}`) : tn("reason_other");
188
- return html`<span class="webauth-row" data-state="none"><span class="note" title=${why}>${tn("notVerified")}<span class="sr-only">: ${why}</span></span>${fixable(st.reason)
210
+ return html`<span class="webauth-row" data-state="none">${tipped("webauth-why", tn("notVerified"), tn("notVerifiedWhy", why), why)}${fixable(st.reason)
189
211
  ? html` · <a href="/settings/access" title=${why}>${tn("settings")}</a>` : null}</span>`;
190
212
  }
191
213
 
@@ -47,11 +47,20 @@ function fields(text) {
47
47
  text.split("\n").forEach(function (line) { var at = line.indexOf("="); if (at > 0) out[line.slice(0, at)] = line.slice(at + 1); });
48
48
  return out;
49
49
  }
50
+ // The process's systemd cgroup. Pure cgroup v2 lists one `0::` row; a hybrid host (WSL, older distros) lists both the
51
+ // unified `0::` row and the v1 `N:name=systemd:` row, with the same path — the 0:: one is what systemd reports as the
52
+ // unit's ControlGroup there too. A pure-v1 host has only the name=systemd row, or a bare `0::/` beside it (cgroup2
53
+ // mounted but unused). Two rows of the same kind, or no usable path, stay unknown.
50
54
  function cgroup(text) {
51
- var rows = text.trim().split("\n").filter(function (r) { return /^(?:0::|\d+:name=systemd:)/.test(r); });
52
- if (rows.length !== 1) throw Error("unknown cgroup");
53
- var p = rows[0].slice(rows[0].indexOf(":", rows[0].indexOf(":") + 1) + 1);
54
- if (p[0] !== "/") throw Error("unknown cgroup");
55
+ var unified = null, named = null;
56
+ text.trim().split("\n").forEach(function (r) {
57
+ var m = /^(\d+):([^:]*):(.*)$/.exec(r);
58
+ if (!m) return;
59
+ if (m[1] === "0" && m[2] === "") { if (unified !== null) throw Error("unknown cgroup"); unified = m[3]; }
60
+ else if (m[2].split(",").indexOf("name=systemd") >= 0) { if (named !== null) throw Error("unknown cgroup"); named = m[3]; }
61
+ });
62
+ var p = unified !== null && unified !== "/" ? unified : named !== null ? named : unified;
63
+ if (!p || p[0] !== "/") throw Error("unknown cgroup");
55
64
  return p;
56
65
  }
57
66
  function inside(p, group) { return !!group && group !== "/" && (p === group || p.indexOf(group + "/") === 0); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@songsid/agend",
3
- "version": "2.3.3-beta.7",
3
+ "version": "2.3.3-beta.8",
4
4
  "description": "Multi-agent fleet daemon — run coding CLIs (Claude, Muse, Codex, OpenCode) from Discord or Telegram",
5
5
  "type": "module",
6
6
  "bin": {