@songsid/agend 2.3.0-beta.26 → 2.3.0-beta.27

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.
Files changed (52) hide show
  1. package/README.md +7 -5
  2. package/README.zh-TW.md +7 -5
  3. package/dist/agent-whats-new.js +12 -2
  4. package/dist/agent-whats-new.js.map +1 -1
  5. package/dist/channel/adapters/discord.d.ts +2 -0
  6. package/dist/channel/adapters/discord.js +11 -1
  7. package/dist/channel/adapters/discord.js.map +1 -1
  8. package/dist/cli.d.ts +1 -0
  9. package/dist/cli.js +29 -0
  10. package/dist/cli.js.map +1 -1
  11. package/dist/config-validator.js +4 -2
  12. package/dist/config-validator.js.map +1 -1
  13. package/dist/daemon.js +3 -1
  14. package/dist/daemon.js.map +1 -1
  15. package/dist/doctor.d.ts +3 -0
  16. package/dist/doctor.js +10 -0
  17. package/dist/doctor.js.map +1 -1
  18. package/dist/fleet-manager.d.ts +8 -1
  19. package/dist/fleet-manager.js +83 -13
  20. package/dist/fleet-manager.js.map +1 -1
  21. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +10 -10
  22. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +10 -10
  23. package/dist/idle-gate-log.d.ts +19 -0
  24. package/dist/idle-gate-log.js +15 -0
  25. package/dist/idle-gate-log.js.map +1 -0
  26. package/dist/locale.js +18 -6
  27. package/dist/locale.js.map +1 -1
  28. package/dist/public-web-gateway.js +3 -0
  29. package/dist/public-web-gateway.js.map +1 -1
  30. package/dist/start-attempt.d.ts +1 -0
  31. package/dist/start-attempt.js +18 -0
  32. package/dist/start-attempt.js.map +1 -0
  33. package/dist/start-failure-notice.d.ts +4 -0
  34. package/dist/start-failure-notice.js +20 -0
  35. package/dist/start-failure-notice.js.map +1 -0
  36. package/dist/start-failure.d.ts +147 -0
  37. package/dist/start-failure.js +281 -0
  38. package/dist/start-failure.js.map +1 -0
  39. package/dist/types.d.ts +2 -0
  40. package/dist/ui/chat-render.js +6 -2
  41. package/dist/ui/chat-strings.js +2 -2
  42. package/dist/ui/panel-chat.js +21 -9
  43. package/dist/ui/shared/app-shell.js +50 -9
  44. package/dist/ui/shared/app.css +34 -14
  45. package/dist/ui/shared/tokens.css +4 -0
  46. package/dist/update-progress.d.ts +3 -1
  47. package/dist/update-progress.js +3 -1
  48. package/dist/update-progress.js.map +1 -1
  49. package/dist/web-api.d.ts +4 -0
  50. package/dist/web-api.js +6 -3
  51. package/dist/web-api.js.map +1 -1
  52. package/package.json +1 -1
@@ -189,6 +189,10 @@ function useChatTick(name, kinds) {
189
189
 
190
190
  /** #1722: the chat's title — the name that leads, the other on hover. */
191
191
  const headerName = ({ primary, secondary }) => (secondary ? html`<span title=${secondary}>${primary}</span>` : primary);
192
+ /** #2478: who the composer writes to — the display name, else the instance's name (a long id made the box two lines). */
193
+ const composerLabel = (inst, name) => (inst && typeof inst.display_name === "string" && inst.display_name.trim()) || name;
194
+ /** #2478: a touch screen (a coarse pointer) — Return is a new line there and only the Send button sends. Asked each time. */
195
+ const touchScreen = () => typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches;
192
196
 
193
197
  export function ChatPanel({ route, navKey }) {
194
198
  const lease = useLease(navKey);
@@ -553,9 +557,9 @@ function Dock({ name, inst, lease, exec, awaiting, workingLabel }) {
553
557
  <${CommandCard} name=${name} lease=${lease} />
554
558
  <${QuickActions} name=${name} inst=${inst} lease=${lease} />
555
559
  ${s.waking[name] ? html`<p class="send-waking" role="status">${t("chat.waking", name)}</p>` : null}
556
- <${Composer} name=${name} lease=${lease} busy=${R().isBusy(exec)} />
560
+ <${Composer} name=${name} label=${composerLabel(inst, name)} lease=${lease} busy=${R().isBusy(exec)} />
557
561
  <div class="composer-row">${guestPage() ? html`<span class="guest-note" title=${t("chat.guestNote")}>${t("chat.guestNote")}</span>` : html`<${ComposerAuth} />`}<span
558
- class="hint" title=${t(guestPage() ? "chat.guestMore" : "chat.composerMore")}>${t("chat.composerHint")}</span></div>
562
+ class="hint" title=${t(guestPage() ? "chat.guestMore" : "chat.composerMore")}>${t(touchScreen() ? "chat.composerHintTouch" : "chat.composerHint")}</span></div>
559
563
  </div></div>`;
560
564
  }
561
565
 
@@ -638,7 +642,7 @@ function replySnippet(r) {
638
642
  return text || (r.media === "photo" ? t("chat.replyPhoto") : r.media === "document" ? t("chat.replyFile") : "");
639
643
  }
640
644
 
641
- function Composer({ name, lease, busy }) {
645
+ function Composer({ name, label = name, lease, busy }) {
642
646
  const s = store.state;
643
647
  const input = useRef(null), fileIn = useRef(null);
644
648
  const [, tick] = useState(0);
@@ -653,8 +657,13 @@ function Composer({ name, lease, busy }) {
653
657
  const guest = guestPage();
654
658
  const palette = guest || files.length || closedFor === value ? null : paletteFor(value);
655
659
  const pick = palette && palette.length ? palette[Math.min(sel, palette.length - 1)] : null;
656
- // Grow with the text, up to a limit: set through the CSSOM, never a style attribute (#1300).
657
- useLayoutEffect(() => { const el = input.current; if (!el) return; el.style.height = "auto"; el.style.height = `${Math.min(el.scrollHeight, 240)}px`; }, [value]);
660
+ // Grow with the text, up to a limit: set through the CSSOM, never a style attribute (#1300). Empty, it is one line
661
+ // (#2478 — a long name's placeholder made the box two lines tall).
662
+ useLayoutEffect(() => {
663
+ const el = input.current; if (!el) return;
664
+ el.style.height = "auto";
665
+ if (value) el.style.height = `${Math.min(el.scrollHeight, 240)}px`;
666
+ }, [value]);
658
667
  useEffect(() => { if (input.current) input.current.focus(); }, [name]);
659
668
  useEffect(() => { refreshTourSpot(); });
660
669
  const setValue = (v) => { store.setDraft(name, v); setSel(0); tick(n => n + 1); };
@@ -689,7 +698,7 @@ function Composer({ name, lease, busy }) {
689
698
  if (pick && e.key === "Tab" && !e.shiftKey) { e.preventDefault(); complete(pick); return; }
690
699
  if (palette && e.key === "Escape" && !e.isComposing) { e.preventDefault(); setClosedFor(value); return; }
691
700
  if (reply && e.key === "Escape" && !e.isComposing) { e.preventDefault(); store.clearReply(name); return; } // #2272
692
- if (R().composerKey(e) === "send") { e.preventDefault(); submit(); }
701
+ if (R().composerKey(e, { touch: touchScreen() }) === "send") { e.preventDefault(); submit(); }
693
702
  };
694
703
  const onInput = (e) => { setValue(e.target.value); };
695
704
  // Files in a paste are attached; a very long text paste goes as a text file — only once it is really attached.
@@ -717,9 +726,12 @@ function Composer({ name, lease, busy }) {
717
726
  <button id="attachBtn" type="button" class="icon-btn" title=${t("chat.attachTitle")} aria-label=${t("chat.attachTitle")} onClick=${() => fileIn.current && fileIn.current.click()}><${Icon} name="attach" /></button>
718
727
  <input ref=${fileIn} type="file" multiple hidden
719
728
  onChange=${(e) => { store.addFiles(name, e.target.files); e.target.value = ""; }} />
720
- <textarea id="msgIn" ref=${input} rows="1" value=${value} placeholder=${t("chat.composerPlaceholder", name)} aria-label=${t("chat.composerPlaceholder", name)}
721
- autocomplete="off" aria-controls=${palette ? "cmdPalette" : undefined} aria-expanded=${palette ? "true" : "false"}
722
- aria-activedescendant=${pick ? optId(pick) : undefined} onInput=${onInput} onKeyDown=${onKeyDown} onPaste=${onPaste}></textarea>
729
+ ${/* #2478: the placeholder is drawn beside the box, one line ending in "…" — a textarea's own placeholder is clipped,
730
+ never ellipsized, and a long name wrapped it to two lines. The box keeps the words as its accessible name. */ ""}
731
+ <span class="composer-field"><textarea id="msgIn" ref=${input} rows="1" value=${value} aria-label=${t("chat.composerPlaceholder", label)}
732
+ enterkeyhint=${touchScreen() ? "enter" : undefined} autocomplete="off" aria-controls=${palette ? "cmdPalette" : undefined} aria-expanded=${palette ? "true" : "false"}
733
+ aria-activedescendant=${pick ? optId(pick) : undefined} onInput=${onInput} onKeyDown=${onKeyDown} onPaste=${onPaste}></textarea>${value ? null
734
+ : html`<span class="composer-ph" aria-hidden="true">${t("chat.composerPlaceholder", label)}</span>`}</span>
723
735
  <button id="stopBtn" type="button" class="btn btn-stop" hidden=${!busy} title=${t("chat.stopReplyTitle")}
724
736
  disabled=${!!s.cancelling[name]} onClick=${() => store.cancelReply(name)}><${Icon} name="stop" size=${14} /><span class="stop-label">${t("chat.stopReply")}</span></button>
725
737
  <button id="sendBtn" type="button" class="btn btn-primary btn-send" hidden=${busy && !content} disabled=${!content}
@@ -70,12 +70,14 @@ export const isMacLike = () => typeof navigator !== "undefined" && /Mac|iPhone|i
70
70
  /** What the row and tooltips show: ⌘K on a Mac, Ctrl K elsewhere. */
71
71
  export const searchShortcut = () => (isMacLike() ? "⌘K" : "Ctrl K");
72
72
  /**
73
- * Whether this page offers search: an admin's page reached directly. A guest's never, and never over the public link —
74
- * the server refuses it there (#2298 review), so no entry, no shortcut, and Ctrl+F stays the browser's.
73
+ * Whether this page offers search: an admin's page. A guest's never. #2478 (the user's request): over the public link
74
+ * too, unless the fleet keeps it off there (web.search.public_link: false — the page says data-search-link="0", the
75
+ * server refuses it), so then no entry, no shortcut, and Ctrl+F stays the browser's.
75
76
  */
76
77
  export function searchOffered(admin) {
77
- const onLink = typeof document !== "undefined" && !!document.body && document.body.dataset.publicLink === "1";
78
- return !!admin && !guestPage() && !onLink;
78
+ const body = typeof document !== "undefined" ? document.body : null;
79
+ const offLink = !!body && body.dataset.publicLink === "1" && body.dataset.searchLink === "0";
80
+ return !!admin && !guestPage() && !offLink;
79
81
  }
80
82
  let searchModule = null;
81
83
  /** Open search; `preset.instance`: narrowed to that agent (Ctrl+F in its chat). */
@@ -644,6 +646,20 @@ function usePageTheme(ref, route) {
644
646
  }, [agent]);
645
647
  }
646
648
 
649
+ /** #2478: how far short of its tallest the view must be, with a text field focused, to be a keyboard (not a browser bar). */
650
+ export const KEYBOARD_MIN_PX = 150;
651
+ /** #2478: the on-screen keyboard is up — something is being typed into, and the view is well short of its tallest. */
652
+ export function keyboardUp(height, tallest, typing) { return !!typing && tallest - height > KEYBOARD_MIN_PX; }
653
+ const NOT_TYPED = /^(button|checkbox|radio|range|color|file|submit|reset|image|hidden)$/i;
654
+ /** #2478: an element the keyboard types into — a textarea, a text-like input, or an editable region. */
655
+ export function isTextEntry(el) {
656
+ if (!el || !el.tagName) return false;
657
+ const tag = String(el.tagName).toUpperCase();
658
+ if (tag === "TEXTAREA") return !el.readOnly && !el.disabled;
659
+ if (tag === "INPUT") return !NOT_TYPED.test(el.type || "text") && !el.readOnly && !el.disabled;
660
+ return el.isContentEditable === true;
661
+ }
662
+
647
663
  export function Shell({ panels, onNewInstance, viewOnly = false }) {
648
664
  const nav = useStore(navStore);
649
665
  const shell = useStore(shellStore);
@@ -653,13 +669,38 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
653
669
  // A language switch re-renders everything and gives the panel a new lease (its key includes the language).
654
670
  useEffect(() => onLang(() => relang(n => n + 1)), []);
655
671
  useEffect(() => { closeDrawer(); }, [nav.seq]);
656
- // The bottom tabs give their room to the keyboard on a phone.
672
+ // The bottom tabs (and the line under the composer) give their room to the keyboard on a phone (#2478).
657
673
  useEffect(() => {
658
674
  const vv = typeof window !== "undefined" ? window.visualViewport : null;
659
- if (!vv) return undefined;
660
- const onResize = () => shellStore.set({ keyboard: window.innerHeight - vv.height > 150 });
661
- vv.addEventListener("resize", onResize);
662
- return () => vv.removeEventListener("resize", onResize);
675
+ if (!vv || typeof document === "undefined") return undefined;
676
+ // The tallest the view has been at this width: what it is with no keyboard. innerHeight is no measure on its own —
677
+ // it shrinks with the keyboard on iOS Safari and, under interactive-widget=resizes-content, on Android — so
678
+ // innerHeight − height stayed near 0 and the strips stayed between the composer and the keyboard.
679
+ // Only a touch screen (a coarse pointer) has an on-screen keyboard: a narrow desk window made shorter is not one.
680
+ // Turned with the keyboard up and the field still focused, the new width has no height without it yet: the keyboard
681
+ // stays up until the field is left, or the view grows from its lowest by more than a keyboard (put away).
682
+ let width = -1, tallest = 0, held = false, lowest = 0;
683
+ const coarse = () => typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches;
684
+ const check = () => {
685
+ const w = Math.round(vv.width), h = vv.height;
686
+ const typing = coarse() && isTextEntry(document.activeElement);
687
+ if (w !== width) { // turned, or a new width: measured again
688
+ held = width !== -1 && typing && shellStore.get().keyboard;
689
+ width = w; tallest = 0; lowest = h;
690
+ }
691
+ if (held) {
692
+ lowest = Math.min(lowest, h);
693
+ if (!typing || h - lowest > KEYBOARD_MIN_PX) held = false; // left the field, or the keyboard was put away
694
+ }
695
+ if (!held) tallest = Math.max(tallest, window.innerHeight || 0, typing ? 0 : h); // the view itself only when not typing
696
+ const up = held || keyboardUp(h, tallest, typing);
697
+ if (up !== shellStore.get().keyboard) shellStore.set({ keyboard: up });
698
+ };
699
+ check();
700
+ vv.addEventListener("resize", check);
701
+ document.addEventListener("focusin", check);
702
+ document.addEventListener("focusout", check);
703
+ return () => { vv.removeEventListener("resize", check); document.removeEventListener("focusin", check); document.removeEventListener("focusout", check); };
663
704
  }, []);
664
705
  const guest = guestPage(); // #1920: a guest's page — no operations, usage, pending changes or owner CSS
665
706
  const cls = ["shell", shell.collapsed ? "sb-collapsed" : "", shell.drawer ? "sb-open" : "", shell.keyboard ? "kb-open" : "", guest ? "guest" : ""].filter(Boolean).join(" ");
@@ -130,9 +130,15 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
130
130
  A short box that clips to its corners (.msg-quote) moves the bar in (--lbar-left) and shortens it (--lbar-inset) so
131
131
  the corner never cuts its ends round.
132
132
  tests/web-left-bar-2330.test.ts keeps a coloured border-left off rounded boxes. */
133
- .lbar, .reply-bar, .msg-quote, .p-log .log-item.k-error, .p-log .log-item.s-failed, .org-node, .us-basis, .us-partial, .cmd-card.bad, .tb-tile { position: relative; }
133
+ .lbar, .reply-bar, .msg-quote, .p-log .log-item.k-error, .p-log .log-item.s-failed, .org-node, .us-basis, .us-partial, .cmd-card.bad, .tb-tile,
134
+ .search-row.on, .search-recent-item:hover, .search-recent-item:focus-visible, .search-ctx-line.here, .tw-line:hover, .tw-line:focus-visible,
135
+ .u-crow:focus-visible { position: relative; }
134
136
  .lbar::before, .reply-bar::before, .msg-quote::before, .p-log .log-item.k-error::before, .p-log .log-item.s-failed::before, .org-node::before,
135
- .us-basis::before, .us-partial::before, .cmd-card.bad::before, .tb-tile::before {
137
+ .us-basis::before, .us-partial::before, .cmd-card.bad::before, .tb-tile::before,
138
+ /* #2478 item 7 (the user's fixed template): a selected, hovered or keyboard-focused row is the straight bar and a flat
139
+ background at most — never a rounded fill, a curved inset edge or a rounded ring. */
140
+ .search-row.on::before, .search-recent-item:hover::before, .search-recent-item:focus-visible::before, .search-ctx-line.here::before,
141
+ .tw-line:hover::before, .tw-line:focus-visible::before, .u-crow:focus-visible::before {
136
142
  content: ""; position: absolute; left: var(--lbar-left, 0); top: var(--lbar-inset, var(--s-2)); bottom: var(--lbar-inset, var(--s-2)); width: 3px;
137
143
  background: var(--lbar-color, var(--accent)); pointer-events: none; }
138
144
  .idc-swatches, .idc-places { border: 0; padding: 0; margin: 0 0 var(--s-3); display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
@@ -645,8 +651,8 @@ html.lb-open .scroller { overflow: hidden; }
645
651
  .p-chat > .panel-head { --bg: var(--chrome-bg); --surface: var(--chrome-surface); --text: var(--chrome-text); --text-2: var(--chrome-text-2); --accent: var(--chrome-accent); background: var(--bg); color: var(--text); }
646
652
  .p-chat.th { background: var(--bg); color: var(--text); }
647
653
  .p-chat.th .msg.user .body { border-radius: var(--bubble-radius, var(--r-xl)); }
648
- .p-chat.th-font-system .thread, .p-chat.th-font-system .composer textarea { font-family: system-ui, -apple-system, "Segoe UI", sans-serif, var(--font-emoji); }
649
- .p-chat.th-font-mono .thread, .p-chat.th-font-mono .composer textarea { font-family: var(--font-mono); }
654
+ .p-chat.th-font-system .thread, .p-chat.th-font-system .composer textarea, .p-chat.th-font-system .composer-ph { font-family: system-ui, -apple-system, "Segoe UI", sans-serif, var(--font-emoji); }
655
+ .p-chat.th-font-mono .thread, .p-chat.th-font-mono .composer textarea, .p-chat.th-font-mono .composer-ph { font-family: var(--font-mono); }
650
656
  .p-chat.th-bubble-thin .msg.user .body { box-shadow: inset 0 0 0 1px var(--border); }
651
657
  .p-chat.th-bubble-accent .msg.user .body { box-shadow: inset 0 0 0 1px var(--accent); }
652
658
  .p-chat.th-agent-bubble .msg.agent .body { background: var(--agent-bubble); border-radius: var(--bubble-radius, var(--r-xl)); padding: 10px var(--s-4); }
@@ -780,6 +786,11 @@ img.amb-p { object-fit: contain; }
780
786
  font-size: var(--fs-md); line-height: var(--lh-md); resize: none; min-height: 24px; max-height: 240px; overflow-y: auto;
781
787
  }
782
788
  .composer textarea::placeholder { color: var(--text-3); }
789
+ /* #2478: the placeholder, drawn over the empty box at its text's place: one line ending in "…" (a textarea's own is
790
+ clipped, never ellipsized). The same size and line as the box's text; it never takes a tap. */
791
+ .composer-field { position: relative; flex: 1; min-width: 0; display: flex; }
792
+ .composer-ph { position: absolute; left: var(--s-1); right: var(--s-1); top: 7px; pointer-events: none; color: var(--text-3);
793
+ font-size: var(--fs-md); line-height: var(--lh-md); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
783
794
  .btn-send { width: 36px; padding: 0; }
784
795
  .btn-send .send-label { display: none; }
785
796
  /* A narrow phone: Stop reply beside Send leaves the message box no room — the label is for screen readers there. */
@@ -952,6 +963,8 @@ img.amb-p { object-fit: contain; }
952
963
  padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
953
964
  }
954
965
  .kb-open .tabs { display: none; }
966
+ /* #2478: with the keyboard up the line under the composer goes too — the box sits on the keyboard; back on blur. */
967
+ .kb-open .composer-row { display: none; }
955
968
  .tab { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px; min-height: 56px; justify-content: center; font-size: 11px; color: var(--text-2); position: relative; }
956
969
  .tab.active { color: var(--text); font-weight: var(--fw-semibold); }
957
970
  .tab-badge { position: absolute; top: 6px; left: calc(50% + 6px); min-width: 16px; height: 16px; padding: 0 4px; border-radius: var(--r-full); background: var(--danger-fill); color: var(--on-danger); font-size: 10px; line-height: 16px; text-align: center; }
@@ -1169,7 +1182,10 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
1169
1182
  background: transparent; color: inherit; font: inherit; line-height: 1.3; text-align: left; cursor: pointer; }
1170
1183
  .u-cg-head + .u-crow { border-top: 0; }
1171
1184
  .u-crow:hover { background: var(--surface-2); }
1172
- .u-crow:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--focus); }
1185
+ /* #2478 (Prism r1): the compact rows sit in .u-cgroup, rounded and clipping (overflow: hidden) — the bar keeps clear of
1186
+ its 12 px corners (4 px in, 6 px off the row's ends) and there is no full-width underline to be rounded at the ends:
1187
+ the bar is the focus mark (hover has none). */
1188
+ .u-crow:focus-visible { outline: none; background: var(--surface-2); --lbar-left: var(--s-1); --lbar-inset: 6px; }
1173
1189
  /* The name never gives way to the plan (#2415); the reset keeps its room on the right. */
1174
1190
  .u-crow-head { display: flex; align-items: center; gap: 6px; min-width: 0; font-size: var(--fs-sm); }
1175
1191
  .u-crow-name { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: var(--fw-semibold); color: var(--text); }
@@ -1584,7 +1600,7 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1584
1600
  .oo-desks:focus-within { position: sticky; left: var(--s-2); bottom: var(--s-2); width: auto; height: auto; max-height: 40%; margin: 0; padding: var(--s-1); clip: auto; overflow-y: auto;
1585
1601
  display: inline-flex; flex-wrap: wrap; gap: 4px; max-width: calc(100% - 2 * var(--s-2)); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); white-space: normal; }
1586
1602
  .oo-desk { font-size: var(--fs-xs); padding: 2px 6px; border-radius: var(--r-sm); background: var(--bg); color: var(--text); border: 1px solid var(--border); }
1587
- .oo-desk:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
1603
+ .oo-desk:focus-visible { outline: none; border-color: var(--focus); box-shadow: inset 0 -2px 0 var(--focus); }
1588
1604
  @media (max-width: 640px) {
1589
1605
  .oo-stage { height: calc(100dvh - 300px); min-height: 340px; }
1590
1606
  .oo-pick .tb-select { max-width: 150px; }
@@ -1908,6 +1924,8 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1908
1924
  .chat-fx-notice > span { display: block; }
1909
1925
 
1910
1926
  /* ── #2293: global search — the sidebar row's shortcut, the header's button (collapsed sidebar, phone), the dialog ── */
1927
+ /* #2478: a touch screen has no Ctrl/⌘ K — the shortcut's hint is not shown there (the row is). */
1928
+ @media (pointer: coarse) { .side-search .kbd-hint { display: none; } }
1911
1929
  .kbd-hint { font: inherit; font-size: var(--fs-xs); color: var(--text-3); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 0 var(--s-1); line-height: 18px; white-space: nowrap; }
1912
1930
  .panel-head .sb-search { display: none; }
1913
1931
  .sb-collapsed .panel-head .sb-search { display: inline-flex; }
@@ -1933,15 +1951,17 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1933
1951
  .search-note { color: var(--warn); }
1934
1952
  .search-recent-head { display: flex; align-items: center; justify-content: space-between; padding: var(--s-2) var(--s-3) 0; color: var(--text-3); font-size: var(--fs-xs); }
1935
1953
  .search-recent ul { list-style: none; margin: 0; padding: 0; }
1936
- .search-recent-item { display: flex; align-items: center; gap: var(--s-2); width: 100%; border: 0; border-radius: var(--r-sm); background: transparent; color: var(--text); padding: var(--s-2) var(--s-3); font-size: var(--fs-sm); text-align: left; cursor: pointer; }
1937
- .search-recent-item:hover { background: var(--surface-2); }
1954
+ .search-recent-item { display: flex; align-items: center; gap: var(--s-2); width: 100%; border: 0; border-radius: 0; --lbar-inset: 4px; background: transparent; color: var(--text); padding: var(--s-2) var(--s-3); font-size: var(--fs-sm); text-align: left; cursor: pointer; }
1955
+ .search-recent-item:hover, .search-recent-item:focus-visible { background: var(--surface-2); }
1956
+ /* Keyboard focus: the bar and a straight underline (no rounded ring). */
1957
+ .search-recent-item:focus-visible { outline: none; box-shadow: inset 0 -2px 0 var(--focus); }
1938
1958
  .search-recent-item svg { color: var(--text-3); flex: none; }
1939
1959
  .search-results.stale { opacity: 0.6; }
1940
1960
  .search-group + .search-group { margin-top: var(--s-2); }
1941
1961
  .search-group-head { display: flex; justify-content: space-between; gap: var(--s-2); padding: var(--s-2) var(--s-3) var(--s-1); font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--text-2); }
1942
1962
  .search-count { color: var(--text-3); font-weight: var(--fw-regular); }
1943
- .search-row { padding: var(--s-2) var(--s-3); border-radius: var(--r-sm); cursor: pointer; }
1944
- .search-row.on { background: var(--surface-2); box-shadow: inset 2px 0 0 var(--accent); }
1963
+ .search-row { padding: var(--s-2) var(--s-3); border-radius: 0; cursor: pointer; }
1964
+ .search-row.on { background: var(--surface-2); }
1945
1965
  .search-row-top { display: flex; align-items: center; gap: var(--s-2); font-size: var(--fs-xs); color: var(--text-2); }
1946
1966
  .search-who { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1947
1967
  .search-kind { color: var(--text-3); }
@@ -1955,8 +1975,8 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1955
1975
  .search-detail-head { display: flex; align-items: center; gap: var(--s-2); padding: var(--s-1) var(--s-1) var(--s-2); font-size: var(--fs-sm); }
1956
1976
  .search-detail-head .chip-btn { display: inline-flex; align-items: center; gap: 4px; }
1957
1977
  .search-ctx { list-style: none; margin: 0; padding: 0 var(--s-1); display: flex; flex-direction: column; gap: var(--s-1); }
1958
- .search-ctx-line { padding: var(--s-2) var(--s-3); border-radius: var(--r-sm); }
1959
- .search-ctx-line.here { background: var(--surface-2); box-shadow: inset 2px 0 0 var(--accent); }
1978
+ .search-ctx-line { padding: var(--s-2) var(--s-3); border-radius: 0; }
1979
+ .search-ctx-line.here { background: var(--surface-2); }
1960
1980
  .search-ctx-text { margin-top: 2px; font-size: var(--fs-sm); white-space: pre-wrap; overflow-wrap: anywhere; }
1961
1981
  .search-detail-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--s-2); padding: var(--s-3) var(--s-1) var(--s-1); }
1962
1982
  @media (max-width: 899px) {
@@ -2132,9 +2152,9 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
2132
2152
  .tw-desc { margin: 0; font-size: var(--fs-xs); overflow-wrap: anywhere; }
2133
2153
  .tw-none { margin: 0; font-size: var(--fs-xs); }
2134
2154
  .tw-lines { display: flex; flex-direction: column; gap: 2px; margin: 0; padding: 0; list-style: none; }
2135
- .tw-line { display: flex; align-items: baseline; gap: var(--s-2); width: 100%; padding: 2px var(--s-1); background: none; border: 0; border-radius: var(--r-sm); color: var(--text); text-align: left; cursor: pointer; font-size: var(--fs-sm); min-width: 0; }
2155
+ .tw-line { display: flex; align-items: baseline; gap: var(--s-2); width: 100%; padding: 2px var(--s-1); background: none; border: 0; border-radius: 0; --lbar-inset: 2px; color: var(--text); text-align: left; cursor: pointer; font-size: var(--fs-sm); min-width: 0; }
2136
2156
  .tw-line:hover { background: var(--surface-2); }
2137
- .tw-line:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
2157
+ .tw-line:focus-visible { outline: none; background: var(--surface-2); box-shadow: inset 0 -2px 0 var(--focus); }
2138
2158
  .tw-kind { flex-shrink: 0; min-width: 4.5em; font-size: 11px; font-weight: var(--fw-semibold); color: var(--text-2); }
2139
2159
  .tw-kind.k-doing { color: var(--accent); }
2140
2160
  .tw-kind.k-blocked, .tw-kind.k-back { color: var(--danger); }
@@ -166,3 +166,7 @@
166
166
  @media (pointer: coarse), (max-width: 899px) {
167
167
  input, select, textarea { --fs-xs: 16px; --fs-sm: 16px; --fs-md: 16px; }
168
168
  }
169
+ /* #2478: the composer's drawn placeholder (a span over the box) is the box's text in size too. */
170
+ @media (pointer: coarse), (max-width: 899px) {
171
+ .composer-ph { --fs-md: 16px; }
172
+ }
@@ -1,6 +1,8 @@
1
1
  import { type UpdateMarker } from "./update-marker.js";
2
2
  export declare function updateElapsedSeconds(startedAt: number, now?: number): number;
3
- /** Render the persisted CLI-side stage while the old fleet is still alive. */
3
+ /** Render the persisted CLI-side stage while the old fleet is still alive.
4
+ * Stage `starting` is owned by RestartProgress in the new process (#2476); the
5
+ * monitor must not post this text then. */
4
6
  export declare function formatUpdateProgress(marker: UpdateMarker & {
5
7
  progress: NonNullable<UpdateMarker["progress"]>;
6
8
  }, now?: number): string;
@@ -6,7 +6,9 @@ export function updateElapsedSeconds(startedAt, now = Date.now()) {
6
6
  function stageLabel(stage) {
7
7
  return t(`update.stage.${stage ?? "preparing"}`);
8
8
  }
9
- /** Render the persisted CLI-side stage while the old fleet is still alive. */
9
+ /** Render the persisted CLI-side stage while the old fleet is still alive.
10
+ * Stage `starting` is owned by RestartProgress in the new process (#2476); the
11
+ * monitor must not post this text then. */
10
12
  export function formatUpdateProgress(marker, now = Date.now()) {
11
13
  const elapsed = updateElapsedSeconds(marker.startedAt, now);
12
14
  const progress = marker.progress;
@@ -1 +1 @@
1
- {"version":3,"file":"update-progress.js","sourceRoot":"","sources":["../src/update-progress.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,aAAa,CAAC;AAChC,OAAO,EAAE,uBAAuB,EAA+C,MAAM,oBAAoB,CAAC;AAE1G,MAAM,UAAU,oBAAoB,CAAC,SAAiB,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IACtE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,SAAS,UAAU,CAAC,KAAsC;IACxD,OAAO,CAAC,CAAC,gBAAgB,KAAK,IAAI,WAAW,EAAE,CAAC,CAAC;AACnD,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,oBAAoB,CAAC,MAA0E,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IAC/H,MAAM,OAAO,GAAG,oBAAoB,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAC5D,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IACjC,IAAI,uBAAuB,CAAC,QAAQ,CAAC,KAAK,cAAc,EAAE,CAAC;QACzD,IAAI,QAAQ,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAChC,OAAO,CAAC,CAAC,yBAAyB,EAAE,QAAQ,CAAC,KAAK,IAAI,eAAe,EAAE,OAAO,CAAC,CAAC;QAClF,CAAC;QACD,IAAI,QAAQ,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;YAClC,OAAO,CAAC,CAAC,2BAA2B,EAAE,QAAQ,CAAC,OAAO,IAAI,GAAG,EAAE,OAAO,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,QAAQ,CAAC,KAAK,KAAK,UAAU;YAClC,CAAC,CAAC,CAAC,CAAC,2BAA2B,EAAE,OAAO,CAAC;YACzC,CAAC,CAAC,CAAC,CAAC,2BAA2B,EAAE,OAAO,CAAC,CAAC;IAC9C,CAAC;IACD,QAAQ,QAAQ,CAAC,KAAK,EAAE,CAAC;QACvB,KAAK,WAAW;YACd,OAAO,CAAC,CAAC,2BAA2B,EAAE,OAAO,CAAC,CAAC;QACjD,KAAK,aAAa;YAChB,OAAO,CAAC,CAAC,6BAA6B,EAAE,OAAO,CAAC,CAAC;QACnD,KAAK,WAAW;YACd,OAAO,CAAC,CAAC,2BAA2B,EAAE,QAAQ,CAAC,OAAO,IAAI,GAAG,EAAE,OAAO,CAAC,CAAC;QAC1E,KAAK,UAAU;YACb,OAAO,CAAC,CAAC,0BAA0B,EAAE,OAAO,CAAC,CAAC;QAChD,KAAK,UAAU;YACb,OAAO,CAAC,CAAC,0BAA0B,EAAE,OAAO,CAAC,CAAC;QAChD,KAAK,UAAU;YACb,OAAO,CAAC,CAAC,iCAAiC,EAAE,QAAQ,CAAC,OAAO,IAAI,GAAG,EAAE,OAAO,CAAC,CAAC;QAChF,KAAK,QAAQ;YACX,OAAO,CAAC,CAAC,wBAAwB,EAAE,UAAU,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,KAAK,IAAI,eAAe,EAAE,OAAO,CAAC,CAAC;IACrH,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"update-progress.js","sourceRoot":"","sources":["../src/update-progress.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,aAAa,CAAC;AAChC,OAAO,EAAE,uBAAuB,EAA+C,MAAM,oBAAoB,CAAC;AAE1G,MAAM,UAAU,oBAAoB,CAAC,SAAiB,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IACtE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,SAAS,UAAU,CAAC,KAAsC;IACxD,OAAO,CAAC,CAAC,gBAAgB,KAAK,IAAI,WAAW,EAAE,CAAC,CAAC;AACnD,CAAC;AAED;;2CAE2C;AAC3C,MAAM,UAAU,oBAAoB,CAAC,MAA0E,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IAC/H,MAAM,OAAO,GAAG,oBAAoB,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAC5D,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IACjC,IAAI,uBAAuB,CAAC,QAAQ,CAAC,KAAK,cAAc,EAAE,CAAC;QACzD,IAAI,QAAQ,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAChC,OAAO,CAAC,CAAC,yBAAyB,EAAE,QAAQ,CAAC,KAAK,IAAI,eAAe,EAAE,OAAO,CAAC,CAAC;QAClF,CAAC;QACD,IAAI,QAAQ,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;YAClC,OAAO,CAAC,CAAC,2BAA2B,EAAE,QAAQ,CAAC,OAAO,IAAI,GAAG,EAAE,OAAO,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,QAAQ,CAAC,KAAK,KAAK,UAAU;YAClC,CAAC,CAAC,CAAC,CAAC,2BAA2B,EAAE,OAAO,CAAC;YACzC,CAAC,CAAC,CAAC,CAAC,2BAA2B,EAAE,OAAO,CAAC,CAAC;IAC9C,CAAC;IACD,QAAQ,QAAQ,CAAC,KAAK,EAAE,CAAC;QACvB,KAAK,WAAW;YACd,OAAO,CAAC,CAAC,2BAA2B,EAAE,OAAO,CAAC,CAAC;QACjD,KAAK,aAAa;YAChB,OAAO,CAAC,CAAC,6BAA6B,EAAE,OAAO,CAAC,CAAC;QACnD,KAAK,WAAW;YACd,OAAO,CAAC,CAAC,2BAA2B,EAAE,QAAQ,CAAC,OAAO,IAAI,GAAG,EAAE,OAAO,CAAC,CAAC;QAC1E,KAAK,UAAU;YACb,OAAO,CAAC,CAAC,0BAA0B,EAAE,OAAO,CAAC,CAAC;QAChD,KAAK,UAAU;YACb,OAAO,CAAC,CAAC,0BAA0B,EAAE,OAAO,CAAC,CAAC;QAChD,KAAK,UAAU;YACb,OAAO,CAAC,CAAC,iCAAiC,EAAE,QAAQ,CAAC,OAAO,IAAI,GAAG,EAAE,OAAO,CAAC,CAAC;QAChF,KAAK,QAAQ;YACX,OAAO,CAAC,CAAC,wBAAwB,EAAE,UAAU,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,KAAK,IAAI,eAAe,EAAE,OAAO,CAAC,CAAC;IACrH,CAAC;AACH,CAAC"}
package/dist/web-api.d.ts CHANGED
@@ -68,6 +68,7 @@ export interface WebApiContext {
68
68
  upload_max_total_mb?: number;
69
69
  search?: {
70
70
  enabled?: boolean;
71
+ public_link?: boolean;
71
72
  };
72
73
  };
73
74
  defaults?: {
@@ -438,6 +439,9 @@ export interface AppShellContext {
438
439
  experimental?: {
439
440
  canvas_worker?: boolean;
440
441
  };
442
+ search?: {
443
+ public_link?: boolean;
444
+ };
441
445
  };
442
446
  } | null;
443
447
  /** #1665: the host's theme and language go into the page itself, so a device paints them before any script reads. */
package/dist/web-api.js CHANGED
@@ -3171,11 +3171,12 @@ export function handleWebRequest(req, res, url, ctx) {
3171
3171
  }
3172
3172
  return true;
3173
3173
  }
3174
- // #2293: global search — an admin's only: the header token or an admin session. Never over the public link (the gateway
3175
- // does not list it either), never a guest (not in GUEST_ROUTES). Asked on arrival and again before the answer is sent.
3174
+ // #2293: global search — an admin's only: the header token or an admin session. Never a guest (not in GUEST_ROUTES).
3175
+ // #2478 (the user's request): over the public link too, unless web.search.public_link is false. Asked on arrival and
3176
+ // again before the answer is sent.
3176
3177
  if (method === "GET" && (path === "/ui/search" || path === "/ui/search/context")) {
3177
3178
  const refusal = () => {
3178
- if (gatewayRequestContext(req))
3179
+ if (gatewayRequestContext(req) && ctx.fleetConfig?.web?.search?.public_link === false)
3179
3180
  return [404, "search_not_over_public_link"];
3180
3181
  if (guestRequestOf(req))
3181
3182
  return [403, "guest_forbidden"];
@@ -4065,6 +4066,8 @@ function shellBodyTag(req, ctx, mode, viewer) {
4065
4066
  + ` data-owner-css-state="${ownerCssState}"${ownerCss ? ` data-owner-css="${attr(ownerCss)}"` : ""}`
4066
4067
  // #1589: the public link shows attachments as downloads only (no inline text or media): its scope is unchanged.
4067
4068
  + `${gatewayRequestContext(req) ? ' data-public-link="1"' : ""}`
4069
+ // #2478: search is offered over the public link unless web.search.public_link is false (the page then shows no entry).
4070
+ + `${gatewayRequestContext(req) && ctx.fleetConfig?.web?.search?.public_link === false ? ' data-search-link="0"' : ""}`
4068
4071
  // #1920 phase 3: a guest's page — the shell shows only what a guest may use, and who it is signed in as.
4069
4072
  + `${mode === "full" && viewer?.guest && viewer.owner ? guestBodyAttrs(ctx, viewer.owner, attr) : ""}>`;
4070
4073
  }