@songsid/agend 2.1.12 → 2.2.0-alpha.1
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/auth/one-time-code.d.ts +19 -0
- package/dist/auth/one-time-code.js +49 -0
- package/dist/auth/one-time-code.js.map +1 -0
- package/dist/auth-api.d.ts +52 -0
- package/dist/auth-api.js +346 -0
- package/dist/auth-api.js.map +1 -0
- package/dist/backend/claude-code.d.ts +4 -0
- package/dist/backend/claude-code.js +97 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.js +59 -30
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-agent.d.ts +66 -0
- package/dist/backend/kiro-agent.js +302 -0
- package/dist/backend/kiro-agent.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +80 -0
- package/dist/backend/kiro-identity.js +537 -0
- package/dist/backend/kiro-identity.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
- package/dist/backend/kiro-v2-store-worker.js +87 -0
- package/dist/backend/kiro-v2-store-worker.js.map +1 -0
- package/dist/backend/kiro-v2-store.d.ts +54 -0
- package/dist/backend/kiro-v2-store.js +131 -0
- package/dist/backend/kiro-v2-store.js.map +1 -0
- package/dist/backend/kiro-v3-identity.d.ts +5 -0
- package/dist/backend/kiro-v3-identity.js +22 -0
- package/dist/backend/kiro-v3-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +83 -7
- package/dist/backend/kiro.js +418 -91
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +45 -3
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +1 -0
- package/dist/channel/adapters/discord.js +29 -10
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +1 -0
- package/dist/channel/adapters/telegram.js +13 -1
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/types.d.ts +9 -1
- package/dist/classic-channel-manager.d.ts +18 -10
- package/dist/classic-channel-manager.js +45 -18
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +225 -127
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +5 -3
- package/dist/command-table.js +29 -27
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +46 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/connection-secrets.d.ts +6 -0
- package/dist/connection-secrets.js +4 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/daemon.d.ts +89 -4
- package/dist/daemon.js +661 -77
- package/dist/daemon.js.map +1 -1
- package/dist/darwin-memory.d.ts +6 -1
- package/dist/darwin-memory.js +17 -6
- package/dist/darwin-memory.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +85 -0
- package/dist/delivery-consumed-watch.js +244 -0
- package/dist/delivery-consumed-watch.js.map +1 -0
- package/dist/delivery-outbox.d.ts +61 -0
- package/dist/delivery-outbox.js +120 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +14 -5
- package/dist/delivery-reconciliation.js +27 -4
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/delivery-reconciliation.test.js +35 -1
- package/dist/delivery-reconciliation.test.js.map +1 -1
- package/dist/event-loop-watch.d.ts +28 -0
- package/dist/event-loop-watch.js +75 -3
- package/dist/event-loop-watch.js.map +1 -1
- package/dist/fleet-context.d.ts +22 -2
- package/dist/fleet-level-config.d.ts +2 -2
- package/dist/fleet-level-config.js +6 -1
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +293 -12
- package/dist/fleet-manager.js +2364 -449
- package/dist/fleet-manager.js.map +1 -1
- package/dist/host-memory.d.ts +4 -0
- package/dist/host-memory.js.map +1 -1
- package/dist/instance-config-impact.js +5 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +10 -5
- package/dist/instance-lifecycle.js +162 -53
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +150 -16
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.d.ts +2 -0
- package/dist/login-controller.js +17 -5
- package/dist/login-controller.js.map +1 -1
- package/dist/memory-pressure.d.ts +7 -8
- package/dist/memory-pressure.js +76 -15
- package/dist/memory-pressure.js.map +1 -1
- package/dist/monotonic-budget.d.ts +2 -0
- package/dist/monotonic-budget.js +28 -0
- package/dist/monotonic-budget.js.map +1 -0
- package/dist/needs-you-hub.d.ts +114 -0
- package/dist/needs-you-hub.js +370 -0
- package/dist/needs-you-hub.js.map +1 -0
- package/dist/needs-you-live.d.ts +116 -0
- package/dist/needs-you-live.js +228 -0
- package/dist/needs-you-live.js.map +1 -0
- package/dist/needs-you.d.ts +132 -0
- package/dist/needs-you.js +164 -0
- package/dist/needs-you.js.map +1 -0
- package/dist/node-version-guard.d.ts +30 -0
- package/dist/node-version-guard.js +50 -0
- package/dist/node-version-guard.js.map +1 -0
- package/dist/operator-control-paths.d.ts +4 -0
- package/dist/operator-control-paths.js +24 -0
- package/dist/operator-control-paths.js.map +1 -0
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +9 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +5 -5
- package/dist/profile-control.js +4 -20
- package/dist/profile-control.js.map +1 -1
- package/dist/public-web-gateway.d.ts +18 -0
- package/dist/public-web-gateway.js +141 -0
- package/dist/public-web-gateway.js.map +1 -0
- package/dist/public-web-link.d.ts +53 -0
- package/dist/public-web-link.js +175 -0
- package/dist/public-web-link.js.map +1 -0
- package/dist/quickstart-api.d.ts +8 -1
- package/dist/quickstart-api.js +86 -48
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +1 -0
- package/dist/quickstart.js +6 -2
- package/dist/quickstart.js.map +1 -1
- package/dist/scheduler/db.d.ts +7 -1
- package/dist/scheduler/db.js +38 -0
- package/dist/scheduler/db.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/retry-label.d.ts +11 -0
- package/dist/scheduler/retry-label.js +24 -0
- package/dist/scheduler/retry-label.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +55 -2
- package/dist/scheduler/scheduler.js +220 -8
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/types.d.ts +22 -0
- package/dist/scheduler/types.js.map +1 -1
- package/dist/secret-store.d.ts +5 -0
- package/dist/secret-store.js +20 -0
- package/dist/secret-store.js.map +1 -1
- package/dist/service-activation.d.ts +110 -0
- package/dist/service-activation.js +302 -0
- package/dist/service-activation.js.map +1 -0
- package/dist/service-installer.d.ts +7 -0
- package/dist/service-installer.js +7 -4
- package/dist/service-installer.js.map +1 -1
- package/dist/service-restart-selection.d.ts +6 -5
- package/dist/service-restart-selection.js +6 -5
- package/dist/service-restart-selection.js.map +1 -1
- package/dist/settings-api.d.ts +10 -2
- package/dist/settings-api.js +175 -72
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.d.ts +20 -0
- package/dist/settings-baseline.js +104 -0
- package/dist/settings-baseline.js.map +1 -0
- package/dist/settings-change.d.ts +21 -0
- package/dist/settings-change.js +133 -0
- package/dist/settings-change.js.map +1 -0
- package/dist/settings-confirmation.d.ts +105 -0
- package/dist/settings-confirmation.js +232 -0
- package/dist/settings-confirmation.js.map +1 -0
- package/dist/settings-control.d.ts +41 -0
- package/dist/settings-control.js +294 -0
- package/dist/settings-control.js.map +1 -0
- package/dist/settings-effect.d.ts +24 -0
- package/dist/settings-effect.js +169 -0
- package/dist/settings-effect.js.map +1 -0
- package/dist/settings-http-confirmation.d.ts +29 -0
- package/dist/settings-http-confirmation.js +123 -0
- package/dist/settings-http-confirmation.js.map +1 -0
- package/dist/settings-instance-patch.d.ts +4 -0
- package/dist/settings-instance-patch.js +26 -0
- package/dist/settings-instance-patch.js.map +1 -0
- package/dist/settings-request-capability.d.ts +16 -0
- package/dist/settings-request-capability.js +32 -0
- package/dist/settings-request-capability.js.map +1 -0
- package/dist/settings-transaction.d.ts +65 -0
- package/dist/settings-transaction.js +223 -0
- package/dist/settings-transaction.js.map +1 -0
- package/dist/setup-auth.d.ts +10 -18
- package/dist/setup-auth.js +14 -38
- package/dist/setup-auth.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +75 -18
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-guide.d.ts +13 -0
- package/dist/setup-guide.js +25 -0
- package/dist/setup-guide.js.map +1 -0
- package/dist/setup-host.d.ts +16 -0
- package/dist/setup-host.js +197 -32
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +3 -0
- package/dist/setup-wizard.js.map +1 -1
- package/dist/slash-authz.d.ts +17 -3
- package/dist/slash-authz.js +5 -0
- package/dist/slash-authz.js.map +1 -1
- package/dist/spawn-gate.js +3 -3
- package/dist/spawn-gate.js.map +1 -1
- package/dist/statusline-watcher.d.ts +5 -0
- package/dist/statusline-watcher.js +2 -0
- package/dist/statusline-watcher.js.map +1 -1
- package/dist/steer-capability.d.ts +6 -0
- package/dist/steer-capability.js +8 -0
- package/dist/steer-capability.js.map +1 -1
- package/dist/sync-work-attribution.d.ts +14 -0
- package/dist/sync-work-attribution.js +43 -8
- package/dist/sync-work-attribution.js.map +1 -1
- package/dist/tips.js +10 -5
- package/dist/tips.js.map +1 -1
- package/dist/tmux-control.d.ts +35 -5
- package/dist/tmux-control.js +291 -57
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.d.ts +20 -6
- package/dist/tmux-manager.js +90 -47
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.d.ts +59 -0
- package/dist/tmux-read.js +213 -0
- package/dist/tmux-read.js.map +1 -0
- package/dist/topic-commands.d.ts +21 -10
- package/dist/topic-commands.js +191 -58
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.js +2 -1
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/tunnel/cloudflared-install.d.ts +2 -0
- package/dist/tunnel/cloudflared-install.js +1 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/purpose-lane.d.ts +14 -0
- package/dist/tunnel/purpose-lane.js +43 -0
- package/dist/tunnel/purpose-lane.js.map +1 -0
- package/dist/turn-reply-guard.d.ts +2 -0
- package/dist/turn-reply-guard.js +10 -1
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/ui/app.html +18 -0
- package/dist/ui/chat-render.js +478 -0
- package/dist/ui/chat-store.js +289 -0
- package/dist/ui/chat-strings.js +69 -0
- package/dist/ui/chat-thread.js +209 -0
- package/dist/ui/chat-tour.js +116 -0
- package/dist/ui/fleet-org.js +142 -0
- package/dist/ui/panel-chat.js +418 -0
- package/dist/ui/panel-fleet.js +413 -0
- package/dist/ui/panel-needs.js +202 -0
- package/dist/ui/panel-settings.js +750 -0
- package/dist/ui/preview.js +266 -0
- package/dist/ui/settings-apply.js +239 -0
- package/dist/ui/settings-confirm.js +186 -0
- package/dist/ui/settings-dialogs.js +546 -0
- package/dist/ui/settings-model.js +277 -0
- package/dist/ui/settings-strings.js +652 -0
- package/dist/ui/settings-wizard.js +140 -0
- package/dist/ui/shared/agend-auth.js +93 -0
- package/dist/ui/shared/app-ctx.js +80 -0
- package/dist/ui/shared/app-html.js +9 -0
- package/dist/ui/shared/app-i18n.js +95 -0
- package/dist/ui/shared/app-nav.js +73 -0
- package/dist/ui/shared/app-needs.js +114 -0
- package/dist/ui/shared/app-route.js +73 -0
- package/dist/ui/shared/app-session.js +66 -0
- package/dist/ui/shared/app-shell.js +344 -0
- package/dist/ui/shared/app-store.js +51 -0
- package/dist/ui/shared/app-stream.js +201 -0
- package/dist/ui/shared/app.css +676 -0
- package/dist/ui/shared/app.js +88 -0
- package/dist/ui/shared/fonts/OFL.txt +92 -0
- package/dist/ui/shared/fonts/README.md +21 -0
- package/dist/ui/shared/fonts/inter.woff2 +0 -0
- package/dist/ui/shared/htm.module.js +1 -0
- package/dist/ui/shared/panel-view.js +655 -0
- package/dist/ui/shared/preact-hooks.module.js +2 -0
- package/dist/ui/shared/preact.module.js +2 -0
- package/dist/ui/shared/signin.js +130 -0
- package/dist/ui/shared/theme.js +29 -0
- package/dist/ui/shared/tokens.css +153 -0
- package/dist/ui/shared/ui-confirm.js +43 -0
- package/dist/ui/shared/ui-dialog.js +45 -0
- package/dist/ui/shared/ui-icons.js +61 -0
- package/dist/ui/shared/ui-menu.js +39 -0
- package/dist/ui/shared/ui-states.js +20 -0
- package/dist/ui/shared/ui-toast.js +18 -0
- package/dist/ui/shared/vendor/htm.LICENSE +202 -0
- package/dist/ui/shared/vendor/preact.LICENSE +21 -0
- package/dist/ui/shared/vendor/vendor.json +39 -0
- package/dist/ui/shared/view-strings.js +309 -0
- package/dist/ui/signin.html +51 -0
- package/dist/update-check.d.ts +6 -2
- package/dist/update-check.js +12 -3
- package/dist/update-check.js.map +1 -1
- package/dist/update-install.d.ts +71 -0
- package/dist/update-install.js +162 -0
- package/dist/update-install.js.map +1 -0
- package/dist/upgrade-notices.d.ts +17 -0
- package/dist/upgrade-notices.js +93 -0
- package/dist/upgrade-notices.js.map +1 -0
- package/dist/usage/statusline-usage.d.ts +2 -0
- package/dist/usage/statusline-usage.js +14 -10
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/view-api.d.ts +6 -1
- package/dist/view-api.js +62 -51
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +71 -14
- package/dist/web-api.js +492 -77
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +71 -27
- package/dist/web-auth.js +169 -69
- package/dist/web-auth.js.map +1 -1
- package/dist/web-body.d.ts +3 -0
- package/dist/web-body.js +55 -0
- package/dist/web-body.js.map +1 -0
- package/dist/web-channel-echo.d.ts +9 -0
- package/dist/web-channel-echo.js +55 -0
- package/dist/web-channel-echo.js.map +1 -0
- package/dist/web-chat-history.d.ts +125 -0
- package/dist/web-chat-history.js +184 -0
- package/dist/web-chat-history.js.map +1 -0
- package/dist/web-continuation.d.ts +7 -0
- package/dist/web-continuation.js +22 -0
- package/dist/web-continuation.js.map +1 -0
- package/dist/web-host-guard.d.ts +34 -0
- package/dist/web-host-guard.js +57 -1
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-login.d.ts +72 -0
- package/dist/web-login.js +132 -0
- package/dist/web-login.js.map +1 -0
- package/dist/web-org.d.ts +55 -0
- package/dist/web-org.js +67 -0
- package/dist/web-org.js.map +1 -0
- package/dist/web-preview.d.ts +77 -0
- package/dist/web-preview.js +240 -0
- package/dist/web-preview.js.map +1 -0
- package/dist/web-request-context.d.ts +10 -0
- package/dist/web-request-context.js +13 -0
- package/dist/web-request-context.js.map +1 -0
- package/dist/web-session.d.ts +167 -0
- package/dist/web-session.js +389 -0
- package/dist/web-session.js.map +1 -0
- package/dist/web-shell-routes.d.ts +59 -0
- package/dist/web-shell-routes.js +96 -0
- package/dist/web-shell-routes.js.map +1 -0
- package/dist/web-upload.d.ts +183 -0
- package/dist/web-upload.js +468 -0
- package/dist/web-upload.js.map +1 -0
- package/dist/website-tips-renderer.d.ts +2 -2
- package/dist/website-tips-renderer.js +3 -3
- package/dist/website-tips-renderer.js.map +1 -1
- package/package.json +8 -5
- package/scripts/preinstall-guard.cjs +36 -0
- package/dist/ui/dashboard.html +0 -774
- package/dist/ui/settings.html +0 -2165
- package/dist/ui/view.html +0 -1139
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// #1408 step 1: the Fleet panel (/ui/fleet[/<tab>]) — tasks, schedules, teams and the fleet's config — and the
|
|
2
|
+
// "New instance" dialog. Behind the session gate (/ui/js/), loaded the first time it is opened.
|
|
3
|
+
//
|
|
4
|
+
// Opening a tab reads its list: a person's navigation, so it counts as use, as the old tab click did (#1374 — no timer
|
|
5
|
+
// here re-reads anything). Every read is under the navigation's lease: a list that arrives after the person moved on
|
|
6
|
+
// is dropped (#1408 §4).
|
|
7
|
+
import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
8
|
+
import { t, register } from "/assets/app-i18n.js";
|
|
9
|
+
import { appStore, useStore } from "/assets/app-store.js";
|
|
10
|
+
import { useLease } from "/assets/app-ctx.js";
|
|
11
|
+
import { PanelHeader, setTitle } from "/assets/app-shell.js";
|
|
12
|
+
import { renavigate } from "/assets/app-nav.js";
|
|
13
|
+
import { chatPath, fleetPath, FLEET_TABS } from "/assets/app-route.js";
|
|
14
|
+
import { Dialog } from "/assets/ui-dialog.js";
|
|
15
|
+
import { Empty, ErrorState, Skeleton } from "/assets/ui-states.js";
|
|
16
|
+
import { Icon } from "/assets/ui-icons.js";
|
|
17
|
+
import { toast } from "/assets/ui-toast.js";
|
|
18
|
+
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
19
|
+
import { confirmedWrite } from "./settings-confirm.js";
|
|
20
|
+
import { OrgChart } from "./fleet-org.js";
|
|
21
|
+
|
|
22
|
+
register("fleet", {
|
|
23
|
+
en: {
|
|
24
|
+
title: "Fleet", tasks: "Tasks", schedules: "Schedules", teams: "Teams", org: "Org chart", config: "Config",
|
|
25
|
+
newTask: "New task", newSchedule: "New schedule", newTeam: "New team",
|
|
26
|
+
noTasks: "No tasks yet", noTasksHint: "Create one to coordinate fleet work.", noSchedules: "No schedules yet", noSchedulesHint: "Automate recurring work with cron.",
|
|
27
|
+
noTeams: "No teams yet", noTeamsHint: "Group instances for coordinated work.", countTasks: "{0} tasks", countSchedules: "{0} schedules", countTeams: "{0} teams",
|
|
28
|
+
claim: "Claim", done: "Done", delete: "Delete", on: "on", off: "off", cancel: "Cancel", create: "Create", creating: "Creating…", save: "Save changes",
|
|
29
|
+
taskTitle: "Title", taskDesc: "Description", priority: "Priority", assignee: "Assignee", unassigned: "Unassigned",
|
|
30
|
+
pNormal: "Normal", pLow: "Low", pHigh: "High", pUrgent: "Urgent",
|
|
31
|
+
cron: "Cron expression", message: "Message", target: "Target instance", label: "Label", teamName: "Team name", members: "Members",
|
|
32
|
+
titleRequired: "Title required", scheduleFieldsRequired: "Cron, message, and target required", teamFieldsRequired: "Name and at least one member required",
|
|
33
|
+
deleteSchedule: "Delete this schedule?", deleteTeam: "Delete team “{0}”?", taskCreated: "Task created", scheduleCreated: "Schedule created", scheduleDeleted: "Schedule deleted",
|
|
34
|
+
teamCreated: "Team created", teamDeleted: "Team deleted", taskDone: "Task updated", configSaved: "Config saved", configRestart: " (restart the fleet to apply channel changes)",
|
|
35
|
+
channel: "Channel", type: "Type", tokenEnv: "Bot token env", groupId: "Group ID", show: "Show", hide: "Hide", access: "Access control", mode: "Mode", locked: "Locked", open: "Open",
|
|
36
|
+
allowedUsers: "Allowed users", allowedUsersNote: "Comma-separated user IDs", defaults: "Defaults", backend: "Backend", roots: "Project roots", addRoot: "Add root", removeRoot: "Remove",
|
|
37
|
+
saveNote: "Saving rewrites fleet.yaml (its comments are lost).",
|
|
38
|
+
newInstance: "New instance", directory: "Directory", directoryHint: "Optional — a workspace is created if empty", topic: "Topic name", topicHint: "From the directory name",
|
|
39
|
+
topicRequired: "Topic name is required when Directory is empty", description: "Description", descriptionHint: "What this instance does", model: "Model", modelHint: "e.g. sonnet, opus, gpt-5",
|
|
40
|
+
branch: "Branch (git worktree)", branchHint: "e.g. feature-x", tags: "Tags", tagsHint: "comma-separated, e.g. dev, review", systemPrompt: "System prompt", systemPromptHint: "Custom instructions for this instance",
|
|
41
|
+
fleetDefault: "Use the fleet default", notInstalled: "(not installed)", deprecated: "(deprecated)", instanceCreated: "Instance created",
|
|
42
|
+
},
|
|
43
|
+
"zh-TW": {
|
|
44
|
+
title: "Fleet", tasks: "Tasks", schedules: "排程", teams: "Teams", org: "組織圖", config: "設定",
|
|
45
|
+
newTask: "新增 task", newSchedule: "新增排程", newTeam: "新增 team",
|
|
46
|
+
noTasks: "還沒有 task", noTasksHint: "建立一個來協調 fleet 的工作。", noSchedules: "還沒有排程", noSchedulesHint: "用 cron 自動執行例行工作。",
|
|
47
|
+
noTeams: "還沒有 team", noTeamsHint: "把 instance 分組一起工作。", countTasks: "{0} 個 task", countSchedules: "{0} 個排程", countTeams: "{0} 個 team",
|
|
48
|
+
claim: "認領", done: "完成", delete: "刪除", on: "開", off: "關", cancel: "取消", create: "建立", creating: "建立中…", save: "儲存變更",
|
|
49
|
+
taskTitle: "標題", taskDesc: "說明", priority: "優先順序", assignee: "負責人", unassigned: "未指派",
|
|
50
|
+
pNormal: "一般", pLow: "低", pHigh: "高", pUrgent: "緊急",
|
|
51
|
+
cron: "Cron 表示式", message: "訊息", target: "目標 instance", label: "標籤", teamName: "Team 名稱", members: "成員",
|
|
52
|
+
titleRequired: "必須填寫標題", scheduleFieldsRequired: "必須填寫 Cron、訊息與目標 instance", teamFieldsRequired: "必須填寫名稱並選擇至少一名成員",
|
|
53
|
+
deleteSchedule: "確定刪除此排程嗎?", deleteTeam: "確定刪除 team「{0}」嗎?", taskCreated: "Task 已建立", scheduleCreated: "排程已建立", scheduleDeleted: "排程已刪除",
|
|
54
|
+
teamCreated: "Team 已建立", teamDeleted: "Team 已刪除", taskDone: "Task 已更新", configSaved: "設定已儲存", configRestart: "(請重新啟動 fleet 以套用頻道變更)",
|
|
55
|
+
channel: "頻道", type: "類型", tokenEnv: "Bot token 環境變數", groupId: "Group ID", show: "顯示", hide: "隱藏", access: "存取控制", mode: "模式", locked: "鎖定", open: "開放",
|
|
56
|
+
allowedUsers: "允許的使用者", allowedUsersNote: "以逗號分隔的使用者 ID", defaults: "預設值", backend: "Backend", roots: "專案根目錄", addRoot: "新增根目錄", removeRoot: "移除",
|
|
57
|
+
saveNote: "儲存會改寫 fleet.yaml(其中的註解會遺失)。",
|
|
58
|
+
newInstance: "新增 instance", directory: "目錄", directoryHint: "可留空——留空會自動建立工作區", topic: "Topic 名稱", topicHint: "預設取自目錄名稱",
|
|
59
|
+
topicRequired: "目錄留空時必須填寫 Topic 名稱", description: "說明", descriptionHint: "這個 instance 做什麼", model: "模型", modelHint: "例如 sonnet、opus、gpt-5",
|
|
60
|
+
branch: "分支(git worktree)", branchHint: "例如 feature-x", tags: "標籤", tagsHint: "以逗號分隔,例如 dev, review", systemPrompt: "System prompt", systemPromptHint: "這個 instance 的自訂指示",
|
|
61
|
+
fleetDefault: "使用 fleet 預設", notInstalled: "(未安裝)", deprecated: "(已淘汰)", instanceCreated: "Instance 已建立",
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const ICONS = { tasks: "tasks", schedules: "clock", teams: "team", org: "org", config: "sliders" };
|
|
66
|
+
const BACKENDS = ["claude-code", "codex", "opencode", "kiro-cli", "antigravity", "grok", "muse"];
|
|
67
|
+
|
|
68
|
+
async function api(method, path, body, lease) {
|
|
69
|
+
const o = { method, headers: { "Content-Type": "application/json" } };
|
|
70
|
+
if (body) o.body = JSON.stringify(body);
|
|
71
|
+
const r = await (lease ? lease.fetch(path, o) : fetch(path, o));
|
|
72
|
+
return r.json();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Load `path` under the lease: `null` while the first load runs, `{ error }` on failure. A refresh (a new `version`,
|
|
77
|
+
* after Claim or Create) keeps showing what is there until the new list arrives — nothing below it is torn down, so
|
|
78
|
+
* an open dialog and its draft survive (#1425 review). Only the latest request's answer is kept.
|
|
79
|
+
*/
|
|
80
|
+
function useLoad(lease, path, version) {
|
|
81
|
+
const [state, setState] = useState(null);
|
|
82
|
+
const seq = useRef(0);
|
|
83
|
+
const shownFor = useRef(null);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const mine = ++seq.current;
|
|
86
|
+
if (shownFor.current !== lease) { shownFor.current = lease; setState(null); }
|
|
87
|
+
(async () => {
|
|
88
|
+
try {
|
|
89
|
+
const d = await api("GET", path, null, lease);
|
|
90
|
+
if (lease.current() && mine === seq.current) setState({ data: d });
|
|
91
|
+
} catch { if (lease.current() && mine === seq.current) setState({ error: true }); }
|
|
92
|
+
})();
|
|
93
|
+
}, [lease, path, version]);
|
|
94
|
+
return state;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function FleetPanel({ route, navKey }) {
|
|
98
|
+
const lease = useLease(navKey);
|
|
99
|
+
const tab = route.tab;
|
|
100
|
+
useEffect(() => { setTitle(`${t("fleet.title")} · ${t(`fleet.${tab}`)}`); }, [tab, navKey]);
|
|
101
|
+
const Body = { tasks: Tasks, schedules: Schedules, teams: Teams, org: Org, config: Config }[tab] || Tasks;
|
|
102
|
+
return html`<div class="panel p-fleet">
|
|
103
|
+
<${PanelHeader} title=${t("fleet.title")} />
|
|
104
|
+
<nav class="seg" aria-label=${t("fleet.title")}>${FLEET_TABS.map(k => html`<a key=${k} href=${fleetPath(k)} class=${`seg-item${k === tab ? " active" : ""}`}
|
|
105
|
+
aria-current=${k === tab ? "page" : undefined}><${Icon} name=${ICONS[k]} size=${16} /><span>${t(`fleet.${k}`)}</span></a>`)}</nav>
|
|
106
|
+
<div class="panel-body"><div class=${`col${tab === "org" ? " col-wide" : ""}`}><${Body} lease=${lease} /></div></div>
|
|
107
|
+
</div>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ListHead({ count, label, action }) {
|
|
111
|
+
return html`<div class="list-head"><span>${count}</span>${action ? html`<button type="button" class="btn btn-primary" onClick=${action.onClick}><${Icon} name="plus" size=${16} />${label}</button>` : null}</div>`;
|
|
112
|
+
}
|
|
113
|
+
function Loaded({ state, children }) {
|
|
114
|
+
if (!state) return html`<${Skeleton} lines=${4} />`;
|
|
115
|
+
if (state.error) return html`<${ErrorState} onRetry=${renavigate} />`;
|
|
116
|
+
return children(state.data);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Tasks({ lease }) {
|
|
120
|
+
const [version, setVersion] = useState(0);
|
|
121
|
+
const [creating, setCreating] = useState(false);
|
|
122
|
+
const state = useLoad(lease, "/ui/tasks", version);
|
|
123
|
+
async function act(id, action) {
|
|
124
|
+
const r = await api("POST", `/ui/tasks/${encodeURIComponent(id)}`, { action });
|
|
125
|
+
if (r.error) toast(r.error, false); else { toast(t("fleet.taskDone")); setVersion(v => v + 1); }
|
|
126
|
+
}
|
|
127
|
+
return html`<${Loaded} state=${state}>${(d) => {
|
|
128
|
+
const tasks = d.tasks || [];
|
|
129
|
+
return html`<${ListHead} count=${t("fleet.countTasks", tasks.length)} label=${t("fleet.newTask")} action=${{ onClick: () => setCreating(true) }} />
|
|
130
|
+
${tasks.length ? html`<ul class="rows">${tasks.map(x => html`<li key=${x.id} class="row-item">
|
|
131
|
+
<span class=${`pill ${x.status}`}>${x.status}</span><span class="grow">${x.title}</span>
|
|
132
|
+
${x.assignee ? html`<a class="link" href=${chatPath(x.assignee)}>${x.assignee}</a>` : null}
|
|
133
|
+
${x.status === "open" ? html`<button type="button" class="btn btn-ghost btn-sm" onClick=${() => act(x.id, "claim")}>${t("fleet.claim")}</button>` : null}
|
|
134
|
+
${x.status === "claimed" ? html`<button type="button" class="btn btn-ghost btn-sm" onClick=${() => act(x.id, "complete")}>${t("fleet.done")}</button>` : null}
|
|
135
|
+
</li>`)}</ul>` : html`<${Empty} icon="tasks" title=${t("fleet.noTasks")} hint=${t("fleet.noTasksHint")} />`}
|
|
136
|
+
`;
|
|
137
|
+
}}</${Loaded}>
|
|
138
|
+
${creating ? html`<${CreateTask} onClose=${() => setCreating(false)} onDone=${() => setVersion(v => v + 1)} />` : null}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function Schedules({ lease }) {
|
|
142
|
+
const [version, setVersion] = useState(0);
|
|
143
|
+
const [creating, setCreating] = useState(false);
|
|
144
|
+
const state = useLoad(lease, "/ui/schedules", version);
|
|
145
|
+
async function del(id) {
|
|
146
|
+
if (!(await confirmDialog({ message: t("fleet.deleteSchedule"), confirmLabel: t("fleet.delete"), danger: true }))) return;
|
|
147
|
+
if (!lease.current()) return; // answered after this tab went: the question no longer stands
|
|
148
|
+
const r = await api("DELETE", `/ui/schedules/${encodeURIComponent(id)}`);
|
|
149
|
+
if (r.error) toast(r.error, false); else { toast(t("fleet.scheduleDeleted")); setVersion(v => v + 1); }
|
|
150
|
+
}
|
|
151
|
+
return html`<${Loaded} state=${state}>${(d) => {
|
|
152
|
+
const list = d.schedules || [];
|
|
153
|
+
return html`<${ListHead} count=${t("fleet.countSchedules", list.length)} label=${t("fleet.newSchedule")} action=${{ onClick: () => setCreating(true) }} />
|
|
154
|
+
${list.length ? html`<ul class="rows">${list.map(x => html`<li key=${x.id} class="row-item">
|
|
155
|
+
<span class=${`pill ${x.enabled ? "open" : "done"}`}>${x.enabled ? t("fleet.on") : t("fleet.off")}</span>
|
|
156
|
+
<span class="grow">${x.label || cronDesc(x.cron)}</span><span class="mono muted" title=${cronDesc(x.cron)}>${x.cron}</span>
|
|
157
|
+
<a class="link" href=${chatPath(x.target)}>${x.target}</a>
|
|
158
|
+
<button type="button" class="btn btn-ghost btn-sm danger" onClick=${() => del(x.id)}>${t("fleet.delete")}</button></li>`)}</ul>`
|
|
159
|
+
: html`<${Empty} icon="clock" title=${t("fleet.noSchedules")} hint=${t("fleet.noSchedulesHint")} />`}
|
|
160
|
+
`;
|
|
161
|
+
}}</${Loaded}>
|
|
162
|
+
${creating ? html`<${CreateSchedule} onClose=${() => setCreating(false)} onDone=${() => setVersion(v => v + 1)} />` : null}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function Teams({ lease }) {
|
|
166
|
+
const [version, setVersion] = useState(0);
|
|
167
|
+
const [creating, setCreating] = useState(false);
|
|
168
|
+
const state = useLoad(lease, "/ui/teams", version);
|
|
169
|
+
async function del(name) {
|
|
170
|
+
if (!(await confirmDialog({ message: t("fleet.deleteTeam", name), confirmLabel: t("fleet.delete"), danger: true }))) return;
|
|
171
|
+
if (!lease.current()) return;
|
|
172
|
+
const r = await api("DELETE", `/ui/teams/${encodeURIComponent(name)}`);
|
|
173
|
+
if (r.error) toast(r.error, false); else { toast(t("fleet.teamDeleted")); setVersion(v => v + 1); }
|
|
174
|
+
}
|
|
175
|
+
return html`<${Loaded} state=${state}>${(d) => {
|
|
176
|
+
const entries = Object.entries(d.teams || {});
|
|
177
|
+
return html`<${ListHead} count=${t("fleet.countTeams", entries.length)} label=${t("fleet.newTeam")} action=${{ onClick: () => setCreating(true) }} />
|
|
178
|
+
${entries.length ? html`<ul class="rows">${entries.map(([name, team]) => html`<li key=${name} class="row-item">
|
|
179
|
+
<span class="grow strong">${name}</span><span class="muted">${(team.members || []).join(", ")}</span>
|
|
180
|
+
<button type="button" class="btn btn-ghost btn-sm danger" onClick=${() => del(name)}>${t("fleet.delete")}</button></li>`)}</ul>`
|
|
181
|
+
: html`<${Empty} icon="team" title=${t("fleet.noTeams")} hint=${t("fleet.noTeamsHint")} />`}
|
|
182
|
+
`;
|
|
183
|
+
}}</${Loaded}>
|
|
184
|
+
${creating ? html`<${CreateTeam} onClose=${() => setCreating(false)} onDone=${() => setVersion(v => v + 1)} />` : null}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** #1389: the org chart — its structure read once per opening, its live state from the app store (fleet-org.js). */
|
|
188
|
+
function Org({ lease }) {
|
|
189
|
+
const state = useLoad(lease, "/ui/org", 0);
|
|
190
|
+
const app = useStore(appStore);
|
|
191
|
+
return html`<${Loaded} state=${state}>${(org) => html`<${OrgChart} org=${org} app=${app} />`}</${Loaded}>`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function Secret({ id, value, onInput, label }) {
|
|
195
|
+
const [shown, setShown] = useState(false);
|
|
196
|
+
return html`<div class="field-row"><input id=${id} type=${shown ? "text" : "password"} value=${value} onInput=${onInput} autocomplete="off" />
|
|
197
|
+
<button type="button" class="icon-btn" aria-label=${shown ? t("fleet.hide") : t("fleet.show")} aria-pressed=${shown ? "true" : "false"} title=${`${shown ? t("fleet.hide") : t("fleet.show")} ${label}`}
|
|
198
|
+
onClick=${() => setShown(!shown)}><${Icon} name="eye" size=${16} /></button></div>`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function Config({ lease }) {
|
|
202
|
+
const state = useLoad(lease, "/ui/config", 0);
|
|
203
|
+
const [form, setForm] = useState(null);
|
|
204
|
+
const [saving, setSaving] = useState(false);
|
|
205
|
+
// Each save owns the form's busy state only while it is the latest: an older save that is decided later (it waited
|
|
206
|
+
// for an admin's confirmation) never unlocks the form under a newer one still on its way (#1453 review).
|
|
207
|
+
const saves = useRef(0);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!state || !state.data) return;
|
|
210
|
+
const c = state.data, ch = c.channel || {}, acc = ch.access || {};
|
|
211
|
+
setForm({ type: ch.type || "", tokenEnv: ch.bot_token_env || "AGEND_BOT_TOKEN", groupId: ch.group_id || "", mode: acc.mode || "locked",
|
|
212
|
+
users: (acc.allowed_users || []).join(", "), backend: (c.defaults || {}).backend || "claude-code", roots: [...(c.project_roots || [])] });
|
|
213
|
+
}, [state]);
|
|
214
|
+
async function save() {
|
|
215
|
+
const mine = ++saves.current;
|
|
216
|
+
const done = () => { if (lease.current() && mine === saves.current) setSaving(false); };
|
|
217
|
+
setSaving(true);
|
|
218
|
+
const body = {
|
|
219
|
+
channel: { group_id: form.groupId.trim(), access: { mode: form.mode, allowed_users: form.users.split(",").map(s => s.trim()).filter(Boolean) } },
|
|
220
|
+
defaults: { backend: form.backend },
|
|
221
|
+
project_roots: form.roots.map(r => r.trim()).filter(Boolean),
|
|
222
|
+
};
|
|
223
|
+
let r;
|
|
224
|
+
// A change to access or the connection may need a fleet admin's confirmation (#1423): the shell follows it.
|
|
225
|
+
try {
|
|
226
|
+
const res = await confirmedWrite("/ui/config", { method: "POST", body, label: t("fleet.config"),
|
|
227
|
+
onPending: () => { toast(t("app.pendingSent")); done(); } });
|
|
228
|
+
r = res.ok ? res.body || {} : { error: (res.body && res.body.error) || `HTTP ${res.status}` };
|
|
229
|
+
} catch (err) { r = { error: err.message }; }
|
|
230
|
+
done();
|
|
231
|
+
if (r.error) toast(r.error, false); else toast(t("fleet.configSaved") + (r.needs_restart ? t("fleet.configRestart") : ""));
|
|
232
|
+
}
|
|
233
|
+
const set = (k) => (e) => setForm({ ...form, [k]: e.target.value });
|
|
234
|
+
return html`<${Loaded} state=${state}>${() => !form ? html`<${Skeleton} lines=${4} />` : html`
|
|
235
|
+
<section class="card"><h3>${t("fleet.channel")}</h3>
|
|
236
|
+
<label class="field"><span>${t("fleet.type")}</span><input value=${form.type} disabled /></label>
|
|
237
|
+
<label class="field"><span>${t("fleet.tokenEnv")}</span><input value=${form.tokenEnv} disabled /></label>
|
|
238
|
+
<div class="field"><label for="cfg-gid">${t("fleet.groupId")}</label><${Secret} id="cfg-gid" value=${form.groupId} onInput=${set("groupId")} label=${t("fleet.groupId")} /></div></section>
|
|
239
|
+
<section class="card"><h3>${t("fleet.access")}</h3>
|
|
240
|
+
<label class="field"><span>${t("fleet.mode")}</span><select value=${form.mode} onChange=${set("mode")}><option value="locked">${t("fleet.locked")}</option><option value="open">${t("fleet.open")}</option></select></label>
|
|
241
|
+
<div class="field"><label for="cfg-users">${t("fleet.allowedUsers")}</label><${Secret} id="cfg-users" value=${form.users} onInput=${set("users")} label=${t("fleet.allowedUsers")} />
|
|
242
|
+
<p class="note">${t("fleet.allowedUsersNote")}</p></div></section>
|
|
243
|
+
<section class="card"><h3>${t("fleet.defaults")}</h3>
|
|
244
|
+
<label class="field"><span>${t("fleet.backend")}</span><select value=${form.backend} onChange=${set("backend")}>
|
|
245
|
+
${[...(BACKENDS.includes(form.backend) ? [] : [form.backend]), ...BACKENDS].map(b => html`<option key=${b} value=${b}>${b}</option>`)}</select></label></section>
|
|
246
|
+
<section class="card"><h3>${t("fleet.roots")}</h3>
|
|
247
|
+
${form.roots.map((r, i) => html`<div key=${i} class="field-row"><input value=${r} placeholder="/path/to/projects" aria-label=${t("fleet.roots")}
|
|
248
|
+
onInput=${(e) => { const roots = [...form.roots]; roots[i] = e.target.value; setForm({ ...form, roots }); }} />
|
|
249
|
+
<button type="button" class="icon-btn" aria-label=${t("fleet.removeRoot")} title=${t("fleet.removeRoot")} onClick=${() => setForm({ ...form, roots: form.roots.filter((_, j) => j !== i) })}><${Icon} name="close" size=${16} /></button></div>`)}
|
|
250
|
+
<button type="button" class="btn btn-ghost" onClick=${() => setForm({ ...form, roots: [...form.roots, ""] })}><${Icon} name="plus" size=${16} />${t("fleet.addRoot")}</button></section>
|
|
251
|
+
<div class="save-row"><button type="button" class="btn btn-primary" disabled=${saving} onClick=${save}>${t("fleet.save")}</button><span class="note">${t("fleet.saveNote")}</span></div>`}</${Loaded}>`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Create dialogs ──
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* One form dialog: fields, Cancel / Create, the request, a toast. Its own lease ends when the dialog goes: a request
|
|
258
|
+
* that finishes after that still reports its toast (it may have created something), but never closes or refreshes
|
|
259
|
+
* whatever is on screen now (#1425 review). `ready` false keeps Create off until the form has what it needs.
|
|
260
|
+
*/
|
|
261
|
+
export function FormDialog({ title, onClose, submit, children, ready = true }) {
|
|
262
|
+
const lease = useLease("form-dialog");
|
|
263
|
+
const [busy, setBusy] = useState(false);
|
|
264
|
+
async function go() {
|
|
265
|
+
if (busy || !ready) return;
|
|
266
|
+
const body = submit.collect();
|
|
267
|
+
if (!body) return;
|
|
268
|
+
setBusy(true);
|
|
269
|
+
let r, handedOver = false;
|
|
270
|
+
// A write that needs a fleet admin's confirmation (#1423; e.g. a new instance with control-bearing settings):
|
|
271
|
+
// the dialog goes at once and the shell follows the request. From then on the dialog is done: the decision is
|
|
272
|
+
// said in a toast, and nothing here closes, refreshes or acts again.
|
|
273
|
+
try {
|
|
274
|
+
const res = await confirmedWrite(submit.path, { method: "POST", body, label: title,
|
|
275
|
+
onPending: () => { handedOver = true; toast(t("app.pendingSent")); if (lease.current()) { setBusy(false); onClose(); } } });
|
|
276
|
+
r = res.ok ? res.body || {} : { error: (res.body && res.body.error) || `HTTP ${res.status}` };
|
|
277
|
+
} catch (err) { r = { error: err.message }; }
|
|
278
|
+
if (r.error) { toast(r.error, false); if (!handedOver && lease.current()) setBusy(false); return; }
|
|
279
|
+
toast(submit.done);
|
|
280
|
+
if (handedOver || !lease.current()) return;
|
|
281
|
+
setBusy(false);
|
|
282
|
+
onClose();
|
|
283
|
+
if (submit.after) submit.after();
|
|
284
|
+
}
|
|
285
|
+
return html`<${Dialog} title=${title} onClose=${onClose} busy=${busy}
|
|
286
|
+
actions=${html`<button type="button" class="btn" onClick=${onClose} disabled=${busy}>${t("fleet.cancel")}</button>
|
|
287
|
+
<button type="button" class="btn btn-primary" onClick=${go} disabled=${busy || !ready}>${busy ? t("fleet.creating") : t("fleet.create")}</button>`}>
|
|
288
|
+
<form class="form" onSubmit=${(e) => { e.preventDefault(); go(); }}><fieldset class="form" disabled=${busy}>${children}</fieldset></form></${Dialog}>`;
|
|
289
|
+
}
|
|
290
|
+
const field = (label, input) => html`<label class="field"><span>${label}</span>${input}</label>`;
|
|
291
|
+
|
|
292
|
+
function CreateTask({ onClose, onDone }) {
|
|
293
|
+
const { instances } = useStore(appStore);
|
|
294
|
+
const [f, setF] = useState({ title: "", desc: "", priority: "normal", assignee: "" });
|
|
295
|
+
const set = (k) => (e) => setF({ ...f, [k]: e.target.value });
|
|
296
|
+
const submit = { path: "/ui/tasks", done: t("fleet.taskCreated"), after: onDone, collect: () => {
|
|
297
|
+
if (!f.title.trim()) { toast(t("fleet.titleRequired"), false); return null; }
|
|
298
|
+
const body = { title: f.title.trim(), priority: f.priority };
|
|
299
|
+
if (f.desc.trim()) body.description = f.desc.trim();
|
|
300
|
+
if (f.assignee) body.assignee = f.assignee;
|
|
301
|
+
return body;
|
|
302
|
+
} };
|
|
303
|
+
return html`<${FormDialog} title=${t("fleet.newTask")} onClose=${onClose} submit=${submit}>
|
|
304
|
+
${field(`${t("fleet.taskTitle")} *`, html`<input value=${f.title} onInput=${set("title")} required />`)}
|
|
305
|
+
${field(t("fleet.taskDesc"), html`<input value=${f.desc} onInput=${set("desc")} />`)}
|
|
306
|
+
${field(t("fleet.priority"), html`<select value=${f.priority} onChange=${set("priority")}><option value="normal">${t("fleet.pNormal")}</option><option value="low">${t("fleet.pLow")}</option><option value="high">${t("fleet.pHigh")}</option><option value="urgent">${t("fleet.pUrgent")}</option></select>`)}
|
|
307
|
+
${field(t("fleet.assignee"), html`<select value=${f.assignee} onChange=${set("assignee")}><option value="">${t("fleet.unassigned")}</option>${instances.map(i => html`<option key=${i.name} value=${i.name}>${i.name}</option>`)}</select>`)}
|
|
308
|
+
</${FormDialog}>`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function CreateSchedule({ onClose, onDone }) {
|
|
312
|
+
const { instances } = useStore(appStore);
|
|
313
|
+
const [f, setF] = useState({ cron: "", msg: "", target: instances[0] ? instances[0].name : "", label: "" });
|
|
314
|
+
const set = (k) => (e) => setF({ ...f, [k]: e.target.value });
|
|
315
|
+
const submit = { path: "/ui/schedules", done: t("fleet.scheduleCreated"), after: onDone, collect: () => {
|
|
316
|
+
if (!f.cron.trim() || !f.msg.trim() || !f.target) { toast(t("fleet.scheduleFieldsRequired"), false); return null; }
|
|
317
|
+
const body = { cron: f.cron.trim(), message: f.msg.trim(), target: f.target, source: "web-user", reply_chat_id: "", reply_thread_id: "" };
|
|
318
|
+
if (f.label.trim()) body.label = f.label.trim();
|
|
319
|
+
return body;
|
|
320
|
+
} };
|
|
321
|
+
return html`<${FormDialog} title=${t("fleet.newSchedule")} onClose=${onClose} submit=${submit}>
|
|
322
|
+
${field(`${t("fleet.cron")} *`, html`<input value=${f.cron} onInput=${set("cron")} placeholder="0 9 * * 1-5" class="mono" />`)}
|
|
323
|
+
${f.cron.trim() ? html`<p class="note">${cronDesc(f.cron)}</p>` : null}
|
|
324
|
+
${field(`${t("fleet.message")} *`, html`<input value=${f.msg} onInput=${set("msg")} />`)}
|
|
325
|
+
${field(`${t("fleet.target")} *`, html`<select value=${f.target} onChange=${set("target")}>${instances.map(i => html`<option key=${i.name} value=${i.name}>${i.name}</option>`)}</select>`)}
|
|
326
|
+
${field(t("fleet.label"), html`<input value=${f.label} onInput=${set("label")} />`)}
|
|
327
|
+
</${FormDialog}>`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function CreateTeam({ onClose, onDone }) {
|
|
331
|
+
const { instances } = useStore(appStore);
|
|
332
|
+
const [f, setF] = useState({ name: "", desc: "", members: [] });
|
|
333
|
+
const toggle = (n) => setF({ ...f, members: f.members.includes(n) ? f.members.filter(x => x !== n) : [...f.members, n] });
|
|
334
|
+
const submit = { path: "/ui/teams", done: t("fleet.teamCreated"), after: onDone, collect: () => {
|
|
335
|
+
if (!f.name.trim() || !f.members.length) { toast(t("fleet.teamFieldsRequired"), false); return null; }
|
|
336
|
+
const body = { name: f.name.trim(), members: f.members };
|
|
337
|
+
if (f.desc.trim()) body.description = f.desc.trim();
|
|
338
|
+
return body;
|
|
339
|
+
} };
|
|
340
|
+
return html`<${FormDialog} title=${t("fleet.newTeam")} onClose=${onClose} submit=${submit}>
|
|
341
|
+
${field(`${t("fleet.teamName")} *`, html`<input value=${f.name} onInput=${(e) => setF({ ...f, name: e.target.value })} />`)}
|
|
342
|
+
${field(t("fleet.taskDesc"), html`<input value=${f.desc} onInput=${(e) => setF({ ...f, desc: e.target.value })} />`)}
|
|
343
|
+
<fieldset class="field"><legend>${t("fleet.members")} *</legend><div class="checks">${instances.map(i => html`<label key=${i.name} class="check">
|
|
344
|
+
<input type="checkbox" checked=${f.members.includes(i.name)} onChange=${() => toggle(i.name)} />${i.name}</label>`)}</div></fieldset>
|
|
345
|
+
</${FormDialog}>`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** "New instance": the sidebar's ✎ opens it from anywhere (#1408 step 3 merges it with Settings' create flow). */
|
|
349
|
+
export function CreateInstanceDialog({ onClose }) {
|
|
350
|
+
const lease = useLease("create-instance");
|
|
351
|
+
const [backends, setBackends] = useState(null);
|
|
352
|
+
const [f, setF] = useState({ dir: "", topic: "", desc: "", backend: "", model: "", branch: "", tags: "", prompt: "" });
|
|
353
|
+
useEffect(() => {
|
|
354
|
+
(async () => {
|
|
355
|
+
try {
|
|
356
|
+
const bd = await api("GET", "/ui/backends", null, lease);
|
|
357
|
+
if (!lease.current()) return;
|
|
358
|
+
const list = bd.backends || [];
|
|
359
|
+
const first = list.find(b => b.installed && !b.deprecated) || list.find(b => b.installed);
|
|
360
|
+
setBackends(list);
|
|
361
|
+
setF(x => ({ ...x, backend: first ? first.name : "" }));
|
|
362
|
+
} catch {
|
|
363
|
+
// The list could not be read: offer the known backends, and keep the explicit choice "fleet default" selected.
|
|
364
|
+
if (lease.current()) { setBackends(BACKENDS.map(name => ({ name, installed: true }))); setF(x => ({ ...x, backend: "" })); }
|
|
365
|
+
}
|
|
366
|
+
})();
|
|
367
|
+
}, [lease]);
|
|
368
|
+
const set = (k) => (e) => setF({ ...f, [k]: e.target.value });
|
|
369
|
+
const submit = { path: "/ui/instances", done: t("fleet.instanceCreated"), collect: () => {
|
|
370
|
+
if (!f.dir.trim() && !f.topic.trim()) { toast(t("fleet.topicRequired"), false); return null; }
|
|
371
|
+
const body = {};
|
|
372
|
+
if (f.dir.trim()) body.directory = f.dir.trim();
|
|
373
|
+
if (f.topic.trim()) body.topic_name = f.topic.trim();
|
|
374
|
+
if (f.desc.trim()) body.description = f.desc.trim();
|
|
375
|
+
if (f.backend) body.backend = f.backend;
|
|
376
|
+
if (f.model.trim()) body.model = f.model.trim();
|
|
377
|
+
if (f.branch.trim()) body.branch = f.branch.trim();
|
|
378
|
+
const tags = f.tags.split(",").map(x => x.trim()).filter(Boolean); if (tags.length) body.tags = tags;
|
|
379
|
+
if (f.prompt.trim()) body.systemPrompt = f.prompt.trim();
|
|
380
|
+
return body;
|
|
381
|
+
} };
|
|
382
|
+
// Create waits for the backend list: a submission before it would go without a backend, and the list arriving
|
|
383
|
+
// afterwards would change the shown choice under a request already sent (#1425 review).
|
|
384
|
+
return html`<${FormDialog} title=${t("fleet.newInstance")} onClose=${onClose} submit=${submit} ready=${backends !== null}>
|
|
385
|
+
${field(t("fleet.directory"), html`<input value=${f.dir} onInput=${set("dir")} placeholder=${t("fleet.directoryHint")} />`)}
|
|
386
|
+
${field(`${t("fleet.topic")}${f.dir.trim() ? "" : " *"}`, html`<input value=${f.topic} onInput=${set("topic")} placeholder=${t("fleet.topicHint")} />`)}
|
|
387
|
+
${field(t("fleet.description"), html`<input value=${f.desc} onInput=${set("desc")} placeholder=${t("fleet.descriptionHint")} />`)}
|
|
388
|
+
${field(t("fleet.backend"), backends ? html`<select value=${f.backend} onChange=${set("backend")}><option value="">${t("fleet.fleetDefault")}</option>
|
|
389
|
+
${backends.map(b => html`<option key=${b.name} value=${b.name} disabled=${!b.installed}>${b.name}${b.deprecated ? ` ${t("fleet.deprecated")}` : ""}${b.installed ? "" : ` ${t("fleet.notInstalled")}`}</option>`)}</select>` : html`<${Skeleton} lines=${1} />`)}
|
|
390
|
+
${field(t("fleet.model"), html`<input value=${f.model} onInput=${set("model")} placeholder=${t("fleet.modelHint")} />`)}
|
|
391
|
+
${field(t("fleet.branch"), html`<input value=${f.branch} onInput=${set("branch")} placeholder=${t("fleet.branchHint")} />`)}
|
|
392
|
+
${field(t("fleet.tags"), html`<input value=${f.tags} onInput=${set("tags")} placeholder=${t("fleet.tagsHint")} />`)}
|
|
393
|
+
${field(t("fleet.systemPrompt"), html`<textarea rows="3" value=${f.prompt} onInput=${set("prompt")} placeholder=${t("fleet.systemPromptHint")}></textarea>`)}
|
|
394
|
+
</${FormDialog}>`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── Cron in words ──
|
|
398
|
+
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
399
|
+
export function cronDesc(expr) {
|
|
400
|
+
if (!expr) return "";
|
|
401
|
+
const p = expr.trim().split(/\s+/);
|
|
402
|
+
if (p.length < 5) return expr;
|
|
403
|
+
const [min, hr, dom, mon, dow] = p;
|
|
404
|
+
const time = hr !== "*" && min !== "*" ? `at ${hr.padStart(2, "0")}:${min.padStart(2, "0")}` : "";
|
|
405
|
+
if (min === "*" && hr === "*") return "Every minute";
|
|
406
|
+
if (min.startsWith("*/")) return `Every ${min.slice(2)} minutes`;
|
|
407
|
+
if (hr.startsWith("*/")) return `Every ${hr.slice(2)} hours`;
|
|
408
|
+
if (dom === "*" && mon === "*" && dow === "*") return `Daily ${time}`;
|
|
409
|
+
if (dom === "*" && mon === "*" && dow === "1-5") return `Weekdays ${time}`;
|
|
410
|
+
if (dom === "*" && mon === "*" && dow !== "*") return `${dow.split(",").map(d => { const n = parseInt(d, 10); return Number.isNaN(n) ? d : (DAYS[n] || d); }).join(", ")} ${time}`;
|
|
411
|
+
if (dom !== "*" && mon === "*") return `${dom}${dom === "1" ? "st" : dom === "2" ? "nd" : dom === "3" ? "rd" : "th"} of month ${time}`;
|
|
412
|
+
return expr;
|
|
413
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// #1408 step 4 = #1386 part (b): the "Needs you" panel (/ui/needs) — everything waiting on the person, from every
|
|
2
|
+
// world, in one list. Behind the session gate (/ui/js/), loaded the first time it is opened.
|
|
3
|
+
//
|
|
4
|
+
// A renderer only (#1386 §4.2, the user's Discord-first rule): the list is the server's one derivation, the app
|
|
5
|
+
// store's `needs`, which arrives over the passive channels (SSE `needs`, the /ui/poll field). This panel reads
|
|
6
|
+
// nothing (#1374: no new GET, no timer that asks the server); its writes are a person's actions:
|
|
7
|
+
// - a prompt's own buttons, through the page's chat store (appStore.chatOwner — whichever chat import booted, a Retry's
|
|
8
|
+
// included): one claim per prompt with Chat (POST /ui/prompt, first answer wins);
|
|
9
|
+
// - Acknowledge on a delivery (POST /ui/needs/ack), which clears it everywhere;
|
|
10
|
+
// - Open: that instance's chat.
|
|
11
|
+
// Something resolved anywhere (Discord, Telegram, the web) leaves the list by itself.
|
|
12
|
+
import { html, useEffect, useState } from "/assets/app-html.js";
|
|
13
|
+
import { t, register } from "/assets/app-i18n.js";
|
|
14
|
+
import { appStore, createStore, useStore } from "/assets/app-store.js";
|
|
15
|
+
import { useLease } from "/assets/app-ctx.js";
|
|
16
|
+
import { PanelHeader, setTitle } from "/assets/app-shell.js";
|
|
17
|
+
import { chatPath } from "/assets/app-route.js";
|
|
18
|
+
import { Empty } from "/assets/ui-states.js";
|
|
19
|
+
import { Icon } from "/assets/ui-icons.js";
|
|
20
|
+
import { toast } from "/assets/ui-toast.js";
|
|
21
|
+
import { notifyStore, notifySupport, reasonText, refreshNotify, setNotify } from "/assets/app-needs.js";
|
|
22
|
+
|
|
23
|
+
register("needs", {
|
|
24
|
+
en: {
|
|
25
|
+
title: "Needs you", empty: "Nothing needs you right now", emptyHint: "When an agent waits for you, it shows here — and in Discord.",
|
|
26
|
+
open: "Open chat", acknowledge: "Acknowledge", acknowledging: "Acknowledging…", answering: "Answering…",
|
|
27
|
+
ageNow: "just now", ageMin: "{0} min", ageHour: "{0} h", ageDay: "{0} d",
|
|
28
|
+
notifyTitle: "Notify me on this device", notifyOn: "Notifications are on for this browser while a tab is open.",
|
|
29
|
+
notifyOff: "Get a desktop notification when something new needs you while this tab is in the background.",
|
|
30
|
+
notifyDenied: "This browser blocked notifications for AgEnD. Allow them in the site settings to turn this on.",
|
|
31
|
+
notifyMobile: "On a phone, rely on the Discord notification: a new item is a new post in General.",
|
|
32
|
+
notifyInsecure: "Notifications need a secure address (localhost or the HTTPS public link), not a plain-HTTP LAN address.",
|
|
33
|
+
notifyUnsupported: "This browser cannot show notifications.", turnOn: "Turn on", turnOff: "Turn off",
|
|
34
|
+
typePrompt: "Fleet prompt", typeAwaiting: "At its terminal", typeInstance: "Instance", typeDelivery: "Delivery",
|
|
35
|
+
},
|
|
36
|
+
"zh-TW": {
|
|
37
|
+
title: "等你處理", empty: "目前沒有需要你處理的事", emptyHint: "agent 在等你時,會出現在這裡——也會出現在 Discord。",
|
|
38
|
+
open: "開啟聊天", acknowledge: "確認", acknowledging: "確認中…", answering: "回覆中…",
|
|
39
|
+
ageNow: "剛剛", ageMin: "{0} 分鐘", ageHour: "{0} 小時", ageDay: "{0} 天",
|
|
40
|
+
notifyTitle: "在這台裝置通知我", notifyOn: "這個瀏覽器的通知已開啟(需要開著分頁)。",
|
|
41
|
+
notifyOff: "分頁在背景時,有新的事等你處理就跳出桌面通知。",
|
|
42
|
+
notifyDenied: "這個瀏覽器封鎖了 AgEnD 的通知。請在網站設定中允許後再開啟。",
|
|
43
|
+
notifyMobile: "在手機上請以 Discord 通知為主:每個新項目都會在 General 發一則新訊息。",
|
|
44
|
+
notifyInsecure: "通知需要安全的網址(localhost 或 HTTPS 公開連結),區網的一般 HTTP 網址不行。",
|
|
45
|
+
notifyUnsupported: "這個瀏覽器無法顯示通知。", turnOn: "開啟", turnOff: "關閉",
|
|
46
|
+
typePrompt: "Fleet 提示", typeAwaiting: "在終端機上", typeInstance: "Instance", typeDelivery: "傳送",
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const tn = (k, ...v) => t(`needs.${k}`, ...v);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The page's claims on what Needs you writes, kept for the life of the page — not of a panel or a render (#1463
|
|
53
|
+
* review): a second click in the same turn, the panel left and opened again, or a list that has not caught up yet all
|
|
54
|
+
* see the same claim.
|
|
55
|
+
* - `acks[id]`: "busy" while its POST is out, "done" once the server took it, until the server's next list drops the
|
|
56
|
+
* item (then forgotten); a failure gives it back.
|
|
57
|
+
* - `prompts[nonce]`: the same, for a prompt answered while the page has no chat store yet (before Chat boots). When
|
|
58
|
+
* it arrives, each such claim is handed to it (holdPrompt) — one claim, never two — and its outcome settles there.
|
|
59
|
+
*/
|
|
60
|
+
export const claims = createStore({ acks: {}, prompts: {} });
|
|
61
|
+
const chatOwner = () => appStore.get().chatOwner || null;
|
|
62
|
+
let handedTo = null;
|
|
63
|
+
appStore.subscribe((st) => {
|
|
64
|
+
// The chat store arrived (or was replaced by a Retry's): the claims taken without it become its own.
|
|
65
|
+
if (st.chatOwner && st.chatOwner !== handedTo) {
|
|
66
|
+
handedTo = st.chatOwner;
|
|
67
|
+
const items = Array.isArray(st.needs) ? st.needs : [];
|
|
68
|
+
for (const nonce of Object.keys(claims.get().prompts)) {
|
|
69
|
+
handedTo.holdPrompt(items.find((i) => i.nonce === nonce) || { nonce, instance: "" });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const items = Array.isArray(st.needs) ? st.needs : [];
|
|
73
|
+
const listed = new Set(items.map((i) => i.id)), nonces = new Set(items.map((i) => i.nonce).filter(Boolean));
|
|
74
|
+
const c = claims.get();
|
|
75
|
+
const acks = Object.fromEntries(Object.entries(c.acks).filter(([id, v]) => v === "busy" || listed.has(id)));
|
|
76
|
+
const prompts = Object.fromEntries(Object.entries(c.prompts).filter(([n, v]) => v === "busy" || nonces.has(n)));
|
|
77
|
+
if (Object.keys(acks).length !== Object.keys(c.acks).length || Object.keys(prompts).length !== Object.keys(c.prompts).length) claims.set({ acks, prompts });
|
|
78
|
+
});
|
|
79
|
+
const setClaim = (kind, key, value) => claims.set((c) => {
|
|
80
|
+
const next = { ...c[kind] };
|
|
81
|
+
if (value) next[key] = value; else delete next[key];
|
|
82
|
+
return { ...c, [kind]: next };
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const ICON = { prompt: "chat", awaiting_input: "clock", instance: "alert", delivery: "send" };
|
|
86
|
+
const BAD = new Set(["crashed", "delivery_failed", "hang", "exited"]);
|
|
87
|
+
|
|
88
|
+
/** "3 min", "2 h": the age shown beside an item (refreshed by a display timer, never by a read). */
|
|
89
|
+
export function age(since, nowMs) {
|
|
90
|
+
const minutes = Math.max(0, Math.floor((nowMs - since) / 60_000));
|
|
91
|
+
if (minutes < 1) return tn("ageNow");
|
|
92
|
+
if (minutes < 60) return tn("ageMin", minutes);
|
|
93
|
+
const hours = Math.floor(minutes / 60);
|
|
94
|
+
return hours < 24 ? tn("ageHour", hours) : tn("ageDay", Math.floor(hours / 24));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The store's list, grouped by instance, in its order (instances by their oldest item; items oldest first). */
|
|
98
|
+
export function groupNeeds(items) {
|
|
99
|
+
const groups = [];
|
|
100
|
+
for (const item of items) {
|
|
101
|
+
const last = groups[groups.length - 1];
|
|
102
|
+
if (last && last.instance === item.instance) last.items.push(item);
|
|
103
|
+
else groups.push({ instance: item.instance, items: [item] });
|
|
104
|
+
}
|
|
105
|
+
return groups;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function NeedsPanel({ navKey }) {
|
|
109
|
+
const lease = useLease(navKey);
|
|
110
|
+
const { needs, chatOwner: owner } = useStore(appStore);
|
|
111
|
+
const items = Array.isArray(needs) ? needs : [];
|
|
112
|
+
const [, tick] = useState(0);
|
|
113
|
+
useEffect(() => { setTitle(tn("title")); }, [navKey]);
|
|
114
|
+
// Ages move on: a redraw every 30 s while the panel is open (display only — nothing is read).
|
|
115
|
+
useEffect(() => { lease.interval(() => tick((n) => n + 1), 30_000); }, [lease]);
|
|
116
|
+
// The chat store owns a prompt's busy/answered state; redraw when it changes (and when it arrives).
|
|
117
|
+
useEffect(() => { if (owner) return owner.subscribe(() => tick((n) => n + 1)); return undefined; }, [owner, lease]);
|
|
118
|
+
const groups = groupNeeds(items);
|
|
119
|
+
const nowMs = Date.now();
|
|
120
|
+
return html`<div class="panel p-needs">
|
|
121
|
+
<${PanelHeader} title=${tn("title")} sub=${items.length ? t("app.needsCount", items.length) : null} />
|
|
122
|
+
<div class="panel-body"><div class="col">
|
|
123
|
+
${!items.length ? html`<${Empty} icon="check" title=${tn("empty")} hint=${tn("emptyHint")} />`
|
|
124
|
+
: groups.map((g) => html`<section key=${g.instance} class="n-group" aria-label=${g.instance}>
|
|
125
|
+
<h2 class="n-group-head"><a href=${chatPath(g.instance)}>${g.instance}</a></h2>
|
|
126
|
+
${g.items.map((item) => html`<${NeedsItem} key=${item.id} item=${item} nowMs=${nowMs} owner=${owner} />`)}
|
|
127
|
+
</section>`)}
|
|
128
|
+
<${NotifyToggle} />
|
|
129
|
+
</div></div>
|
|
130
|
+
</div>`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function NeedsItem({ item, nowMs, owner }) {
|
|
134
|
+
const c = useStore(claims);
|
|
135
|
+
// A prompt: the page's chat store holds the one claim (Chat and Needs you, any panel, any render).
|
|
136
|
+
const p = item.type === "prompt" && item.nonce && owner ? owner.state.prompts[item.nonce] : null;
|
|
137
|
+
const fallback = item.nonce ? c.prompts[item.nonce] : undefined;
|
|
138
|
+
const promptBusy = p ? p.busy || p.resolved : !!fallback;
|
|
139
|
+
const answer = async (action) => {
|
|
140
|
+
const store = chatOwner();
|
|
141
|
+
if (store) { store.answerByNonce(item, action); return; }
|
|
142
|
+
// Only before Chat has booted: the page's own claim on this nonce, taken now (synchronously), kept until the
|
|
143
|
+
// server's list drops the prompt (or the answer fails).
|
|
144
|
+
if (claims.get().prompts[item.nonce]) return;
|
|
145
|
+
setClaim("prompts", item.nonce, "busy");
|
|
146
|
+
let r = null;
|
|
147
|
+
try {
|
|
148
|
+
const res = await fetch("/ui/prompt", { method: "POST", headers: { "Content-Type": "application/json" },
|
|
149
|
+
body: JSON.stringify({ instance: item.instance, nonce: item.nonce, action }) });
|
|
150
|
+
r = await res.json().catch(() => ({}));
|
|
151
|
+
} catch (err) { r = { error: err && err.message ? err.message : t("app.failed") }; }
|
|
152
|
+
// The chat store may have arrived meanwhile and taken this claim over: its outcome is settled there.
|
|
153
|
+
const now = chatOwner();
|
|
154
|
+
if (r && r.answered) { if (now) setClaim("prompts", item.nonce, null); else setClaim("prompts", item.nonce, "done"); return; }
|
|
155
|
+
setClaim("prompts", item.nonce, null);
|
|
156
|
+
if (now) now.releasePrompt(item.nonce);
|
|
157
|
+
toast((r && r.error) || t("app.failed"), false);
|
|
158
|
+
};
|
|
159
|
+
const ack = c.acks[item.id];
|
|
160
|
+
const acknowledge = async () => {
|
|
161
|
+
if (claims.get().acks[item.id]) return; // the page's claim, taken synchronously
|
|
162
|
+
setClaim("acks", item.id, "busy");
|
|
163
|
+
let res = null, body = null;
|
|
164
|
+
try {
|
|
165
|
+
res = await fetch("/ui/needs/ack", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: item.id }) });
|
|
166
|
+
body = await res.json().catch(() => ({}));
|
|
167
|
+
} catch { body = { error: t("app.failed") }; }
|
|
168
|
+
// Taken: it stays claimed until the server's next `needs` drops it — here, everywhere at once. Refused: free again.
|
|
169
|
+
setClaim("acks", item.id, res && res.ok ? "done" : null);
|
|
170
|
+
toast((body && (body.message || body.error)) || t("app.failed"), !!(res && res.ok));
|
|
171
|
+
};
|
|
172
|
+
return html`<article class=${`n-item${BAD.has(item.reason) ? " bad" : ""}`}>
|
|
173
|
+
<span class="n-icon"><${Icon} name=${ICON[item.type] || "alert"} size=${18} /></span>
|
|
174
|
+
<div class="n-body">
|
|
175
|
+
<div class="n-title"><strong>${reasonText(item)}</strong><span class="n-age">${age(item.since, nowMs)}</span></div>
|
|
176
|
+
${item.detail ? html`<p class="n-detail">${item.detail}</p>` : null}
|
|
177
|
+
<div class="n-actions">
|
|
178
|
+
${item.type === "prompt" && Array.isArray(item.actions) ? item.actions.map((a) => html`<button key=${a.id} type="button" class="btn btn-sm"
|
|
179
|
+
disabled=${promptBusy} onClick=${() => answer(a.id)}>${(p ? p.busy : fallback === "busy") ? tn("answering") : a.label}</button>`) : null}
|
|
180
|
+
${item.type === "delivery" ? html`<button type="button" class="btn btn-sm" disabled=${!!ack} onClick=${acknowledge}>
|
|
181
|
+
<${Icon} name="check" size=${14} />${ack === "busy" ? tn("acknowledging") : tn("acknowledge")}</button>` : null}
|
|
182
|
+
<a class="btn btn-sm btn-ghost" href=${chatPath(item.instance)}>${tn("open")}</a>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</article>`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The device's choice, as the page knows it (app-needs.js notifyStore): every mounted toggle shows the same. */
|
|
189
|
+
function NotifyToggle() {
|
|
190
|
+
const support = notifySupport();
|
|
191
|
+
const { state, asking } = useStore(notifyStore);
|
|
192
|
+
useEffect(() => { refreshNotify(); }, []);
|
|
193
|
+
const on = state === "on";
|
|
194
|
+
const toggle = () => { setNotify(!on); };
|
|
195
|
+
const note = support === "mobile" ? tn("notifyMobile") : support === "insecure" ? tn("notifyInsecure")
|
|
196
|
+
: support !== "ok" ? tn("notifyUnsupported") : state === "denied" ? tn("notifyDenied") : on ? tn("notifyOn") : tn("notifyOff");
|
|
197
|
+
return html`<section class="card n-notify"><h3>${tn("notifyTitle")}</h3>
|
|
198
|
+
<p class="note">${note}</p>
|
|
199
|
+
${support === "ok" ? html`<div><button type="button" class=${`btn btn-sm${on ? "" : " btn-primary"}`} aria-pressed=${on ? "true" : "false"} disabled=${asking} onClick=${toggle}>
|
|
200
|
+
<${Icon} name="bell" size=${14} />${on ? tn("turnOff") : tn("turnOn")}</button></div>` : null}
|
|
201
|
+
</section>`;
|
|
202
|
+
}
|