@songsid/agend 2.2.0-alpha.2 → 2.2.0-alpha.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/README.md +4 -4
- package/README.zh-TW.md +4 -4
- package/dist/agent-endpoint.d.ts +4 -1
- package/dist/agent-endpoint.js +14 -2
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/atomic-write.d.ts +21 -0
- package/dist/atomic-write.js +65 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/codex.d.ts +7 -0
- package/dist/backend/codex.js +7 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/muse.d.ts +7 -0
- package/dist/backend/muse.js +7 -0
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/binary-probe.d.ts +80 -0
- package/dist/binary-probe.js +182 -0
- package/dist/binary-probe.js.map +1 -0
- package/dist/cache-service.d.ts +17 -1
- package/dist/cache-service.js +39 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +5 -1
- package/dist/channel/adapters/discord.js +56 -11
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +38 -1
- package/dist/channel/adapters/telegram.js +82 -0
- 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/tool-router.d.ts +8 -1
- package/dist/channel/tool-router.js +4 -2
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +21 -0
- package/dist/cli.js +133 -38
- package/dist/cli.js.map +1 -1
- package/dist/connection-secrets.d.ts +5 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/cpu-profile.js +50 -0
- package/dist/cpu-profile.js.map +1 -1
- package/dist/daemon.d.ts +12 -0
- package/dist/daemon.js +166 -26
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +1 -0
- package/dist/delivery-outbox.js +12 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/discord-general.d.ts +15 -0
- package/dist/discord-general.js +24 -0
- package/dist/discord-general.js.map +1 -0
- package/dist/discord-permissions.d.ts +33 -0
- package/dist/discord-permissions.js +42 -0
- package/dist/discord-permissions.js.map +1 -0
- package/dist/env-file.d.ts +11 -0
- package/dist/env-file.js +27 -0
- package/dist/env-file.js.map +1 -0
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-context.js.map +1 -1
- package/dist/fleet-level-config.d.ts +8 -1
- package/dist/fleet-level-config.js +31 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +76 -7
- package/dist/fleet-manager.js +610 -142
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-stop-budget.d.ts +22 -0
- package/dist/fleet-stop-budget.js +95 -0
- package/dist/fleet-stop-budget.js.map +1 -0
- package/dist/kiro-transcript-lane.d.ts +8 -4
- package/dist/kiro-transcript-lane.js +66 -44
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/locale.js +88 -0
- package/dist/locale.js.map +1 -1
- package/dist/next-steps.d.ts +5 -0
- package/dist/next-steps.js +26 -0
- package/dist/next-steps.js.map +1 -0
- package/dist/outbound-handlers.js +8 -0
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +4 -0
- package/dist/outbound-schemas.js +6 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/outbox-open-error.d.ts +7 -0
- package/dist/outbox-open-error.js +15 -0
- package/dist/outbox-open-error.js.map +1 -0
- package/dist/pane-context-cache.d.ts +25 -0
- package/dist/pane-context-cache.js +73 -0
- package/dist/pane-context-cache.js.map +1 -0
- package/dist/paths.d.ts +0 -7
- package/dist/paths.js +8 -2
- package/dist/paths.js.map +1 -1
- package/dist/provider-probe.d.ts +15 -0
- package/dist/provider-probe.js +20 -0
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-link-progress.d.ts +75 -0
- package/dist/public-link-progress.js +204 -0
- package/dist/public-link-progress.js.map +1 -0
- package/dist/public-web-gateway.js +4 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +9 -2
- package/dist/public-web-link.js +89 -15
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +93 -20
- package/dist/quickstart-api.js +198 -63
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +15 -0
- package/dist/quickstart.js +100 -29
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +98 -0
- package/dist/reply-buttons-controller.js +174 -0
- package/dist/reply-buttons-controller.js.map +1 -0
- package/dist/reply-buttons.d.ts +113 -0
- package/dist/reply-buttons.js +208 -0
- package/dist/reply-buttons.js.map +1 -0
- package/dist/reply-dedup.js +3 -1
- package/dist/reply-dedup.js.map +1 -1
- package/dist/restart-guard.d.ts +1 -0
- package/dist/restart-guard.js +8 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/service-activation.d.ts +15 -2
- package/dist/service-activation.js +81 -4
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.js +19 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.js +53 -12
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-authority.d.ts +11 -0
- package/dist/settings-authority.js +128 -0
- package/dist/settings-authority.js.map +1 -0
- package/dist/settings-change.d.ts +5 -0
- package/dist/settings-change.js +4 -3
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +3 -0
- package/dist/settings-confirmation.js +7 -1
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.js +19 -7
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +1 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-redaction.d.ts +18 -0
- package/dist/settings-redaction.js +58 -0
- package/dist/settings-redaction.js.map +1 -0
- package/dist/setup-form.d.ts +9 -1
- package/dist/setup-form.js +25 -1
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.js +13 -3
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +26 -19
- package/dist/setup-wizard.js.map +1 -1
- package/dist/systemd-runtime.d.ts +15 -0
- package/dist/systemd-runtime.js +41 -0
- package/dist/systemd-runtime.js.map +1 -0
- package/dist/tmux-control.d.ts +22 -0
- package/dist/tmux-control.js +82 -17
- package/dist/tmux-control.js.map +1 -1
- package/dist/token-env-name.d.ts +37 -0
- package/dist/token-env-name.js +93 -0
- package/dist/token-env-name.js.map +1 -0
- package/dist/tool-permissions.js +5 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +7 -16
- package/dist/topic-commands.js +48 -84
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +3 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +68 -0
- package/dist/transcript-sources.js +336 -29
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +39 -0
- package/dist/transcript-turns.js +58 -0
- package/dist/transcript-turns.js.map +1 -0
- package/dist/tunnel/cloudflared-install.d.ts +42 -0
- package/dist/tunnel/cloudflared-install.js +194 -9
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/deb-extract.d.ts +9 -0
- package/dist/tunnel/deb-extract.js +94 -0
- package/dist/tunnel/deb-extract.js.map +1 -0
- package/dist/turn-reply-guard.d.ts +3 -0
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/ui/app.html +3 -0
- package/dist/ui/chat-render.js +32 -6
- package/dist/ui/chat-store.js +21 -1
- package/dist/ui/chat-strings.js +45 -0
- package/dist/ui/chat-thread.js +109 -52
- package/dist/ui/first-run.js +64 -0
- package/dist/ui/html-attachment.js +26 -0
- package/dist/ui/icons/apple-touch-icon.png +0 -0
- package/dist/ui/icons/favicon.ico +0 -0
- package/dist/ui/icons/favicon.svg +8 -0
- package/dist/ui/panel-chat.js +16 -7
- package/dist/ui/panel-fleet.js +5 -5
- package/dist/ui/panel-settings.js +18 -8
- package/dist/ui/preview-panel.js +47 -7
- package/dist/ui/preview.js +36 -13
- package/dist/ui/settings-apply.js +3 -1
- package/dist/ui/settings-dialogs.js +95 -25
- package/dist/ui/settings-discord.js +130 -0
- package/dist/ui/settings-model.js +21 -0
- package/dist/ui/settings-strings.js +46 -6
- package/dist/ui/settings-token.js +54 -0
- package/dist/ui/settings-wizard.js +63 -29
- package/dist/ui/shared/app-shell.js +19 -33
- package/dist/ui/shared/app-store.js +5 -3
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +34 -2
- package/dist/ui/shared/app.js +2 -1
- package/dist/ui/shared/instance-nav.js +310 -0
- package/dist/ui/shared/panel-view.js +12 -190
- package/dist/ui/shared/view-roster-store.js +5 -0
- package/dist/ui/shared/view-strings.js +12 -0
- package/dist/ui/signin.html +3 -0
- package/dist/update-dispatch.d.ts +1 -1
- package/dist/update-dispatch.js +1 -1
- package/dist/update-install.d.ts +15 -3
- package/dist/update-install.js +41 -13
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.d.ts +22 -0
- package/dist/update-launch.js +106 -0
- package/dist/update-launch.js.map +1 -0
- package/dist/update-marker.js +3 -8
- package/dist/update-marker.js.map +1 -1
- package/dist/usage/format-rich.js +7 -1
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +4 -0
- package/dist/usage/providers.js +2 -1
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.js +48 -2
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.d.ts +40 -0
- package/dist/view-api.js +28 -10
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +8 -0
- package/dist/web-api.js +53 -17
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +8 -0
- package/dist/web-auth.js +34 -8
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +21 -0
- package/dist/web-chat-history.js +44 -0
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-host-guard.d.ts +18 -2
- package/dist/web-host-guard.js +30 -5
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-icons.d.ts +21 -0
- package/dist/web-icons.js +38 -0
- package/dist/web-icons.js.map +1 -0
- package/dist/web-preview.d.ts +9 -0
- package/dist/web-preview.js +15 -9
- package/dist/web-preview.js.map +1 -1
- package/dist/web-session.d.ts +7 -1
- package/dist/web-session.js +23 -2
- package/dist/web-session.js.map +1 -1
- package/launcher/postinstall.cjs +8 -4
- package/package.json +1 -1
- package/templates/systemd.service.ejs +3 -3
|
@@ -100,7 +100,18 @@ register("settings", {
|
|
|
100
100
|
tokenEnv: "Bot token env var",
|
|
101
101
|
groupId: "Group / guild id",
|
|
102
102
|
nameField: "Name",
|
|
103
|
-
|
|
103
|
+
tokenShow: "Show the token", tokenHide: "Hide the token", tokenIsBot: "This is {0}.", tokenStatus: "Bot token",
|
|
104
|
+
tokenSet: "Token set", tokenSetAs: "Token set · {0}", tokenMissing: "Token missing", tokenReplace: "Replace",
|
|
105
|
+
tokenStoredAs: "The token is stored on this computer in ~/.agend/.env under this name — generated, and only needed if you manage .env yourself.",
|
|
106
|
+
tokenSavedRestart: "Token saved. This connection is not running, so it starts with the new token when AgEnD restarts (on the host: agend restart).",
|
|
107
|
+
newBotTokenHint: "Paste the bot's token. It is checked with the platform, then stored on this computer — never in the browser.",
|
|
108
|
+
connectionIdOptional: "Connection id (optional)",
|
|
109
|
+
connConnectedAs: "Connected · {0}", connReconnecting: "Reconnecting…", connStarting: "Starting…", connFailed: "Could not connect",
|
|
110
|
+
connNotRunning: "Not running", connTokenMissing: "Token missing", connRejected: "Token rejected — replace it",
|
|
111
|
+
discordInvite: "Invite {0} to your server", discordTheBot: "the bot", discordWaitingJoin: "Waiting for the bot to join a server…",
|
|
112
|
+
discordCheckAgain: "Check again", discordNoServerYet: "Invite the bot first, or type the server id",
|
|
113
|
+
discordIntentNote: "Turn on Message Content Intent for this bot in the Discord developer portal (it cannot be set from here):", discordPortalBot: "Developer portal → Bot",
|
|
114
|
+
discordMissingIntent: "Turn on Message Content Intent in the Discord developer portal → Bot, then restart AgEnD.",
|
|
104
115
|
noAgents: "No agents yet — click + New.",
|
|
105
116
|
noBots: "No bots configured.",
|
|
106
117
|
currentVersion: "Current version",
|
|
@@ -123,7 +134,7 @@ register("settings", {
|
|
|
123
134
|
wizardButton: "Setup wizard",
|
|
124
135
|
wizardTitle: "Set up AgEnD",
|
|
125
136
|
wizardStep: "Step {0} of {1}",
|
|
126
|
-
wizardRerun: "A fleet is already configured — finishing
|
|
137
|
+
wizardRerun: "A fleet is already configured — finishing adds a new connection; nothing already there is replaced.",
|
|
127
138
|
wizardBack: "Back",
|
|
128
139
|
wizardNext: "Next",
|
|
129
140
|
wizardFinish: "Create and start",
|
|
@@ -131,6 +142,14 @@ register("settings", {
|
|
|
131
142
|
wizardBackendFound: "Found on this host: {0}",
|
|
132
143
|
wizardBackendNone: "No backend CLI found on this host — install one first.",
|
|
133
144
|
wizardAgentName: "First agent name",
|
|
145
|
+
wizardAgentWhich: "Which agent answers on this bot",
|
|
146
|
+
wizardAgentNew: "A new agent",
|
|
147
|
+
wizardAgentExisting: "An agent you already have",
|
|
148
|
+
wizardAgentPick: "Agent",
|
|
149
|
+
wizardAgentExistingNote: "It keeps its directory, backend and settings; only its chat connection becomes this bot (if it has one now, it moves here).",
|
|
150
|
+
wizardAgentNewName: "New agent name",
|
|
151
|
+
wizardTopicNotCarried: "Its topic ({0}) is in the chat of its old connection, so it is not carried over: in the new chat it has no topic until one is bound.",
|
|
152
|
+
wizardTopicGeneral: "It is the General agent: in the new chat it takes that chat's General topic.",
|
|
134
153
|
wizardNeedDir: "Enter an absolute working directory.",
|
|
135
154
|
wizardTelegramHint: "Create a bot with @BotFather, then add it to a group.",
|
|
136
155
|
wizardDiscordHint: "Create an application in the Discord developer portal and invite the bot to your server.",
|
|
@@ -165,7 +184,7 @@ register("settings", {
|
|
|
165
184
|
restartFleetConfirm: "Restart AgEnD now?\n\nEvery agent stops and starts again, work in progress is interrupted, and the panel is unreachable for up to a minute. A notice is posted to your chat channel.",
|
|
166
185
|
restartFleetStarted: "Restarting AgEnD — this page keeps checking.",
|
|
167
186
|
restartFleetBusy: "Restarting…",
|
|
168
|
-
restartFleetTitle: "Restarting AgEnD…",
|
|
187
|
+
restartFleetTitle: "Restarting AgEnD…", connStartingNow: "Starting the new connection — no restart needed…",
|
|
169
188
|
restartFleetWaiting: "AgEnD is restarting. This page keeps checking and will say when it is back — there is nothing to press.",
|
|
170
189
|
working: "Working…",
|
|
171
190
|
restartFleetRefused: "Restart refused: {0}",
|
|
@@ -310,6 +329,7 @@ register("settings", {
|
|
|
310
329
|
editMode: "Edit",
|
|
311
330
|
viewMode: "View",
|
|
312
331
|
copy: "Copy",
|
|
332
|
+
devDownload: "Download fleet.yaml",
|
|
313
333
|
devEditor: "fleet.yaml editor",
|
|
314
334
|
devApply: "Apply & save",
|
|
315
335
|
devApplyHint: "Writes these settings and applies them, like Apply changes.",
|
|
@@ -424,7 +444,18 @@ register("settings", {
|
|
|
424
444
|
tokenEnv: "Bot token 環境變數",
|
|
425
445
|
groupId: "群組 / guild id",
|
|
426
446
|
nameField: "名稱",
|
|
427
|
-
|
|
447
|
+
tokenShow: "顯示 token", tokenHide: "隱藏 token", tokenIsBot: "這是 {0}。", tokenStatus: "Bot token",
|
|
448
|
+
tokenSet: "已設定 token", tokenSetAs: "已設定 token · {0}", tokenMissing: "缺少 token", tokenReplace: "更換",
|
|
449
|
+
tokenStoredAs: "token 存在這台電腦的 ~/.agend/.env,用這個名稱——自動產生,只有自行管理 .env 時才需要。",
|
|
450
|
+
tokenSavedRestart: "已儲存 token。這個連線目前沒有在執行,AgEnD 重新啟動時會用新的 token 啟動(在主機上:agend restart)。",
|
|
451
|
+
newBotTokenHint: "貼上 bot 的 token。會先向平台確認,再存到這台電腦——絕不存在瀏覽器。",
|
|
452
|
+
connectionIdOptional: "連線 id(選填)",
|
|
453
|
+
connConnectedAs: "已連線 · {0}", connReconnecting: "重新連線中…", connStarting: "啟動中…", connFailed: "無法連線",
|
|
454
|
+
connNotRunning: "未執行", connTokenMissing: "缺少 token", connRejected: "token 被拒——請更換",
|
|
455
|
+
discordInvite: "邀請 {0} 加入你的伺服器", discordTheBot: "bot", discordWaitingJoin: "等待 bot 加入伺服器…",
|
|
456
|
+
discordCheckAgain: "再檢查一次", discordNoServerYet: "請先邀請 bot,或輸入伺服器 id",
|
|
457
|
+
discordIntentNote: "請在 Discord 開發者入口網站為這個 bot 開啟 Message Content Intent(這裡無法代為設定):", discordPortalBot: "開發者入口網站 → Bot",
|
|
458
|
+
discordMissingIntent: "請到 Discord 開發者入口網站 → Bot 開啟 Message Content Intent,然後重新啟動 AgEnD。",
|
|
428
459
|
noAgents: "還沒有 agent — 點 + 新增。",
|
|
429
460
|
noBots: "尚未設定機器人。",
|
|
430
461
|
currentVersion: "目前版本",
|
|
@@ -447,7 +478,7 @@ register("settings", {
|
|
|
447
478
|
wizardButton: "設定精靈",
|
|
448
479
|
wizardTitle: "設定 AgEnD",
|
|
449
480
|
wizardStep: "第 {0} 步,共 {1} 步",
|
|
450
|
-
wizardRerun: "已經有設定好的 fleet ——
|
|
481
|
+
wizardRerun: "已經有設定好的 fleet —— 完成後會新增一個連線,不會取代任何既有的設定。",
|
|
451
482
|
wizardBack: "上一步",
|
|
452
483
|
wizardNext: "下一步",
|
|
453
484
|
wizardFinish: "建立並啟動",
|
|
@@ -455,6 +486,14 @@ register("settings", {
|
|
|
455
486
|
wizardBackendFound: "在這台主機上找到:{0}",
|
|
456
487
|
wizardBackendNone: "這台主機上找不到任何後端 CLI —— 請先安裝一個。",
|
|
457
488
|
wizardAgentName: "第一個 agent 名稱",
|
|
489
|
+
wizardAgentWhich: "由哪個 agent 在這個機器人上回覆",
|
|
490
|
+
wizardAgentNew: "新的 agent",
|
|
491
|
+
wizardAgentExisting: "既有的 agent",
|
|
492
|
+
wizardAgentPick: "Agent",
|
|
493
|
+
wizardAgentExistingNote: "它的目錄、backend 和設定都不變;只有聊天連線改成這個機器人(如果它目前已有連線,會移到這裡)。",
|
|
494
|
+
wizardAgentNewName: "新 agent 名稱",
|
|
495
|
+
wizardTopicNotCarried: "它的 topic({0})屬於舊連線的聊天室,不會帶過去:在新的聊天室裡,綁定新的 topic 之前它沒有 topic。",
|
|
496
|
+
wizardTopicGeneral: "它是 General agent:在新的聊天室裡會改用那裡的 General topic。",
|
|
458
497
|
wizardNeedDir: "請輸入絕對路徑的工作目錄。",
|
|
459
498
|
wizardTelegramHint: "用 @BotFather 建立 bot,然後把它加進一個群組。",
|
|
460
499
|
wizardDiscordHint: "在 Discord 開發者後台建立應用程式,並把 bot 邀請到你的伺服器。",
|
|
@@ -489,7 +528,7 @@ register("settings", {
|
|
|
489
528
|
restartFleetConfirm: "現在重新啟動 AgEnD?\n\n所有 agent 都會停止再啟動,進行中的工作會被中斷,面板會有最多約一分鐘無法連線。聊天頻道會收到一則通知。",
|
|
490
529
|
restartFleetStarted: "AgEnD 重新啟動中 —— 此頁面會持續確認。",
|
|
491
530
|
restartFleetBusy: "重啟中…",
|
|
492
|
-
restartFleetTitle: "AgEnD 重新啟動中…",
|
|
531
|
+
restartFleetTitle: "AgEnD 重新啟動中…", connStartingNow: "正在啟動新連線——不需要重新啟動…",
|
|
493
532
|
restartFleetWaiting: "AgEnD 正在重新啟動。此頁面會持續確認,恢復後會告訴你 —— 不需要再按任何按鈕。",
|
|
494
533
|
working: "執行中…",
|
|
495
534
|
restartFleetRefused: "重新啟動被拒絕:{0}",
|
|
@@ -634,6 +673,7 @@ register("settings", {
|
|
|
634
673
|
editMode: "編輯",
|
|
635
674
|
viewMode: "檢視",
|
|
636
675
|
copy: "複製",
|
|
676
|
+
devDownload: "下載 fleet.yaml",
|
|
637
677
|
devEditor: "fleet.yaml 編輯器",
|
|
638
678
|
devApply: "套用並儲存",
|
|
639
679
|
devApplyHint: "寫入這些設定並套用,和「套用變更」相同。",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// #1519 P1 (docs/design/ux-onboarding-walkthrough.md §5.2): the one bot-token field — the setup wizard, "New
|
|
2
|
+
// connection" and a connection's "Replace token" use it. A password field (show/hide), Verify asks the platform who the
|
|
3
|
+
// token belongs to (Telegram getMe, Discord /users/@me) and shows the bot's name before anything is written. The token
|
|
4
|
+
// lives in this field's state only: never in storage, a URL, a label or the page's other state, and the server never
|
|
5
|
+
// hands it back — afterwards a connection shows "Token set · @bot".
|
|
6
|
+
import { html, useState } from "/assets/app-html.js";
|
|
7
|
+
import { t } from "/assets/app-i18n.js";
|
|
8
|
+
import { Icon } from "/assets/ui-icons.js";
|
|
9
|
+
import { api } from "./settings-confirm.js";
|
|
10
|
+
|
|
11
|
+
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
12
|
+
|
|
13
|
+
/** Who this token is: { valid, username?, reason? } — one outbound check, the token sent once and not kept. */
|
|
14
|
+
export async function verifyBotToken(platform, token) {
|
|
15
|
+
const res = await api("/api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "verify", platform, token }) })
|
|
16
|
+
.catch(() => ({ ok: false, body: {} }));
|
|
17
|
+
// A Discord bot's invite and portal page come with it (#1519 P3).
|
|
18
|
+
if (res.ok && res.body && res.body.identity) return { ...res.body.identity, invite: res.body.invite_url || null, portal: res.body.portal_url || null };
|
|
19
|
+
return { valid: false, reason: (res.body && res.body.error) || tn("verifyFailed") };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A bot's name as it is addressed: @name, once. */
|
|
23
|
+
export const botHandle = (username) => (username ? `@${String(username).replace(/^@/, "")}` : "");
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The field. props: id, platform, value, onInput(token) (the parent drops its verified identity on a change and moves
|
|
27
|
+
* its credential revision, so a Verify still on its way for the old value cannot land — #1529 review),
|
|
28
|
+
* identity ({ valid, username, reason } | null), busy (the field is locked meanwhile), onVerify(), hint.
|
|
29
|
+
*/
|
|
30
|
+
export function TokenField({ id, platform, value, onInput, identity, busy, onVerify, hint }) {
|
|
31
|
+
const [shown, setShown] = useState(false);
|
|
32
|
+
return html`<div class="field token-field"><label for=${id}>${tn("wizardToken")}</label>
|
|
33
|
+
<div class="token-row">
|
|
34
|
+
<input id=${id} type=${shown ? "text" : "password"} autocomplete="off" autocapitalize="off" spellcheck="false" disabled=${!!busy}
|
|
35
|
+
placeholder=${platform === "discord" ? "MTA…" : "123456:ABC-DEF…"} value=${value} onInput=${(e) => onInput(e.target.value.trim())} />
|
|
36
|
+
<button type="button" class="icon-btn" aria-pressed=${shown ? "true" : "false"} title=${shown ? tn("tokenHide") : tn("tokenShow")}
|
|
37
|
+
aria-label=${shown ? tn("tokenHide") : tn("tokenShow")} onClick=${() => setShown(!shown)}><${Icon} name="eye" size=${16} /></button>
|
|
38
|
+
</div>
|
|
39
|
+
<p class="note">${hint || (platform === "telegram" ? tn("wizardTokenTelegram") : tn("wizardTokenDiscord"))}</p>
|
|
40
|
+
<div class="dlg-inline-actions"><button type="button" class="btn btn-sm" disabled=${busy || !value} onClick=${onVerify}>${tn("wizardVerify")}</button>
|
|
41
|
+
${identity ? html`<span class=${`feedback${identity.valid ? " ok" : " error"}`} role="status">${identity.valid
|
|
42
|
+
? tn("tokenIsBot", botHandle(identity.username) || tn("verified")) : (identity.reason || tn("verifyFailed"))}</span>` : null}</div>
|
|
43
|
+
</div>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The generated env var a token is stored under, read-only with a copy button (the "Advanced" view). */
|
|
47
|
+
export function TokenEnvNote({ name }) {
|
|
48
|
+
const [copied, setCopied] = useState(false);
|
|
49
|
+
if (!name) return null;
|
|
50
|
+
const copy = async () => { try { await navigator.clipboard.writeText(name); setCopied(true); } catch { /* the name is on screen */ } };
|
|
51
|
+
return html`<div class="field"><span>${tn("tokenEnv")}</span>
|
|
52
|
+
<div class="dlg-inline-actions"><code>${name}</code><button type="button" class="btn btn-sm btn-ghost" onClick=${copy}>${copied ? tn("copied") : tn("copy")}</button></div>
|
|
53
|
+
<p class="note">${tn("tokenStoredAs")}</p></div>`;
|
|
54
|
+
}
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
// configuration and, when it finishes, writes the files (a write an admin may have to confirm, #1423) and hands over
|
|
3
3
|
// to the ordinary Apply: the app's runner (settings-apply.js) posts the apply and watches its job, with the same
|
|
4
4
|
// progress, deadline and restart recovery as every other change.
|
|
5
|
-
import { html, useEffect, useState } from "/assets/app-html.js";
|
|
5
|
+
import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
6
6
|
import { t } from "/assets/app-i18n.js";
|
|
7
7
|
import { useLease } from "/assets/app-ctx.js";
|
|
8
8
|
import { Dialog } from "/assets/ui-dialog.js";
|
|
9
9
|
import { api } from "./settings-confirm.js";
|
|
10
10
|
import { startOperation } from "./settings-apply.js";
|
|
11
11
|
import { BACKENDS, DEFAULT_SCHEMA, impactOf, toYaml } from "./settings-model.js";
|
|
12
|
-
import { Select } from "./settings-dialogs.js";
|
|
12
|
+
import { Drawer, Select } from "./settings-dialogs.js";
|
|
13
|
+
import { botHandle, TokenEnvNote, TokenField, verifyBotToken } from "./settings-token.js";
|
|
14
|
+
import { DiscordServerPicker } from "./settings-discord.js";
|
|
13
15
|
|
|
14
16
|
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
15
17
|
const STEPS = 4;
|
|
@@ -18,6 +20,10 @@ const post = (path, body) => api(path, { method: "POST", body: JSON.stringify(bo
|
|
|
18
20
|
export function SetupWizard({ ctx, onClose }) {
|
|
19
21
|
const lease = useLease("wizard");
|
|
20
22
|
const [w, setW] = useState(null);
|
|
23
|
+
// The credential's revision (#1529 review): a token or platform change moves it, and a Verify, Detect or plan that
|
|
24
|
+
// began before is dropped when it lands — an answer about an old token never names, or plans for, a new one.
|
|
25
|
+
const rev = useRef(0);
|
|
26
|
+
const bump = () => { rev.current++; };
|
|
21
27
|
const [busy, setBusy] = useState("");
|
|
22
28
|
const [err, setErr] = useState("");
|
|
23
29
|
useEffect(() => {
|
|
@@ -25,37 +31,51 @@ export function SetupWizard({ ctx, onClose }) {
|
|
|
25
31
|
const env = await api("/api/settings/quickstart/environment").catch(() => null);
|
|
26
32
|
if (!lease.current()) return;
|
|
27
33
|
const e = (env && env.body) || { backends: [], channels: [], has_fleet: false };
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
// #1519 P7: with agents already there, the bot is for one of them by default; a new agent gets a free name.
|
|
35
|
+
const agents = Array.isArray(e.agents) ? e.agents : [];
|
|
36
|
+
const names = new Set(agents.map((a) => a.name));
|
|
37
|
+
let n = 1; while (names.has(`agent-${n}`)) n++;
|
|
38
|
+
setW({ step: 1, env: e, backend: (e.backends || [])[0] || "claude-code", working_directory: "", instance_name: `agent-${n}`,
|
|
39
|
+
agent: agents.length ? "existing" : "new", existing_name: agents.length ? agents[0].name : "",
|
|
40
|
+
platform: "telegram", token: "", group_id: "", guild_id: "", general_channel_id: "", admin_user_id: "",
|
|
30
41
|
identity: null, guilds: [], plan: null, offset: 0 });
|
|
31
42
|
})();
|
|
32
43
|
}, [lease]);
|
|
33
44
|
if (!w) return html`<${Dialog} title=${tn("wizardTitle")} onClose=${onClose}><p class="note">…</p></${Dialog}>`;
|
|
34
|
-
|
|
45
|
+
// Every field the plan or a probe is about moves the revision (#1529 review r2): a plan or Detect that began before an
|
|
46
|
+
// edit — target, admin, backend, directory, name — is dropped when it lands, never committed or written over the edit.
|
|
47
|
+
const set = (k) => (v) => { bump(); setW((x) => ({ ...x, [k]: v })); };
|
|
48
|
+
// No token env: the plan generates one (#1519 P1: AGEND_<PLATFORM>_<ID>_TOKEN, unique) and the commit carries the plan's.
|
|
49
|
+
// An existing agent (#1519 P7) is named and nothing else: the server keeps its directory and backend.
|
|
35
50
|
const input = () => ({
|
|
36
|
-
platform: w.platform,
|
|
51
|
+
platform: w.platform,
|
|
52
|
+
...(w.agent === "existing" ? { instance_name: w.existing_name, existing_agent: true }
|
|
53
|
+
: { backend: w.backend, working_directory: w.working_directory, instance_name: w.instance_name }),
|
|
37
54
|
group_id: w.group_id || undefined, guild_id: w.guild_id || undefined, general_channel_id: w.general_channel_id || undefined,
|
|
38
55
|
admin_user_id: w.admin_user_id || undefined,
|
|
39
56
|
});
|
|
40
57
|
|
|
41
58
|
const verify = async () => {
|
|
59
|
+
const at = rev.current, token = w.token, platform = w.platform;
|
|
60
|
+
const live = () => lease.current() && rev.current === at;
|
|
42
61
|
setBusy(tn("wizardVerifying")); setErr("");
|
|
43
|
-
const
|
|
44
|
-
if (!lease.current()) return;
|
|
45
|
-
const identity = (res.body && res.body.identity) || { valid: false };
|
|
62
|
+
const identity = await verifyBotToken(platform, token);
|
|
63
|
+
if (!live()) { if (lease.current()) setBusy(""); return; }
|
|
46
64
|
let guilds = [];
|
|
47
|
-
if (identity.valid &&
|
|
48
|
-
const g = await post("/api/settings/quickstart/probe", { action: "guilds", token
|
|
49
|
-
if (!lease.current()) return;
|
|
65
|
+
if (identity.valid && platform === "discord") {
|
|
66
|
+
const g = await post("/api/settings/quickstart/probe", { action: "guilds", token });
|
|
67
|
+
if (!live()) { if (lease.current()) setBusy(""); return; }
|
|
50
68
|
guilds = (g.body && g.body.guilds) || [];
|
|
51
69
|
}
|
|
52
70
|
setBusy("");
|
|
53
|
-
setW((x) => ({ ...x, identity, guilds }));
|
|
71
|
+
setW((x) => (x.token === token && x.platform === platform ? { ...x, identity, guilds } : x));
|
|
54
72
|
};
|
|
55
73
|
const detect = async () => {
|
|
74
|
+
const at = rev.current;
|
|
56
75
|
setBusy(tn("wizardWaitingStart")); setErr("");
|
|
57
76
|
const res = await post("/api/settings/quickstart/probe", { action: "await-telegram-start", token: w.token, offset: w.offset || 0 });
|
|
58
77
|
if (!lease.current()) return;
|
|
78
|
+
if (rev.current !== at) { setBusy(""); return; }
|
|
59
79
|
setBusy("");
|
|
60
80
|
if (!res.ok) { setErr((res.body && res.body.error) || tn("applyFailed")); return; }
|
|
61
81
|
if (res.body.found) setW((x) => ({ ...x, offset: res.body.offset, group_id: String(res.body.found.groupId), admin_user_id: String(res.body.found.userId) }));
|
|
@@ -63,14 +83,19 @@ export function SetupWizard({ ctx, onClose }) {
|
|
|
63
83
|
};
|
|
64
84
|
const next = async () => {
|
|
65
85
|
setErr("");
|
|
66
|
-
if (w.step === 1) {
|
|
86
|
+
if (w.step === 1) {
|
|
87
|
+
if (w.agent !== "existing" && !w.working_directory.startsWith("/")) { setErr(tn("wizardNeedDir")); return; }
|
|
88
|
+
setW({ ...w, step: 2 }); return;
|
|
89
|
+
}
|
|
67
90
|
if (w.step === 2) { setW({ ...w, step: 3 }); return; }
|
|
68
91
|
if (w.step === 3) {
|
|
69
92
|
if (!w.identity || !w.identity.valid) { setErr(tn("wizardNeedVerify")); return; }
|
|
93
|
+
const at = rev.current;
|
|
70
94
|
setBusy(tn("wizardPlanLoading"));
|
|
71
95
|
const res = await post("/api/settings/quickstart/plan", input());
|
|
72
96
|
if (!lease.current()) return;
|
|
73
97
|
setBusy("");
|
|
98
|
+
if (rev.current !== at) return;
|
|
74
99
|
if (!res.ok) { setErr((res.body && res.body.error) || tn("applyFailed")); return; }
|
|
75
100
|
setW((x) => ({ ...x, plan: res.body, step: 4 }));
|
|
76
101
|
return;
|
|
@@ -84,7 +109,9 @@ export function SetupWizard({ ctx, onClose }) {
|
|
|
84
109
|
* commit's body until that write completes, then drops it.
|
|
85
110
|
*/
|
|
86
111
|
const finish = () => {
|
|
87
|
-
|
|
112
|
+
// The plan's target, all of it (#1529 review): its connection id and its generated token env — the commit refuses
|
|
113
|
+
// either if it was taken since, instead of quietly picking another.
|
|
114
|
+
const body = { ...input(), channel_id: w.plan.channel_id, token_env: w.plan.token_env, token_env_generated: true, token: w.token };
|
|
88
115
|
// It writes the connection and the first agent: it costs what a connection change does (the server's schema).
|
|
89
116
|
const handed = startOperation([{ label: tn("wizardTitle"), impact: impactOf((ctx && ctx.schema) || DEFAULT_SCHEMA, "fleet.channels"),
|
|
90
117
|
request: { method: "POST", url: "/api/settings/quickstart/commit", body, sensitive: true } }]);
|
|
@@ -94,28 +121,31 @@ export function SetupWizard({ ctx, onClose }) {
|
|
|
94
121
|
};
|
|
95
122
|
|
|
96
123
|
let body;
|
|
124
|
+
const agents = Array.isArray(w.env.agents) ? w.env.agents : [];
|
|
97
125
|
if (w.step === 1) {
|
|
98
|
-
|
|
126
|
+
const which = agents.length ? html`<div class="seg-inline" role="group" aria-label=${tn("wizardAgentWhich")}>
|
|
127
|
+
${["existing", "new"].map((k) => html`<button key=${k} type="button" class=${`btn${w.agent === k ? " btn-primary" : ""}`} aria-pressed=${w.agent === k ? "true" : "false"}
|
|
128
|
+
onClick=${() => set("agent")(k)}>${k === "existing" ? tn("wizardAgentExisting") : tn("wizardAgentNew")}</button>`)}</div>` : null;
|
|
129
|
+
body = w.agent === "existing" ? html`${which}
|
|
130
|
+
<div class="field"><label for="wz-agent">${tn("wizardAgentPick")}</label>
|
|
131
|
+
<${Select} id="wz-agent" value=${w.existing_name} onChange=${set("existing_name")} options=${agents.map((a) => a.name)} />
|
|
132
|
+
<p class="note">${tn("wizardAgentExistingNote")}</p></div>` : html`${which}<div class="field"><label for="wz-be">${tn("wizardBackend")}</label>
|
|
99
133
|
<${Select} id="wz-be" value=${w.backend} onChange=${set("backend")} options=${w.env.backends && w.env.backends.length ? w.env.backends : BACKENDS} />
|
|
100
134
|
<p class="note">${w.env.backends && w.env.backends.length ? tn("wizardBackendFound", w.env.backends.join(", ")) : tn("wizardBackendNone")}</p></div>
|
|
101
135
|
<div class="field"><label for="wz-wd">${tn("workingDir")}</label><input id="wz-wd" type="text" placeholder="/home/you/projects/app" value=${w.working_directory} onInput=${(e) => set("working_directory")(e.target.value.trim())} /></div>
|
|
102
|
-
<div class="field"><label for="wz-name">${tn("wizardAgentName")}</label><input id="wz-name" type="text" value=${w.instance_name} onInput=${(e) => set("instance_name")(e.target.value.trim())} /></div>`;
|
|
136
|
+
<div class="field"><label for="wz-name">${agents.length ? tn("wizardAgentNewName") : tn("wizardAgentName")}</label><input id="wz-name" type="text" value=${w.instance_name} onInput=${(e) => set("instance_name")(e.target.value.trim())} /></div>`;
|
|
103
137
|
} else if (w.step === 2) {
|
|
104
138
|
body = html`<div class="seg-inline" role="group" aria-label=${tn("wizardPlatform")}>
|
|
105
139
|
${["telegram", "discord"].map((p) => html`<button key=${p} type="button" class=${`btn${w.platform === p ? " btn-primary" : ""}`} aria-pressed=${w.platform === p ? "true" : "false"}
|
|
106
|
-
onClick=${() => setW({ ...
|
|
140
|
+
onClick=${() => { bump(); setW((x) => ({ ...x, platform: p, identity: null })); }}>${p === "telegram" ? "Telegram" : "Discord"}</button>`)}</div>
|
|
107
141
|
<p class="note">${w.platform === "telegram" ? tn("wizardTelegramHint") : tn("wizardDiscordHint")}</p>`;
|
|
108
142
|
} else if (w.step === 3) {
|
|
109
|
-
body = html
|
|
110
|
-
|
|
111
|
-
<p class="note">${w.platform === "telegram" ? tn("wizardTokenTelegram") : tn("wizardTokenDiscord")}</p></div>
|
|
112
|
-
<div class="field"><label for="wz-env">${tn("tokenEnv")}</label><input id="wz-env" type="text" value=${w.token_env} onInput=${(e) => set("token_env")(e.target.value.trim())} /></div>
|
|
113
|
-
<div class="dlg-inline-actions"><button type="button" class="btn btn-sm" disabled=${!!busy} onClick=${verify}>${tn("wizardVerify")}</button>
|
|
114
|
-
${w.identity ? html`<span class=${`feedback${w.identity.valid ? "" : " error"}`} role="status">${w.identity.valid ? (w.identity.username || tn("verified")) : (w.identity.reason || tn("verifyFailed"))}</span>` : null}</div>
|
|
143
|
+
body = html`<${TokenField} id="wz-token" platform=${w.platform} value=${w.token} identity=${w.identity} busy=${!!busy}
|
|
144
|
+
onInput=${(v) => { bump(); setW((x) => ({ ...x, token: v, identity: null })); }} onVerify=${verify} />
|
|
115
145
|
${w.platform === "discord" ? html`
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
146
|
+
<${DiscordServerPicker} idPrefix="wz" token=${w.identity && w.identity.valid ? w.token : ""} bot=${w.identity && botHandle(w.identity.username)}
|
|
147
|
+
invite=${w.identity && w.identity.invite} portal=${w.identity && w.identity.portal} guilds=${w.guilds} onGuilds=${(g) => setW((x) => ({ ...x, guilds: g }))}
|
|
148
|
+
guild=${w.guild_id} onGuild=${set("guild_id")} channel=${w.general_channel_id} onChannel=${set("general_channel_id")} />
|
|
119
149
|
<div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" placeholder=${tn("discordUserPlaceholder")} value=${w.admin_user_id} onInput=${(e) => set("admin_user_id")(e.target.value.trim())} /></div>`
|
|
120
150
|
: html`
|
|
121
151
|
<div class="field"><label for="wz-group">${tn("groupIdField")}</label><input id="wz-group" type="text" placeholder="-1001234567890" value=${w.group_id} onInput=${(e) => set("group_id")(e.target.value.trim())} />
|
|
@@ -125,8 +155,12 @@ export function SetupWizard({ ctx, onClose }) {
|
|
|
125
155
|
} else {
|
|
126
156
|
body = !w.plan ? html`<p class="note">${tn("wizardPlanLoading")}</p>` : html`
|
|
127
157
|
<p><strong>${tn("wizardWillWrite")}</strong></p>
|
|
128
|
-
<pre class="s-yaml">${toYaml({ channels: [w.plan.channel], instances: { [w.plan.instance.name]: { working_directory: w.plan.instance.working_directory, backend: w.plan.instance.backend
|
|
129
|
-
|
|
158
|
+
<pre class="s-yaml">${toYaml({ channels: [w.plan.channel], instances: { [w.plan.instance.name]: { working_directory: w.plan.instance.working_directory, backend: w.plan.instance.backend, channel_id: w.plan.instance.channel_id,
|
|
159
|
+
...(w.plan.instance.topic && w.plan.instance.topic.to != null ? { topic_id: w.plan.instance.topic.to } : {}) } } })}</pre>
|
|
160
|
+
${w.plan.instance && w.plan.instance.existing ? html`<p class="note">${tn("wizardAgentExistingNote")}</p>` : null}
|
|
161
|
+
${w.plan.instance && w.plan.instance.topic ? html`<p class="feedback warning">${w.plan.instance.topic.to == null
|
|
162
|
+
? tn("wizardTopicNotCarried", w.plan.instance.topic.from) : tn("wizardTopicGeneral")}</p>` : null}
|
|
163
|
+
<${Drawer} title=${tn("advancedSection")}><${TokenEnvNote} name=${w.plan.token_env} /></${Drawer}>
|
|
130
164
|
${(w.plan.warnings || []).map((x, i) => html`<p key=${i} class="feedback warning">${x}</p>`)}`;
|
|
131
165
|
}
|
|
132
166
|
return html`<${Dialog} title=${tn("wizardTitle")} onClose=${onClose} wide=${true} busy=${busy === tn("wizardCommitting")}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// #1408 §2/§7: the app's frame — the sidebar (a drawer on a phone), the main area with its panel, the bottom tabs on a
|
|
2
2
|
// phone, the connection line, toasts. Panels render their own header through <PanelHeader>, so every panel's title,
|
|
3
3
|
// status and ⋯ actions sit in the same place.
|
|
4
|
-
import { html, useEffect, useState } from "./app-html.js";
|
|
4
|
+
import { html, useEffect, useLayoutEffect, useRef, useState } from "./app-html.js";
|
|
5
5
|
import { Icon } from "./ui-icons.js";
|
|
6
6
|
import { Toasts } from "./ui-toast.js";
|
|
7
7
|
import { ConfirmHost } from "./ui-confirm.js";
|
|
@@ -11,6 +11,7 @@ import { navStore } from "./app-nav.js";
|
|
|
11
11
|
import { chatPath, viewPath, settingsPath, routeKey, NEEDS_PATH } from "./app-route.js";
|
|
12
12
|
import { SessionMenu } from "./app-session.js";
|
|
13
13
|
import { ErrorState, Skeleton } from "./ui-states.js";
|
|
14
|
+
import { InstanceNav, RosterNav } from "./instance-nav.js";
|
|
14
15
|
|
|
15
16
|
/** Shell state shared with panels: the phone drawer, the collapsed sidebar, extensions a panel module adds. */
|
|
16
17
|
export const shellStore = createStore({ drawer: false, collapsed: readCollapsed(), footer: [], keyboard: false, dialog: null, side: null });
|
|
@@ -25,6 +26,14 @@ export function setSideSection(Component) {
|
|
|
25
26
|
return () => { if (shellStore.get().side === entry) shellStore.set({ side: null }); };
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
// The sidebar's instance list (alpha.2, N1): one component on every page. keepActiveInView lives with it.
|
|
30
|
+
export { keepActiveInView } from "./instance-nav.js";
|
|
31
|
+
|
|
32
|
+
/** #1519 P7: "New agent" from anywhere (the sidebar's +, an empty chat): the app registers what opens it. */
|
|
33
|
+
let newInstanceOpener = null;
|
|
34
|
+
export function setNewInstanceOpener(fn) { newInstanceOpener = fn; }
|
|
35
|
+
export function requestNewInstance() { if (newInstanceOpener) newInstanceOpener(); }
|
|
36
|
+
|
|
28
37
|
/** An app-level dialog (New instance): rendered by the shell until closed, whatever panel is showing. */
|
|
29
38
|
export function showDialog(Component, props = {}) { shellStore.set({ dialog: { Component, props } }); }
|
|
30
39
|
export function closeDialog() { shellStore.set({ dialog: null }); }
|
|
@@ -90,29 +99,6 @@ export function statusClass(i, exec, awaiting) {
|
|
|
90
99
|
}
|
|
91
100
|
export { statusLabel };
|
|
92
101
|
|
|
93
|
-
function tooltip(i) {
|
|
94
|
-
const src = (s) => (s === "instance" || s === "classic" ? "configured" : s === "fleet-default" ? "fleet default" : s);
|
|
95
|
-
const model = i.model ? (i.model_source === "live" || i.model_source === "cli-default" || i.model_source === "unresolved" ? i.model : `${i.model} (${src(i.model_source)})`) : null;
|
|
96
|
-
const effort = i.effort ? `effort:${i.effort}${i.effort_source ? ` (${src(i.effort_source)})` : ""}` : null;
|
|
97
|
-
const ctx = i.context_pct != null ? `ctx:${Math.round(i.context_pct)}%` : null;
|
|
98
|
-
const cost = i.cost > 0 ? `$${i.cost.toFixed(2)}` : null;
|
|
99
|
-
return [i.name, i.backend, model, effort, ctx, cost, i.status].filter(Boolean).join(" · ");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function InstanceRow({ i, active, exec, awaiting }) {
|
|
103
|
-
const alias = typeof i.display_name === "string" && i.display_name.trim() && i.display_name.trim() !== i.name ? i.display_name.trim() : "";
|
|
104
|
-
const cls = statusClass(i, exec, awaiting);
|
|
105
|
-
// Two lines: the name (never shortened to make room for a badge, #1408 rough edge 6), then the alias and/or
|
|
106
|
-
// "needs you". Everything else is in the tooltip.
|
|
107
|
-
return html`<li><a class=${`inst${active ? " active" : ""}`} href=${chatPath(i.name)} title=${tooltip(i)} aria-current=${active ? "page" : undefined}
|
|
108
|
-
onClick=${closeDrawer}>
|
|
109
|
-
<span class=${`dot ${cls}`} aria-hidden="true"></span>
|
|
110
|
-
<span class="inst-text"><span class="inst-name">${i.name}</span>
|
|
111
|
-
${alias || awaiting != null ? html`<span class="inst-sub">${alias ? html`<span class="inst-alias">${alias}</span>` : null}
|
|
112
|
-
${awaiting != null ? html`<span class="badge-await" title=${awaiting || t("app.approxNote")}>${t("app.needsYou")}</span>` : null}</span>` : null}
|
|
113
|
-
<span class="sr-only">${statusLabel(i, exec, awaiting)}</span></span></a></li>`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
102
|
/** Sign in, and come back here afterwards (the sign-in page accepts only the app's own pages). */
|
|
117
103
|
export function signInHref() {
|
|
118
104
|
const here = typeof location !== "undefined" ? location.pathname + location.search : "/view";
|
|
@@ -122,7 +108,6 @@ export function signInHref() {
|
|
|
122
108
|
function Sidebar({ route, onNewInstance, viewOnly }) {
|
|
123
109
|
const app = useStore(appStore);
|
|
124
110
|
const shell = useStore(shellStore);
|
|
125
|
-
const current = route && route.panel === "chat" ? route.instance : null;
|
|
126
111
|
const on = (panel) => !!route && route.panel === panel;
|
|
127
112
|
// A count badge is hidden at 0; its number is also in the link's accessible name.
|
|
128
113
|
const navLink = (panel, href, icon, label, count = 0) => html`<a class=${`side-row${on(panel) ? " active" : ""}`} href=${href}
|
|
@@ -137,7 +122,7 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
|
|
|
137
122
|
<button type="button" class="icon-btn side-collapse" onClick=${toggleSidebar} aria-label=${narrow() ? t("app.closeMenu") : t("app.collapse")} title=${narrow() ? t("app.closeMenu") : t("app.collapse")} aria-controls="sidebar"><${Icon} name="sidebar" /></button>
|
|
138
123
|
</div>
|
|
139
124
|
<nav class="side-nav" aria-label=${t("app.menu")}>${navLink("view", "/view", "view", t("app.view"))}</nav>
|
|
140
|
-
${shell.side ? html`<${shell.side.Component} />` : html
|
|
125
|
+
${shell.side ? html`<${shell.side.Component} />` : html`<${ViewOnlyNav} route=${route} />`}
|
|
141
126
|
<div class="side-foot">
|
|
142
127
|
<${Prefs} />
|
|
143
128
|
<a class="side-row" href=${signInHref()}><${Icon} name="user" /><span>${t("app.signIn")}</span></a>
|
|
@@ -148,19 +133,15 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
|
|
|
148
133
|
<div class="side-head">
|
|
149
134
|
<a class="brand" href="/ui" onClick=${closeDrawer}>${t("app.brand")}</a>
|
|
150
135
|
<button type="button" class="icon-btn side-collapse" onClick=${toggleSidebar} aria-label=${narrow() ? t("app.closeMenu") : t("app.collapse")} title=${narrow() ? t("app.closeMenu") : t("app.collapse")} aria-controls="sidebar"><${Icon} name="sidebar" /></button>
|
|
151
|
-
<button type="button" class="icon-btn" onClick=${onNewInstance} aria-label=${t("app.newInstance")} title=${t("app.newInstance")}><${Icon} name="
|
|
136
|
+
<button type="button" class="icon-btn" onClick=${onNewInstance} aria-label=${t("app.newInstance")} title=${t("app.newInstance")}><${Icon} name="plus" /></button>
|
|
152
137
|
</div>
|
|
153
138
|
<nav class="side-nav" aria-label=${t("app.menu")}>
|
|
154
139
|
${navLink("needs", NEEDS_PATH, "inbox", t("app.needsNav"), needsCount)}
|
|
155
140
|
${navLink("fleet", "/ui/fleet", "fleet", t("app.fleet"))}
|
|
156
141
|
${navLink("view", "/view", "view", t("app.view"))}
|
|
157
142
|
</nav>
|
|
158
|
-
${shell.side ? html`<${shell.side.Component} />` : html
|
|
159
|
-
|
|
160
|
-
${app.instances.length ? html`<ul class="inst-list">${app.instances.map(i => html`<${InstanceRow} key=${i.name} i=${i} active=${i.name === current}
|
|
161
|
-
exec=${app.exec[i.name]} awaiting=${Object.prototype.hasOwnProperty.call(app.awaiting, i.name) ? app.awaiting[i.name] : null} />`)}</ul>`
|
|
162
|
-
: html`<p class="side-empty">${t("app.noInstances")}</p>`}
|
|
163
|
-
</div>`}
|
|
143
|
+
${shell.side ? html`<${shell.side.Component} />` : html`<${InstanceNav} route=${route} items=${app.instances} loaded=${app.ready !== false}
|
|
144
|
+
exec=${app.exec} awaiting=${app.awaiting} onPick=${closeDrawer} />`}
|
|
164
145
|
<div class="side-foot">
|
|
165
146
|
${navLink("settings", settingsPath(), "settings", t("app.settings"))}
|
|
166
147
|
${shell.footer.map(({ key, Component }) => html`<${Component} key=${key} />`)}
|
|
@@ -170,6 +151,11 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
|
|
|
170
151
|
</aside>`;
|
|
171
152
|
}
|
|
172
153
|
|
|
154
|
+
/** The anonymous View reader's list: View's roster read (no status frames without a session), the same component. */
|
|
155
|
+
function ViewOnlyNav({ route }) {
|
|
156
|
+
return html`<${RosterNav} route=${route} viewOnly=${true} onPick=${closeDrawer} />`;
|
|
157
|
+
}
|
|
158
|
+
|
|
173
159
|
function Prefs() {
|
|
174
160
|
const [theme, setTheme] = useState(typeof AgendTheme !== "undefined" ? AgendTheme.get() : "system");
|
|
175
161
|
const [l, setL] = useState(lang());
|
|
@@ -21,8 +21,9 @@ export function createStore(initial) {
|
|
|
21
21
|
/** The app's store. `ready`: a status frame has arrived (before it, an unknown instance may just not be listed yet).
|
|
22
22
|
* `exec`: each instance's raw execution state (working / idle / stuck / null); `awaiting`: what it
|
|
23
23
|
* waits on at its terminal (text, "" when unknown), absent when it waits on nothing. `hydration`: a late catch-up
|
|
24
|
-
* (app-stream.js catchUp) — "catching" / "retrying" / "failed" / "ok"; `retryHydration` asks for it again.
|
|
25
|
-
|
|
24
|
+
* (app-stream.js catchUp) — "catching" / "retrying" / "failed" / "ok"; `retryHydration` asks for it again.
|
|
25
|
+
* `connections`: how many chat connections the fleet has (#1519 P7: 0 is web-only — the first-run card); null until known. */
|
|
26
|
+
export const appStore = createStore({ ready: false, instances: [], uptime: 0, exec: {}, awaiting: {}, connection: "connecting", needs: [], connections: null,
|
|
26
27
|
hydration: "none", retryHydration: null });
|
|
27
28
|
|
|
28
29
|
/** Fold one status frame (stream or poll) into the store. */
|
|
@@ -34,7 +35,8 @@ export function applyStatus(d) {
|
|
|
34
35
|
exec[i.name] = (i.execution_state !== undefined ? i.execution_state : i.state) ?? null;
|
|
35
36
|
if (i.state === "awaiting_input") awaiting[i.name] = typeof i.interaction_summary === "string" ? i.interaction_summary : "";
|
|
36
37
|
}
|
|
37
|
-
appStore.set({ ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting
|
|
38
|
+
appStore.set({ ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting,
|
|
39
|
+
connections: typeof d.connections === "number" ? d.connections : null });
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/** One "activity" event: an instance started or stopped working. */
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// older snapshot. A failed read is retried a few times with a growing delay, and then waits for the person (Retry);
|
|
16
16
|
// the state is told as "hydration" events. Polls never overlap: a poll asked for while one runs follows it.
|
|
17
17
|
|
|
18
|
-
const EVENTS = ["status", "message", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs"];
|
|
18
|
+
const EVENTS = ["status", "message", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons"];
|
|
19
19
|
const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
|
|
20
20
|
const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
|
|
21
21
|
const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed
|
|
@@ -83,6 +83,7 @@ export function createStream(opts) {
|
|
|
83
83
|
if (typeof d.cursor === "string") state.cursor = d.cursor;
|
|
84
84
|
if (Array.isArray(d.deliveries)) emit("deliveries", d.deliveries);
|
|
85
85
|
if (Array.isArray(d.prompts)) emit("prompts", d.prompts);
|
|
86
|
+
if (Array.isArray(d.reply_buttons)) for (const u of d.reply_buttons) emit("reply_buttons", u); // #1266
|
|
86
87
|
if (Array.isArray(d.needs)) emit("needs", { items: d.needs });
|
|
87
88
|
return true;
|
|
88
89
|
} catch { return false; /* the next tick, or the catch-up's retry, tries again */ }
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -54,7 +54,11 @@ a { color: inherit; text-decoration: none; }
|
|
|
54
54
|
.link:hover { text-decoration: underline; }
|
|
55
55
|
|
|
56
56
|
/* ── The frame ── */
|
|
57
|
-
|
|
57
|
+
/* The page itself never scrolls: only its panes do. The shell is the containing block of anything absolutely positioned
|
|
58
|
+
inside it (an .sr-only label), so its overflow: hidden clips it — an absolute element whose containing block is the
|
|
59
|
+
viewport escapes every scroll container and made the whole page scroll (alpha.2: two scrollbars on /ui/chat). */
|
|
60
|
+
.shell { display: flex; height: 100vh; height: 100dvh; overflow: hidden; position: relative; }
|
|
61
|
+
html:has(.shell), html:has(.shell) body { overflow: hidden; }
|
|
58
62
|
.skip { position: absolute; left: var(--s-2); top: -60px; z-index: 100; background: var(--accent-fill); color: var(--on-accent); padding: var(--s-2) var(--s-3); border-radius: var(--r-sm); }
|
|
59
63
|
.skip:focus { top: var(--s-2); }
|
|
60
64
|
|
|
@@ -76,7 +80,7 @@ a { color: inherit; text-decoration: none; }
|
|
|
76
80
|
.side-row .meta { color: var(--text-3); font-size: var(--fs-xs); max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
77
81
|
.side-row.toggle { cursor: pointer; color: var(--text-2); }
|
|
78
82
|
.side-row.toggle input { margin: 0; accent-color: var(--accent); }
|
|
79
|
-
.side-section { flex: 1; min-height: 0; overflow-y: auto; padding: var(--s-2) var(--s-2); }
|
|
83
|
+
.side-section { flex: 1; min-height: 0; overflow-y: auto; padding: var(--s-2) var(--s-2); position: relative; } /* its .sr-only labels scroll (and clip) with it */
|
|
80
84
|
/* The sidebar's list scroller: thin and in the theme's colours, not the UA's light bar with arrows. WebKit/Blink take
|
|
81
85
|
the pseudo-elements; Chromium ignores them once scrollbar-color is set, so the standard properties are Firefox-only. */
|
|
82
86
|
.side-section::-webkit-scrollbar { width: 10px; }
|
|
@@ -159,6 +163,10 @@ a { color: inherit; text-decoration: none; }
|
|
|
159
163
|
.empty-title { color: var(--text); font-size: var(--fs-lg); font-weight: var(--fw-semibold); }
|
|
160
164
|
.empty-hint { font-size: var(--fs-sm); }
|
|
161
165
|
.empty .btn { margin-top: var(--s-3); }
|
|
166
|
+
/* #1519 P7: the first-run card (a fleet with no chat connection), above an empty chat or a Settings list. */
|
|
167
|
+
.first-run-stack { width: 100%; max-width: var(--col); }
|
|
168
|
+
.first-run h3 { display: flex; align-items: center; gap: var(--s-2); }
|
|
169
|
+
.first-run p { color: var(--text-2); font-size: var(--fs-sm); }
|
|
162
170
|
.error-state { display: flex; flex-direction: column; align-items: center; gap: var(--s-3); padding: var(--s-8) var(--s-4); color: var(--danger); text-align: center; }
|
|
163
171
|
.error-state p { color: var(--text); }
|
|
164
172
|
.skeleton { display: flex; flex-direction: column; gap: var(--s-3); padding: var(--s-6) var(--s-5); max-width: var(--col); margin: 0 auto; width: 100%; opacity: 0; animation: appear 0s linear 300ms forwards; }
|
|
@@ -319,6 +327,7 @@ a { color: inherit; text-decoration: none; }
|
|
|
319
327
|
.pv-newer { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; padding: var(--s-2) var(--s-3); border-radius: var(--r-sm);
|
|
320
328
|
background: var(--surface); font-size: var(--fs-xs); }
|
|
321
329
|
.pv-newer > span { flex: 1; min-width: 0; }
|
|
330
|
+
.pv-newer > .pv-newer-note { flex-basis: 100%; color: var(--text-3); }
|
|
322
331
|
.pv-panel-body { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0 var(--s-3) var(--s-3); }
|
|
323
332
|
.pv-panel-body .pv-holder { flex: 1; min-height: 0; margin: 0; display: flex; flex-direction: column; }
|
|
324
333
|
.pv-panel-body.show-code .pv-holder { display: none; }
|
|
@@ -337,6 +346,12 @@ a { color: inherit; text-decoration: none; }
|
|
|
337
346
|
@container (max-width: 560px) {
|
|
338
347
|
.pv-panel-bar :not(.pv-panel-run) > .lbl { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
339
348
|
}
|
|
349
|
+
/* #1266: an agent reply's buttons — choices under the text; one answers for everyone. */
|
|
350
|
+
.rb-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin-top: var(--s-2); }
|
|
351
|
+
.rb-btn { max-width: 100%; white-space: normal; text-align: left; }
|
|
352
|
+
.rb-btn.chosen, .rb-btn.chosen:disabled { border-color: var(--ok); color: var(--text); background: color-mix(in srgb, var(--ok) 14%, transparent); font-weight: var(--fw-semibold); opacity: 1; }
|
|
353
|
+
.rb-btn:disabled:not(.chosen) { opacity: 0.55; }
|
|
354
|
+
.rb-note { font-size: var(--fs-xs); color: var(--text-3); }
|
|
340
355
|
.pv-menu { position: relative; }
|
|
341
356
|
.pv-menu > summary { list-style: none; cursor: pointer; padding: 2px var(--s-2); border-radius: var(--r-sm); color: var(--text-3); }
|
|
342
357
|
.pv-menu > summary::-webkit-details-marker { display: none; }
|
|
@@ -484,6 +499,19 @@ a { color: inherit; text-decoration: none; }
|
|
|
484
499
|
.v-filter-box input::placeholder { color: var(--text-3); }
|
|
485
500
|
.v-filter-clear { width: 28px; height: 28px; }
|
|
486
501
|
.v-filter .note { padding: var(--s-1) var(--s-1) 0; font-variant-numeric: tabular-nums; }
|
|
502
|
+
/* alpha.2 (N1): status and CLI chips under the filter box — each a <details> with checkboxes. */
|
|
503
|
+
.nav-facets { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--s-1) var(--s-2); padding: var(--s-2) var(--s-1) 0; }
|
|
504
|
+
.nav-facet { font-size: var(--fs-xs); color: var(--text-2); }
|
|
505
|
+
.nav-facet > summary { cursor: pointer; list-style: none; padding: 2px var(--s-2); border-radius: var(--r-sm); }
|
|
506
|
+
.nav-facet > summary::-webkit-details-marker { display: none; }
|
|
507
|
+
.nav-facet > summary::after { content: " ▾"; color: var(--text-3); }
|
|
508
|
+
.nav-facet[open] > summary::after { content: " ▴"; }
|
|
509
|
+
.nav-facet > summary:hover { background: var(--hover); }
|
|
510
|
+
.nav-facet > summary:focus-visible { outline: 2px solid var(--accent); }
|
|
511
|
+
.nav-facet-n { display: inline-block; min-width: 16px; padding: 0 4px; border-radius: 8px; background: var(--accent); color: var(--on-accent, #fff); text-align: center; font-variant-numeric: tabular-nums; }
|
|
512
|
+
.nav-facet-list { display: flex; flex-direction: column; gap: 2px; padding: var(--s-1) var(--s-2); }
|
|
513
|
+
.nav-chip { display: flex; align-items: center; gap: var(--s-2); cursor: pointer; color: var(--text); }
|
|
514
|
+
.nav-facet-reset { font-size: var(--fs-xs); }
|
|
487
515
|
|
|
488
516
|
.p-view .panel-actions .btn { gap: var(--s-1); }
|
|
489
517
|
.v-term { flex: 1; min-height: 0; overflow: auto; scrollbar-gutter: stable; padding: 5px 6px; background: #000000; position: relative; }
|
|
@@ -587,6 +615,10 @@ a { color: inherit; text-decoration: none; }
|
|
|
587
615
|
.chip-x:hover { color: var(--text); background: var(--surface); }
|
|
588
616
|
.dlg-impact { margin-right: auto; align-self: center; }
|
|
589
617
|
.dlg-inline-actions { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
|
|
618
|
+
/* #1519 P1: the bot-token field — the input and its show/hide button on one row. */
|
|
619
|
+
.token-row { display: flex; align-items: center; gap: var(--s-1); }
|
|
620
|
+
.token-row input { flex: 1; min-width: 0; }
|
|
621
|
+
.feedback.ok { color: var(--ok, var(--text)); }
|
|
590
622
|
.seg-inline { display: inline-flex; gap: var(--s-1); }
|
|
591
623
|
.btn.on { background: var(--surface-2); color: var(--text); font-weight: var(--fw-semibold); }
|
|
592
624
|
.card h4 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text-2); }
|