@songsid/agend 2.3.0-beta.22 → 2.3.0-beta.24
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 +2 -2
- package/README.zh-TW.md +2 -2
- package/dist/agent-cli.js +4 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +1 -0
- package/dist/agent-endpoint.js +4 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +5 -0
- package/dist/agent-whats-new.js +16 -1
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +7 -0
- package/dist/auth-api.js +54 -21
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/codex-package.d.ts +30 -0
- package/dist/backend/codex-package.js +133 -0
- package/dist/backend/codex-package.js.map +1 -0
- package/dist/backend/codex.d.ts +2 -0
- package/dist/backend/codex.js +9 -2
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.js +3 -1
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +9 -1
- package/dist/backend/kiro.js +102 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/opencode-permission.js +1 -1
- package/dist/backend/opencode-permission.js.map +1 -1
- package/dist/backend/opencode.js +2 -1
- package/dist/backend/opencode.js.map +1 -1
- package/dist/binary-probe.js +1 -1
- package/dist/bug-report-cli.js +11 -10
- package/dist/bug-report-cli.js.map +1 -1
- package/dist/bug-report.js +11 -10
- package/dist/bug-report.js.map +1 -1
- package/dist/bundled-skills.d.ts +7 -3
- package/dist/bundled-skills.js +16 -7
- package/dist/bundled-skills.js.map +1 -1
- package/dist/channel/adapters/discord.js +3 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-server.js +12 -4
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +3 -1
- package/dist/channel/mcp-tools.js +36 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/child-process.d.ts +21 -0
- package/dist/child-process.js +54 -0
- package/dist/child-process.js.map +1 -0
- package/dist/cli.js +188 -204
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +1 -1
- package/dist/command-table.js +1 -1
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +20 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +3 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +8 -0
- package/dist/daemon.js +31 -8
- package/dist/daemon.js.map +1 -1
- package/dist/darwin-memory.js +1 -1
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +42 -0
- package/dist/doctor.js.map +1 -1
- package/dist/export-import.js +4 -3
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-level-config.d.ts +1 -1
- package/dist/fleet-level-config.js +2 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +45 -0
- package/dist/fleet-manager.js +324 -36
- package/dist/fleet-manager.js.map +1 -1
- package/dist/full-restart.d.ts +5 -5
- package/dist/full-restart.js +5 -5
- package/dist/full-restart.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +48 -10
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +7 -8
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +319 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +529 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +17 -4
- package/dist/general-knowledge/steering/core-rules.md +1 -1
- package/dist/instance-config-impact.js +10 -3
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +4 -4
- package/dist/locale.js +296 -4
- package/dist/locale.js.map +1 -1
- package/dist/login-flows.js +1 -1
- package/dist/login-shell.js +3 -1
- package/dist/login-shell.js.map +1 -1
- package/dist/ls-rows.js +1 -1
- package/dist/needs-you-hub.d.ts +98 -1
- package/dist/needs-you-hub.js +313 -44
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you-live.d.ts +5 -2
- package/dist/needs-you-live.js +6 -3
- package/dist/needs-you-live.js.map +1 -1
- package/dist/needs-you.d.ts +58 -3
- package/dist/needs-you.js +36 -3
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-schemas.d.ts +3 -3
- package/dist/public-link-progress.d.ts +12 -1
- package/dist/public-link-progress.js +5 -10
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-tunnel-config.d.ts +5 -0
- package/dist/public-tunnel-config.js +45 -1
- package/dist/public-tunnel-config.js.map +1 -1
- package/dist/public-web-link.js +6 -5
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart.js +15 -9
- package/dist/quickstart.js.map +1 -1
- package/dist/resource-report.js +1 -1
- package/dist/scheduler/task-board.d.ts +41 -1
- package/dist/scheduler/task-board.js +152 -5
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/sd-notify.js +2 -1
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-installer.d.ts +69 -0
- package/dist/service-installer.js +182 -0
- package/dist/service-installer.js.map +1 -1
- package/dist/service-owner.d.ts +44 -0
- package/dist/service-owner.js +48 -0
- package/dist/service-owner.js.map +1 -0
- package/dist/service-plan.d.ts +8 -0
- package/dist/service-plan.js +13 -0
- package/dist/service-plan.js.map +1 -1
- package/dist/settings-api.d.ts +4 -0
- package/dist/settings-api.js +21 -3
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +88 -4
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +2 -0
- package/dist/settings-confirmation.js +14 -8
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-control.js +2 -1
- package/dist/settings-control.js.map +1 -1
- package/dist/settings-effect.js +2 -0
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.d.ts +2 -0
- package/dist/settings-http-confirmation.js +3 -2
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-request-capability.d.ts +4 -1
- package/dist/settings-request-capability.js +11 -3
- package/dist/settings-request-capability.js.map +1 -1
- package/dist/setup-auth.d.ts +1 -0
- package/dist/setup-auth.js +2 -1
- package/dist/setup-auth.js.map +1 -1
- package/dist/setup-host.d.ts +1 -0
- package/dist/setup-host.js +7 -3
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +1 -1
- package/dist/setup-wizard.js.map +1 -1
- package/dist/task-board-summary.d.ts +101 -0
- package/dist/task-board-summary.js +177 -0
- package/dist/task-board-summary.js.map +1 -0
- package/dist/task-board-waker.d.ts +8 -1
- package/dist/task-board-waker.js +15 -6
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tmux-control.js +1 -1
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-exec-helper.d.ts +13 -3
- package/dist/tmux-exec-helper.js +34 -29
- package/dist/tmux-exec-helper.js.map +1 -1
- package/dist/tmux-manager.js +1 -1
- package/dist/tmux-read.js +1 -1
- package/dist/tmux-read.js.map +1 -1
- package/dist/topic-commands.d.ts +3 -0
- package/dist/topic-commands.js +29 -18
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +5 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +2 -0
- package/dist/transcript-sources.js +4 -0
- package/dist/transcript-sources.js.map +1 -1
- package/dist/tunnel/cloudflared-install.js +3 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +1 -1
- package/dist/tunnel/cloudflared.js +51 -10
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/named-diagnostics.d.ts +13 -0
- package/dist/tunnel/named-diagnostics.js +52 -0
- package/dist/tunnel/named-diagnostics.js.map +1 -0
- package/dist/tunnel/types.d.ts +2 -1
- package/dist/tunnel/types.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/ui/app-effects.js +10 -4
- package/dist/ui/chat-strings.js +3 -0
- package/dist/ui/chat-thread.js +14 -7
- package/dist/ui/effect-trial-offer.js +2 -31
- package/dist/ui/effects-waker.js +2 -70
- package/dist/ui/fleet-task-card.js +5 -5
- package/dist/ui/fleet-task-common.js +43 -9
- package/dist/ui/fleet-task-work.js +107 -0
- package/dist/ui/fleet-tasks.js +46 -12
- package/dist/ui/image-lightbox.js +39 -1
- package/dist/ui/image-zoom.js +242 -0
- package/dist/ui/panel-chat.js +24 -13
- package/dist/ui/panel-details.js +2 -0
- package/dist/ui/panel-fleet.js +6 -2
- package/dist/ui/panel-needs.js +21 -6
- package/dist/ui/panel-settings.js +35 -13
- package/dist/ui/preview-panel.js +2 -1
- package/dist/ui/search-dialog.js +2 -1
- package/dist/ui/settings-appearance.js +44 -10
- package/dist/ui/settings-dialogs.js +33 -17
- package/dist/ui/settings-discord.js +6 -2
- package/dist/ui/settings-guests.js +2 -1
- package/dist/ui/settings-model.js +2 -11
- package/dist/ui/settings-mods.js +8 -3
- package/dist/ui/settings-strings.js +32 -0
- package/dist/ui/settings-token.js +3 -1
- package/dist/ui/settings-web-authority.js +6 -3
- package/dist/ui/settings-wizard.js +13 -7
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-needs.js +2 -2
- package/dist/ui/shared/app-store.js +2 -2
- package/dist/ui/shared/app.css +93 -32
- package/dist/ui/shared/app.js +5 -1
- package/dist/ui/shared/effect-app-runtime.js +7 -5
- package/dist/ui/shared/effect-chat-runtime.js +2 -7
- package/dist/ui/shared/effect-cursor.js +3 -2
- package/dist/ui/shared/effect-display-metadata.js +39 -0
- package/dist/ui/shared/effect-registry.js +4 -2
- package/dist/ui/shared/effect-runtime-loader.js +3 -1
- package/dist/ui/shared/effect-stock-trust.js +5 -5
- package/dist/ui/shared/feature-flags.js +127 -0
- package/dist/ui/shared/identity-color.js +6 -3
- package/dist/ui/shared/instance-nav.js +3 -2
- package/dist/ui/shared/panel-view.js +14 -29
- package/dist/ui/shared/text-undo.js +210 -0
- package/dist/ui/shared/ui-dialog.js +4 -3
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/usage-dialog.js +29 -8
- package/dist/ui/shared/view-strings.js +2 -4
- package/dist/ui/web-terminal/terminal.html +10 -10
- package/dist/ui/web-terminal/terminal.js +58 -12
- package/dist/update-dispatch.js +1 -1
- package/dist/update-launch.d.ts +1 -1
- package/dist/update-launch.js +1 -2
- package/dist/update-launch.js.map +1 -1
- package/dist/update-preflight.js +1 -1
- package/dist/usage/providers.js +1 -1
- package/dist/usage/providers.js.map +1 -1
- package/dist/view-api.js +1 -1
- package/dist/web-api.d.ts +6 -0
- package/dist/web-api.js +163 -21
- package/dist/web-api.js.map +1 -1
- package/dist/web-app-icon-worker.js +2 -1
- package/dist/web-app-icon-worker.js.map +1 -1
- package/dist/web-app-icon.d.ts +10 -8
- package/dist/web-app-icon.js +14 -13
- package/dist/web-app-icon.js.map +1 -1
- package/dist/web-appearance-tools.d.ts +1 -0
- package/dist/web-appearance-tools.js +7 -3
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance.d.ts +4 -10
- package/dist/web-appearance.js +4 -3
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-auth.d.ts +8 -4
- package/dist/web-auth.js +28 -17
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +14 -0
- package/dist/web-chat-history.js +26 -1
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-cookie-names.d.ts +5 -0
- package/dist/web-cookie-names.js +12 -0
- package/dist/web-cookie-names.js.map +1 -0
- package/dist/web-experimental-http.d.ts +2 -0
- package/dist/web-experimental-http.js +12 -0
- package/dist/web-experimental-http.js.map +1 -0
- package/dist/web-experimental.d.ts +11 -0
- package/dist/web-experimental.js +49 -0
- package/dist/web-experimental.js.map +1 -0
- package/dist/web-guest-access.d.ts +1 -0
- package/dist/web-guest-access.js +4 -2
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-mod-store.d.ts +2 -0
- package/dist/web-mod-store.js +10 -0
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-png.d.ts +4 -0
- package/dist/web-png.js +10 -2
- package/dist/web-png.js.map +1 -1
- package/dist/web-preview.d.ts +2 -0
- package/dist/web-preview.js +2 -0
- package/dist/web-preview.js.map +1 -1
- package/dist/web-returning-device.d.ts +4 -2
- package/dist/web-returning-device.js +12 -5
- package/dist/web-returning-device.js.map +1 -1
- package/dist/web-settings-patch.d.ts +2 -1
- package/dist/web-settings-patch.js +5 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +1 -0
- package/dist/web-settings-tools.js +14 -10
- package/dist/web-settings-tools.js.map +1 -1
- package/dist/web-terminal.js +1 -1
- package/dist/web-theme-store.d.ts +2 -2
- package/dist/web-theme-store.js +6 -3
- package/dist/web-theme-store.js.map +1 -1
- package/package.json +11 -11
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// Public (/assets/): an anonymous View reader gets the same list, fed by View's roster read instead of status frames.
|
|
10
10
|
import { html, useLayoutEffect, useRef, useState } from "./app-html.js";
|
|
11
11
|
import { t } from "./app-i18n.js";
|
|
12
|
+
import { recordEdit } from "./text-undo.js";
|
|
12
13
|
import { appStore, createStore, guestPage, useStore } from "./app-store.js";
|
|
13
14
|
import { chatPath, detailsPath, logPath, viewPath } from "./app-route.js";
|
|
14
15
|
import { Icon } from "./ui-icons.js";
|
|
@@ -648,9 +649,9 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
648
649
|
<label class="v-filter-box"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("filterPh")}</span>
|
|
649
650
|
<input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck=${false} placeholder=${tn("filterPh")} value=${f.q}
|
|
650
651
|
onInput=${(e) => setNavFilter({ q: e.target.value })}
|
|
651
|
-
onKeyDown=${(e) => { if (e.key === "Escape" && f.q) { e.preventDefault(); e.stopPropagation(); setNavFilter({ q: "" }); } }} />
|
|
652
|
+
onKeyDown=${(e) => { if (e.key === "Escape" && f.q) { e.preventDefault(); e.stopPropagation(); recordEdit(e.target); setNavFilter({ q: "" }); } }} />
|
|
652
653
|
${f.q ? html`<button type="button" class="icon-btn v-filter-clear" aria-label=${tn("filterClear")} title=${tn("filterClear")}
|
|
653
|
-
onClick=${() => { setNavFilter({ q: "" }); filterRef.current && filterRef.current.focus(); }}><${Icon} name="close" size=${14} /></button>` : null}
|
|
654
|
+
onClick=${() => { recordEdit(filterRef.current); setNavFilter({ q: "" }); filterRef.current && filterRef.current.focus(); }}><${Icon} name="close" size=${14} /></button>` : null}
|
|
654
655
|
</label>
|
|
655
656
|
<div class="nav-facets">
|
|
656
657
|
<div class="nav-facet-groups">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// #1408 step 2: the View panel (/view, /view/<name>) — every agent's live terminal, its
|
|
1
|
+
// #1408 step 2: the View panel (/view, /view/<name>) — every agent's live terminal, its identity card, the AI usage
|
|
2
2
|
// panel, in the app shell. Public (/assets/): under `web.view_access: open` an anonymous reader opens it in the
|
|
3
3
|
// View-only shell, so nothing here needs a session to show, and the one write (Edit profile) asks for one.
|
|
4
4
|
//
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Everything recurring is a passive read (#1374) taken through the navigation's lease: the pane every 800 ms and the
|
|
7
7
|
// roster every 5 s while the panel is mounted and the tab is visible, usage every 60 s while its dialog is open. They
|
|
8
8
|
// stop when the panel goes.
|
|
9
|
-
import { html, useEffect,
|
|
9
|
+
import { html, useEffect, useRef, useState } from "./app-html.js";
|
|
10
10
|
import { t } from "./app-i18n.js";
|
|
11
11
|
import { appStore, createStore, useStore } from "./app-store.js";
|
|
12
12
|
import { useLease } from "./app-ctx.js";
|
|
@@ -16,7 +16,7 @@ import { HeaderTools, VIEW_SCALE, textSizeStore } from "./header-tools.js";
|
|
|
16
16
|
import { readStream } from "./read-stream.js";
|
|
17
17
|
// The usage dialog's metric row lives with the dialog now (usage-dialog.js); re-exported for what imported it here.
|
|
18
18
|
export { UsageMetric } from "./usage-dialog.js";
|
|
19
|
-
import {
|
|
19
|
+
import { STATUS_KEYS } from "./instance-nav.js";
|
|
20
20
|
import { viewStore } from "./view-roster-store.js";
|
|
21
21
|
|
|
22
22
|
/** #1580: the app's stream says the fleet is not answering (a restart): the View's polls skip their turn until it is. */
|
|
@@ -189,7 +189,7 @@ export function ViewPanel({ route, navKey }) {
|
|
|
189
189
|
else if (!name) body = html`<div data-trust="panel-content" class="panel-body"><${Skeleton} lines=${6} /></div>`;
|
|
190
190
|
else if (!it) body = html`<div data-trust="panel-content" class="panel-body center"><${Empty} icon="alert" title=${tn("notFound", { name })} hint=${tn("notFoundHint")} /></div>`;
|
|
191
191
|
else body = html`<${Terminal} key=${name} name=${name} lease=${lease} density=${density} />
|
|
192
|
-
<${
|
|
192
|
+
<${ViewIdentity} it=${it} viewOnly=${viewOnly} onEdit=${() => setDialog("edit")} />`;
|
|
193
193
|
|
|
194
194
|
return html`<div data-effect-panel="view" class="panel p-view">
|
|
195
195
|
<${PanelHeader} title=${it ? (it.display_name || it.instance_name) : tn("title")}
|
|
@@ -287,33 +287,18 @@ function glyphRatio(pre) {
|
|
|
287
287
|
return cellRatio || 0.6;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
/** The
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const avatar = useRef(null);
|
|
294
|
-
// The avatar is fetched once per instance (cache-busted then), not on every 5-second roster refresh.
|
|
295
|
-
useLayoutEffect(() => {
|
|
296
|
-
if (!avatar.current || !it.has_avatar) return;
|
|
297
|
-
avatar.current.src = `/api/avatar/${encodeURIComponent(it.instance_name)}?t=${Date.now()}`;
|
|
298
|
-
}, [it.instance_name, it.has_avatar]);
|
|
299
|
-
const initial = (it.display_name || it.instance_name || "?").trim().charAt(0) || "?";
|
|
300
|
-
const ctx = it.context_pct != null ? ` · ctx ${Math.round(it.context_pct)}%` : "";
|
|
301
|
-
const toggle = () => { const next = !open; setOpen(next); store("agend_view_card_expanded", next ? "1" : "0"); };
|
|
302
|
-
// #1629 r4 §1: the identity card, above the profile row while it is expanded.
|
|
290
|
+
/** The identity card under the terminal, with Edit profile (or Sign in to edit) below it. Always on: the old one-line
|
|
291
|
+
* strip is gone, and IdentityCard itself is null when the card is empty, so the button lives here rather than inside it. */
|
|
292
|
+
function ViewIdentity({ it, viewOnly, onEdit }) {
|
|
303
293
|
const card = useCard(it.instance_name, it.card_v);
|
|
304
|
-
return html
|
|
305
|
-
|
|
306
|
-
<div class="v-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
294
|
+
return html`<div data-trust="panel-content" class="v-id">
|
|
295
|
+
${card ? html`<${IdentityCard} name=${it.instance_name} card=${card} />` : null}
|
|
296
|
+
<div class="v-id-actions">
|
|
297
|
+
${viewOnly
|
|
298
|
+
? html`<a class="btn btn-sm" href=${signInHref()}><${Icon} name="edit" size=${14} />${tn("signInToEdit")}</a>`
|
|
299
|
+
: html`<button type="button" class="btn btn-sm" onClick=${onEdit}><${Icon} name="edit" size=${14} />${tn("editProfile")}</button>`}
|
|
310
300
|
</div>
|
|
311
|
-
|
|
312
|
-
<${Icon} name="chevron" size=${16} cls=${open ? "caret" : "caret flipped"} /></button>
|
|
313
|
-
${viewOnly
|
|
314
|
-
? html`<a class="btn btn-sm" href=${signInHref()}><${Icon} name="edit" size=${14} />${tn("signInToEdit")}</a>`
|
|
315
|
-
: html`<button type="button" class="btn btn-sm" onClick=${onEdit}><${Icon} name="edit" size=${14} />${tn("editProfile")}</button>`}
|
|
316
|
-
</section>`;
|
|
301
|
+
</div>`;
|
|
317
302
|
}
|
|
318
303
|
|
|
319
304
|
/** Edit profile: display name, role, description, avatar. A write: it needs a session (CSRF via agend-auth.js). */
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// #2342: undo and redo in every text field — one history per field, kept here instead of the browser's.
|
|
2
|
+
//
|
|
3
|
+
// The browser's own undo is one stack for the whole page (Chromium and WebKit alike): Ctrl+Z in one box undoes the
|
|
4
|
+
// last edit in whichever box had it, and any script write to a field's value (a completion, a clear, a re-render
|
|
5
|
+
// that differs from the box) wipes the stack. So the keys are taken here, but only in a text field:
|
|
6
|
+
// - undo: Ctrl+Z (⌘Z on a Mac); redo: Ctrl+Shift+Z or Ctrl+Y (⌘⇧Z on a Mac, where Ctrl+Y is not redo). Anywhere else
|
|
7
|
+
// (a button, the page, a select) nothing is taken; a key with Alt, or mid-IME-composition, is left alone too.
|
|
8
|
+
// - a field's steps are recorded from its edits (beforeinput, input): typing is grouped into one step until a pause
|
|
9
|
+
// (UNDO.PAUSE_MS), a space, a different kind of edit (typing ↔ deleting), a paste, a line break or the caret moving
|
|
10
|
+
// elsewhere; an IME composition is one step, recorded when it ends.
|
|
11
|
+
// - the browser's own Undo and Redo commands (its Edit menu, a context menu: a trusted beforeinput historyUndo /
|
|
12
|
+
// historyRedo) go through the same history: cancelled and done here. Where a browser cannot cancel them (or a script
|
|
13
|
+
// runs execCommand("undo"), with no beforeinput at all), its own undo goes ahead and what it leaves resets the field's
|
|
14
|
+
// history (never a step that would undo the undo).
|
|
15
|
+
// - a script's change to a field's value the person should be able to undo (a completion, a pick, putting text back)
|
|
16
|
+
// calls recordEdit(field) first: the change becomes a step. Any other change (a clear after send, a reload after
|
|
17
|
+
// save) resets the field's history, as the browser's own undo does — a sent message is never undone back into the box.
|
|
18
|
+
// - undo and redo write the value and the caret back and dispatch `input`, so the page's state (Preact) follows.
|
|
19
|
+
// - password fields are left to the browser: no secret is kept here.
|
|
20
|
+
|
|
21
|
+
import { useState } from "./app-html.js";
|
|
22
|
+
|
|
23
|
+
export const UNDO = { PAUSE_MS: 1000, MAX_STEPS: 100 };
|
|
24
|
+
|
|
25
|
+
const TEXT_TYPES = new Set(["text", "search", "url", "email", "tel", "number", ""]);
|
|
26
|
+
/** A field this keeps a history for: a textarea, or a text-like input (never a password) that is not read-only or disabled. */
|
|
27
|
+
export function isTextField(el) {
|
|
28
|
+
if (!el || el.readOnly || el.disabled) return false;
|
|
29
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
30
|
+
if (tag === "textarea") return true;
|
|
31
|
+
return tag === "input" && TEXT_TYPES.has((el.getAttribute && el.getAttribute("type") || el.type || "").toLowerCase());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const histories = new WeakMap();
|
|
35
|
+
/** The value and the caret of `el`. A number input has no selection: its caret is null. */
|
|
36
|
+
function snap(el) {
|
|
37
|
+
let s = null, e = null;
|
|
38
|
+
try { s = el.selectionStart; e = el.selectionEnd; } catch { /* a number input */ }
|
|
39
|
+
return { v: String(el.value ?? ""), s: s ?? null, e: e ?? null };
|
|
40
|
+
}
|
|
41
|
+
function historyFor(el) {
|
|
42
|
+
let h = histories.get(el);
|
|
43
|
+
if (!h) { h = { undo: [], redo: [], last: snap(el), kind: null, at: 0, marked: null, composing: false }; histories.set(el, h); }
|
|
44
|
+
return h;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Bring the history up to the field's value: a change made by a script since the last edit is a step when recordEdit
|
|
48
|
+
* marked it, and otherwise resets the history (what the browser does too).
|
|
49
|
+
*/
|
|
50
|
+
function sync(el, h) {
|
|
51
|
+
const now = String(el.value ?? "");
|
|
52
|
+
if (now === h.last.v) { h.marked = null; return; }
|
|
53
|
+
if (h.marked) { push(h.undo, h.marked); h.redo = []; }
|
|
54
|
+
else { h.undo = []; h.redo = []; }
|
|
55
|
+
h.marked = null;
|
|
56
|
+
h.last = snap(el);
|
|
57
|
+
h.kind = null;
|
|
58
|
+
}
|
|
59
|
+
function push(list, s) {
|
|
60
|
+
list.push(s);
|
|
61
|
+
if (list.length > UNDO.MAX_STEPS) list.shift();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The kind of an edit, for grouping: consecutive typing (or deleting) of one kind is one step. */
|
|
65
|
+
function kindOf(e) {
|
|
66
|
+
const t = e.inputType || "";
|
|
67
|
+
if (t === "insertText") return e.data && /\s/.test(e.data) ? "space" : "type";
|
|
68
|
+
if (t === "deleteContentBackward" || t === "deleteContentForward") return "delete";
|
|
69
|
+
return "other"; // a paste, a drop, a cut, a line break, a word delete: its own step
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Called before a script changes `el`'s value in a way the person may undo: the value now is the step to go back to. */
|
|
73
|
+
export function recordEdit(el) {
|
|
74
|
+
if (!isTextField(el)) return;
|
|
75
|
+
const h = historyFor(el);
|
|
76
|
+
sync(el, h);
|
|
77
|
+
h.marked = snap(el);
|
|
78
|
+
h.kind = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The history kept for `el` (tests). */
|
|
82
|
+
export function historyOf(el) {
|
|
83
|
+
const h = histories.get(el);
|
|
84
|
+
return h ? { undo: h.undo.map((s) => s.v), redo: h.redo.map((s) => s.v), last: h.last.v } : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Put snapshot `s` into `el` and tell the page (an `input` event, as an edit would). */
|
|
88
|
+
function apply(el, s, type, win) {
|
|
89
|
+
el.value = s.v;
|
|
90
|
+
if (s.s != null) { try { el.setSelectionRange(s.s, s.e ?? s.s); } catch { /* a number input */ } }
|
|
91
|
+
const Ev = win && typeof win.InputEvent === "function" ? win.InputEvent : win && win.Event;
|
|
92
|
+
if (Ev) el.dispatchEvent(new Ev("input", { bubbles: true, ...(Ev === win.InputEvent ? { inputType: type } : {}) }));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Undo (or redo) on `el`. False when there is nothing to do.
|
|
97
|
+
*/
|
|
98
|
+
export function step(el, redo, win = typeof window !== "undefined" ? window : null) {
|
|
99
|
+
if (!isTextField(el)) return false;
|
|
100
|
+
const h = historyFor(el);
|
|
101
|
+
sync(el, h);
|
|
102
|
+
const from = redo ? h.redo : h.undo, to = redo ? h.undo : h.redo;
|
|
103
|
+
if (!from.length) return false;
|
|
104
|
+
to.push(h.last);
|
|
105
|
+
h.last = from.pop();
|
|
106
|
+
h.kind = null;
|
|
107
|
+
h.applying = true;
|
|
108
|
+
try { apply(el, h.last, redo ? "historyRedo" : "historyUndo", win); } finally { h.applying = false; }
|
|
109
|
+
h.last = snap(el);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Which history key `e` is, or null: "undo", "redo". `mac`: ⌘ is the modifier and Ctrl+Y is not redo. */
|
|
114
|
+
export function historyKey(e, mac) {
|
|
115
|
+
if (e.altKey || e.isComposing || e.keyCode === 229) return null;
|
|
116
|
+
const mod = mac ? e.metaKey && !e.ctrlKey : e.ctrlKey && !e.metaKey;
|
|
117
|
+
if (!mod) return null;
|
|
118
|
+
const k = (e.key || "").toLowerCase();
|
|
119
|
+
if (k === "z") return e.shiftKey ? "redo" : "undo";
|
|
120
|
+
if (k === "y" && !e.shiftKey && !mac) return "redo";
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Keep a history for every text field under `doc` and take the undo/redo keys in them. Returns an uninstall function.
|
|
126
|
+
*/
|
|
127
|
+
export function installTextUndo({ doc = document, win = typeof window !== "undefined" ? window : null, mac = false } = {}) {
|
|
128
|
+
const field = (e) => (isTextField(e.target) ? e.target : null);
|
|
129
|
+
const onBefore = (e) => {
|
|
130
|
+
const el = field(e);
|
|
131
|
+
// WebKit commits a composition after compositionend (deleteCompositionText, insertFromComposition): its step is
|
|
132
|
+
// the one compositionstart began.
|
|
133
|
+
if (!el || e.isComposing || /Composition/.test(e.inputType || "")) return;
|
|
134
|
+
const h = historyFor(el);
|
|
135
|
+
if (h.applying || h.composing) return; // a browser that does not flag its composition events
|
|
136
|
+
if (e.inputType === "historyUndo" || e.inputType === "historyRedo") {
|
|
137
|
+
if (e.cancelable) { e.preventDefault(); step(el, e.inputType === "historyRedo", win); }
|
|
138
|
+
else h.native = true;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
sync(el, h);
|
|
142
|
+
const kind = kindOf(e), now = Date.now(), at = snap(el);
|
|
143
|
+
// The same run of typing (or deleting) goes on: one step. A space ends a word's step (the next word is a new one).
|
|
144
|
+
// Anything else starts a new step from the value now.
|
|
145
|
+
const same = kind === "space" ? h.kind === "type" : kind !== "other" && kind === h.kind;
|
|
146
|
+
const goesOn = same && now - h.at < UNDO.PAUSE_MS && at.s === h.last.s && at.e === h.last.e;
|
|
147
|
+
if (!goesOn) { push(h.undo, at); h.redo = []; }
|
|
148
|
+
h.kind = kind;
|
|
149
|
+
h.at = now;
|
|
150
|
+
};
|
|
151
|
+
const onInput = (e) => {
|
|
152
|
+
const el = field(e);
|
|
153
|
+
if (!el) return;
|
|
154
|
+
const h = historyFor(el);
|
|
155
|
+
if (h.applying) return;
|
|
156
|
+
// The browser's own undo or redo went ahead (one it could not cancel, or a script's execCommand("undo"), which has no
|
|
157
|
+
// beforeinput): what it left is a change of its own — the history starts afresh.
|
|
158
|
+
if (h.native || e.inputType === "historyUndo" || e.inputType === "historyRedo") { h.native = false; h.undo = []; h.redo = []; h.kind = null; }
|
|
159
|
+
h.last = snap(el); // mid-composition too: the value the next edit starts from
|
|
160
|
+
};
|
|
161
|
+
const onCompStart = (e) => {
|
|
162
|
+
const el = field(e);
|
|
163
|
+
if (!el) return;
|
|
164
|
+
const h = historyFor(el);
|
|
165
|
+
sync(el, h);
|
|
166
|
+
push(h.undo, snap(el)); h.redo = [];
|
|
167
|
+
h.composing = true; h.kind = null;
|
|
168
|
+
};
|
|
169
|
+
const onCompEnd = (e) => {
|
|
170
|
+
const el = field(e);
|
|
171
|
+
if (!el) return;
|
|
172
|
+
const h = historyFor(el);
|
|
173
|
+
h.composing = false;
|
|
174
|
+
h.last = snap(el); // WebKit's last input comes after compositionend: onInput updates it again
|
|
175
|
+
};
|
|
176
|
+
const onKey = (e) => {
|
|
177
|
+
const el = field(e);
|
|
178
|
+
if (!el) return;
|
|
179
|
+
const which = historyKey(e, mac);
|
|
180
|
+
if (!which || historyFor(el).composing) return;
|
|
181
|
+
e.preventDefault(); // never the browser's page-wide stack, even when this field has no step
|
|
182
|
+
step(el, which === "redo", win);
|
|
183
|
+
};
|
|
184
|
+
doc.addEventListener("beforeinput", onBefore, true);
|
|
185
|
+
doc.addEventListener("input", onInput, true);
|
|
186
|
+
doc.addEventListener("compositionstart", onCompStart, true);
|
|
187
|
+
doc.addEventListener("compositionend", onCompEnd, true);
|
|
188
|
+
doc.addEventListener("keydown", onKey, true);
|
|
189
|
+
return () => {
|
|
190
|
+
doc.removeEventListener("beforeinput", onBefore, true);
|
|
191
|
+
doc.removeEventListener("input", onInput, true);
|
|
192
|
+
doc.removeEventListener("compositionstart", onCompStart, true);
|
|
193
|
+
doc.removeEventListener("compositionend", onCompEnd, true);
|
|
194
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const trimmed = (v) => v.trim();
|
|
199
|
+
/**
|
|
200
|
+
* A field whose owner keeps its value normalized (trimmed, or a number): the box shows the text as typed and the owner
|
|
201
|
+
* gets `norm(text)`. Binding the box to the normalized value would rewrite it while the person types (a space vanishes,
|
|
202
|
+
* "1.50" turns into "1.5") and lose the field's undo history. When the owner's value becomes something the text does not
|
|
203
|
+
* normalize to (a clear, a load), the box shows `show(value)`.
|
|
204
|
+
* Returns [text for the box, onText(typed) → the normalized value].
|
|
205
|
+
*/
|
|
206
|
+
export function useTypedText(value, norm = trimmed, show = (v) => (v == null ? "" : String(v))) {
|
|
207
|
+
const [raw, setRaw] = useState(null);
|
|
208
|
+
const text = raw !== null && Object.is(norm(raw), value) ? raw : show(value);
|
|
209
|
+
return [text, (typed) => { setRaw(typed); return norm(typed); }];
|
|
210
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// #1408 §7: one dialog for the whole app — the native <dialog> opened with showModal(), so the browser traps focus,
|
|
2
2
|
// makes the rest inert and closes it on Esc. Focus goes back to whatever opened it. On a phone it is a bottom sheet
|
|
3
|
-
// with its own × (app.css). Text and buttons only; no style attribute (#1300).
|
|
3
|
+
// with its own × (app.css). Text and buttons only; no style attribute (#1300). `drawer`: on a desk it comes in from the
|
|
4
|
+
// right edge at full height, the page still in view beside it (the task board's card); a phone keeps the bottom sheet.
|
|
4
5
|
import { html, useEffect, useLayoutEffect, useRef } from "./app-html.js";
|
|
5
6
|
import { Icon } from "./ui-icons.js";
|
|
6
7
|
import { t } from "./app-i18n.js";
|
|
@@ -9,7 +10,7 @@ import { t } from "./app-i18n.js";
|
|
|
9
10
|
* <${Dialog} title="Create task" onClose=${…} actions=${html`…`}>…body…</${Dialog}>
|
|
10
11
|
* Rendered = open; the parent unmounts it to close. `busy` keeps Esc and the backdrop from closing it mid-request.
|
|
11
12
|
*/
|
|
12
|
-
export function Dialog({ title, onClose, actions, children, busy = false, wide = false, label }) {
|
|
13
|
+
export function Dialog({ title, onClose, actions, children, busy = false, wide = false, drawer = false, label }) {
|
|
13
14
|
const ref = useRef(null);
|
|
14
15
|
const opener = useRef(null);
|
|
15
16
|
const busyRef = useRef(busy);
|
|
@@ -35,7 +36,7 @@ export function Dialog({ title, onClose, actions, children, busy = false, wide =
|
|
|
35
36
|
d.addEventListener("click", onClick);
|
|
36
37
|
return () => { d.removeEventListener("cancel", onCancel); d.removeEventListener("click", onClick); };
|
|
37
38
|
}, [onClose]);
|
|
38
|
-
return html`<dialog data-trust="dialog" ref=${ref} class=${`dlg${wide ? " dlg-wide" : ""}`} aria-label=${label || title}>
|
|
39
|
+
return html`<dialog data-trust="dialog" ref=${ref} class=${`dlg${wide ? " dlg-wide" : ""}${drawer ? " dlg-drawer" : ""}`} aria-label=${label || title}>
|
|
39
40
|
<div class="dlg-box">
|
|
40
41
|
<div class="dlg-head"><h2 class="dlg-title">${title}</h2>
|
|
41
42
|
<button type="button" class="icon-btn dlg-x" aria-label=${t("app.close")} title=${t("app.close")} onClick=${() => { if (!busyRef.current) onClose(); }}><${Icon} name="close" /></button></div>
|
|
@@ -7,6 +7,8 @@ const P = {
|
|
|
7
7
|
menu: () => html`<path d="M4 6h16M4 12h16M4 18h16"/>`,
|
|
8
8
|
sidebar: () => html`<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16"/>`,
|
|
9
9
|
plus: () => html`<path d="M12 5v14M5 12h14"/>`,
|
|
10
|
+
minus: () => html`<path d="M5 12h14"/>`, // #2333: the lightbox's zoom out
|
|
11
|
+
fitwidth: () => html`<path d="M3 4v16M21 4v16M7 12h10M10 9l-3 3 3 3M14 9l3 3-3 3"/>`, // #2333: fit width
|
|
10
12
|
edit: () => html`<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/>`,
|
|
11
13
|
more: () => html`<circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/>`,
|
|
12
14
|
chat: () => html`<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>`,
|
|
@@ -107,9 +107,20 @@ export function providerTitle(p) {
|
|
|
107
107
|
* tooltip and for a screen reader.
|
|
108
108
|
*/
|
|
109
109
|
export function planBadge(plan) {
|
|
110
|
-
const
|
|
111
|
-
|
|
110
|
+
const text = String(plan || "").trim();
|
|
111
|
+
const m = /^(\S+)(?: [^()]+)? \((\d+(?:\.\d+)?)x\)$/i.exec(text);
|
|
112
|
+
if (m) return `${m[1]} ×${m[2]}`;
|
|
113
|
+
// #2412: a long name ("Self Serve Business Usage Based", Codex's plan_type spelled out) says only its kind, so the badge
|
|
114
|
+
// never takes the row from the provider's name; the full name is the badge's tooltip.
|
|
115
|
+
if (text.length > PLAN_BADGE_MAX) {
|
|
116
|
+
for (const [word, short] of PLAN_KINDS) if (word.test(text)) return short;
|
|
117
|
+
}
|
|
118
|
+
return text;
|
|
112
119
|
}
|
|
120
|
+
const PLAN_BADGE_MAX = 14;
|
|
121
|
+
/** The kinds a long plan name is shortened to, the most specific first. */
|
|
122
|
+
const PLAN_KINDS = [[/\benterprise\b/i, "Enterprise"], [/\bbusiness\b/i, "Business"], [/\bteam\b/i, "Team"], [/\bedu(?:cation)?\b/i, "Edu"],
|
|
123
|
+
[/\bpro\+/i, "Pro+"], [/\bpro\b/i, "Pro"], [/\bplus\b/i, "Plus"], [/\bmax\b/i, "Max"], [/\bfree\b/i, "Free"]];
|
|
113
124
|
/** "Updated 3 min ago" (#1585): how old the numbers are — the panel may show the last snapshot while a fresh one comes. */
|
|
114
125
|
export function updatedAgo(fetchedAt, now = Date.now()) {
|
|
115
126
|
const ms = now - new Date(fetchedAt).getTime();
|
|
@@ -124,7 +135,10 @@ export function updatedAgo(fetchedAt, now = Date.now()) {
|
|
|
124
135
|
*/
|
|
125
136
|
export function UsageBody({ owner }) {
|
|
126
137
|
const lease = useLease(owner);
|
|
127
|
-
const [data, setData] = useState(undefined); // undefined: loading; null: failed
|
|
138
|
+
const [data, setData] = useState(undefined); // undefined: loading; null: failed, with nothing read before
|
|
139
|
+
// The last read failed while numbers were already shown: they stay, with a quiet note (#2236/#2289 for the server's
|
|
140
|
+
// rows; here for the read itself — a dropped or failed read must not flash the error over numbers already on screen).
|
|
141
|
+
const [failed, setFailed] = useState(false);
|
|
128
142
|
const [order, setOrder] = useState(() => { try { const a = JSON.parse(stored(USAGE_ORDER_KEY) || "[]"); return Array.isArray(a) ? a.filter((x) => typeof x === "string") : []; } catch { return []; } });
|
|
129
143
|
const reads = useMemo(() => readStream(lease), [lease]);
|
|
130
144
|
// Refresh (force) supersedes a read on its way; the minute's tick waits for it.
|
|
@@ -134,19 +148,26 @@ export function UsageBody({ owner }) {
|
|
|
134
148
|
try {
|
|
135
149
|
const r = await lease.fetch(`/api/ai-usage${force ? "?force=1" : ""}`);
|
|
136
150
|
if (!reads.live(token)) return;
|
|
151
|
+
// A refused or failed answer (a 500, a proxy's 502 page, a session that lapsed) is a failed read, not numbers.
|
|
152
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
137
153
|
const d = await r.json();
|
|
138
|
-
if (
|
|
139
|
-
|
|
154
|
+
if (!d || !Array.isArray(d.providers)) throw new Error("no providers");
|
|
155
|
+
if (reads.live(token)) { setData(d); setFailed(false); }
|
|
156
|
+
} catch {
|
|
157
|
+
if (!reads.live(token)) return;
|
|
158
|
+
setData((had) => (had && Array.isArray(had.providers) ? had : null)); // the last numbers stay; none: the error
|
|
159
|
+
setFailed(true);
|
|
160
|
+
}
|
|
140
161
|
finally { reads.end(token); }
|
|
141
162
|
};
|
|
142
163
|
useEffect(() => { load(false); lease.interval(() => load(false), 60_000); }, [lease]);
|
|
143
164
|
// #1585: the server answered with the last snapshot while it fetches a fresh one — read again shortly (the same
|
|
144
165
|
// cache: no vendor call of its own) so the fresh numbers replace it without waiting for the minute's tick.
|
|
145
166
|
useEffect(() => {
|
|
146
|
-
if (!data || !data.refreshing) return undefined;
|
|
167
|
+
if (!data || !data.refreshing || failed) return undefined;
|
|
147
168
|
const timer = setTimeout(() => load(false), 4_000);
|
|
148
169
|
return () => clearTimeout(timer);
|
|
149
|
-
}, [data]);
|
|
170
|
+
}, [data, failed]);
|
|
150
171
|
const key = (p) => String(p.id || p.name || "");
|
|
151
172
|
const ordered = (providers) => {
|
|
152
173
|
const rank = new Map(order.map((id, i) => [id, i]));
|
|
@@ -182,7 +203,7 @@ export function UsageBody({ owner }) {
|
|
|
182
203
|
: html`${(p.metrics || []).map((m, j) => html`<${UsageMetric} key=${j} m=${m} />`)}
|
|
183
204
|
${!p.stale && p.hint ? html`<p class="note">${usageText(p.hint, p.hintI18n)}</p>` : !(p.metrics || []).length ? html`<p class="note">${tn("usage.no_data")}</p>` : null}`}
|
|
184
205
|
</section>`)}
|
|
185
|
-
<div class="u-foot"><span class="note" title=${new Date(data.fetchedAt).toLocaleString()}>${updatedAgo(data.fetchedAt)}${data.refreshing ? html` · <span class="u-refreshing">${tn("usageRefreshing")}</span>` : null}</span>
|
|
206
|
+
<div class="u-foot"><span class="note" title=${new Date(data.fetchedAt).toLocaleString()}>${updatedAgo(data.fetchedAt)}${failed ? html` · <span class="u-refresh-failed" role="status">${tn("usageRefreshFailed")}</span>` : data.refreshing ? html` · <span class="u-refreshing">${tn("usageRefreshing")}</span>` : null}</span>
|
|
186
207
|
${byAgent() ? html`<a class="btn btn-ghost btn-sm u-by-agent" href="/ui/fleet/usage" title=${tn("usageByAgentTitle")}>${tn("usageByAgent")}</a>` : null}
|
|
187
208
|
<button type="button" class="btn btn-sm" onClick=${() => load(true)}>${tn("usageRefresh")}</button></div>`;
|
|
188
209
|
}
|
|
@@ -37,6 +37,7 @@ register("view", {
|
|
|
37
37
|
"usageUpdatedAgo": "Updated {age} ago",
|
|
38
38
|
"usageUpdatedJustNow": "Updated just now",
|
|
39
39
|
"usageRefreshing": "Refreshing…",
|
|
40
|
+
"usageRefreshFailed": "Couldn't refresh — showing the last numbers",
|
|
40
41
|
"usageMinutes": "{n} min",
|
|
41
42
|
"usageHours": "{n} h",
|
|
42
43
|
"usageRefresh": "Refresh",
|
|
@@ -164,7 +165,6 @@ register("view", {
|
|
|
164
165
|
"usageButton": "Usage",
|
|
165
166
|
"helpButton": "Help",
|
|
166
167
|
"close": "Close",
|
|
167
|
-
"expandProfile": "Show the profile",
|
|
168
168
|
"rosterFailed": "Failed to load roster: {error}\n(Is the fleet running?)",
|
|
169
169
|
"paneFailed": "⚠ Could not load pane for {name} (HTTP {status}).",
|
|
170
170
|
"signInToSave": "Sign in to save — your session may have ended.",
|
|
@@ -207,7 +207,6 @@ register("view", {
|
|
|
207
207
|
"textSize": "Text size: {size}",
|
|
208
208
|
"profileTitle": "Edit {name}'s profile",
|
|
209
209
|
"avatar": "Avatar",
|
|
210
|
-
"collapseProfile": "Hide the profile",
|
|
211
210
|
"instancesLabel": "Instances",
|
|
212
211
|
"favorites": "Favorites",
|
|
213
212
|
"favAdd": "Add {name} to favorites",
|
|
@@ -270,6 +269,7 @@ register("view", {
|
|
|
270
269
|
"usageUpdatedAgo": "{age}前更新",
|
|
271
270
|
"usageUpdatedJustNow": "剛剛更新",
|
|
272
271
|
"usageRefreshing": "更新中…",
|
|
272
|
+
"usageRefreshFailed": "無法更新,顯示上次的數據",
|
|
273
273
|
"usageMinutes": "{n} 分鐘",
|
|
274
274
|
"usageHours": "{n} 小時",
|
|
275
275
|
"usageRefresh": "重新整理",
|
|
@@ -397,7 +397,6 @@ register("view", {
|
|
|
397
397
|
"usageButton": "用量",
|
|
398
398
|
"helpButton": "說明",
|
|
399
399
|
"close": "關閉",
|
|
400
|
-
"expandProfile": "顯示個人資料",
|
|
401
400
|
"rosterFailed": "載入 instance 清單失敗:{error}\n(Fleet 是否正在執行?)",
|
|
402
401
|
"paneFailed": "⚠ 無法載入 {name} 的 pane(HTTP {status})。",
|
|
403
402
|
"signInToSave": "請先登入才能儲存:你的 session 可能已結束。",
|
|
@@ -440,7 +439,6 @@ register("view", {
|
|
|
440
439
|
"textSize": "字級:{size}",
|
|
441
440
|
"profileTitle": "編輯 {name} 的個人資料",
|
|
442
441
|
"avatar": "頭像",
|
|
443
|
-
"collapseProfile": "隱藏個人資料",
|
|
444
442
|
"instancesLabel": "Instances",
|
|
445
443
|
"favorites": "最愛",
|
|
446
444
|
"favAdd": "把 {name} 加入最愛",
|
|
@@ -13,25 +13,25 @@
|
|
|
13
13
|
<header id="bar">
|
|
14
14
|
<span id="title">AgEnD terminal</span>
|
|
15
15
|
<span id="ttl" hidden></span>
|
|
16
|
-
<button id="btn-close" type="button" hidden title="End this session now (the command is stopped and the terminal closes)">Stop</button>
|
|
16
|
+
<button id="btn-close" type="button" hidden title="End this session now (the command is stopped and the terminal closes)" data-i18n="stop" data-i18n-title="stopTitle">Stop</button>
|
|
17
17
|
</header>
|
|
18
18
|
<section id="gate">
|
|
19
|
-
<p>This terminal is connected to <strong>one command only</strong> and closes when it exits or when the time limit lapses. There is no shell behind it.</p>
|
|
20
|
-
<p>Paste the <strong>access token</strong> you were given: privately in chat, or in the dashboard's sign-in dialog. It is separate from this link; the link alone grants nothing.</p>
|
|
19
|
+
<p data-i18n-html="gateOne">This terminal is connected to <strong>one command only</strong> and closes when it exits or when the time limit lapses. There is no shell behind it.</p>
|
|
20
|
+
<p data-i18n-html="gateToken">Paste the <strong>access token</strong> you were given: privately in chat, or in the dashboard's sign-in dialog. It is separate from this link; the link alone grants nothing.</p>
|
|
21
21
|
<form id="gate-form" autocomplete="off">
|
|
22
|
-
<input id="token" type="text" inputmode="text" autocapitalize="characters" spellcheck="false" placeholder="access token" maxlength="40" required>
|
|
23
|
-
<button type="submit">Open</button>
|
|
22
|
+
<input id="token" type="text" inputmode="text" autocapitalize="characters" spellcheck="false" placeholder="access token" data-i18n-placeholder="tokenPlaceholder" maxlength="40" required>
|
|
23
|
+
<button type="submit" data-i18n="open">Open</button>
|
|
24
24
|
</form>
|
|
25
25
|
<p id="gate-msg" role="alert"></p>
|
|
26
|
-
<noscript><p role="alert">JavaScript is required: the token is sent with a script, never as part of a page URL
|
|
26
|
+
<noscript><p role="alert">JavaScript is required: the token is sent with a script, never as part of a page URL. 需要啟用 JavaScript:存取權杖由程式送出,絕不會放進網址。</p></noscript>
|
|
27
27
|
</section>
|
|
28
28
|
<main id="term" hidden></main>
|
|
29
29
|
<form id="code-row" hidden autocomplete="off">
|
|
30
|
-
<label for="code">Sign-in code</label>
|
|
31
|
-
<input id="code" type="password" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="1100" placeholder="paste the code here">
|
|
32
|
-
<button type="submit">Send</button>
|
|
30
|
+
<label for="code" data-i18n="codeLabel">Sign-in code</label>
|
|
31
|
+
<input id="code" type="password" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="1100" placeholder="paste the code here" data-i18n-placeholder="codePlaceholder">
|
|
32
|
+
<button type="submit" data-i18n="send">Send</button>
|
|
33
33
|
<span id="code-msg" role="status"></span>
|
|
34
|
-
<p id="code-hint">The terminal does not show what you paste. Paste the code once, then press Enter or Send. Ctrl+C is off on this page — use Stop to cancel.</p>
|
|
34
|
+
<p id="code-hint" data-i18n="codeHint">The terminal does not show what you paste. Paste the code once, then press Enter or Send. Ctrl+C is off on this page — use Stop to cancel.</p>
|
|
35
35
|
</form>
|
|
36
36
|
<footer id="keys" hidden>
|
|
37
37
|
<button data-seq="\u001b" type="button">Esc</button>
|