@songsid/agend 2.3.0-beta.16 → 2.3.0-beta.17
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.
- package/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/backend/claude-code.js +13 -4
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.js +15 -2
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend-command.d.ts +2 -0
- package/dist/backend-command.js +3 -1
- package/dist/backend-command.js.map +1 -1
- package/dist/bug-report-cli.d.ts +30 -0
- package/dist/bug-report-cli.js +138 -0
- package/dist/bug-report-cli.js.map +1 -0
- package/dist/bug-report.d.ts +47 -0
- package/dist/bug-report.js +136 -0
- package/dist/bug-report.js.map +1 -0
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +93 -8
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +6 -0
- package/dist/config-validator.js +13 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +14 -1
- package/dist/daemon.js +59 -3
- package/dist/daemon.js.map +1 -1
- package/dist/diagnostic-redaction.d.ts +10 -0
- package/dist/diagnostic-redaction.js +167 -0
- package/dist/diagnostic-redaction.js.map +1 -0
- package/dist/fleet-manager.d.ts +18 -0
- package/dist/fleet-manager.js +109 -8
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/bug-report/SKILL.md +31 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +22 -1
- package/dist/instance-lifecycle.d.ts +8 -1
- package/dist/instance-lifecycle.js +68 -12
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +55 -2
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.d.ts +45 -0
- package/dist/login-controller.js +66 -3
- package/dist/login-controller.js.map +1 -1
- package/dist/ls-attach-resolve.d.ts +3 -2
- package/dist/ls-attach-resolve.js +10 -4
- package/dist/ls-attach-resolve.js.map +1 -1
- package/dist/ls-rows.d.ts +12 -0
- package/dist/ls-rows.js +82 -3
- package/dist/ls-rows.js.map +1 -1
- package/dist/needs-you.d.ts +10 -0
- package/dist/needs-you.js +2 -1
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.js +31 -5
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/quickstart-api.d.ts +9 -0
- package/dist/quickstart-api.js +82 -0
- package/dist/quickstart-api.js.map +1 -1
- package/dist/service-installer.js +12 -0
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-effect.js +15 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +23 -7
- package/dist/setup-form.js.map +1 -1
- package/dist/ui/backend-signin.js +297 -0
- package/dist/ui/chat-msg-actions.js +176 -0
- package/dist/ui/chat-store.js +33 -3
- package/dist/ui/chat-strings.js +8 -2
- package/dist/ui/chat-thread.js +44 -2
- package/dist/ui/panel-chat.js +37 -0
- package/dist/ui/panel-needs.js +4 -2
- package/dist/ui/panel-settings.js +6 -3
- package/dist/ui/shared/app-shell.js +2 -1
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +42 -0
- package/dist/ui/shared/app.js +2 -0
- package/dist/ui/shared/usage-dialog.js +26 -3
- package/dist/ui/shared/view-strings.js +10 -0
- package/dist/ui/web-terminal/terminal.html +1 -1
- package/dist/update-launch.d.ts +9 -0
- package/dist/update-launch.js +38 -12
- package/dist/update-launch.js.map +1 -1
- package/dist/usage/format-rich.js +6 -3
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +2 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/i18n.d.ts +13 -1
- package/dist/usage/i18n.js +29 -0
- package/dist/usage/i18n.js.map +1 -1
- package/dist/usage/providers.d.ts +43 -8
- package/dist/usage/providers.js +58 -20
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.d.ts +2 -0
- package/dist/usage/usage-api.js +180 -55
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/user-systemd.d.ts +24 -0
- package/dist/user-systemd.js +75 -0
- package/dist/user-systemd.js.map +1 -0
- package/dist/web-api.d.ts +10 -1
- package/dist/web-api.js +134 -5
- package/dist/web-api.js.map +1 -1
- package/dist/web-backend-login.d.ts +130 -0
- package/dist/web-backend-login.js +225 -0
- package/dist/web-backend-login.js.map +1 -0
- package/dist/web-chat-history.d.ts +31 -0
- package/dist/web-chat-history.js +49 -1
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +5 -1
- package/dist/web-commands.js +1 -1
- package/dist/web-commands.js.map +1 -1
- package/dist/web-guest-access.d.ts +3 -2
- package/dist/web-guest-access.js +10 -6
- package/dist/web-guest-access.js.map +1 -1
- package/dist/windows-shortcuts.d.ts +78 -0
- package/dist/windows-shortcuts.js +203 -0
- package/dist/windows-shortcuts.js.map +1 -0
- package/launcher/agend-install.cjs +548 -0
- package/launcher/install-admission.cjs +4 -39
- package/launcher/postinstall.cjs +14 -12
- package/package.json +3 -2
- package/scripts/preinstall-guard.cjs +2 -5
- package/templates/windows-shortcuts.ps1 +181 -0
- package/launcher/old-updater-note.cjs +0 -48
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// #2272: a chat message's own actions, beyond the hover strip — the message menu (right-click, long-press on a phone),
|
|
2
|
+
// swipe left to reply (as in Telegram), and R on a focused message. One install per thread element.
|
|
3
|
+
//
|
|
4
|
+
// - Right-click on a message opens Reply / Copy text / Copy link. Shift+right-click, a right-click with text selected,
|
|
5
|
+
// or one on a link, an image or a field keeps the browser's own menu.
|
|
6
|
+
// - A long press (500 ms without moving) opens the same menu; the click and the browser menu it would also cause are
|
|
7
|
+
// swallowed.
|
|
8
|
+
// - Swipe left on a message: it follows the finger (up to 64 px); let go past 56 px and it is a reply. A mostly
|
|
9
|
+
// vertical move is scrolling, never a reply. A swipe right is the phone drawer's (app-swipe.js), on a message too: the
|
|
10
|
+
// two never fight.
|
|
11
|
+
// - R (no modifier) on a focused message replies; ↑/↓ move to the message before or after (one message is in the Tab
|
|
12
|
+
// order); Escape closes the menu.
|
|
13
|
+
|
|
14
|
+
const LONG_PRESS_MS = 500, MOVE_TOLERANCE = 10, SWIPE_REPLY_PX = 56, SWIPE_MAX_PX = 64;
|
|
15
|
+
|
|
16
|
+
/** The message element an event is on (a person's or an agent's message of this thread), or null. */
|
|
17
|
+
function messageEl(list, target) {
|
|
18
|
+
const el = target && target.closest ? target.closest(".msg[data-key]") : null;
|
|
19
|
+
return el && list.contains(el) ? el : null;
|
|
20
|
+
}
|
|
21
|
+
/** The browser's own menu stays for: Shift, selected text, a link, an image, a field, code. */
|
|
22
|
+
function keepsNativeMenu(e, doc) {
|
|
23
|
+
if (e.shiftKey) return true;
|
|
24
|
+
const sel = doc.getSelection ? doc.getSelection() : null;
|
|
25
|
+
if (sel && !sel.isCollapsed && String(sel).trim()) return true;
|
|
26
|
+
return !!(e.target && e.target.closest && e.target.closest("a[href], img, video, input, textarea, select, pre, code, [contenteditable='true']"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Install the gestures on `list` (the thread element). `deps`:
|
|
31
|
+
* messageOf(key) → the row; reply(row); copyText(text) → Promise<boolean>; linkOf(row) → string; notice(text);
|
|
32
|
+
* canReply() → boolean (false: no Reply — a page that cannot send); t(key).
|
|
33
|
+
* Returns a function that removes everything (and closes an open menu).
|
|
34
|
+
*/
|
|
35
|
+
export function installMessageActions(list, deps) {
|
|
36
|
+
const doc = list.ownerDocument, win = doc.defaultView;
|
|
37
|
+
let menu = null, press = null, swipe = null, swallowClick = false;
|
|
38
|
+
|
|
39
|
+
function closeMenu() {
|
|
40
|
+
if (!menu) return;
|
|
41
|
+
menu.remove(); menu = null;
|
|
42
|
+
doc.removeEventListener("pointerdown", outside, true);
|
|
43
|
+
doc.removeEventListener("keydown", onMenuKey, true);
|
|
44
|
+
win?.removeEventListener("scroll", closeMenu, true);
|
|
45
|
+
win?.removeEventListener("blur", closeMenu);
|
|
46
|
+
}
|
|
47
|
+
function outside(e) { if (menu && !menu.contains(e.target)) closeMenu(); }
|
|
48
|
+
function onMenuKey(e) {
|
|
49
|
+
if (!menu) return;
|
|
50
|
+
if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); closeMenu(); return; }
|
|
51
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
const items = [...menu.querySelectorAll("button")], i = items.indexOf(doc.activeElement);
|
|
54
|
+
items[(i + (e.key === "ArrowDown" ? 1 : items.length - 1)) % items.length]?.focus();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** The message menu at (x, y), within the window; focus on its first item. */
|
|
58
|
+
function openMenu(row, x, y) {
|
|
59
|
+
closeMenu();
|
|
60
|
+
const items = [];
|
|
61
|
+
if (deps.canReply?.() !== false) items.push(["reply", deps.t("chat.reply"), () => deps.reply(row)]);
|
|
62
|
+
items.push(["copyText", deps.t("chat.copyText"), async () => { const ok = await deps.copyText(String(row.click?.label ?? row.text ?? "")); deps.notice(deps.t(ok ? "chat.copied" : "chat.copyFailed"), ok); }]);
|
|
63
|
+
items.push(["copyLink", deps.t("chat.copyLink"), async () => { const ok = await deps.copyText(deps.linkOf(row)); deps.notice(deps.t(ok ? "chat.copied" : "chat.copyFailed"), ok); }]);
|
|
64
|
+
menu = doc.createElement("div");
|
|
65
|
+
menu.className = "msg-menu";
|
|
66
|
+
menu.setAttribute("role", "menu");
|
|
67
|
+
menu.setAttribute("aria-label", deps.t("chat.msgMenu"));
|
|
68
|
+
menu.setAttribute("data-trust", "menu");
|
|
69
|
+
for (const [id, label, run] of items) {
|
|
70
|
+
const b = doc.createElement("button");
|
|
71
|
+
b.type = "button"; b.className = "menu-item"; b.setAttribute("role", "menuitem"); b.dataset.item = id; b.textContent = label;
|
|
72
|
+
b.addEventListener("click", () => { closeMenu(); void run(); });
|
|
73
|
+
menu.append(b);
|
|
74
|
+
}
|
|
75
|
+
doc.body.append(menu);
|
|
76
|
+
const w = win?.innerWidth ?? 1024, h = win?.innerHeight ?? 768, r = menu.getBoundingClientRect();
|
|
77
|
+
menu.style.left = `${Math.max(4, Math.min(x, w - r.width - 4))}px`;
|
|
78
|
+
menu.style.top = `${Math.max(4, Math.min(y, h - r.height - 4))}px`;
|
|
79
|
+
doc.addEventListener("pointerdown", outside, true);
|
|
80
|
+
doc.addEventListener("keydown", onMenuKey, true);
|
|
81
|
+
win?.addEventListener("scroll", closeMenu, true);
|
|
82
|
+
win?.addEventListener("blur", closeMenu);
|
|
83
|
+
menu.querySelector("button")?.focus();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function onContextMenu(e) {
|
|
87
|
+
if (swallowClick) { e.preventDefault(); return; } // the long press already opened ours
|
|
88
|
+
cancelPress(); // Android's long press fires this itself: one menu
|
|
89
|
+
const el = messageEl(list, e.target);
|
|
90
|
+
if (!el || keepsNativeMenu(e, doc)) return;
|
|
91
|
+
const row = deps.messageOf(el.dataset.key);
|
|
92
|
+
if (!row) return;
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
openMenu(row, e.clientX, e.clientY);
|
|
95
|
+
}
|
|
96
|
+
function onKeyDown(e) {
|
|
97
|
+
// ↑/↓ on a focused message: the one before or after it (one message is in the Tab order: the thread's rove()).
|
|
98
|
+
if ((e.key === "ArrowUp" || e.key === "ArrowDown") && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey
|
|
99
|
+
&& e.target && e.target.matches && e.target.matches(".msg[data-key]") && list.contains(e.target)) {
|
|
100
|
+
const all = [...list.querySelectorAll(".msg[data-key]")], i = all.indexOf(e.target);
|
|
101
|
+
const to = all[i + (e.key === "ArrowDown" ? 1 : -1)];
|
|
102
|
+
if (to) { e.preventDefault(); deps.rove?.(to.dataset.key); to.focus(); to.scrollIntoView?.({ block: "nearest" }); }
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (e.key !== "r" && e.key !== "R") return;
|
|
106
|
+
if (e.ctrlKey || e.metaKey || e.altKey || deps.canReply?.() === false) return;
|
|
107
|
+
const el = e.target && e.target.matches && e.target.matches(".msg[data-key]") && list.contains(e.target) ? e.target : null;
|
|
108
|
+
const row = el ? deps.messageOf(el.dataset.key) : null;
|
|
109
|
+
if (!row) return;
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
deps.reply(row);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function onTouchStart(e) {
|
|
115
|
+
if (e.touches.length !== 1) { cancelPress(); cancelSwipe(); return; }
|
|
116
|
+
const el = messageEl(list, e.target);
|
|
117
|
+
if (!el) return;
|
|
118
|
+
const t0 = e.touches[0];
|
|
119
|
+
const native = keepsNativeMenu({ target: e.target, shiftKey: false }, doc); // a link, an image, code: the browser's
|
|
120
|
+
press = native ? null : { el, x: t0.clientX, y: t0.clientY, timer: setTimeout(() => {
|
|
121
|
+
const row = deps.messageOf(el.dataset.key);
|
|
122
|
+
press = null; cancelSwipe();
|
|
123
|
+
if (!row) return;
|
|
124
|
+
swallowClick = true; setTimeout(() => { swallowClick = false; }, 700);
|
|
125
|
+
openMenu(row, t0.clientX, t0.clientY);
|
|
126
|
+
}, LONG_PRESS_MS) };
|
|
127
|
+
swipe = deps.canReply?.() === false || native ? null : { el, x: t0.clientX, y: t0.clientY, dx: 0, locked: null };
|
|
128
|
+
}
|
|
129
|
+
function onTouchMove(e) {
|
|
130
|
+
const p = e.touches[0];
|
|
131
|
+
if (press && (Math.abs(p.clientX - press.x) > MOVE_TOLERANCE || Math.abs(p.clientY - press.y) > MOVE_TOLERANCE)) cancelPress();
|
|
132
|
+
if (!swipe) return;
|
|
133
|
+
const dx = p.clientX - swipe.x, dy = p.clientY - swipe.y;
|
|
134
|
+
if (swipe.locked === null && (Math.abs(dx) > MOVE_TOLERANCE || Math.abs(dy) > MOVE_TOLERANCE)) swipe.locked = dx < 0 && Math.abs(dy) < -dx / 2 ? "x" : "no";
|
|
135
|
+
if (swipe.locked !== "x") return;
|
|
136
|
+
swipe.dx = Math.max(0, Math.min(-dx, SWIPE_MAX_PX)); // how far left, as a positive distance
|
|
137
|
+
swipe.el.style.transform = `translateX(${-swipe.dx}px)`;
|
|
138
|
+
swipe.el.classList.toggle("swipe-ready", swipe.dx >= SWIPE_REPLY_PX);
|
|
139
|
+
}
|
|
140
|
+
function onTouchEnd() {
|
|
141
|
+
cancelPress();
|
|
142
|
+
if (!swipe) return;
|
|
143
|
+
const { el, dx, locked } = swipe;
|
|
144
|
+
cancelSwipe();
|
|
145
|
+
if (locked === "x" && dx >= SWIPE_REPLY_PX) { const row = deps.messageOf(el.dataset.key); if (row) deps.reply(row); }
|
|
146
|
+
}
|
|
147
|
+
function cancelPress() { if (press) { clearTimeout(press.timer); press = null; } }
|
|
148
|
+
function cancelSwipe() {
|
|
149
|
+
if (!swipe) return;
|
|
150
|
+
swipe.el.style.transform = ""; swipe.el.classList.remove("swipe-ready");
|
|
151
|
+
swipe = null;
|
|
152
|
+
}
|
|
153
|
+
function onClickCapture(e) { if (swallowClick) { e.preventDefault(); e.stopPropagation(); } }
|
|
154
|
+
|
|
155
|
+
// A message focused by a click or a tap becomes the one Tab comes back to.
|
|
156
|
+
function onFocusIn(e) { const el = e.target && e.target.matches && e.target.matches(".msg[data-key]") ? e.target : null; if (el && list.contains(el)) deps.rove?.(el.dataset.key); }
|
|
157
|
+
list.addEventListener("focusin", onFocusIn);
|
|
158
|
+
list.addEventListener("contextmenu", onContextMenu);
|
|
159
|
+
list.addEventListener("keydown", onKeyDown);
|
|
160
|
+
list.addEventListener("touchstart", onTouchStart, { passive: true });
|
|
161
|
+
list.addEventListener("touchmove", onTouchMove, { passive: true });
|
|
162
|
+
list.addEventListener("touchend", onTouchEnd, { passive: true });
|
|
163
|
+
list.addEventListener("touchcancel", onTouchEnd, { passive: true });
|
|
164
|
+
list.addEventListener("click", onClickCapture, true);
|
|
165
|
+
return () => {
|
|
166
|
+
closeMenu(); cancelPress(); cancelSwipe();
|
|
167
|
+
list.removeEventListener("focusin", onFocusIn);
|
|
168
|
+
list.removeEventListener("contextmenu", onContextMenu);
|
|
169
|
+
list.removeEventListener("keydown", onKeyDown);
|
|
170
|
+
list.removeEventListener("touchstart", onTouchStart);
|
|
171
|
+
list.removeEventListener("touchmove", onTouchMove);
|
|
172
|
+
list.removeEventListener("touchend", onTouchEnd);
|
|
173
|
+
list.removeEventListener("touchcancel", onTouchEnd);
|
|
174
|
+
list.removeEventListener("click", onClickCapture, true);
|
|
175
|
+
};
|
|
176
|
+
}
|
package/dist/ui/chat-store.js
CHANGED
|
@@ -42,7 +42,7 @@ const COMPACT_PAGE_LIMIT_MS = 11 * 60_000;
|
|
|
42
42
|
/** Build a store. `deps` (fetch, toast, announce, t, now) are injected so tests can drive every path. */
|
|
43
43
|
export function createChatStore(deps) {
|
|
44
44
|
const s = {
|
|
45
|
-
msgs: {}, drafts: {}, failedSends: {}, pendingFiles: {}, sending: {}, inFlightFiles: {}, sendFailures: {}, instanceStatus: {}, waking: {},
|
|
45
|
+
msgs: {}, drafts: {}, replies: {}, failedSends: {}, pendingFiles: {}, sending: {}, inFlightFiles: {}, sendFailures: {}, instanceStatus: {}, waking: {},
|
|
46
46
|
prompts: {}, workingSince: {}, stopping: {}, cancelling: {}, scrollMemo: {}, historyRead: new Set(), rbBusy: new Set(),
|
|
47
47
|
pollMine: readPollMine(),
|
|
48
48
|
// #1823 (#1828 review): the server's clock minus this device's. A poll's deadline is the server's; a page whose clock
|
|
@@ -250,6 +250,7 @@ export function createChatStore(deps) {
|
|
|
250
250
|
}
|
|
251
251
|
async function send(target) {
|
|
252
252
|
const txt = (s.drafts[target] || "").trim();
|
|
253
|
+
const quote = s.replies[target]; // #2272: the message this one replies to (its row; the server reads the rest)
|
|
253
254
|
const files = (s.pendingFiles[target] || []).slice();
|
|
254
255
|
if ((!txt && !files.length) || !target || s.sending[target]) return;
|
|
255
256
|
s.sending[target] = true;
|
|
@@ -263,12 +264,16 @@ export function createChatStore(deps) {
|
|
|
263
264
|
try {
|
|
264
265
|
const ids = [];
|
|
265
266
|
for (const f of files) ids.push((await uploadFile(target, f)).id);
|
|
266
|
-
|
|
267
|
+
const body = ids.length ? { instance: target, message: txt, attachments: ids } : { instance: target, message: txt };
|
|
268
|
+
if (quote) body.replyTo = { boot: quote.boot, id: quote.id };
|
|
269
|
+
r = await api("POST", "/ui/send", body);
|
|
267
270
|
} catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
|
|
268
271
|
s.sending[target] = false;
|
|
269
272
|
delete s.waking[target];
|
|
270
273
|
delete s.inFlightFiles[target];
|
|
271
|
-
if (r && !r.error) { changed(target); return; }
|
|
274
|
+
if (r && !r.error) { if (s.replies[target] === quote) delete s.replies[target]; changed(target); return; }
|
|
275
|
+
// #2272: the quoted message is gone: the reply goes, the text and files stay (sent again without it, or re-aimed).
|
|
276
|
+
if (r && r.reason === "reply_gone" && s.replies[target] === quote) delete s.replies[target];
|
|
272
277
|
if (files.length) s.pendingFiles[target] = R().checkFiles(files, s.pendingFiles[target] || []).kept;
|
|
273
278
|
if (txt) {
|
|
274
279
|
const shown = s.current === target;
|
|
@@ -331,6 +336,30 @@ export function createChatStore(deps) {
|
|
|
331
336
|
function discardFailed(target) { delete s.failedSends[target]; changed(target); }
|
|
332
337
|
/** Typing stays local, except a failed send's Retry button must follow edits too. */
|
|
333
338
|
function setDraft(target, value) { s.drafts[target] = value; if (s.sendFailures[target]) changed(target); }
|
|
339
|
+
/**
|
|
340
|
+
* #2272: reply to a message of this chat — what the bar above the composer shows (its sender and first words); only
|
|
341
|
+
* its row is sent. One at a time per chat: a new one replaces it.
|
|
342
|
+
*/
|
|
343
|
+
function startReply(target, m) {
|
|
344
|
+
if (!target || !m || typeof m.boot !== "string" || typeof m.id !== "number") return;
|
|
345
|
+
s.replies[target] = { boot: m.boot, id: m.id, sender: String(m.sender || ""), text: String(m.click?.label ?? m.text ?? "").replace(/\s+/g, " ").trim().slice(0, 160),
|
|
346
|
+
media: (m.attachments || []).length ? ((m.attachments || []).some(a => a.kind === "photo") ? "photo" : "document") : undefined };
|
|
347
|
+
changed(target);
|
|
348
|
+
}
|
|
349
|
+
function clearReply(target) { if (s.replies[target]) { delete s.replies[target]; changed(target); } }
|
|
350
|
+
/**
|
|
351
|
+
* #2272: the quoted message, drawn for a jump: in what this chat holds, or after reading the longest history the
|
|
352
|
+
* server keeps for it (the newest 500). false: no longer kept.
|
|
353
|
+
*/
|
|
354
|
+
async function findQuoted(name, key) {
|
|
355
|
+
const has = () => (s.msgs[name] || []).some(m => `${m.boot}-${m.id}` === key);
|
|
356
|
+
if (has()) return true;
|
|
357
|
+
try {
|
|
358
|
+
const r = await api("GET", `/ui/history?instance=${encodeURIComponent(name)}&limit=500`);
|
|
359
|
+
if (r && Array.isArray(r.messages)) { s.msgs[name] = R().mergeMessages(s.msgs[name], r.messages, 500); changed(name, "msgs"); }
|
|
360
|
+
} catch { /* the jump says it is not there */ }
|
|
361
|
+
return has();
|
|
362
|
+
}
|
|
334
363
|
|
|
335
364
|
// ── Stop the reply under way (the composer's Stop, or Esc). Set before the request goes, so a held Esc or a second
|
|
336
365
|
// click sends nothing more (#1317); "Stopping…" until the agent goes idle, or a minute passes.
|
|
@@ -425,6 +454,7 @@ export function createChatStore(deps) {
|
|
|
425
454
|
onPrompt, applyPrompts, resolvePrompt, answerPrompt, answerByNonce, holdPrompt, releasePrompt, promptsFor,
|
|
426
455
|
applyReplyButtons, clickReplyButton,
|
|
427
456
|
uploadFile, send, canRetrySend, retrySend, addFiles, removeFile, attachPastedText, fileBackAsText, isPasted, putBack, discardFailed, setDraft,
|
|
457
|
+
startReply, clearReply, findQuoted,
|
|
428
458
|
cancelReply, isUser, runCommand, dismissCommand, commandFree,
|
|
429
459
|
};
|
|
430
460
|
}
|
package/dist/ui/chat-strings.js
CHANGED
|
@@ -10,6 +10,9 @@ register("chat", {
|
|
|
10
10
|
sendFailure_instance_stopped: "This instance was stopped or removed. Start it on the host before retrying.",
|
|
11
11
|
sendFailure_delivery_unavailable: "The instance could not receive this message. Check its status on the host, then retry.",
|
|
12
12
|
sendFailure_delivery_changed: "The session or instance changed before this message was sent. Sign in again if needed, then retry.",
|
|
13
|
+
sendFailure_reply_gone: "The message you replied to is no longer in this chat. Your text is still here: send it without the reply, or reply to another message.",
|
|
14
|
+
reply: "Reply", replyingTo: "Replying to {0}", replyCancel: "Cancel the reply (Esc)", replyHidden: "an earlier message", replyPhoto: "[Photo]", replyFile: "[File]",
|
|
15
|
+
replyGone: "That message is no longer in this chat.", replyJump: "Show the message this replies to", copyText: "Copy text", copyLink: "Copy link", msgMenu: "Message",
|
|
13
16
|
|
|
14
17
|
effectTrialReason_checking: "Checking this session…",
|
|
15
18
|
effectTrialReason_localAdmin: "Sign in as an admin outside the public link to Try.",
|
|
@@ -35,7 +38,7 @@ register("chat", {
|
|
|
35
38
|
compactTimeout: "No sign that compaction finished within {0} minutes — check the agent's own chat.",
|
|
36
39
|
compactLost: "Can't follow this compaction: the command did not reach the agent, or the agent stopped or changed backend.",
|
|
37
40
|
compactNotCompacted: "kiro did not compact the conversation: it was too short, or the compaction was stopped.",
|
|
38
|
-
cmdChoose: "Choose", cmdCurrent: "current", chipCompact: "Compact", chipClear: "Clear…", chipContext: "Context {0}% used",
|
|
41
|
+
cmdChoose: "Choose", cmdCurrent: "current", cmdSignIn: "Sign in", cmdSignInTo: "Sign in to {0}", chipCompact: "Compact", chipClear: "Clear…", chipContext: "Context {0}% used",
|
|
39
42
|
chipModel: "Model: {0} — choose another", chipEffort: "Effort: {0} — choose another",
|
|
40
43
|
cmd_ctx: "Context use, model and effort", cmd_compact: "Compact the conversation", cmd_clear: "Clear the conversation (asks first)",
|
|
41
44
|
cmd_model: "Show or switch the model", cmd_backend: "Switch the CLI backend, then model and effort (restarts)", cmd_effort: "Show or set the reasoning effort", cmd_cancel: "Stop the current reply",
|
|
@@ -144,6 +147,9 @@ register("chat", {
|
|
|
144
147
|
sendFailure_instance_stopped: "這個實例已停止或移除。請先在主機啟動它,再重試。",
|
|
145
148
|
sendFailure_delivery_unavailable: "實例目前無法接收這則訊息。請在主機檢查它的狀態,再重試。",
|
|
146
149
|
sendFailure_delivery_changed: "訊息送出前,登入或實例狀態已改變。必要時請重新登入,再重試。",
|
|
150
|
+
sendFailure_reply_gone: "你回覆的訊息已不在這個聊天裡。內容還在:可以不帶回覆直接送出,或改回覆其他訊息。",
|
|
151
|
+
reply: "回覆", replyingTo: "回覆 {0}", replyCancel: "取消回覆(Esc)", replyHidden: "之前的一則訊息", replyPhoto: "[圖片]", replyFile: "[檔案]",
|
|
152
|
+
replyGone: "那則訊息已不在這個聊天裡。", replyJump: "顯示被回覆的訊息", copyText: "複製文字", copyLink: "複製連結", msgMenu: "訊息",
|
|
147
153
|
|
|
148
154
|
effectTrialReason_checking: "正在確認工作階段…",
|
|
149
155
|
effectTrialReason_localAdmin: "請在公開連結以外的儀表板以管理員登入後試播。",
|
|
@@ -169,7 +175,7 @@ register("chat", {
|
|
|
169
175
|
compactTimeout: "{0} 分鐘內沒有看到壓縮完成的紀錄,請到代理自己的對話確認。",
|
|
170
176
|
compactLost: "無法再追蹤這次壓縮:指令沒有送到代理,或代理已停止、換了後端。",
|
|
171
177
|
compactNotCompacted: "kiro 沒有壓縮對話:對話太短,或壓縮被中止了。",
|
|
172
|
-
cmdChoose: "選擇", cmdCurrent: "目前", chipCompact: "壓縮", chipClear: "清空…", chipContext: "已用 {0}% 的 context",
|
|
178
|
+
cmdChoose: "選擇", cmdCurrent: "目前", cmdSignIn: "登入", cmdSignInTo: "登入 {0}", chipCompact: "壓縮", chipClear: "清空…", chipContext: "已用 {0}% 的 context",
|
|
173
179
|
chipModel: "模型:{0}——選擇其他", chipEffort: "推理強度:{0}——選擇其他",
|
|
174
180
|
cmd_ctx: "context 用量、模型與推理強度", cmd_compact: "壓縮對話", cmd_clear: "清空對話(會先確認)",
|
|
175
181
|
cmd_model: "查看或切換模型", cmd_backend: "切換 CLI backend,再選模型和推理強度(會重新啟動)", cmd_effort: "查看或設定推理強度", cmd_cancel: "停止目前的回覆",
|
package/dist/ui/chat-thread.js
CHANGED
|
@@ -28,6 +28,7 @@ const P = () => globalThis.AgendPreview;
|
|
|
28
28
|
/** #1672: a status line longer than this (lines, or characters) is folded until it is opened. */
|
|
29
29
|
export const STATUS_FOLD_LINES = 6;
|
|
30
30
|
export const STATUS_FOLD_CHARS = 600;
|
|
31
|
+
const REPLY = '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></svg>';
|
|
31
32
|
const COPY = '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
32
33
|
|
|
33
34
|
/** Escapes & " ' < >: safe for text and inside a quoted attribute. */
|
|
@@ -141,7 +142,22 @@ export function createThread(list, scroller, opts) {
|
|
|
141
142
|
const buttons = x.role === "agent" ? replyButtonsHtml(x.buttons) : "";
|
|
142
143
|
// A person's click on a reply button shows as the button's label, never the line the agent was given (its value).
|
|
143
144
|
const clicked = x.role === "user" && x.click && typeof x.click.label === "string";
|
|
144
|
-
|
|
145
|
+
// #2272: reply to it — a hover strip at its top right (phones: swipe left or long-press; R on a focused message).
|
|
146
|
+
const key = msgKey(x);
|
|
147
|
+
const act = opts.canReply?.() === false ? "" : `<div data-trust="thread-control" class="msg-actions"><button type="button" class="chip-btn icon-only" data-act="reply" data-arg="${escAttr(key)}" title="${escAttr(tr("chat.reply"))}" aria-label="${escAttr(tr("chat.reply"))}">${REPLY}</button></div>`;
|
|
148
|
+
return `<div class="msg ${u ? "user" : "agent"}" data-key="${escAttr(key)}" tabindex="-1">${act}<div class="meta"><span class="sender">${escAttr(x.sender)}</span>${time}${ticks}</div>${quoteHtml(x.replyTo)}<div class="body">${clicked ? clickHtml(x.click) : `<div class="md">${renderedMarkdown(x.text, x.role === "agent", opts.emojis, opts.mentions)}</div>`}${R().attachmentsHtml(x.attachments, { htmlCards: x.role === "agent", goneTitle: tr("chat.attGone"), goneLabel: tr("chat.attUnavailable"), inlinePreviews: !PUBLIC_LINK() })}${buttons}</div>${tools}</div>`;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* #2272: what a reply quotes, above its body — the quoted sender and first words (or that it had files), as the server
|
|
152
|
+
* read them when it was sent; a button that shows that message. A guest that may not see it gets only "an earlier
|
|
153
|
+
* message". Text only: everything is escaped.
|
|
154
|
+
*/
|
|
155
|
+
function quoteHtml(q) {
|
|
156
|
+
if (!q || typeof q !== "object" || typeof q.boot !== "string" || typeof q.id !== "number") return "";
|
|
157
|
+
const words = q.hidden ? tr("chat.replyHidden")
|
|
158
|
+
: (typeof q.text === "string" && q.text) || (q.media === "photo" ? tr("chat.replyPhoto") : q.media === "document" ? tr("chat.replyFile") : "");
|
|
159
|
+
const who = !q.hidden && typeof q.sender === "string" && q.sender ? `<span class="quote-sender">${escAttr(q.sender)}</span> ` : "";
|
|
160
|
+
return `<button type="button" data-trust="thread-control" class="msg-quote" data-act="jumpQuote" data-arg="${escAttr(`${q.boot}-${q.id}`)}" title="${escAttr(tr("chat.replyJump"))}"><span class="quote-mark">${REPLY}</span> ${who}<span class="quote-text">${escAttr(words)}</span></button>`;
|
|
145
161
|
}
|
|
146
162
|
/**
|
|
147
163
|
* A button click as a chip: 🔘 and the label (escaped: the label is the agent's text). With `of` (live, while its
|
|
@@ -656,8 +672,20 @@ export function createThread(list, scroller, opts) {
|
|
|
656
672
|
}
|
|
657
673
|
armDay();
|
|
658
674
|
armPolls();
|
|
675
|
+
rove();
|
|
659
676
|
return added;
|
|
660
677
|
}
|
|
678
|
+
/**
|
|
679
|
+
* #2272 (review): one message in the thread is in the Tab order — the last one focused while it is still drawn, else
|
|
680
|
+
* the newest; ↑/↓ move between messages (chat-msg-actions.js). The rest are -1, so Tab passes the thread in one step.
|
|
681
|
+
*/
|
|
682
|
+
let roving = null;
|
|
683
|
+
function rove(key) {
|
|
684
|
+
if (key !== undefined) roving = key;
|
|
685
|
+
const at = (roving && nodes.has(roving) ? nodes.get(roving).node : null) || [...list.querySelectorAll(".msg[data-key]")].at(-1) || null;
|
|
686
|
+
for (const el of list.querySelectorAll('.msg[data-key][tabindex="0"]')) if (el !== at) el.setAttribute("tabindex", "-1");
|
|
687
|
+
if (at) at.setAttribute("tabindex", "0");
|
|
688
|
+
}
|
|
661
689
|
|
|
662
690
|
function onScroll() {
|
|
663
691
|
// #2158: the thread got shorter under a pinned view (text settling on a cold load, a card) and the browser clamped
|
|
@@ -742,7 +770,21 @@ export function createThread(list, scroller, opts) {
|
|
|
742
770
|
if (trialStatus(row) === "ready" && opts.canTryEffect?.(row) === true) opts.startEffectTrial?.(row);
|
|
743
771
|
else opts.fixEffectTrial?.(row, trialStatus(row));
|
|
744
772
|
} else if (act === "showButtons") { const x = msgs.find(y => y.buttons && y.buttons.id === el.dataset.arg); if (x) reveal(msgKey(x)); }
|
|
773
|
+
else if (act === "reply") { const x = msgs.find(y => msgKey(y) === el.dataset.arg); if (x) opts.startReply?.(x); }
|
|
774
|
+
else if (act === "jumpQuote") void jumpTo(el.dataset.arg);
|
|
745
775
|
}
|
|
776
|
+
/**
|
|
777
|
+
* #2272: show the message a reply quotes, flashed — drawn now, or after the chat reads back the longest history the
|
|
778
|
+
* server keeps; otherwise it says it is no longer there.
|
|
779
|
+
*/
|
|
780
|
+
async function jumpTo(key) {
|
|
781
|
+
if (typeof key !== "string" || !key) return;
|
|
782
|
+
if (reveal(key)) return;
|
|
783
|
+
const found = opts.findQuoted ? await opts.findQuoted(key) : false;
|
|
784
|
+
if (!found || !reveal(key)) opts.notice?.(tr("chat.replyGone"));
|
|
785
|
+
}
|
|
786
|
+
/** #2272: a drawn message's row (for the message menu and gestures), or undefined. */
|
|
787
|
+
function messageOf(key) { return msgs.find(y => msgKey(y) === key); }
|
|
746
788
|
|
|
747
789
|
/** The thread goes (the chat left, or another instance opened): every preview in it stops first. */
|
|
748
790
|
/**
|
|
@@ -785,5 +827,5 @@ export function createThread(list, scroller, opts) {
|
|
|
785
827
|
armPolls();
|
|
786
828
|
}
|
|
787
829
|
|
|
788
|
-
return { render, onScroll, onClick, jumpLatest, reveal, refreshCards, park, attach, dispose, nodes, get unseen() { return unseen; } };
|
|
830
|
+
return { render, onScroll, onClick, jumpLatest, reveal, jumpTo, messageOf, rove, refreshCards, park, attach, dispose, nodes, get unseen() { return unseen; } };
|
|
789
831
|
}
|
package/dist/ui/panel-chat.js
CHANGED
|
@@ -66,6 +66,7 @@ import "./chat-strings.js";
|
|
|
66
66
|
import { confirmedWrite, selfConfirmed } from "./settings-confirm.js";
|
|
67
67
|
import { createChatStore } from "./chat-store.js";
|
|
68
68
|
import { createThread, msgKey } from "./chat-thread.js";
|
|
69
|
+
import { installMessageActions } from "./chat-msg-actions.js";
|
|
69
70
|
import { createEmojiIndex } from "./chat-emojis.js";
|
|
70
71
|
import { pollPublicPreview } from "./preview-status.js";
|
|
71
72
|
import { createMentionIndex } from "./chat-mentions.js";
|
|
@@ -419,6 +420,10 @@ function threadEntry(name, doc) {
|
|
|
419
420
|
canTryEffect: x => getAppEffects()?.canTry(x) === true,
|
|
420
421
|
startEffectTrial: x => { const seq = navStore.get().seq; void getAppEffects()?.startTrial(x).then(ok => { if (!ok && e.sink && navStore.get().seq === seq && document.hasFocus?.() === true && document.visibilityState === "visible") toast(t("chat.effectTrialUnavailable")); }); },
|
|
421
422
|
clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
|
|
423
|
+
// #2272: reply to a message (the bar above the composer), and show the one a reply quotes.
|
|
424
|
+
startReply: (x) => { store.startReply(name, x); doc.getElementById("msgIn")?.focus(); },
|
|
425
|
+
findQuoted: (key) => store.findQuoted(name, key),
|
|
426
|
+
notice: (text) => toast(text, false),
|
|
422
427
|
replyButtonBusy: (id) => store.state.rbBusy.has(id),
|
|
423
428
|
pollMine: (id) => store.state.pollMine[id], // #1823: this device's own vote
|
|
424
429
|
serverNow: () => Date.now() + store.state.clockSkew, // #1823: the deadline is the server's
|
|
@@ -484,8 +489,18 @@ function Thread({ name, th }) {
|
|
|
484
489
|
const onScroll = () => { store.state.scrollMemo[name] = thread.onScroll(); };
|
|
485
490
|
sc.addEventListener("scroll", onScroll, { passive: true });
|
|
486
491
|
ls.addEventListener("click", thread.onClick);
|
|
492
|
+
// #2272: the message menu, swipe left / long-press on a phone, R on a focused message.
|
|
493
|
+
const offActions = installMessageActions(ls, {
|
|
494
|
+
t, messageOf: thread.messageOf, rove: thread.rove, reply: (x) => { store.startReply(name, x); document.getElementById("msgIn")?.focus(); },
|
|
495
|
+
copyText, notice: (text, ok = true) => toast(text, ok),
|
|
496
|
+
linkOf: (x) => `${location.origin}/ui/chat/${encodeURIComponent(name)}#m=${msgKey(x)}`,
|
|
497
|
+
});
|
|
498
|
+
// A copied message link (#m=<boot>-<id>): show that message, flashed.
|
|
499
|
+
const linked = /^#m=([0-9a-f]{1,32}-\d{1,15})$/.exec(location.hash);
|
|
500
|
+
if (linked) void thread.jumpTo(linked[1]);
|
|
487
501
|
return () => {
|
|
488
502
|
unfollow();
|
|
503
|
+
offActions();
|
|
489
504
|
sc.removeEventListener("scroll", onScroll);
|
|
490
505
|
ls.removeEventListener("click", thread.onClick);
|
|
491
506
|
e.sink = null;
|
|
@@ -525,6 +540,13 @@ function Dock({ name, inst, lease, exec, awaiting, workingLabel }) {
|
|
|
525
540
|
</div></div>`;
|
|
526
541
|
}
|
|
527
542
|
|
|
543
|
+
/** #2233: the public link's page — the dashboard's Sign in is refused there, so it is never offered (#2262 review). */
|
|
544
|
+
const publicLinkPage = () => typeof document !== "undefined" && !!document.body && document.body.dataset.publicLink === "1";
|
|
545
|
+
/** #2233: a backend's "not signed in" choice offers Sign in (the module behind the session gate, loaded when asked). */
|
|
546
|
+
function openBackendSignIn(backend) {
|
|
547
|
+
import("./backend-signin.js").then((m) => m.openSignIn({ backend }), () => toast(t("app.loadFailed"), false));
|
|
548
|
+
}
|
|
549
|
+
|
|
528
550
|
/** #1269: what the last chat command answered — its text, why it was refused, or a list to choose from. */
|
|
529
551
|
function CommandCard({ name, lease }) {
|
|
530
552
|
const c = store.state.commands[name];
|
|
@@ -543,6 +565,8 @@ function CommandCard({ name, lease }) {
|
|
|
543
565
|
${c.choices ? html`<div class="cmd-choices" role="group" aria-label=${t("chat.cmdChoose")}>${c.choices.options.map((o) => html`<button key=${o.id} type="button"
|
|
544
566
|
class=${`btn btn-sm${o.id === c.choices.current ? " current" : ""}`} aria-current=${o.id === c.choices.current ? "true" : undefined}
|
|
545
567
|
disabled=${!!o.disabled} title=${o.disabled || undefined} onClick=${() => { if (!o.disabled) run(o.id); }}>${o.label}</button>`)}</div>` : null}
|
|
568
|
+
${c.choices && c.choices.options.some((o) => o.signIn) && !publicLinkPage() ? html`<div class="cmd-choices" role="group" aria-label=${t("chat.cmdSignIn")}>${c.choices.options.filter((o) => o.signIn).map((o) => html`<button
|
|
569
|
+
key=${`signin:${o.signIn}`} type="button" class="btn btn-sm btn-primary" onClick=${() => openBackendSignIn(o.signIn)}>${t("chat.cmdSignInTo", o.signIn)}</button>`)}</div>` : null}
|
|
546
570
|
</div>`;
|
|
547
571
|
}
|
|
548
572
|
|
|
@@ -589,6 +613,12 @@ function WorkBar({ name, lease, exec, awaiting, workingLabel }) {
|
|
|
589
613
|
: html`<span class="elapsed" title=${t("chat.elapsedTitle")} aria-hidden="true">${since == null ? "" : R().formatElapsed(performance.now() - since)}</span>`}</div>`;
|
|
590
614
|
}
|
|
591
615
|
|
|
616
|
+
/** #2272: the reply bar's quote — the first ~80 characters, or what it had instead of text. */
|
|
617
|
+
function replySnippet(r) {
|
|
618
|
+
const text = r.text ? (r.text.length > 80 ? `${r.text.slice(0, 79).trimEnd()}…` : r.text) : "";
|
|
619
|
+
return text || (r.media === "photo" ? t("chat.replyPhoto") : r.media === "document" ? t("chat.replyFile") : "");
|
|
620
|
+
}
|
|
621
|
+
|
|
592
622
|
function Composer({ name, lease, busy }) {
|
|
593
623
|
const s = store.state;
|
|
594
624
|
const input = useRef(null), fileIn = useRef(null);
|
|
@@ -597,6 +627,7 @@ function Composer({ name, lease, busy }) {
|
|
|
597
627
|
const [closedFor, setClosedFor] = useState(null); // the draft the palette was closed on (Esc); it opens again on change
|
|
598
628
|
const value = s.drafts[name] || "";
|
|
599
629
|
const files = s.pendingFiles[name] || [];
|
|
630
|
+
const reply = s.replies[name]; // #2272: the message this one will reply to
|
|
600
631
|
const content = !!value.trim() || files.length > 0;
|
|
601
632
|
// A file waiting: what goes is a message with its file, never a command (no palette, no command line). A guest never
|
|
602
633
|
// runs one (#1920: the server refuses its slash text), so it has no palette either.
|
|
@@ -637,6 +668,7 @@ function Composer({ name, lease, busy }) {
|
|
|
637
668
|
}
|
|
638
669
|
if (pick && e.key === "Tab" && !e.shiftKey) { e.preventDefault(); complete(pick); return; }
|
|
639
670
|
if (palette && e.key === "Escape" && !e.isComposing) { e.preventDefault(); setClosedFor(value); return; }
|
|
671
|
+
if (reply && e.key === "Escape" && !e.isComposing) { e.preventDefault(); store.clearReply(name); return; } // #2272
|
|
640
672
|
if (R().composerKey(e) === "send") { e.preventDefault(); submit(); }
|
|
641
673
|
};
|
|
642
674
|
const onInput = (e) => { setValue(e.target.value); };
|
|
@@ -655,6 +687,11 @@ function Composer({ name, lease, busy }) {
|
|
|
655
687
|
<span class="cmd-name mono">/${c.name}${c.arg ? html` <span class="cmd-arg">${c.arg}</span>` : null}</span><span class="cmd-desc">${t(`chat.cmd_${c.name}`)}</span></li>`)
|
|
656
688
|
: html`<li class="cmd-none" role="option" aria-selected="false">${t("chat.cmdNotCommand")}</li>`}
|
|
657
689
|
</ul>` : null}
|
|
690
|
+
${reply ? html`<div class="reply-bar" data-trust="composer" role="status">
|
|
691
|
+
<span class="reply-mark" aria-hidden="true"><svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></svg></span><span class="reply-label">${t("chat.replyingTo", reply.sender)}</span>
|
|
692
|
+
<span class="reply-text">${replySnippet(reply)}</span>
|
|
693
|
+
<button type="button" class="icon-btn reply-cancel" title=${t("chat.replyCancel")} aria-label=${t("chat.replyCancel")} onClick=${() => { store.clearReply(name); input.current?.focus(); }}><${Icon} name="close" size=${14} /></button>
|
|
694
|
+
</div>` : null}
|
|
658
695
|
${files.length ? html`<div class="pending-files">${files.map((f, i) => html`<${FileChip} key=${`${f.name}-${i}`} f=${f} i=${i} name=${name} />`)}</div>` : null}
|
|
659
696
|
<div class="composer">
|
|
660
697
|
<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>
|
package/dist/ui/panel-needs.js
CHANGED
|
@@ -20,11 +20,12 @@ import { Empty } from "/assets/ui-states.js";
|
|
|
20
20
|
import { Icon } from "/assets/ui-icons.js";
|
|
21
21
|
import { toast } from "/assets/ui-toast.js";
|
|
22
22
|
import { notifyStore, notifySupport, reasonText, refreshNotify, setNotify } from "/assets/app-needs.js";
|
|
23
|
+
import { openSignIn, signInOffered } from "./backend-signin.js";
|
|
23
24
|
|
|
24
25
|
register("needs", {
|
|
25
26
|
en: {
|
|
26
27
|
title: "Needs you", empty: "Nothing needs you right now", emptyHint: "When an agent waits for you, it shows here — and in your chat app's General.", emptyHintWeb: "When an agent waits for you, it shows here.",
|
|
27
|
-
open: "Open chat", acknowledge: "Acknowledge", acknowledging: "Acknowledging…", answering: "Answering…",
|
|
28
|
+
open: "Open chat", signIn: "Sign in", acknowledge: "Acknowledge", acknowledging: "Acknowledging…", answering: "Answering…",
|
|
28
29
|
ageNow: "just now", ageMin: "{0} min", ageHour: "{0} h", ageDay: "{0} d",
|
|
29
30
|
notifyTitle: "Notify me on this device", notifyOn: "Notifications are on for this browser while a tab is open.",
|
|
30
31
|
notifyOff: "Get a desktop notification when something new needs you while this tab is in the background.",
|
|
@@ -36,7 +37,7 @@ register("needs", {
|
|
|
36
37
|
},
|
|
37
38
|
"zh-TW": {
|
|
38
39
|
title: "等你處理", empty: "目前沒有需要你處理的事", emptyHint: "agent 在等你時,會出現在這裡——也會出現在聊天 app 的 General。", emptyHintWeb: "agent 在等你時,會出現在這裡。",
|
|
39
|
-
open: "開啟聊天", acknowledge: "確認", acknowledging: "確認中…", answering: "回覆中…",
|
|
40
|
+
open: "開啟聊天", signIn: "登入", acknowledge: "確認", acknowledging: "確認中…", answering: "回覆中…",
|
|
40
41
|
ageNow: "剛剛", ageMin: "{0} 分鐘", ageHour: "{0} 小時", ageDay: "{0} 天",
|
|
41
42
|
notifyTitle: "在這台裝置通知我", notifyOn: "這個瀏覽器的通知已開啟(需要開著分頁)。",
|
|
42
43
|
notifyOff: "分頁在背景時,有新的事等你處理就跳出桌面通知。",
|
|
@@ -185,6 +186,7 @@ function NeedsItem({ item, nowMs, owner }) {
|
|
|
185
186
|
disabled=${promptBusy} onClick=${() => answer(a.id)}>${(p ? p.busy : fallback === "busy") ? tn("answering") : a.label}</button>`) : null}
|
|
186
187
|
${item.type === "delivery" ? html`<button type="button" class="btn btn-sm" disabled=${!!ack} onClick=${acknowledge}>
|
|
187
188
|
<${Icon} name="check" size=${14} />${ack === "busy" ? tn("acknowledging") : tn("acknowledge")}</button>` : null}
|
|
189
|
+
${item.reason === "auth_paused" && item.signIn && signInOffered() ? html`<button type="button" class="btn btn-sm btn-primary" onClick=${() => openSignIn(item.signIn)}>${tn("signIn")}</button>` : null}
|
|
188
190
|
<a class="btn btn-sm btn-ghost" href=${chatPath(item.instance)}>${tn("open")}</a>
|
|
189
191
|
</div>
|
|
190
192
|
</div>
|
|
@@ -45,6 +45,7 @@ import "./chat-strings.js";
|
|
|
45
45
|
import { AppearanceSection } from "./settings-appearance.js";
|
|
46
46
|
import { GuestsCard } from "./settings-guests.js";
|
|
47
47
|
import { WebAuthorityCard } from "./settings-web-authority.js";
|
|
48
|
+
import { SignInList } from "./backend-signin.js";
|
|
48
49
|
|
|
49
50
|
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
50
51
|
/** What a refused change says to a person: the server's code in words (the code itself only for one not listed). */
|
|
@@ -475,8 +476,10 @@ function Agents({ ctx, search, openDialog }) {
|
|
|
475
476
|
};
|
|
476
477
|
const head = html`<div class="list-head"><span>${Object.keys(insts).length === 1 ? tn("countAgent") : tn("countAgents", Object.keys(insts).length)}</span>
|
|
477
478
|
<button type="button" class="btn btn-primary" onClick=${() => openDialog({ kind: "create" })}><${Icon} name="plus" size=${16} />${tn("newAgent")}</button></div>`;
|
|
478
|
-
|
|
479
|
-
|
|
479
|
+
// #2233: after the agents, each login on this machine with Sign in (backend-signin.js).
|
|
480
|
+
const signIns = html`<${SignInList} />`;
|
|
481
|
+
if (!Object.keys(insts).length) return html`${head}<${Empty} icon="bot" title=${tn("noAgents")} />${signIns}`;
|
|
482
|
+
if (!groups.length) return html`${head}<${Empty} icon="search" title=${tn("noAgentMatch", search.trim())} />${signIns}`;
|
|
480
483
|
return html`${head}<div class="s-list">${groups.map(({ group, names }) => {
|
|
481
484
|
const folded = collapsed.has(group) && !search.trim(); // searching opens every group
|
|
482
485
|
return html`<section key=${group} class="s-group">
|
|
@@ -508,7 +511,7 @@ function Agents({ ctx, search, openDialog }) {
|
|
|
508
511
|
</div>`;
|
|
509
512
|
})}
|
|
510
513
|
</section>`;
|
|
511
|
-
})}</div
|
|
514
|
+
})}</div>${signIns}`;
|
|
512
515
|
}
|
|
513
516
|
|
|
514
517
|
function ConnectionRetry({ id, ctx }) {
|
|
@@ -20,7 +20,7 @@ import { OwnerCssKeepBar } from "./owner-css.js";
|
|
|
20
20
|
import { AgentWidget, fleetBinds, sidebarWidget, sidebarWidgetStore } from "./agent-widget.js";
|
|
21
21
|
|
|
22
22
|
/** Shell state shared with panels: the phone drawer, the collapsed sidebar, extensions a panel module adds. */
|
|
23
|
-
export const shellStore = createStore({ drawer: false, collapsed: readCollapsed(), keyboard: false, dialog: null, side: null });
|
|
23
|
+
export const shellStore = createStore({ drawer: false, collapsed: readCollapsed(), keyboard: false, dialog: null, side: null, banner: null });
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* The mounted panel's own sidebar section, in place of the instance list (View's roster: its groups, filter and order,
|
|
@@ -617,6 +617,7 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
|
|
|
617
617
|
<main id="main" class="main" tabindex="-1" inert=${shell.drawer && narrow() ? true : undefined}>
|
|
618
618
|
<${ConnectionLine} />
|
|
619
619
|
${viewOnly || guest ? null : html`<${OperationLine} route=${nav.route} />`}
|
|
620
|
+
${viewOnly || guest || !shell.banner ? null : html`<${shell.banner} />`}
|
|
620
621
|
<${Outlet} route=${nav.route} seq=${nav.seq} panels=${panels} />
|
|
621
622
|
</main>
|
|
622
623
|
${guest ? null : html`<${UsageRail} route=${nav.route} />`}
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
// #1218: steps_tick — an instance's transcript moved while a Log watches it (SSE only: a poll page asks the Log itself).
|
|
33
33
|
// #1918: traffic — a message between instances moved; delegation — a task opened, or its report closed it (SSE only: the
|
|
34
34
|
// org canvas reads /ui/traffic when it has no stream).
|
|
35
|
-
|
|
35
|
+
// #2233: backend_login — a dashboard sign-in started, moved or ended (no details: the page reads its own view).
|
|
36
|
+
const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "steps_tick", "clock", "traffic", "delegation", "backend_login"];
|
|
36
37
|
const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
|
|
37
38
|
const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
|
|
38
39
|
const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed
|