@songsid/agend 2.3.4-beta.8 → 2.3.4
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/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/agent-cli-instructions.md +1 -1
- package/dist/agent-credentials.d.ts +24 -0
- package/dist/agent-credentials.js +66 -0
- package/dist/agent-credentials.js.map +1 -0
- package/dist/agent-endpoint.d.ts +3 -1
- package/dist/agent-endpoint.js +13 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/auth-api.js +5 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +28 -4
- package/dist/authority/session-authorizations.js +63 -17
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/backend/antigravity.js +3 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/codex-footer-hints.d.ts +4 -0
- package/dist/backend/codex-footer-hints.js +27 -0
- package/dist/backend/codex-footer-hints.js.map +1 -0
- package/dist/backend/codex-metadata.d.ts +14 -0
- package/dist/backend/codex-metadata.js +45 -0
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex-speed.d.ts +14 -0
- package/dist/backend/codex-speed.js +35 -0
- package/dist/backend/codex-speed.js.map +1 -0
- package/dist/backend/codex.js +17 -5
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro.js +11 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +5 -3
- package/dist/backend/muse.js.map +1 -1
- package/dist/cache-ledger.d.ts +95 -0
- package/dist/cache-ledger.js +276 -4
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-service.d.ts +5 -0
- package/dist/cache-service.js +16 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.js +11 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +51 -0
- package/dist/channel/adapters/telegram.js +235 -17
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +2 -0
- package/dist/config-validator.js +5 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/cross-instance-notice.d.ts +10 -0
- package/dist/cross-instance-notice.js +67 -5
- package/dist/cross-instance-notice.js.map +1 -1
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +11 -4
- package/dist/daemon.js.map +1 -1
- package/dist/export-import.d.ts +7 -0
- package/dist/export-import.js +44 -10
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -0
- package/dist/fleet-manager.d.ts +93 -4
- package/dist/fleet-manager.js +451 -39
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/concepts/SKILL.md +3 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +7 -2
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/instance-config-impact.js +2 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +6 -2
- package/dist/instance-lifecycle.js +11 -6
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instance-topic-delete.d.ts +77 -0
- package/dist/instance-topic-delete.js +123 -0
- package/dist/instance-topic-delete.js.map +1 -0
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.js +1 -1
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +33 -1
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +28 -0
- package/dist/log-service.js +29 -0
- package/dist/log-service.js.map +1 -1
- package/dist/log-steps.d.ts +4 -3
- package/dist/log-steps.js +32 -7
- package/dist/log-steps.js.map +1 -1
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +43 -1
- package/dist/logger.js.map +1 -1
- package/dist/missed-offline.d.ts +25 -0
- package/dist/missed-offline.js +79 -0
- package/dist/missed-offline.js.map +1 -0
- package/dist/needs-you-hub.d.ts +6 -1
- package/dist/needs-you-hub.js +30 -7
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +16 -2
- package/dist/needs-you.js +9 -1
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.js +3 -3
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.js +5 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/scheduler/db.d.ts +2 -1
- package/dist/scheduler/db.js +23 -9
- package/dist/scheduler/db.js.map +1 -1
- package/dist/settings-api.d.ts +13 -0
- package/dist/settings-api.js +22 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +16 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-instance-patch.js +27 -0
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/tool-progress.d.ts +2 -0
- package/dist/tool-progress.js +19 -3
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.js +4 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/chat-md-strings.js +9 -0
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/chat-thread.js +53 -3
- package/dist/ui/fleet-usage.js +90 -27
- package/dist/ui/panel-chat.js +5 -2
- package/dist/ui/panel-needs.js +13 -5
- package/dist/ui/preview-panel.js +78 -2
- package/dist/ui/settings-codex-copy.js +19 -0
- package/dist/ui/settings-dialogs.js +8 -0
- package/dist/ui/settings-model.js +16 -2
- package/dist/ui/settings-strings.js +2 -2
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app.css +19 -0
- package/dist/ui/shared/panel-view.js +77 -4
- package/dist/ui/shared/usage-dialog.js +18 -1
- package/dist/ui/shared/view-history.css +17 -0
- package/dist/ui/shared/view-history.js +438 -0
- package/dist/ui/shared/view-strings.js +16 -0
- package/dist/usage/capacity.d.ts +69 -0
- package/dist/usage/capacity.js +119 -0
- package/dist/usage/capacity.js.map +1 -0
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +3 -0
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/profile-timeline.d.ts +16 -0
- package/dist/usage/profile-timeline.js +58 -0
- package/dist/usage/profile-timeline.js.map +1 -0
- package/dist/usage/quota-history.d.ts +43 -0
- package/dist/usage/quota-history.js +168 -0
- package/dist/usage/quota-history.js.map +1 -0
- package/dist/usage/usage-api.d.ts +31 -0
- package/dist/usage/usage-api.js +80 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/usage-report.d.ts +25 -1
- package/dist/usage-report.js +55 -4
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.d.ts +6 -1
- package/dist/usage-rollup.js +16 -4
- package/dist/usage-rollup.js.map +1 -1
- package/dist/view-api.d.ts +16 -0
- package/dist/view-api.js +100 -16
- package/dist/view-api.js.map +1 -1
- package/dist/view-history.d.ts +84 -0
- package/dist/view-history.js +116 -0
- package/dist/view-history.js.map +1 -0
- package/dist/web-api.d.ts +6 -0
- package/dist/web-api.js +8 -2
- package/dist/web-api.js.map +1 -1
- package/dist/web-session-authorization.d.ts +13 -2
- package/dist/web-session-authorization.js +10 -3
- package/dist/web-session-authorization.js.map +1 -1
- package/launcher/private-home.cjs +1 -1
- package/package.json +1 -1
package/dist/ui/fleet-usage.js
CHANGED
|
@@ -14,11 +14,20 @@ register("fusage", {
|
|
|
14
14
|
en: {
|
|
15
15
|
range: "Range", today: "Today", r7d: "7 days", month: "This month", all: "All", custom: "Custom", from: "From", to: "To", apply: "Apply",
|
|
16
16
|
group: "Group by", g_instance: "Agent", g_backend: "Backend", g_model: "Model", g_profile: "Credential profile", g_team: "Team (as configured now)",
|
|
17
|
-
csv: "Download CSV", basis: "API-equivalent at list price (
|
|
17
|
+
csv: "Download CSV", basis: "API-equivalent at list price ({0}) — not money spent.", basisMore: "Subscriptions are not billed per token.",
|
|
18
18
|
amount: "API-equivalent", input: "Input", output: "Output", reasoning: "Reasoning", read: "Cache read", write: "Cache write", total: "Total tokens", requests: "Requests", messages: "Messages",
|
|
19
19
|
notPriced: "not priced", partial: "partial", noTeam: "(no team)", unknown: "unknown",
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// #3066 §3: what a row's numbers are, said once each as a small mark with its tooltip (never a pill per row).
|
|
21
|
+
t_not_priced: "Not priced: {0}", t_partial: "Part of this row's models have no price", t_write_unreported: "Codex/Muse don't report cache writes",
|
|
22
|
+
t_cost_reported: "Amount as the CLI reported it", t_estimated: "Estimated from text size (Kiro records no token counts)", t_reasoning_none: "Not reported by this CLI",
|
|
23
|
+
sourceNote: "Some hours were read before AgEnD recorded their backend and profile.", profileUnrecorded: "profile not recorded",
|
|
24
|
+
l_partial: "◐ partly priced", l_cost_reported: "† amount from the CLI", l_estimated: "≈ estimated",
|
|
25
|
+
// #3066 §2: output speed (Claude after the first token, Codex over the response window).
|
|
26
|
+
speed: "Speed", speedUnit: "{0} tok/s", speedCoverage: "{0} of {1} requests timed", speedSpread: "p10–p90: {0}–{1} tok/s",
|
|
27
|
+
speedNone: "no timing in its transcripts", speedByModel: "Speed by model",
|
|
28
|
+
speedBasis: "Output tokens ÷ the model's own response time (tool time excluded); method after tokdash.",
|
|
29
|
+
|
|
30
|
+
f_includes_estimates: "includes estimates (Kiro)", estimatedPart: "of which estimated: {0}",
|
|
22
31
|
scanning: "Scanning — totals may still grow.", updated: "Updated {0} · read every 10 min", since: "Collected since {0}", notYet: "Not collected yet",
|
|
23
32
|
refresh: "Refresh",
|
|
24
33
|
notMeasured: "Not measured", nm_credit_billed: "Kiro is credit-billed and records no tokens", nm_unsupported: "AgEnD can't read this CLI's usage yet", nm_no_data: "nothing read yet",
|
|
@@ -42,11 +51,18 @@ register("fusage", {
|
|
|
42
51
|
"zh-TW": {
|
|
43
52
|
range: "期間", today: "今天", r7d: "7 天", month: "本月", all: "全部", custom: "自訂", from: "從", to: "到", apply: "套用",
|
|
44
53
|
group: "分組", g_instance: "Agent", g_backend: "Backend", g_model: "模型", g_profile: "憑證 profile", g_team: "Team(依目前設定)",
|
|
45
|
-
csv: "下載 CSV", basis: "依牌價換算的 API
|
|
54
|
+
csv: "下載 CSV", basis: "依牌價換算的 API 等值({0})——不是實際花費。", basisMore: "訂閱方案不按 token 計費。",
|
|
46
55
|
amount: "API 等值", input: "輸入", output: "輸出", reasoning: "推理", read: "快取讀取", write: "快取寫入", total: "token 總計", requests: "請求數", messages: "訊息數",
|
|
47
56
|
notPriced: "未定價", partial: "部分", noTeam: "(不屬於任何 team)", unknown: "未知",
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
t_not_priced: "未定價:{0}", t_partial: "這一列有部分模型沒有價格", t_write_unreported: "Codex/Muse 不回報快取寫入",
|
|
58
|
+
t_cost_reported: "金額為 CLI 自行回報", t_estimated: "依文字長度估算(Kiro 不記錄 token 數)", t_reasoning_none: "這個 CLI 不回報",
|
|
59
|
+
sourceNote: "部分時段在 AgEnD 記錄 backend 與 profile 之前就已讀取。", profileUnrecorded: "未記錄 profile",
|
|
60
|
+
l_partial: "◐ 部分定價", l_cost_reported: "† 金額由 CLI 回報", l_estimated: "≈ 估算值",
|
|
61
|
+
speed: "速度", speedUnit: "{0} token/秒", speedCoverage: "{1} 個請求中有 {0} 個可計時", speedSpread: "p10–p90:{0}–{1} token/秒",
|
|
62
|
+
speedNone: "transcript 沒有計時資訊", speedByModel: "各模型速度",
|
|
63
|
+
speedBasis: "輸出 token ÷ 模型本身的回應時間(不含工具執行時間);方法參考 tokdash。",
|
|
64
|
+
|
|
65
|
+
f_includes_estimates: "含估算值(Kiro)", estimatedPart: "其中估算:{0}",
|
|
50
66
|
scanning: "掃描中——總數可能還會增加。", updated: "更新於 {0} · 每 10 分鐘讀取一次", since: "自 {0} 起收集", notYet: "尚未收集",
|
|
51
67
|
refresh: "重新整理",
|
|
52
68
|
notMeasured: "未量測", nm_credit_billed: "Kiro 以 credit 計費,不記錄 token", nm_unsupported: "AgEnD 目前還讀不到這個 CLI 的用量", nm_no_data: "尚未讀到任何資料",
|
|
@@ -95,10 +111,6 @@ export function keyText(group, key) {
|
|
|
95
111
|
return key;
|
|
96
112
|
}
|
|
97
113
|
const writes = (r) => (r.cacheWrite5m || 0) + (r.cacheWrite1h || 0) + (r.cacheWrite || 0);
|
|
98
|
-
function flagText(f, r) {
|
|
99
|
-
if (f === "not_priced") return tu("f_not_priced", (r.unpriced || []).join(", "));
|
|
100
|
-
return ["cache_write_not_reported", "source_unknown", "estimated", "includes_estimates", "cost_reported"].includes(f) ? tu(`f_${f}`) : f;
|
|
101
|
-
}
|
|
102
114
|
|
|
103
115
|
// ── CSV (design §4): the daily rows of this same answer; messages on their own rows (they are per agent, not per model).
|
|
104
116
|
const CSV_COLUMNS = ["date", "zone", "instance", "backend", "model", "tier", "profile", "input", "output", "reasoning", "cache_read", "cache_write_5m", "cache_write_1h", "cache_write", "requests", "messages", "amount", "basis", "flags", "estimated"];
|
|
@@ -318,25 +330,73 @@ function Heatmap({ report, dates }) {
|
|
|
318
330
|
</section>`;
|
|
319
331
|
}
|
|
320
332
|
|
|
321
|
-
|
|
333
|
+
/**
|
|
334
|
+
* A mark with what it means: shown as its sign, said (title, and aria-label for readers) as words. role="img" makes it
|
|
335
|
+
* nameable (#3067 review, Prism P3: aria-label is not allowed on a generic span).
|
|
336
|
+
*/
|
|
337
|
+
const mark = (sign, words, cls = "us-mark") => html`<span class=${cls} role="img" title=${words} aria-label=${words}>${sign}</span>`;
|
|
338
|
+
/** "—" that says why there is no number (nameable the same way). */
|
|
339
|
+
const dash = (words) => html`<span class="us-none" role="img" title=${words} aria-label=${words}>—</span>`;
|
|
340
|
+
/** The marks a row's numbers carry (#3066 §3): ◐ partly priced, † the CLI's own amount, ≈ estimated. */
|
|
341
|
+
export function rowMarks(r) {
|
|
342
|
+
const flags = r.flags || [];
|
|
343
|
+
return { partial: r.priced === "partial", cost: flags.includes("cost_reported"), est: flags.includes("estimated") || r.estimated === true,
|
|
344
|
+
writeUnreported: flags.includes("cache_write_not_reported") && writes(r) === 0, unpriced: r.amount === null || r.amount === undefined };
|
|
345
|
+
}
|
|
346
|
+
/** A profile as shown: Codex's older hours were never tagged with one. */
|
|
347
|
+
const profileText = (p) => (p === "unrecorded" ? tu("profileUnrecorded") : p);
|
|
348
|
+
|
|
349
|
+
function Row({ r, group, max, measure, cols }) {
|
|
322
350
|
const share = max > 0 ? Math.max(0.01, measure(r) / max) : 0;
|
|
323
|
-
const meta = [r.backends.join(", "), r.models.join(", "), r.profiles.join(", ")].filter(Boolean).join(" · ");
|
|
351
|
+
const meta = [r.backends.join(", "), r.models.join(", "), r.profiles.map(profileText).join(", ")].filter(Boolean).join(" · ");
|
|
352
|
+
const m = rowMarks(r);
|
|
353
|
+
// Estimated rows (Kiro): every number is an estimate — "≈" before each, the reason in its tooltip.
|
|
354
|
+
const num = (text) => (m.est ? html`${mark("≈", tu("t_estimated"), "us-mark us-approx")}${text}` : text);
|
|
355
|
+
const key = group === "profile" ? profileText(keyText(group, r.key)) : keyText(group, r.key);
|
|
324
356
|
return html`<tr class="us-row">
|
|
325
|
-
<th scope="row" class="us-name"><span class="mono">${
|
|
357
|
+
<th scope="row" class="us-name"><span class="mono">${key}</span>
|
|
326
358
|
<svg class="us-bar" aria-hidden="true"><rect class="us-bar-bg" width="100%" height="6" rx="3" /><rect class="us-bar-fg" width=${`${Math.round(share * 100)}%`} height="6" rx="3" /></svg>
|
|
327
|
-
${group !== "instance" || meta ? html`<span class="us-meta">${meta}</span>` : null}
|
|
328
|
-
|
|
329
|
-
<td data-label=${tu("
|
|
330
|
-
<td data-label=${tu("
|
|
331
|
-
|
|
332
|
-
<td data-label=${tu("
|
|
333
|
-
<td data-label=${tu("
|
|
334
|
-
<td data-label=${tu("
|
|
335
|
-
<td data-label=${tu("total")} class="num">${tokens(r.total)}</td>
|
|
359
|
+
${group !== "instance" || meta ? html`<span class="us-meta">${meta}</span>` : null}</th>
|
|
360
|
+
<td data-label=${tu("amount")} class=${`num${m.unpriced ? " muted" : ""}`}>${m.unpriced ? dash(tu("t_not_priced", (r.unpriced || []).join(", "))) : html`${num(money(r.amount))}${m.partial ? mark("◐", tu("t_partial")) : null}${m.cost ? mark("†", tu("t_cost_reported")) : null}`}</td>
|
|
361
|
+
<td data-label=${tu("input")} class="num">${num(tokens(r.input))}</td>
|
|
362
|
+
<td data-label=${tu("output")} class="num">${num(tokens(r.output))}</td>
|
|
363
|
+
${cols.reasoning ? html`<td data-label=${tu("reasoning")} class="num">${r.reasoningReported === false ? dash(tu("t_reasoning_none")) : num(tokens(r.reasoning || 0))}</td>` : null}
|
|
364
|
+
<td data-label=${tu("read")} class="num">${num(tokens(r.cacheRead))}</td>
|
|
365
|
+
<td data-label=${tu("write")} class="num">${m.writeUnreported ? dash(tu("t_write_unreported")) : num(tokens(writes(r)))}</td>
|
|
366
|
+
<td data-label=${tu("total")} class="num">${num(tokens(r.total))}</td>
|
|
336
367
|
<td data-label=${tu("requests")} class="num">${n(r.requests)}</td>
|
|
337
|
-
|
|
368
|
+
${cols.messages ? html`<td data-label=${tu("messages")} class="num">${r.messages === null ? "—" : n(r.messages)}</td>` : null}
|
|
369
|
+
${cols.speed ? html`<td data-label=${tu("speed")} class="num">${r.speed ? speedText(r.speed) : dash(tu("speedNone"))}</td>` : null}
|
|
338
370
|
</tr>`;
|
|
339
371
|
}
|
|
372
|
+
/** #3066 §2: a rate as shown — whole tok/s, one decimal below 10. */
|
|
373
|
+
export const speedText = (s) => tu("speedUnit", s.tokPerSec >= 10 ? Math.round(s.tokPerSec).toLocaleString("en-US") : s.tokPerSec.toFixed(1));
|
|
374
|
+
/** Columns worth a place (#3066 §3): Reasoning only when some row reports it; Messages only when some row has them; Speed (§2) only when some response was timed. */
|
|
375
|
+
export function usageColumns(rows) {
|
|
376
|
+
return { reasoning: rows.some((r) => r.reasoningReported !== false), messages: rows.some((r) => r.messages !== null && r.messages !== undefined),
|
|
377
|
+
speed: rows.some((r) => !!r.speed) };
|
|
378
|
+
}
|
|
379
|
+
/** #3066 §2: each model's speed — rate · requests timed (coverage) · p10–p90 from 30 timed. */
|
|
380
|
+
function SpeedCard({ report }) {
|
|
381
|
+
const list = report.speedByModel || [];
|
|
382
|
+
if (!list.length) return null;
|
|
383
|
+
return html`<section class="us-speed card" aria-labelledby="us-speed-h">
|
|
384
|
+
<h3 id="us-speed-h">${tu("speedByModel")}</h3>
|
|
385
|
+
<ul class="rows">${list.map((m) => html`<li key=${m.model} class="row-item"><span class="grow strong mono">${m.model}</span>
|
|
386
|
+
<span class="num">${speedText(m.speed)}</span>
|
|
387
|
+
<span class="muted">${[m.requests > 0 ? tu("speedCoverage", n(m.speed.timedRequests), n(m.requests)) : null,
|
|
388
|
+
m.speed.p10 !== null && m.speed.p90 !== null ? tu("speedSpread", Math.round(m.speed.p10), Math.round(m.speed.p90)) : null].filter(Boolean).join(" · ")}</span></li>`)}</ul>
|
|
389
|
+
<p class="note">${tu("speedBasis")}</p>
|
|
390
|
+
</section>`;
|
|
391
|
+
}
|
|
392
|
+
/** The legend under the table: only the marks that appear, and the one footnote when an hour's source was not recorded. */
|
|
393
|
+
function Legend({ rows }) {
|
|
394
|
+
const marks = rows.map(rowMarks);
|
|
395
|
+
const parts = [marks.some((m) => m.partial) && tu("l_partial"), marks.some((m) => m.cost) && tu("l_cost_reported"), marks.some((m) => m.est) && tu("l_estimated")].filter(Boolean);
|
|
396
|
+
const source = rows.some((r) => (r.flags || []).includes("source_unknown"));
|
|
397
|
+
if (!parts.length && !source) return null;
|
|
398
|
+
return html`<p class="note us-key">${parts.join(" · ")}${parts.length && source ? html`<br />` : null}${source ? tu("sourceNote") : null}</p>`;
|
|
399
|
+
}
|
|
340
400
|
|
|
341
401
|
/** The tab's body for one answer of GET /ui/usage-report. */
|
|
342
402
|
export function UsageReport({ report, choice, setChoice, refresh, fetchedAt }) {
|
|
@@ -355,6 +415,7 @@ export function UsageReport({ report, choice, setChoice, refresh, fetchedAt }) {
|
|
|
355
415
|
const zones = zonesChanged(report);
|
|
356
416
|
const csvFrom = report.dailyTruncated ? report.dailyFrom : report.range.kind === "all" ? "all" : report.range.from;
|
|
357
417
|
const rows = report.rows.filter((r) => r.total > 0 || (r.messages || 0) > 0);
|
|
418
|
+
const cols = usageColumns(rows);
|
|
358
419
|
const anyAmount = rows.some((r) => r.amount !== null && r.amount > 0);
|
|
359
420
|
const measure = (r) => (anyAmount ? r.amount || 0 : r.total);
|
|
360
421
|
const max = Math.max(0, ...rows.map(measure));
|
|
@@ -375,7 +436,7 @@ export function UsageReport({ report, choice, setChoice, refresh, fetchedAt }) {
|
|
|
375
436
|
<label>${tu("from")} <input type="date" value=${draft.from} max=${draft.to} onInput=${(e) => setDraft({ ...draft, from: e.target.value })} /></label>
|
|
376
437
|
<label>${tu("to")} <input type="date" value=${draft.to} min=${draft.from} onInput=${(e) => setDraft({ ...draft, to: e.target.value })} /></label>
|
|
377
438
|
<button type="submit" class="btn btn-sm">${tu("apply")}</button></form>` : null}
|
|
378
|
-
<p class="us-basis" role="note">${tu("basis", report.pricesChecked)}</p>
|
|
439
|
+
<p class="us-basis" role="note" title=${tu("basisMore")}>${tu("basis", report.pricesChecked)}</p>
|
|
379
440
|
${noDaily ? html`<p class="us-partial" role="status">${tu("noDaily")}</p>` : report.dailyTruncated ? html`<p class="us-partial" role="status">${tu("partialDays", report.dailyFrom)}</p>` : null}
|
|
380
441
|
${zones ? html`<p class="us-partial" role="status">${tu("zoneChanged", zones.join(", "), report.zone)}</p>` : null}
|
|
381
442
|
<p class="note us-fresh">${tu("updated", new Date(fetchedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }))} · ${Number.isFinite(since) ? tu("since", new Date(since).toLocaleDateString()) : tu("notYet")} · ${tu("zoneNote", report.zone)}${group === "team" ? ` ${tu("teamsNote")}` : ""}
|
|
@@ -389,10 +450,12 @@ export function UsageReport({ report, choice, setChoice, refresh, fetchedAt }) {
|
|
|
389
450
|
<div class="ch-metric"><dt>${tu("messages")}</dt><dd>${report.totals.messages === null ? "—" : n(report.totals.messages)}</dd></div>
|
|
390
451
|
</dl>
|
|
391
452
|
${rows.length ? html`<table class="us-table"><thead><tr><th scope="col">${tu(`g_${group}`)}</th><th scope="col" class="num">${tu("amount")}</th>
|
|
392
|
-
<th scope="col" class="num">${tu("input")}</th><th scope="col" class="num">${tu("output")}</th
|
|
393
|
-
<th scope="col" class="num">${tu("total")}</th><th scope="col" class="num">${tu("requests")}</th
|
|
394
|
-
<tbody>${rows.map((r) => html`<${Row} key=${r.key} r=${r} group=${group} max=${max} measure=${measure} />`)}</tbody></table
|
|
453
|
+
<th scope="col" class="num">${tu("input")}</th><th scope="col" class="num">${tu("output")}</th>${cols.reasoning ? html`<th scope="col" class="num">${tu("reasoning")}</th>` : null}<th scope="col" class="num">${tu("read")}</th><th scope="col" class="num">${tu("write")}</th>
|
|
454
|
+
<th scope="col" class="num">${tu("total")}</th><th scope="col" class="num">${tu("requests")}</th>${cols.messages ? html`<th scope="col" class="num">${tu("messages")}</th>` : null}${cols.speed ? html`<th scope="col" class="num">${tu("speed")}</th>` : null}</tr></thead>
|
|
455
|
+
<tbody>${rows.map((r) => html`<${Row} key=${r.key} r=${r} group=${group} max=${max} measure=${measure} cols=${cols} />`)}</tbody></table>
|
|
456
|
+
<${Legend} rows=${rows} />`
|
|
395
457
|
: html`<p class="note">${tu("noRows")}</p>`}
|
|
458
|
+
<${SpeedCard} report=${report} />
|
|
396
459
|
<${Trend} report=${report} dates=${dates} />
|
|
397
460
|
<${Heatmap} report=${report} dates=${dates} />`}
|
|
398
461
|
${report.notMeasured.length || report.shared.length ? html`<section class="us-rest" aria-labelledby="us-rest-h"><h3 id="us-rest-h">${tu("notMeasured")}</h3>
|
package/dist/ui/panel-chat.js
CHANGED
|
@@ -77,7 +77,7 @@ import { createEmojiIndex } from "./chat-emojis.js";
|
|
|
77
77
|
import { pollPublicPreview } from "./preview-status.js";
|
|
78
78
|
import { createMentionIndex } from "./chat-mentions.js";
|
|
79
79
|
import { setPanelFeedback } from "./preview-panel-actions.js";
|
|
80
|
-
import { PreviewPanel, claimPanel, openPanel, panelClaims, panelStore, panelVersion, shownKey } from "./preview-panel.js";
|
|
80
|
+
import { PreviewPanel, claimPanel, openMarkdown, openPanel, panelClaims, panelStore, panelVersion, shownKey } from "./preview-panel.js";
|
|
81
81
|
import { needsArgument, paletteFor, parseCommandLine } from "./chat-commands.js";
|
|
82
82
|
import { installTour, refreshTourSpot } from "./chat-tour.js";
|
|
83
83
|
import { LightboxHost, openLightbox } from "./image-lightbox.js";
|
|
@@ -438,7 +438,8 @@ function threadEntry(name, doc) {
|
|
|
438
438
|
replyButtonBusy: (id) => store.state.rbBusy.has(id),
|
|
439
439
|
pollMine: (id) => store.state.pollMine[id], // #1823: this device's own vote
|
|
440
440
|
serverNow: () => Date.now() + store.state.clockSkew, // #1823: the deadline is the server's
|
|
441
|
-
panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o), version: panelVersion, claim: claimPanel, claims: panelClaims
|
|
441
|
+
panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o), version: panelVersion, claim: claimPanel, claims: panelClaims, // #1481, #1704
|
|
442
|
+
openMarkdown: (spec) => openMarkdown(spec) }, // M-2448: a Markdown file in the side panel
|
|
442
443
|
openImage: openLightbox, // an image, full size, in the page
|
|
443
444
|
emojis: (id) => !!emojiIndex && emojiIndex.known(id), // #1630: the fleet's server emoji
|
|
444
445
|
mentions: (kind, id) => (mentionIndex ? mentionIndex.name(kind, id) : null), // #1682: mention names
|
|
@@ -797,6 +798,8 @@ function DeleteDialog({ name, onClose }) {
|
|
|
797
798
|
} catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
|
|
798
799
|
if (r && r.error) { toast(r.error, false); if (!handedOver && lease.current()) setBusy(false); return; }
|
|
799
800
|
toast(t("chat.instanceDeleted", name));
|
|
801
|
+
// The agent's own topic went too, or stayed with the reason (the server's own words: no new string here).
|
|
802
|
+
if (r.topic_note) toast(r.topic_note, r.topic_deleted !== false);
|
|
800
803
|
if (handedOver || !lease.current()) return;
|
|
801
804
|
setBusy(false);
|
|
802
805
|
onClose();
|
package/dist/ui/panel-needs.js
CHANGED
|
@@ -34,7 +34,7 @@ register("needs", {
|
|
|
34
34
|
notifyMobile: "On a phone, rely on your chat app's notification: a new item is a new post in General.", notifyMobileWeb: "On a phone, open this page to see what needs you.",
|
|
35
35
|
notifyInsecure: "Notifications need a secure address (localhost or the HTTPS public link), not a plain-HTTP LAN address.",
|
|
36
36
|
notifyUnsupported: "This browser cannot show notifications.", turnOn: "Turn on", turnOff: "Turn off",
|
|
37
|
-
typePrompt: "Fleet prompt", typeAwaiting: "At its terminal", typeInstance: "Instance", typeDelivery: "Delivery",
|
|
37
|
+
typePrompt: "Fleet prompt", typeAwaiting: "At its terminal", typeInstance: "Instance", typeDelivery: "Delivery", typeMissed: "Missed while offline",
|
|
38
38
|
},
|
|
39
39
|
"zh-TW": {
|
|
40
40
|
title: "等你處理", empty: "目前沒有需要你處理的事", emptyHint: "agent 在等你時,會出現在這裡——也會出現在聊天 app 的 General。", emptyHintWeb: "agent 在等你時,會出現在這裡。",
|
|
@@ -47,10 +47,18 @@ register("needs", {
|
|
|
47
47
|
notifyMobile: "在手機上請以聊天 app 的通知為主:每個新項目都會在 General 發一則新訊息。", notifyMobileWeb: "在手機上請打開這個頁面查看等你處理的事。",
|
|
48
48
|
notifyInsecure: "通知需要安全的網址(localhost 或 HTTPS 公開連結),區網的一般 HTTP 網址不行。",
|
|
49
49
|
notifyUnsupported: "這個瀏覽器無法顯示通知。", turnOn: "開啟", turnOff: "關閉",
|
|
50
|
-
typePrompt: "Fleet 提示", typeAwaiting: "在終端機上", typeInstance: "Instance", typeDelivery: "傳送",
|
|
50
|
+
typePrompt: "Fleet 提示", typeAwaiting: "在終端機上", typeInstance: "Instance", typeDelivery: "傳送", typeMissed: "離線時錯過",
|
|
51
51
|
},
|
|
52
52
|
});
|
|
53
53
|
const tn = (k, ...v) => t(`needs.${k}`, ...v);
|
|
54
|
+
/** Stale shells keep the old app-i18n.js; a missing key would otherwise show as app.needs_reason_*. */
|
|
55
|
+
function itemReason(item) {
|
|
56
|
+
const text = reasonText(item);
|
|
57
|
+
if (typeof text === "string" && text.startsWith("app.needs_reason_") && (item.type === "missed" || item.reason === "missed_offline")) {
|
|
58
|
+
return tn("typeMissed");
|
|
59
|
+
}
|
|
60
|
+
return text;
|
|
61
|
+
}
|
|
54
62
|
|
|
55
63
|
/**
|
|
56
64
|
* The page's claims on what Needs you writes, kept for the life of the page — not of a panel or a render (#1463
|
|
@@ -86,7 +94,7 @@ const setClaim = (kind, key, value) => claims.set((c) => {
|
|
|
86
94
|
return { ...c, [kind]: next };
|
|
87
95
|
});
|
|
88
96
|
|
|
89
|
-
const ICON = { prompt: "chat", awaiting_input: "clock", instance: "alert", delivery: "send", task: "tasks" };
|
|
97
|
+
const ICON = { prompt: "chat", awaiting_input: "clock", instance: "alert", delivery: "send", task: "tasks", missed: "clock" };
|
|
90
98
|
const BAD = new Set(["crashed", "delivery_failed", "hang", "exited"]);
|
|
91
99
|
|
|
92
100
|
/**
|
|
@@ -195,12 +203,12 @@ function NeedsItem({ item, nowMs, owner }) {
|
|
|
195
203
|
return html`<article data-trust="permission" class=${`n-item${BAD.has(item.reason) ? " bad" : ""}`}>
|
|
196
204
|
<span class="n-icon"><${Icon} name=${ICON[item.type] || "alert"} size=${18} /></span>
|
|
197
205
|
<div class="n-body">
|
|
198
|
-
<div class="n-title"><strong>${
|
|
206
|
+
<div class="n-title"><strong>${itemReason(item)}</strong><span class="n-age">${age(item.since, nowMs)}</span></div>
|
|
199
207
|
${detailOf(item) ? html`<p class="n-detail">${detailOf(item)}</p>` : null}
|
|
200
208
|
<div class="n-actions">
|
|
201
209
|
${item.type === "prompt" && Array.isArray(item.actions) ? item.actions.filter((a) => !a.input).map((a) => html`<button key=${a.id} type="button" class="btn btn-sm"
|
|
202
210
|
disabled=${promptBusy} onClick=${() => answer(a.id)}>${(p ? p.busy : fallback === "busy") ? tn("answering") : a.label}</button>`) : null}
|
|
203
|
-
${item.type === "delivery" ? html`<button type="button" class="btn btn-sm" disabled=${!!ack} onClick=${acknowledge}>
|
|
211
|
+
${item.type === "delivery" || item.type === "missed" ? html`<button type="button" class="btn btn-sm" disabled=${!!ack} onClick=${acknowledge}>
|
|
204
212
|
<${Icon} name="check" size=${14} />${ack === "busy" ? tn("acknowledging") : tn("acknowledge")}</button>` : null}
|
|
205
213
|
${(item.reason === "auth_paused" || item.reason === "backend_signin") && item.signIn && signInOffered() ? html`<button type="button" class="btn btn-sm btn-primary" onClick=${() => openSignIn(item.signIn)}>${tn("signIn")}</button>` : null}
|
|
206
214
|
${item.type === "task" && item.card ? html`<a class="btn btn-sm" href=${`/ui/fleet?card=${encodeURIComponent(item.card.code)}`}>${tn("openCard")}</a>` : null}
|
package/dist/ui/preview-panel.js
CHANGED
|
@@ -20,6 +20,8 @@ import { chatTime, onDayChange } from "./chat-time.js";
|
|
|
20
20
|
import { PanelActions } from "./preview-panel-actions.js";
|
|
21
21
|
import { htmlTitle, versionsOf, lineDiff, diffHunks, DIFF_LIMITS } from "./preview-versions.js";
|
|
22
22
|
import { pagesOf, pageIdOf } from "./preview-pages.js";
|
|
23
|
+
import { drawMarkdown, TEXT_CARD_MAX } from "./chat-thread.js";
|
|
24
|
+
import "./chat-md-strings.js"; // M-2448: the Markdown panel's own words (an older cached chat-strings.js lacks them)
|
|
23
25
|
|
|
24
26
|
export { htmlTitle };
|
|
25
27
|
|
|
@@ -57,6 +59,16 @@ export function openPanel(spec, { run = false } = {}) {
|
|
|
57
59
|
set({ open: { ...spec, kind: "html" }, run: run ? ++runs : 0 });
|
|
58
60
|
retire(old); // State changes first: cleanup reentry cannot close a newer panel.
|
|
59
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* M-2448: show a Markdown file in the panel — `spec`: { key, instance, msgKey, att: { id, name, size }, sender, ts,
|
|
64
|
+
* text? } (text: what the card's Show already read). Nothing runs: it is rendered by the chat's own Markdown renderer.
|
|
65
|
+
* The block shown before stops, as for any open.
|
|
66
|
+
*/
|
|
67
|
+
export function openMarkdown(spec) {
|
|
68
|
+
const old = state.open;
|
|
69
|
+
set({ open: { ...spec, kind: "md" }, run: 0 });
|
|
70
|
+
retire(old);
|
|
71
|
+
}
|
|
60
72
|
/** Close publishes the empty slot before old-owner callbacks can reenter. */
|
|
61
73
|
export function closePanel() {
|
|
62
74
|
const old = state.open; if (!old) return;
|
|
@@ -363,7 +375,35 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
363
375
|
}
|
|
364
376
|
return () => { live = false; };
|
|
365
377
|
}, [shown, open && open.key, prev && prev.key]);
|
|
378
|
+
// M-2448: a Markdown file's text (read once per open: the same capped, same-origin read as the chat's card) and
|
|
379
|
+
// which view shows it — rendered (the chat's Markdown renderer, which escapes first) or its source.
|
|
380
|
+
const [md, setMd] = useState(null); // { state: "loading" | "ready" | "failed", text?, reason? }
|
|
381
|
+
const [mdView, setMdView] = useState("rendered");
|
|
382
|
+
const [mdPlain, setMdPlain] = useState(false);
|
|
383
|
+
const mdBody = useRef(null);
|
|
384
|
+
const isMd = !!open && open.kind === "md";
|
|
385
|
+
useEffect(() => {
|
|
386
|
+
if (!isMd) { setMd(null); return undefined; }
|
|
387
|
+
setMdView("rendered");
|
|
388
|
+
if (typeof open.text === "string") { setMd({ state: "ready", text: open.text }); return undefined; }
|
|
389
|
+
let live = true;
|
|
390
|
+
setMd({ state: "loading" });
|
|
391
|
+
loadHtmlAttachment(open.att, undefined, { ask: TEXT_CARD_MAX, max: TEXT_CARD_MAX }).then((r) => {
|
|
392
|
+
if (!live) return;
|
|
393
|
+
setMd(r.ok ? { state: "ready", text: r.code } : { state: "failed", reason: r.reason === "over" ? t("chat.tcOver") : attReason(r) });
|
|
394
|
+
}, () => { if (live) setMd({ state: "failed", reason: t("chat.pvAttFailed") }); });
|
|
395
|
+
return () => { live = false; };
|
|
396
|
+
}, [isMd && open.key]);
|
|
397
|
+
useEffect(() => {
|
|
398
|
+
if (!isMd || !md || md.state !== "ready" || mdView !== "rendered" || !mdBody.current) return;
|
|
399
|
+
setMdPlain(drawMarkdown(mdBody.current, md.text) === "plain");
|
|
400
|
+
}, [isMd && open.key, md, mdView]);
|
|
366
401
|
const fileName = open && open.att ? open.att.name : undefined;
|
|
402
|
+
// The divider between the thread and the panel (both kinds): drag, or the arrow keys, Home and End.
|
|
403
|
+
function divider0() {
|
|
404
|
+
return html`<div data-trust="preview" class="pv-divider" ref=${divider} role="separator" aria-orientation="vertical" tabindex="0" aria-label=${t("chat.pvResize")}
|
|
405
|
+
aria-valuemin=${String(MIN_PANEL)} onKeyDown=${onKey} onPointerDown=${onDown}></div>`;
|
|
406
|
+
}
|
|
367
407
|
// Esc in the panel closes it (and is not the chat's "stop the reply").
|
|
368
408
|
const onPanelKey = (e) => {
|
|
369
409
|
if (e.key !== "Escape" || e.isComposing) return;
|
|
@@ -372,6 +412,8 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
372
412
|
};
|
|
373
413
|
|
|
374
414
|
if (!open) return null;
|
|
415
|
+
if (isMd) return html`${divider0()}<${MarkdownPanel} open=${open} name=${name} md=${md} view=${mdView} setView=${setMdView} plain=${mdPlain}
|
|
416
|
+
body=${mdBody} panel=${panel} fs=${fs} enterFs=${enterFs} leaveFs=${leaveFs} onKey=${onPanelKey} reveal=${reveal} />`;
|
|
375
417
|
const going = run.state === "starting" || run.state === "running";
|
|
376
418
|
const a = P().availability();
|
|
377
419
|
// #1554: a device that has not allowed previews is offered the same "Allow previews" here (the same confirm). Allowing
|
|
@@ -390,8 +432,7 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
390
432
|
if (want && pending.current === want && panelStore.get().run === want) start();
|
|
391
433
|
};
|
|
392
434
|
const source = t("chat.pvSource", open.sender || name, timeOf(open.ts));
|
|
393
|
-
return html
|
|
394
|
-
aria-valuemin=${String(MIN_PANEL)} onKeyDown=${onKey} onPointerDown=${onDown}></div>
|
|
435
|
+
return html`${divider0()}
|
|
395
436
|
<aside data-trust="preview" class=${`pv-panel${fs ? ` pv-fs pv-fs-${fs}` : ""}`} ref=${panel} aria-label=${t("chat.pvPanel")} onKeyDown=${onPanelKey}>
|
|
396
437
|
<div class="pv-panel-head">
|
|
397
438
|
<button type="button" class="icon-btn only-narrow pv-panel-close" title=${t("chat.pvBack")} aria-label=${t("chat.pvBack")} onClick=${closePanel}><${Icon} name="back" size=${18} /></button>
|
|
@@ -440,6 +481,41 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
440
481
|
</aside>`;
|
|
441
482
|
}
|
|
442
483
|
|
|
484
|
+
/**
|
|
485
|
+
* M-2448: a Markdown file in the panel — the HTML preview's frame (head, source, close, full screen; a full-screen
|
|
486
|
+
* sheet on a phone), with Rendered / Source and Download. Nothing in the file runs: Rendered is the chat's Markdown
|
|
487
|
+
* renderer (escaped first, no raw HTML), Source is text.
|
|
488
|
+
*/
|
|
489
|
+
function MarkdownPanel({ open, name, md, view, setView, plain, body, panel, fs, enterFs, leaveFs, onKey, reveal }) {
|
|
490
|
+
const source = t("chat.pvSource", open.sender || name, timeOf(open.ts));
|
|
491
|
+
const ready = md && md.state === "ready";
|
|
492
|
+
return html`<aside data-trust="preview" class=${`pv-panel pv-md-panel${fs ? ` pv-fs pv-fs-${fs}` : ""}`} ref=${panel} aria-label=${t("chat.pvMdPanel")} onKeyDown=${onKey}>
|
|
493
|
+
<div class="pv-panel-head">
|
|
494
|
+
<button type="button" class="icon-btn only-narrow pv-panel-close" title=${t("chat.pvBack")} aria-label=${t("chat.pvBack")} onClick=${closePanel}><${Icon} name="back" size=${18} /></button>
|
|
495
|
+
<div class="pv-panel-title"><strong>${open.att ? open.att.name : t("chat.pvMdPanel")}</strong>
|
|
496
|
+
<button type="button" class="pv-source" title=${source} onClick=${() => { if (window.matchMedia && window.matchMedia("(max-width: 899px)").matches) closePanel(); reveal(open.msgKey); }}>${source}</button></div>
|
|
497
|
+
<button type="button" class="icon-btn only-wide pv-panel-close" title=${t("chat.pvClose")} aria-label=${t("chat.pvClose")} onClick=${closePanel}><${Icon} name="close" size=${18} /></button>
|
|
498
|
+
</div>
|
|
499
|
+
<div class="pv-panel-bar">
|
|
500
|
+
<div class="seg-inline" role="group" aria-label=${t("chat.pvMdPanel")}>
|
|
501
|
+
<button type="button" class=${`btn btn-sm${view === "rendered" ? " on" : ""}`} aria-pressed=${view === "rendered" ? "true" : "false"} title=${t("chat.pvMdRendered")} onClick=${() => setView("rendered")}><${Icon} name="eye" size=${14} /><span class="lbl">${t("chat.pvMdRendered")}</span></button>
|
|
502
|
+
<button type="button" class=${`btn btn-sm${view === "source" ? " on" : ""}`} aria-pressed=${view === "source" ? "true" : "false"} title=${t("chat.pvMdSource")} onClick=${() => setView("source")}><${Icon} name="code" size=${14} /><span class="lbl">${t("chat.pvMdSource")}</span></button>
|
|
503
|
+
</div>
|
|
504
|
+
<span class="grow"></span>
|
|
505
|
+
<button type="button" class=${`btn btn-sm only-wide pv-panel-fs${fs ? " on" : ""}`} aria-pressed=${fs ? "true" : "false"} title=${t(fs ? "chat.pvFsExit" : "chat.pvFs")} aria-label=${t(fs ? "chat.pvFsExit" : "chat.pvFs")}
|
|
506
|
+
onClick=${() => (fs ? leaveFs() : enterFs())}><${Icon} name=${fs ? "fullscreenexit" : "fullscreen"} size=${14} /></button>
|
|
507
|
+
${open.att ? html`<a class="btn btn-sm pv-panel-dl" href=${`/ui/file/${open.att.id}`} download=${open.att.name} title=${t("chat.pvDownload")}><${Icon} name="download" size=${14} /><span class="lbl">${t("chat.pvDownload")}</span></a>` : null}
|
|
508
|
+
</div>
|
|
509
|
+
${md && md.state === "loading" ? html`<div class="pv-note" role="status">${t("chat.pvLoading")}</div>` : null}
|
|
510
|
+
${md && md.state === "failed" ? html`<div class="pv-note" role="alert">${md.reason}</div>` : null}
|
|
511
|
+
${ready && view === "rendered" && plain ? html`<div class="pv-note" role="status">${t("chat.tcPlain")}</div>` : null}
|
|
512
|
+
<div class="pv-panel-body show-code">
|
|
513
|
+
${ready && view === "rendered" ? html`<div class="pv-code pv-md" ref=${body}></div>` : null}
|
|
514
|
+
${ready && view === "source" ? html`<pre class="pv-code"><code>${md.text}</code></pre>` : null}
|
|
515
|
+
</div>
|
|
516
|
+
</aside>`;
|
|
517
|
+
}
|
|
518
|
+
|
|
443
519
|
/** The Changes view: hunks of the line diff as a unified diff shows them, drawn as text (never as HTML). */
|
|
444
520
|
function Changes({ changes }) {
|
|
445
521
|
if (!changes || changes.state === "loading") return html`<div class="pv-note pv-diff-note">${t("chat.pvLoading")}</div>`;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// M-2446: the agent dialog's Codex speed words, lazy and fixed. A tab opened before an upgrade may keep the old
|
|
2
|
+
// settings-strings module while the dialog module is fresh: these words live here, registered with the dialog.
|
|
3
|
+
import { register } from "/assets/app-i18n.js";
|
|
4
|
+
register("settings", {
|
|
5
|
+
en: {
|
|
6
|
+
codexSpeed: "Speed",
|
|
7
|
+
codexSpeedDefault: "Codex default",
|
|
8
|
+
codexSpeedStandard: "Standard",
|
|
9
|
+
codexSpeedFast: "Fast",
|
|
10
|
+
codexSpeedHint: "Fast: uses plan limits at 2.5× (OpenAI). Only for models that offer Fast; otherwise the agent starts without it and the log says why. Changing it restarts the agent.",
|
|
11
|
+
},
|
|
12
|
+
"zh-TW": {
|
|
13
|
+
codexSpeed: "速度",
|
|
14
|
+
codexSpeedDefault: "Codex 預設",
|
|
15
|
+
codexSpeedStandard: "標準",
|
|
16
|
+
codexSpeedFast: "快速",
|
|
17
|
+
codexSpeedHint: "快速:方案額度以 2.5 倍消耗(OpenAI)。只適用於提供 Fast 的模型;不支援時 Agent 會照常啟動,並在記錄中說明原因。變更後會重新啟動 Agent。",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -25,6 +25,10 @@ import {
|
|
|
25
25
|
chLabel, dashboardDefault, hasOwn, impactOf, newConnectionImpact, nonNegative, nonNegativeInteger, positive, replyGuardSupported, sameValue, visibilityDefault,
|
|
26
26
|
classicWebEchoEffective, toolSetDefault, worstImpact,
|
|
27
27
|
} from "./settings-model.js";
|
|
28
|
+
import "./settings-codex-copy.js"; // M-2446: the Speed field's words (a retained old settings-strings lacks them)
|
|
29
|
+
/** M-2446: Codex speed choices (backend_options.codex.speed); "" = unset. Local, not imported: a stale tab may keep an
|
|
30
|
+
* older settings-model.js, and a new named import would fail this module's load. */
|
|
31
|
+
const CODEX_SPEED_CHOICES = ["", "standard", "fast"];
|
|
28
32
|
/** #2342: a field by id, for recordEdit before a pick or a reset writes it. */
|
|
29
33
|
const byId = (id) => (typeof document !== "undefined" ? document.getElementById(id) : null);
|
|
30
34
|
|
|
@@ -298,6 +302,10 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
|
|
|
298
302
|
${inheritedBackend ? html`<${Inherited} ...${inh("backend", f.backend, "#g-be", "backend")} />` : null}</div>
|
|
299
303
|
<div class="field">${L(tn("model"), "instance.model", "ag-model")}<${ModelSelect} id="ag-model" value=${f.model} onChange=${set("model")}
|
|
300
304
|
backend=${f.backend || defaults.backend || "claude-code"} instance=${name} inherit=${tn("inheritValue", defaults.model || tn("modelCliDefault"))} /></div>
|
|
305
|
+
${(f.backend || defaults.backend || "claude-code") === "codex" ? html`<div class="field codex-speed">${L(tn("codexSpeed"), "instance.backend_options", "ag-speed")}
|
|
306
|
+
<${Select} id="ag-speed" value=${f.codex_speed} onChange=${set("codex_speed")}
|
|
307
|
+
options=${CODEX_SPEED_CHOICES.map((v) => ({ value: v, label: tn(v === "fast" ? "codexSpeedFast" : v === "standard" ? "codexSpeedStandard" : "codexSpeedDefault") }))} />
|
|
308
|
+
<p class="note">${tn("codexSpeedHint")}</p></div>` : null}
|
|
301
309
|
</div>
|
|
302
310
|
<div class="field">${L(tn("workingDir"), "instance.working_directory", "ag-wd")}<input id="ag-wd" type="text" value=${f.working_directory} onInput=${(e) => set("working_directory")(e.target.value)} /></div>
|
|
303
311
|
<div class="field">${L(`${tn("autoPause")} (${tn("minutes")})`, "instance.auto_pause_after", "ag-auto")}
|
|
@@ -167,6 +167,11 @@ export function groupAgents(instances, live, filter, groupFor = (n) => groupOf(i
|
|
|
167
167
|
* (the key is removed); the visibility baseline is what the picker showed, so a value it cannot show (a typo, read as
|
|
168
168
|
* unset) is never rewritten by an unrelated edit (#1302).
|
|
169
169
|
*/
|
|
170
|
+
/** The agent's own Codex speed, as the picker shows it ("" when unset or not one AgEnD knows). */
|
|
171
|
+
export function codexSpeedOf(inst) {
|
|
172
|
+
const v = inst && inst.backend_options && inst.backend_options.codex && inst.backend_options.codex.speed;
|
|
173
|
+
return v === "fast" || v === "standard" ? v : "";
|
|
174
|
+
}
|
|
170
175
|
export function agentPatch(form, inst, defaults, visibilityBaseline, statusEmojisBaseline) {
|
|
171
176
|
const hang = inst.hang_detector || {};
|
|
172
177
|
const next = {
|
|
@@ -190,7 +195,7 @@ export function agentPatch(form, inst, defaults, visibilityBaseline, statusEmoji
|
|
|
190
195
|
office_access: separateChoice(form.dashboard.office),
|
|
191
196
|
office_pack_access: separateChoice(form.dashboard.office_packs),
|
|
192
197
|
};
|
|
193
|
-
return changedFields(next, {
|
|
198
|
+
return Object.assign(changedFields(next, {
|
|
194
199
|
backend: inst.backend || defaults.backend || "claude-code",
|
|
195
200
|
model: (inst.model && inst.model.trim()) || null,
|
|
196
201
|
working_directory: inst.working_directory || "",
|
|
@@ -214,7 +219,15 @@ export function agentPatch(form, inst, defaults, visibilityBaseline, statusEmoji
|
|
|
214
219
|
dashboard: dashboardPatchValue(dashboardForm(inst)),
|
|
215
220
|
office_access: separateChoice(dashboardForm(inst).office),
|
|
216
221
|
office_pack_access: separateChoice(dashboardForm(inst).office_packs),
|
|
217
|
-
});
|
|
222
|
+
}), codexSpeedPatch(form, inst));
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* M-2446: only the Codex speed key, merged per backend by the server (settings-instance-patch.ts), so the agent's other
|
|
226
|
+
* backend options are kept; back to "" removes it (null). Nothing when it did not change.
|
|
227
|
+
*/
|
|
228
|
+
function codexSpeedPatch(form, inst) {
|
|
229
|
+
const next = form.codex_speed || "";
|
|
230
|
+
return next === codexSpeedOf(inst) ? {} : { backend_options: { codex: { speed: next || null } } };
|
|
218
231
|
}
|
|
219
232
|
|
|
220
233
|
/** The agent form's starting values (an override shows the inherited value while "inherit" is on). */
|
|
@@ -245,6 +258,7 @@ export function agentForm(inst, defaults, name = "") {
|
|
|
245
258
|
display_name: inst.display_name ?? defaults.display_name ?? "",
|
|
246
259
|
status_emojis: inst.status_emojis || null,
|
|
247
260
|
dashboard: dashboardForm(inst),
|
|
261
|
+
codex_speed: codexSpeedOf(inst),
|
|
248
262
|
inherit: {
|
|
249
263
|
auto_pause_after: inherit("auto_pause_after"), hang: !hasOwn(hang, "timeout_minutes"), agent_mode: inherit("agent_mode"),
|
|
250
264
|
tool_set: inherit("tool_set"), tool_progress: inherit("tool_progress"), cross_instance_visibility: inherit("cross_instance_visibility"),
|
|
@@ -166,7 +166,7 @@ register("settings", {
|
|
|
166
166
|
failed: "Failed",
|
|
167
167
|
actionFailed: "{0} failed",
|
|
168
168
|
noAgentMatch: "No agents match \"{0}\".",
|
|
169
|
-
deleteAgent: "Delete agent \"{0}\"? This will be applied only after you confirm the pending changes.",
|
|
169
|
+
deleteAgent: "Delete agent \"{0}\" and its chat topic (on Discord, its channel and history)? This will be applied only after you confirm the pending changes.",
|
|
170
170
|
nameRequired: "name required",
|
|
171
171
|
tokenEnvRequired: "token env required",
|
|
172
172
|
removeBot: "Remove bot \"{0}\"? (the token in .env is left untouched)",
|
|
@@ -823,7 +823,7 @@ register("settings", {
|
|
|
823
823
|
failed: "失敗",
|
|
824
824
|
actionFailed: "{0}失敗",
|
|
825
825
|
noAgentMatch: "沒有符合「{0}」的 Agent。",
|
|
826
|
-
deleteAgent: "確定刪除 Agent「{0}
|
|
826
|
+
deleteAgent: "確定刪除 Agent「{0}」及它的聊天 topic(在 Discord 上是它的頻道與訊息記錄)嗎?只有在確認套用待處理變更後才會生效。",
|
|
827
827
|
nameRequired: "必須填寫名稱",
|
|
828
828
|
tokenEnvRequired: "必須填寫 token 環境變數",
|
|
829
829
|
removeBot: "確定移除機器人「{0}」嗎?(.env 中的 token 會保留)",
|
|
@@ -111,7 +111,7 @@ register("app", {
|
|
|
111
111
|
confirmTitle: "Please confirm", confirmOk: "Continue", cancel: "Cancel",
|
|
112
112
|
sidePanelLeave: "Leave {0} without saving? Your changes there will be lost.", sidePanelDiscard: "Discard changes",
|
|
113
113
|
// The server's own wording (src/locale.ts needs.reason.*), so the web says what Discord says.
|
|
114
|
-
needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_question: "Asking you a question", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_kiro_choice: "Kiro needs your decision", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_backend_signin: "Backend sign-in needed", needs_reason_config_invalid: "Configuration file needs attention", needs_reason_config_fallback: "Running on stored configuration", config_history_edit: "{0}, line {1}, column {2}: {3}. AgEnD is still running on the previous settings. The next restart would use the stored valid copy from {4}. Correct the YAML error; the file has not been changed by AgEnD.", config_history_no_history: "{0}, line {1}, column {2}: {3}. AgEnD is still running on the previous settings. No stored valid copy is available, so the next restart cannot recover automatically. Correct the YAML error before restarting; the file has not been changed by AgEnD.", config_history_fallback: "{0}, line {1}, column {2}: {3}. AgEnD started using the stored valid copy from {4}. Correct the YAML error and reload the settings. The file on disk has not been changed by AgEnD.", needs_reason_crashed: "Crashed", needs_reason_usage_limit: "Usage limit reached · reset time unknown", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
|
|
114
|
+
needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_question: "Asking you a question", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_kiro_choice: "Kiro needs your decision", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_backend_signin: "Backend sign-in needed", needs_reason_config_invalid: "Configuration file needs attention", needs_reason_config_fallback: "Running on stored configuration", config_history_edit: "{0}, line {1}, column {2}: {3}. AgEnD is still running on the previous settings. The next restart would use the stored valid copy from {4}. Correct the YAML error; the file has not been changed by AgEnD.", config_history_no_history: "{0}, line {1}, column {2}: {3}. AgEnD is still running on the previous settings. No stored valid copy is available, so the next restart cannot recover automatically. Correct the YAML error before restarting; the file has not been changed by AgEnD.", config_history_fallback: "{0}, line {1}, column {2}: {3}. AgEnD started using the stored valid copy from {4}. Correct the YAML error and reload the settings. The file on disk has not been changed by AgEnD.", needs_reason_crashed: "Crashed", needs_reason_usage_limit: "Usage limit reached · reset time unknown", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_missed_offline: "Missed while AgEnD was offline", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
|
|
115
115
|
},
|
|
116
116
|
"zh-TW": {
|
|
117
117
|
brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet 概覽", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance", newAgent: "新增 Agent",
|
|
@@ -156,6 +156,6 @@ register("app", {
|
|
|
156
156
|
search: "搜尋", searchTitle: "搜尋聊天、agent 之間的訊息與任務({0})",
|
|
157
157
|
confirmTitle: "請確認", confirmOk: "繼續", cancel: "取消",
|
|
158
158
|
sidePanelLeave: "不儲存就離開「{0}」?你在那裡的修改會遺失。", sidePanelDiscard: "捨棄修改",
|
|
159
|
-
needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_question: "正在問你問題", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_kiro_choice: "Kiro 需要你決定", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_backend_signin: "後端需要登入", needs_reason_config_invalid: "設定檔需要處理", needs_reason_config_fallback: "正在使用儲存的設定副本", config_history_edit: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 仍使用先前的設定執行。下次重新啟動會改用 {4} 儲存的有效副本。請修正 YAML 錯誤;AgEnD 未更動此檔案。", config_history_no_history: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 仍使用先前的設定執行。沒有可用的有效副本,下次重新啟動將無法自動恢復。請在重新啟動前修正 YAML 錯誤;AgEnD 未更動此檔案。", config_history_fallback: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 已使用 {4} 儲存的有效副本啟動。請修正 YAML 錯誤並重新載入設定。AgEnD 未更動磁碟上的檔案。", needs_reason_crashed: "已崩潰", needs_reason_usage_limit: "用量已達上限 · 恢復時間未知", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
|
|
159
|
+
needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_question: "正在問你問題", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_kiro_choice: "Kiro 需要你決定", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_backend_signin: "後端需要登入", needs_reason_config_invalid: "設定檔需要處理", needs_reason_config_fallback: "正在使用儲存的設定副本", config_history_edit: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 仍使用先前的設定執行。下次重新啟動會改用 {4} 儲存的有效副本。請修正 YAML 錯誤;AgEnD 未更動此檔案。", config_history_no_history: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 仍使用先前的設定執行。沒有可用的有效副本,下次重新啟動將無法自動恢復。請在重新啟動前修正 YAML 錯誤;AgEnD 未更動此檔案。", config_history_fallback: "{0} 第 {1} 行、第 {2} 欄:{3}。AgEnD 已使用 {4} 儲存的有效副本啟動。請修正 YAML 錯誤並重新載入設定。AgEnD 未更動磁碟上的檔案。", needs_reason_crashed: "已崩潰", needs_reason_usage_limit: "用量已達上限 · 恢復時間未知", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_missed_offline: "AgEnD 離線時錯過", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
|
|
160
160
|
},
|
|
161
161
|
});
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -816,6 +816,10 @@ img.amb-p { object-fit: contain; }
|
|
|
816
816
|
.pv-panel-body { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0 var(--s-3) var(--s-3); }
|
|
817
817
|
.pv-panel-body .pv-holder { flex: 1; min-height: 0; margin: 0; display: flex; flex-direction: column; }
|
|
818
818
|
.pv-panel-body.show-code .pv-holder { display: none; }
|
|
819
|
+
/* M-2448: a Markdown file in the panel — the chat's Markdown look, scrolling inside the panel (never the page). */
|
|
820
|
+
.pv-code.pv-md { font-family: var(--font-ui); font-size: var(--fs-md); line-height: 1.6; white-space: normal; padding: var(--s-3) var(--s-4); }
|
|
821
|
+
.pv-md .md { max-width: 860px; overflow-wrap: anywhere; }
|
|
822
|
+
.pv-md .tc-pre { white-space: pre-wrap; font-family: var(--font-mono); font-size: var(--fs-xs); }
|
|
819
823
|
.preview-frame.fill { flex: 1; height: 100%; min-height: 0; }
|
|
820
824
|
.pv-code { flex: 1; min-height: 0; margin: 0; overflow: auto; font-family: var(--font-mono); font-size: var(--fs-xs); line-height: 1.5;
|
|
821
825
|
background: var(--surface); border-radius: var(--r-sm); padding: var(--s-3); white-space: pre-wrap; overflow-wrap: anywhere; color: var(--text); }
|
|
@@ -1392,6 +1396,11 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
|
|
|
1392
1396
|
.u-stale { font-weight: var(--fw-semibold); }
|
|
1393
1397
|
.u-stale-detail { color: var(--text-3); font-size: var(--fs-xs); }
|
|
1394
1398
|
.u-sub { color: var(--text-3); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
|
|
1399
|
+
/* #3066 §1: a weekly quota's measured 100% under its bar, and its notes (the fleet-only limit always). */
|
|
1400
|
+
.u-cap { margin-top: 4px; font-size: var(--fs-xs); }
|
|
1401
|
+
.u-cap-line { color: var(--text-2); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
1402
|
+
.u-cap-wait .u-cap-line { color: var(--text-3); }
|
|
1403
|
+
.u-cap-note { color: var(--text-3); overflow-wrap: anywhere; }
|
|
1395
1404
|
.u-err { color: var(--warn); font-size: var(--fs-sm); }
|
|
1396
1405
|
.u-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); }
|
|
1397
1406
|
/* #1673: the usage rail beside the page on a wide screen (header-tools.js UsageRail; shown only at min-width 1200px). */
|
|
@@ -2111,6 +2120,16 @@ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
|
|
|
2111
2120
|
.us-meta { display: block; font-size: var(--fs-xs); color: var(--text-3); overflow-wrap: anywhere; }
|
|
2112
2121
|
.us-est { display: block; font-size: var(--fs-xs); font-weight: var(--fw-regular); color: var(--text-3); }
|
|
2113
2122
|
.us-flags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
|
|
2123
|
+
/* #3066 §3: a row's numbers carry small marks (◐ partly priced, † the CLI's amount, ≈ estimated) and "—" with a reason,
|
|
2124
|
+
each said in its tooltip; the key under the table names only the marks that appear. */
|
|
2125
|
+
.us-mark { margin-left: 2px; color: var(--text-3); font-size: var(--fs-xs); cursor: help; }
|
|
2126
|
+
.us-mark.us-approx { margin-left: 0; margin-right: 1px; }
|
|
2127
|
+
.us-none { cursor: help; }
|
|
2128
|
+
.us-key { margin-top: 6px; }
|
|
2129
|
+
/* #3066 §2: each model's speed after the first token. */
|
|
2130
|
+
.us-speed { margin-top: 12px; }
|
|
2131
|
+
.us-speed .row-item { gap: 8px; flex-wrap: wrap; }
|
|
2132
|
+
.us-speed .num { font-variant-numeric: tabular-nums; }
|
|
2114
2133
|
.us-trend, .us-heat { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
|
|
2115
2134
|
.us-trend h3, .us-heat h3, .us-rest h3 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); }
|
|
2116
2135
|
.us-scroll { overflow-x: auto; max-width: 100%; }
|