dsh-tower 2.1.0 → 2.3.0
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/CHANGELOG.md +103 -0
- package/COMPATIBILITY.md +5 -1
- package/CONTRIBUTING.md +64 -5
- package/PRINCIPLES.md +145 -0
- package/README.en.md +7 -4
- package/README.md +7 -4
- package/SECURITY.md +6 -0
- package/{lib → adapters}/archive.js +1 -1
- package/{lib → adapters}/caseSensitivity.js +1 -1
- package/adapters/default.js +51 -10
- package/{lib → adapters}/git.js +1 -1
- package/{lib → adapters}/leases.js +1 -1
- package/{lib → adapters}/legacy.js +1 -1
- package/adapters/persistence.js +543 -0
- package/adapters/store-fs.js +28 -1
- package/adapters/store.js +204 -0
- package/{lib → adapters}/tests.js +9 -5
- package/adapters/vcs-git.js +15 -1
- package/{lib → adapters}/workspaceLock.js +1 -1
- package/{lib → adapters}/worktreeOwnership.js +1 -1
- package/app/archive.js +4 -6
- package/app/batch.js +9 -10
- package/app/board.js +2 -4
- package/app/checkCompletion.js +9 -10
- package/app/enrichedMissions.js +2 -4
- package/app/findings.js +6 -7
- package/app/gate.js +1 -2
- package/app/init.js +24 -29
- package/app/merge.js +58 -51
- package/app/messages.js +7 -8
- package/app/mission.js +5 -5
- package/app/plan.js +8 -5
- package/app/provision.js +12 -14
- package/app/read.js +6 -3
- package/app/rebase.js +14 -16
- package/app/recording.js +7 -8
- package/app/review.js +15 -8
- package/app/roster.js +14 -15
- package/{lib → app}/spawn-prechecks.js +4 -3
- package/app/spawn.js +12 -13
- package/app/teardown.js +8 -9
- package/app/tower.js +89 -35
- package/docs/README.md +6 -2
- package/docs/adr/0001-ports-adapters-as-data.md +49 -0
- package/docs/adr/0002-single-state-json.md +50 -0
- package/docs/adr/0003-allow-agent-control-default-true.md +48 -0
- package/docs/adr/0004-surface-budget.md +50 -0
- package/docs/adr/README.md +51 -0
- package/docs/agents/workflow.md +51 -0
- package/docs/architecture/overview.md +167 -36
- package/docs/guide/hot-reload.md +1 -1
- package/docs/guide/release.md +1 -1
- package/docs/operations/incident.md +82 -0
- package/docs/operations/runbook.md +12 -3
- package/docs/reference/blockers.md +2 -2
- package/docs/reference/config.generated.md +1 -0
- package/docs/reference/doctor.md +48 -2
- package/docs/reference/errors.generated.md +41 -37
- package/docs/reference/errors.md +53 -41
- package/docs/reference/events.md +38 -6
- package/docs/reference/surface-budget.md +128 -0
- package/docs/reference/tools.generated.md +5 -4
- package/docs/reference/tools.md +4 -4
- package/docs/zh/guide/hot-reload.md +1 -1
- package/docs/zh/reference/blockers.md +2 -2
- package/docs/zh/reference/doctor.md +29 -1
- package/docs/zh/reference/tools.md +4 -4
- package/domain/batch.js +2 -2
- package/domain/config/schema.js +25 -1
- package/domain/contracts/outputs.js +6 -2
- package/domain/contracts/tool-inputs/ops.js +1 -0
- package/domain/errors.js +297 -42
- package/domain/events.js +207 -0
- package/domain/integration/gate.js +17 -2
- package/domain/integration/merge.js +35 -0
- package/domain/lockBudget.js +30 -0
- package/domain/missions/completion.js +14 -0
- package/domain/missions/lifecycle.js +9 -3
- package/domain/missions/plan.js +14 -0
- package/domain/missions/review.js +90 -7
- package/domain/read/views.js +3 -0
- package/domain/state/archive.js +1 -1
- package/domain/state.js +1 -1
- package/domain/surface.js +248 -0
- package/{lib → host}/doctor.js +116 -9
- package/{lib → host}/modeStore.js +1 -1
- package/{lib → host}/tool-registry.js +106 -31
- package/host/tower.js +32 -0
- package/index.js +12 -9
- package/package.json +10 -3
- package/ports/archive.js +18 -0
- package/ports/caseSensitivity.js +15 -0
- package/ports/clock.js +10 -2
- package/ports/config.js +19 -0
- package/ports/contract.js +38 -0
- package/ports/events.js +24 -0
- package/ports/ids.js +10 -2
- package/ports/index.js +54 -2
- package/ports/ownership.js +17 -0
- package/ports/state.js +16 -0
- package/ports/store.js +52 -3
- package/ports/vcs.js +25 -4
- package/scripts/architecture-waivers.json +19 -0
- package/scripts/architecture.mjs +634 -23
- package/scripts/check-docs.mjs +8 -35
- package/scripts/check-pack.mjs +68 -39
- package/scripts/check-philosophy.mjs +266 -0
- package/scripts/domain-metrics.mjs +13 -4
- package/scripts/generate-config-reference.mjs +14 -1
- package/scripts/generate-error-reference.mjs +68 -14
- package/scripts/generate-events-reference.mjs +83 -0
- package/scripts/generate-tools-reference.mjs +11 -0
- package/scripts/issue-audit.mjs +2 -2
- package/scripts/lib/surface.mjs +87 -0
- package/scripts/pack-manifest.txt +58 -22
- package/scripts/regression-audit.mjs +164 -0
- package/scripts/release-check.mjs +1 -1
- package/scripts/surface-baseline.json +21 -0
- package/scripts/surface-baseline.mjs +120 -0
- package/tools/adopt.js +2 -3
- package/tools/batch.js +2 -3
- package/tools/complete.js +2 -3
- package/tools/diff.js +3 -3
- package/tools/doctor.js +7 -10
- package/tools/finding.js +2 -3
- package/tools/findings.js +3 -3
- package/tools/import-legacy.js +3 -5
- package/tools/inbox.js +3 -3
- package/tools/index.js +7 -7
- package/tools/init.js +3 -6
- package/tools/interrupt.js +2 -3
- package/tools/legacy-map.js +3 -5
- package/tools/merge.js +3 -3
- package/tools/mission.js +2 -3
- package/tools/plan.js +6 -5
- package/tools/preflight.js +2 -3
- package/tools/rebase.js +3 -3
- package/tools/recover.js +38 -13
- package/tools/reload.js +3 -6
- package/tools/resource.js +3 -5
- package/tools/review.js +3 -4
- package/tools/roster.js +2 -3
- package/tools/send.js +2 -2
- package/tools/spawn.js +8 -5
- package/tools/status.js +3 -3
- package/tools/teardown.js +2 -3
- package/tools/test.js +2 -2
- package/types/index.d.ts +1 -1
- package/ui-bundle/README.md +37 -3
- package/ui-bundle/client.cards.js +356 -0
- package/ui-bundle/client.denied.js +142 -0
- package/ui-bundle/client.dock.js +333 -0
- package/ui-bundle/client.helpers.js +271 -0
- package/ui-bundle/client.js +79 -1747
- package/ui-bundle/client.locales.js +247 -0
- package/ui-bundle/client.mode.js +206 -0
- package/ui-bundle/client.overlay.js +64 -0
- package/ui-bundle/client.styles.js +151 -0
- package/ui-bundle/client.toolview.js +82 -0
- package/ui-bundle/package.json +1 -0
- package/lib/persistence.js +0 -356
- package/lib/store.js +0 -133
- /package/{lib → adapters}/stateFile.js +0 -0
- /package/{lib → app}/briefing.js +0 -0
- /package/{lib → app}/spawn-support.js +0 -0
- /package/{lib → host}/compatibility.js +0 -0
- /package/{lib → host}/config.js +0 -0
- /package/{lib → host}/hmr.js +0 -0
- /package/{lib → host}/modeState.js +0 -0
- /package/{lib → host}/presets.js +0 -0
package/ui-bundle/client.js
CHANGED
|
@@ -1,33 +1,19 @@
|
|
|
1
1
|
// dsh-tower-ui 0.2.2 — optional Web Client surfaces for the Tower plugin.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// Harness Client package imports):
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// 3. `shell.overlay` — a transient, non-blocking status strip for Tower tool
|
|
11
|
-
// start/result. The overlay slot is root-scoped, so it cannot read chat
|
|
12
|
-
// state itself; the chat-driven keyed toolview components publish the
|
|
13
|
-
// transition into a module-local observable that the overlay injects.
|
|
14
|
-
// 4. Dynamic `tool.call.toolview` entries for the tools Tower mode denies:
|
|
15
|
-
// registered at a lower priority than the native views while the
|
|
16
|
-
// displayed session is in Tower mode, disposed when it leaves the mode so
|
|
17
|
-
// the native toolviews return. Denial results render as a distinct
|
|
18
|
-
// `Tower mode · disabled` row instead of a generic tool error.
|
|
19
|
-
// 5. locale dictionaries for the `tower-ui` namespace (en/zh).
|
|
3
|
+
// Entry module. It registers, all plain React (no iframe, DOM writes, Host RPC
|
|
4
|
+
// or Harness Client package imports): the twelve keyed tool cards, the
|
|
5
|
+
// history-derived composer dock chip/summary, the root-scoped overlay status
|
|
6
|
+
// strip, the dynamic mode-aware denied-tool rows (#12), and the `tower-ui`
|
|
7
|
+
// locale dictionaries (en/zh). Tower mode has no Client RPC: the dock derives
|
|
8
|
+
// it from the durable `/tower`, `tower_mode` and `tower_control` history in the
|
|
9
|
+
// `useChat` legacy slice, so a page refresh re-derives the same stance.
|
|
20
10
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// The bundle is a plain-JS artifact loaded by `window.__ModuleLoader__`; React
|
|
28
|
-
// comes from the browser module table. Theme references are restricted to
|
|
29
|
-
// `--dsw-alias-*` tokens. `window.__TOWER_UI_DIAG__` is a read-only diagnostic
|
|
30
|
-
// object used by tests (it is not DOM and changes nothing).
|
|
11
|
+
// Since #71 the surfaces live in package-local sibling chunks
|
|
12
|
+
// (`client.<surface>.js`) loaded through `require.async`; each registers
|
|
13
|
+
// through this same `__ModuleLoader__` surface with `{ id, chunk, factory }`.
|
|
14
|
+
// React comes from the browser module table, theme references stay on
|
|
15
|
+
// `--dsw-alias-*` tokens, and `window.__TOWER_UI_DIAG__` is a read-only test
|
|
16
|
+
// seam (not DOM, no state).
|
|
31
17
|
window.__ModuleLoader__.load({
|
|
32
18
|
id: 'dsh-tower-ui',
|
|
33
19
|
factory(require) {
|
|
@@ -38,258 +24,14 @@ window.__ModuleLoader__.load({
|
|
|
38
24
|
const VERSION = '0.2.2'
|
|
39
25
|
const DIAG_KEY = '__TOWER_UI_DIAG__'
|
|
40
26
|
const NOTICE_MS = 4500
|
|
41
|
-
const MAX_TEXT = 150
|
|
42
27
|
// Native toolviews register at priority 0; the slot core renders the
|
|
43
28
|
// lowest priority, so a negative priority shadows them only while this
|
|
44
29
|
// registration is alive.
|
|
45
30
|
const DENIED_PRIORITY = -10
|
|
46
31
|
|
|
47
|
-
|
|
48
|
-
'tower_status',
|
|
49
|
-
'tower_roster',
|
|
50
|
-
'tower_spawn',
|
|
51
|
-
'tower_mission',
|
|
52
|
-
'tower_complete',
|
|
53
|
-
'tower_review',
|
|
54
|
-
'tower_preflight',
|
|
55
|
-
'tower_merge',
|
|
56
|
-
'tower_teardown',
|
|
57
|
-
'tower_interrupt',
|
|
58
|
-
'tower_doctor',
|
|
59
|
-
'tower_batch',
|
|
60
|
-
]
|
|
61
|
-
const TOOL_SET = new Set(TOOL_NAMES)
|
|
62
|
-
// Mirrors lib/config.js DEFAULT_DENY_TOOLS; overridden by the
|
|
63
|
-
// `deniedWhileActive` list of the latest `tower_mode` output when present.
|
|
64
|
-
const DEFAULT_DENIED_TOOLS = [
|
|
65
|
-
'create_goal',
|
|
66
|
-
'todo_write',
|
|
67
|
-
'team_task_create',
|
|
68
|
-
'team_task_update',
|
|
69
|
-
'team_task_reopen',
|
|
70
|
-
'team_task_list',
|
|
71
|
-
'spawn_teammate',
|
|
72
|
-
'wait_agent',
|
|
73
|
-
'workflow',
|
|
74
|
-
'ralph',
|
|
75
|
-
'subagent',
|
|
76
|
-
'subagent_fork',
|
|
77
|
-
]
|
|
78
|
-
const TOOL_LABEL_KEYS = {
|
|
79
|
-
tower_status: 'tool.status',
|
|
80
|
-
tower_roster: 'tool.roster',
|
|
81
|
-
tower_spawn: 'tool.spawn',
|
|
82
|
-
tower_mission: 'tool.mission',
|
|
83
|
-
tower_complete: 'tool.complete',
|
|
84
|
-
tower_review: 'tool.review',
|
|
85
|
-
tower_preflight: 'tool.preflight',
|
|
86
|
-
tower_merge: 'tool.merge',
|
|
87
|
-
tower_teardown: 'tool.teardown',
|
|
88
|
-
tower_interrupt: 'tool.interrupt',
|
|
89
|
-
tower_doctor: 'tool.doctor',
|
|
90
|
-
tower_batch: 'tool.batch',
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ---------------------------------------------------------------------
|
|
94
|
-
// Locale dictionaries (the Client locale service owns activation/language;
|
|
95
|
-
// the slot registration's `locale` option supplies the bound `t` prop).
|
|
96
|
-
// ---------------------------------------------------------------------
|
|
97
|
-
const EN = {
|
|
98
|
-
'card.title': 'Tower',
|
|
99
|
-
'card.running': 'running…',
|
|
100
|
-
'card.done': 'done',
|
|
101
|
-
'card.failed': 'failed',
|
|
102
|
-
'card.error': 'Error',
|
|
103
|
-
'card.blockers': 'Blockers',
|
|
104
|
-
'card.noBlockers': 'no blockers',
|
|
105
|
-
'card.checks': 'Checks',
|
|
106
|
-
'card.warnings': 'Warnings',
|
|
107
|
-
'card.checkOk': '{name} ✓',
|
|
108
|
-
'card.checkFail': '{name} (exit {code})',
|
|
109
|
-
'card.checksCount': '{passed}/{total} checks passed',
|
|
110
|
-
'card.tasks': 'tasks {done}/{total}',
|
|
111
|
-
'card.openTasks': '{count} open task(s)',
|
|
112
|
-
'card.noOpenTasks': 'no open tasks',
|
|
113
|
-
'card.statusHeadline': '{total} missions · {active} active',
|
|
114
|
-
'card.statusDetail': 'roster {roster} · findings {findings} · rev {revision}',
|
|
115
|
-
'card.rosterHeadline': '{live} live / {total} agents',
|
|
116
|
-
'card.staleCount': '{count} stale',
|
|
117
|
-
'card.otherCount': '{count} inactive',
|
|
118
|
-
'card.agentLine': '{name} · {role} · {mission} · {activity}',
|
|
119
|
-
'card.reviewLine': '{status} · merge {merge} · round {round}',
|
|
120
|
-
'card.mergeReady': 'ready to merge',
|
|
121
|
-
'card.mergeBlocked': '{count} blocker(s)',
|
|
122
|
-
'card.merged': 'merged {commit}',
|
|
123
|
-
'card.alreadyMerged': 'already merged',
|
|
124
|
-
'card.spawnLine': '{role} {name} → {mission}',
|
|
125
|
-
'card.worktreeLine': 'worktree {branch}',
|
|
126
|
-
'card.completeLine': '{mission} completed · {commits} commit(s)',
|
|
127
|
-
'card.unread': '{count} unread',
|
|
128
|
-
'card.interruptYes': 'interrupted {name}',
|
|
129
|
-
'card.interruptNo': '{name} is not running',
|
|
130
|
-
'card.previousActivity': 'previous activity {activity}',
|
|
131
|
-
'card.teardownLine': '{removed} removed · {kept} kept',
|
|
132
|
-
'card.dryRun': 'dry run',
|
|
133
|
-
'card.doctorLine': 'doctor {status}',
|
|
134
|
-
'card.doctorChecks': '{pass} pass · {warn} warn · {fail} fail',
|
|
135
|
-
'card.batchLine': 'batch {status}',
|
|
136
|
-
'card.batchMembers': '{count} member(s) · batch {status}',
|
|
137
|
-
'card.batchMissing': '{count} missing check(s)',
|
|
138
|
-
'card.batchFailed': '{count} failed check(s)',
|
|
139
|
-
'card.missionLine': '{id} {status}',
|
|
140
|
-
'card.missionMissing': 'mission',
|
|
141
|
-
'card.reviewPolicy': 'review {policy}',
|
|
142
|
-
'card.owner': 'owner {owner}',
|
|
143
|
-
'card.unknown': 'unknown',
|
|
144
|
-
'card.summary': '{text}',
|
|
145
|
-
'card.sep': ' · ',
|
|
146
|
-
'tool.status': 'Status',
|
|
147
|
-
'tool.roster': 'Roster',
|
|
148
|
-
'tool.spawn': 'Spawn',
|
|
149
|
-
'tool.mission': 'Mission',
|
|
150
|
-
'tool.complete': 'Complete',
|
|
151
|
-
'tool.review': 'Review',
|
|
152
|
-
'tool.preflight': 'Preflight',
|
|
153
|
-
'tool.merge': 'Merge',
|
|
154
|
-
'tool.teardown': 'Teardown',
|
|
155
|
-
'tool.interrupt': 'Interrupt',
|
|
156
|
-
'tool.doctor': 'Doctor',
|
|
157
|
-
'tool.batch': 'Batch',
|
|
158
|
-
'dock.hint.title': '/tower · /tower on [message] · /tower off [message]',
|
|
159
|
-
'dock.running': '{tool} running…',
|
|
160
|
-
'dock.statusCounts': '{total} missions · {active} active · {open} open',
|
|
161
|
-
'dock.rosterCounts': '{live} live / {total} agents',
|
|
162
|
-
'dock.review': 'review {status} · {merge}',
|
|
163
|
-
'dock.ready': 'ready to merge',
|
|
164
|
-
'dock.blocked': '{count} blocker(s)',
|
|
165
|
-
'dock.merged': 'merged {commit}',
|
|
166
|
-
'dock.complete': '{mission} completed · {commits} commit(s)',
|
|
167
|
-
'dock.spawn': '{role} {name} → {mission}',
|
|
168
|
-
'dock.removed': '{count} worktree(s) removed',
|
|
169
|
-
'dock.interrupt': '{name} interrupted',
|
|
170
|
-
'dock.mission': '{id} {status}',
|
|
171
|
-
'dock.preflight': 'preflight {status}',
|
|
172
|
-
'dock.batch': 'batch {status}',
|
|
173
|
-
'dock.doctor': 'doctor {status}',
|
|
174
|
-
'dock.generic': 'done',
|
|
175
|
-
'dock.mode.onLabel': 'on',
|
|
176
|
-
'dock.mode.unknownLabel': 'mode unknown',
|
|
177
|
-
'dock.mode.unknownTooltip': 'The loaded history has Tower activity but no mode event. Run /tower or tower_mode to confirm the current stance.',
|
|
178
|
-
'dock.mode.tooltip': 'Tower mode is on for this session. Run /tower off in the composer to leave Tower mode.',
|
|
179
|
-
'dock.mode.lastKnown': 'last known from this browser session; run /tower or tower_mode to confirm',
|
|
180
|
-
'dock.mode.repo': 'repo {repo}',
|
|
181
|
-
'dock.mode.lockHeld': 'workspace lock held',
|
|
182
|
-
'dock.mode.lockFree': 'workspace lock free',
|
|
183
|
-
'dock.mode.lastBy': 'last change by {by}',
|
|
184
|
-
'deny.label': 'Tower mode · disabled',
|
|
185
|
-
'deny.hint': 'Run /tower off in the composer to use this tool again.',
|
|
186
|
-
'deny.running': 'Tower mode blocks this tool while it is active',
|
|
187
|
-
'deny.generic': '{tool} (Tower mode active)',
|
|
188
|
-
'deny.error': 'Tool error while Tower mode is active',
|
|
189
|
-
'strip.started': 'Tower · {tool} started',
|
|
190
|
-
'strip.finished': 'Tower · {tool} finished',
|
|
191
|
-
'strip.failed': 'Tower · {tool} failed',
|
|
192
|
-
}
|
|
193
|
-
const ZH = {
|
|
194
|
-
'card.title': 'Tower',
|
|
195
|
-
'card.running': '运行中…',
|
|
196
|
-
'card.done': '已完成',
|
|
197
|
-
'card.failed': '失败',
|
|
198
|
-
'card.error': '错误',
|
|
199
|
-
'card.blockers': '阻塞项',
|
|
200
|
-
'card.noBlockers': '无阻塞',
|
|
201
|
-
'card.checks': '检查',
|
|
202
|
-
'card.warnings': '警告',
|
|
203
|
-
'card.checkOk': '{name} ✓',
|
|
204
|
-
'card.checkFail': '{name}(退出码 {code})',
|
|
205
|
-
'card.checksCount': '通过 {passed}/{total} 项检查',
|
|
206
|
-
'card.tasks': '任务 {done}/{total}',
|
|
207
|
-
'card.openTasks': '{count} 个未完成任务',
|
|
208
|
-
'card.noOpenTasks': '无未完成任务',
|
|
209
|
-
'card.statusHeadline': '{total} 个任务线 · {active} 活跃',
|
|
210
|
-
'card.statusDetail': '名册 {roster} · 发现 {findings} · 修订 {revision}',
|
|
211
|
-
'card.rosterHeadline': '{live} 个运行中 / 共 {total} 个',
|
|
212
|
-
'card.staleCount': '{count} 个疑似停滞',
|
|
213
|
-
'card.otherCount': '{count} 个非活跃',
|
|
214
|
-
'card.agentLine': '{name} · {role} · {mission} · {activity}',
|
|
215
|
-
'card.reviewLine': '{status} · 合并建议 {merge} · 第 {round} 轮',
|
|
216
|
-
'card.mergeReady': '可以合并',
|
|
217
|
-
'card.mergeBlocked': '{count} 个阻塞',
|
|
218
|
-
'card.merged': '已合并 {commit}',
|
|
219
|
-
'card.alreadyMerged': '已合并(补记录)',
|
|
220
|
-
'card.spawnLine': '{role} {name} → {mission}',
|
|
221
|
-
'card.worktreeLine': '工作树 {branch}',
|
|
222
|
-
'card.completeLine': '{mission} 已完成 · {commits} 个提交',
|
|
223
|
-
'card.unread': '{count} 条未读',
|
|
224
|
-
'card.interruptYes': '已中断 {name}',
|
|
225
|
-
'card.interruptNo': '{name} 未在运行',
|
|
226
|
-
'card.previousActivity': '先前活动 {activity}',
|
|
227
|
-
'card.teardownLine': '移除 {removed} · 保留 {kept}',
|
|
228
|
-
'card.dryRun': '演练',
|
|
229
|
-
'card.doctorLine': '体检 {status}',
|
|
230
|
-
'card.doctorChecks': '通过 {pass} · 警告 {warn} · 失败 {fail}',
|
|
231
|
-
'card.batchLine': '批量 {status}',
|
|
232
|
-
'card.batchMembers': '{count} 个成员 · 批次 {status}',
|
|
233
|
-
'card.batchMissing': '{count} 项缺失检查',
|
|
234
|
-
'card.batchFailed': '{count} 项失败检查',
|
|
235
|
-
'card.missionLine': '{id} {status}',
|
|
236
|
-
'card.missionMissing': '任务线',
|
|
237
|
-
'card.reviewPolicy': '评审 {policy}',
|
|
238
|
-
'card.owner': '负责人 {owner}',
|
|
239
|
-
'card.unknown': '未知',
|
|
240
|
-
'card.summary': '{text}',
|
|
241
|
-
'card.sep': ' · ',
|
|
242
|
-
'tool.status': '状态',
|
|
243
|
-
'tool.roster': '名册',
|
|
244
|
-
'tool.spawn': '派发',
|
|
245
|
-
'tool.mission': '任务线',
|
|
246
|
-
'tool.complete': '完成',
|
|
247
|
-
'tool.review': '评审',
|
|
248
|
-
'tool.preflight': '预检',
|
|
249
|
-
'tool.merge': '合并',
|
|
250
|
-
'tool.teardown': '清理',
|
|
251
|
-
'tool.interrupt': '中断',
|
|
252
|
-
'tool.doctor': '体检',
|
|
253
|
-
'tool.batch': '批量',
|
|
254
|
-
'dock.hint.title': '/tower · /tower on [message] · /tower off [message]',
|
|
255
|
-
'dock.running': '{tool} 运行中…',
|
|
256
|
-
'dock.statusCounts': '{total} 个任务线 · {active} 活跃 · {open} 未完成',
|
|
257
|
-
'dock.rosterCounts': '{live} 个运行中 / 共 {total} 个',
|
|
258
|
-
'dock.review': '评审 {status} · {merge}',
|
|
259
|
-
'dock.ready': '可以合并',
|
|
260
|
-
'dock.blocked': '{count} 个阻塞',
|
|
261
|
-
'dock.merged': '已合并 {commit}',
|
|
262
|
-
'dock.complete': '{mission} 已完成 · {commits} 个提交',
|
|
263
|
-
'dock.spawn': '{role} {name} → {mission}',
|
|
264
|
-
'dock.removed': '移除 {count} 个工作树',
|
|
265
|
-
'dock.interrupt': '已中断 {name}',
|
|
266
|
-
'dock.mission': '{id} {status}',
|
|
267
|
-
'dock.preflight': '预检 {status}',
|
|
268
|
-
'dock.batch': '批量 {status}',
|
|
269
|
-
'dock.doctor': '体检 {status}',
|
|
270
|
-
'dock.generic': '已完成',
|
|
271
|
-
'dock.mode.onLabel': '已开启',
|
|
272
|
-
'dock.mode.unknownLabel': '模式未知',
|
|
273
|
-
'dock.mode.unknownTooltip': '已加载的历史中有 Tower 活动,但没有模式事件。执行 /tower 或 tower_mode 以确认当前状态。',
|
|
274
|
-
'dock.mode.tooltip': '此会话已开启 Tower 模式。在输入框中执行 /tower off 可退出。',
|
|
275
|
-
'dock.mode.lastKnown': '来自此浏览器会话的最后已知状态;请执行 /tower 或 tower_mode 确认',
|
|
276
|
-
'dock.mode.repo': '仓库 {repo}',
|
|
277
|
-
'dock.mode.lockHeld': '工作区锁已持有',
|
|
278
|
-
'dock.mode.lockFree': '工作区锁空闲',
|
|
279
|
-
'dock.mode.lastBy': '最近变更者 {by}',
|
|
280
|
-
'deny.label': 'Tower 模式 · 已禁用',
|
|
281
|
-
'deny.hint': '在输入框中执行 /tower off 后即可重新使用该工具。',
|
|
282
|
-
'deny.running': 'Tower 模式启用期间该工具被禁用',
|
|
283
|
-
'deny.generic': '{tool}(Tower 模式启用中)',
|
|
284
|
-
'deny.error': 'Tower 模式启用期间的工具错误',
|
|
285
|
-
'strip.started': 'Tower · {tool} 开始',
|
|
286
|
-
'strip.finished': 'Tower · {tool} 完成',
|
|
287
|
-
'strip.failed': 'Tower · {tool} 失败',
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// ---------------------------------------------------------------------
|
|
32
|
+
// -------------------------------------------------------------------
|
|
291
33
|
// Diagnostics (read-only test seam; not DOM, not Host state).
|
|
292
|
-
//
|
|
34
|
+
// -------------------------------------------------------------------
|
|
293
35
|
const diagnostics = {
|
|
294
36
|
plugin: 'dsh-tower-ui',
|
|
295
37
|
version: VERSION,
|
|
@@ -337,1464 +79,66 @@ window.__ModuleLoader__.load({
|
|
|
337
79
|
}
|
|
338
80
|
}
|
|
339
81
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
card: {
|
|
352
|
-
display: 'flex',
|
|
353
|
-
flexDirection: 'column',
|
|
354
|
-
gap: '4px',
|
|
355
|
-
minWidth: 0,
|
|
356
|
-
maxWidth: '100%',
|
|
357
|
-
boxSizing: 'border-box',
|
|
358
|
-
margin: '2px 0 2px 4px',
|
|
359
|
-
padding: '8px 10px',
|
|
360
|
-
border: '0.5px solid var(--dsw-alias-border-l1)',
|
|
361
|
-
borderRadius: '8px',
|
|
362
|
-
background: 'var(--dsw-alias-bg-layer-1)',
|
|
363
|
-
color: 'var(--dsw-alias-label-secondary)',
|
|
364
|
-
fontSize: '12px',
|
|
365
|
-
lineHeight: '18px',
|
|
366
|
-
},
|
|
367
|
-
header: { display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0 },
|
|
368
|
-
title: { color: 'var(--dsw-alias-label-primary)', flex: 'none' },
|
|
369
|
-
dim: {
|
|
370
|
-
color: 'var(--dsw-alias-label-tertiary)',
|
|
371
|
-
minWidth: 0,
|
|
372
|
-
overflow: 'hidden',
|
|
373
|
-
textOverflow: 'ellipsis',
|
|
374
|
-
whiteSpace: 'nowrap',
|
|
375
|
-
},
|
|
376
|
-
row: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
377
|
-
rowError: { color: 'var(--dsw-alias-state-error-primary)' },
|
|
378
|
-
rowWarn: { color: 'var(--dsw-alias-state-warn-primary)' },
|
|
379
|
-
rowOk: { color: 'var(--dsw-alias-state-success-primary)' },
|
|
380
|
-
hint: {
|
|
381
|
-
display: 'inline-flex',
|
|
382
|
-
alignItems: 'center',
|
|
383
|
-
gap: '4px',
|
|
384
|
-
padding: '0 6px',
|
|
385
|
-
fontSize: '11px',
|
|
386
|
-
lineHeight: '18px',
|
|
387
|
-
color: 'inherit',
|
|
388
|
-
opacity: 0.55,
|
|
389
|
-
userSelect: 'none',
|
|
390
|
-
whiteSpace: 'nowrap',
|
|
391
|
-
},
|
|
392
|
-
dock: {
|
|
393
|
-
display: 'inline-flex',
|
|
394
|
-
alignItems: 'center',
|
|
395
|
-
gap: '6px',
|
|
396
|
-
minWidth: 0,
|
|
397
|
-
maxWidth: '100%',
|
|
398
|
-
padding: '1px 8px',
|
|
399
|
-
fontSize: '11px',
|
|
400
|
-
lineHeight: '18px',
|
|
401
|
-
color: 'var(--dsw-alias-label-tertiary)',
|
|
402
|
-
whiteSpace: 'nowrap',
|
|
403
|
-
},
|
|
404
|
-
dockTool: { color: 'var(--dsw-alias-label-secondary)', flex: 'none' },
|
|
405
|
-
dockText: {
|
|
406
|
-
minWidth: 0,
|
|
407
|
-
overflow: 'hidden',
|
|
408
|
-
textOverflow: 'ellipsis',
|
|
409
|
-
color: 'var(--dsw-alias-label-tertiary)',
|
|
410
|
-
},
|
|
411
|
-
stripWrap: {
|
|
412
|
-
position: 'fixed',
|
|
413
|
-
left: '50%',
|
|
414
|
-
bottom: '16px',
|
|
415
|
-
transform: 'translateX(-50%)',
|
|
416
|
-
display: 'flex',
|
|
417
|
-
justifyContent: 'center',
|
|
418
|
-
pointerEvents: 'none',
|
|
419
|
-
zIndex: 9100,
|
|
420
|
-
maxWidth: 'calc(100vw - 32px)',
|
|
421
|
-
},
|
|
422
|
-
stripCard: {
|
|
423
|
-
pointerEvents: 'none',
|
|
424
|
-
display: 'inline-flex',
|
|
425
|
-
alignItems: 'center',
|
|
426
|
-
gap: '6px',
|
|
427
|
-
maxWidth: '100%',
|
|
428
|
-
padding: '4px 10px',
|
|
429
|
-
border: '0.5px solid var(--dsw-alias-border-l1)',
|
|
430
|
-
borderRadius: '999px',
|
|
431
|
-
background: 'var(--dsw-alias-bg-overlay)',
|
|
432
|
-
color: 'var(--dsw-alias-label-secondary)',
|
|
433
|
-
fontSize: '12px',
|
|
434
|
-
lineHeight: '18px',
|
|
435
|
-
whiteSpace: 'nowrap',
|
|
436
|
-
overflow: 'hidden',
|
|
437
|
-
textOverflow: 'ellipsis',
|
|
438
|
-
},
|
|
439
|
-
modeChip: {
|
|
440
|
-
display: 'inline-flex',
|
|
441
|
-
alignItems: 'center',
|
|
442
|
-
gap: '6px',
|
|
443
|
-
padding: '0 8px',
|
|
444
|
-
border: '0.5px solid var(--dsw-alias-border-l2)',
|
|
445
|
-
borderRadius: '999px',
|
|
446
|
-
background: 'var(--dsw-alias-bg-layer-1)',
|
|
447
|
-
color: 'var(--dsw-alias-label-secondary)',
|
|
448
|
-
fontSize: '11px',
|
|
449
|
-
lineHeight: '18px',
|
|
450
|
-
whiteSpace: 'nowrap',
|
|
451
|
-
userSelect: 'none',
|
|
452
|
-
},
|
|
453
|
-
modeChipLabel: { color: 'var(--dsw-alias-label-primary)' },
|
|
454
|
-
modeChipOn: { color: 'var(--dsw-alias-state-success-primary)' },
|
|
455
|
-
denyCard: {
|
|
456
|
-
display: 'flex',
|
|
457
|
-
flexDirection: 'column',
|
|
458
|
-
gap: '4px',
|
|
459
|
-
minWidth: 0,
|
|
460
|
-
maxWidth: '100%',
|
|
461
|
-
boxSizing: 'border-box',
|
|
462
|
-
margin: '2px 0 2px 4px',
|
|
463
|
-
padding: '8px 10px',
|
|
464
|
-
border: '0.5px solid var(--dsw-alias-border-l2)',
|
|
465
|
-
borderRadius: '8px',
|
|
466
|
-
background: 'var(--dsw-alias-bg-layer-1)',
|
|
467
|
-
color: 'var(--dsw-alias-label-secondary)',
|
|
468
|
-
fontSize: '12px',
|
|
469
|
-
lineHeight: '18px',
|
|
470
|
-
},
|
|
471
|
-
denyLabel: { color: 'var(--dsw-alias-state-warn-primary)', flex: 'none' },
|
|
472
|
-
denyHint: { color: 'var(--dsw-alias-label-tertiary)' },
|
|
473
|
-
}
|
|
474
|
-
function dotStyle(tone) {
|
|
475
|
-
return {
|
|
476
|
-
width: '6px',
|
|
477
|
-
height: '6px',
|
|
478
|
-
flex: 'none',
|
|
479
|
-
borderRadius: '999px',
|
|
480
|
-
background: TONE[tone] || TONE.none,
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// ---------------------------------------------------------------------
|
|
485
|
-
// Small pure helpers. Every one of them is defensive: a tool result is
|
|
486
|
-
// durable user-facing data and may be an older shape, a partial stream, an
|
|
487
|
-
// error, or plain text instead of JSON.
|
|
488
|
-
// ---------------------------------------------------------------------
|
|
489
|
-
function fill(template, params) {
|
|
490
|
-
if (typeof template !== 'string') return String(template)
|
|
491
|
-
if (params === undefined || params === null) return template
|
|
492
|
-
return template.replace(/\{(\w+)\}/g, (match, name) =>
|
|
493
|
-
Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match,
|
|
494
|
-
)
|
|
495
|
-
}
|
|
496
|
-
function translate(t, key, params) {
|
|
497
|
-
if (typeof t === 'function') {
|
|
498
|
-
try {
|
|
499
|
-
const value = t(key, params)
|
|
500
|
-
if (typeof value === 'string') return value
|
|
501
|
-
} catch (error) {
|
|
502
|
-
/* fall through to the bundled English dictionary */
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
return fill(EN[key] !== undefined ? EN[key] : key, params)
|
|
506
|
-
}
|
|
507
|
-
function seatT(props) {
|
|
508
|
-
return props && typeof props.t === 'function' ? props.t : null
|
|
509
|
-
}
|
|
510
|
-
function toolLabel(t, name) {
|
|
511
|
-
if (typeof name !== 'string' || name === '') return translate(t, 'card.title')
|
|
512
|
-
return translate(t, TOOL_LABEL_KEYS[name] || 'card.title')
|
|
513
|
-
}
|
|
514
|
-
function isTowerTool(name) {
|
|
515
|
-
return typeof name === 'string' && TOOL_SET.has(name)
|
|
516
|
-
}
|
|
517
|
-
function truncate(value, max) {
|
|
518
|
-
const text = String(value === undefined || value === null ? '' : value)
|
|
519
|
-
.replace(/\s+/g, ' ')
|
|
520
|
-
.trim()
|
|
521
|
-
const limit = max === undefined ? MAX_TEXT : max
|
|
522
|
-
return text.length > limit ? text.slice(0, limit - 1) + '…' : text
|
|
523
|
-
}
|
|
524
|
-
function firstLine(value, max) {
|
|
525
|
-
const text = String(value === undefined || value === null ? '' : value)
|
|
526
|
-
const line = text.split('\n')[0]
|
|
527
|
-
return truncate(line, max)
|
|
528
|
-
}
|
|
529
|
-
function shortCommit(value) {
|
|
530
|
-
if (typeof value !== 'string' || value === '') return ''
|
|
531
|
-
return value.slice(0, 7)
|
|
532
|
-
}
|
|
533
|
-
function isPlainObject(value) {
|
|
534
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
535
|
-
}
|
|
536
|
-
function textOfBlock(block) {
|
|
537
|
-
if (!isPlainObject(block)) return ''
|
|
538
|
-
const parts = []
|
|
539
|
-
if (Array.isArray(block.content)) {
|
|
540
|
-
for (const item of block.content) {
|
|
541
|
-
if (!isPlainObject(item)) continue
|
|
542
|
-
if (typeof item.text === 'string') parts.push(item.text)
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
if (parts.length === 0 && typeof block.text === 'string') parts.push(block.text)
|
|
546
|
-
return parts.join('\n')
|
|
547
|
-
}
|
|
548
|
-
function errorOfBlock(block) {
|
|
549
|
-
if (!isPlainObject(block)) return null
|
|
550
|
-
if (isPlainObject(block.error)) return block.error
|
|
551
|
-
if (block.isError === true) return { name: 'ToolError', code: 'error' }
|
|
552
|
-
return null
|
|
553
|
-
}
|
|
554
|
-
function parsePayload(block) {
|
|
555
|
-
if (!isPlainObject(block)) return null
|
|
556
|
-
const error = errorOfBlock(block)
|
|
557
|
-
if (error !== null) return { __error: true, error }
|
|
558
|
-
const text = textOfBlock(block).trim()
|
|
559
|
-
if (text === '') return null
|
|
560
|
-
if (text[0] === '{' || text[0] === '[') {
|
|
561
|
-
try {
|
|
562
|
-
const parsed = JSON.parse(text)
|
|
563
|
-
if (isPlainObject(parsed)) return parsed
|
|
564
|
-
if (Array.isArray(parsed)) return { items: parsed }
|
|
565
|
-
} catch (error) {
|
|
566
|
-
/* fall through to text */
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
return { text }
|
|
570
|
-
}
|
|
571
|
-
function blockersOf(value) {
|
|
572
|
-
if (!Array.isArray(value)) return []
|
|
573
|
-
const out = []
|
|
574
|
-
for (const entry of value) {
|
|
575
|
-
if (typeof entry === 'string' && entry.trim() !== '') out.push(entry.trim())
|
|
576
|
-
else if (isPlainObject(entry)) {
|
|
577
|
-
const code = typeof entry.code === 'string' ? entry.code : 'blocker'
|
|
578
|
-
const detail = typeof entry.detail === 'string' ? entry.detail : ''
|
|
579
|
-
out.push(detail === '' ? code : code + ': ' + detail)
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
return out
|
|
583
|
-
}
|
|
584
|
-
function missionBlocksOf(value) {
|
|
585
|
-
if (!Array.isArray(value)) return []
|
|
586
|
-
return value.filter((entry) => typeof entry === 'string' && entry.trim() !== '').map((entry) => entry.trim())
|
|
587
|
-
}
|
|
588
|
-
function countsOf(payload) {
|
|
589
|
-
if (!isPlainObject(payload) || !isPlainObject(payload.counts)) return null
|
|
590
|
-
const counts = payload.counts
|
|
591
|
-
let total = 0
|
|
592
|
-
const byStatus = {}
|
|
593
|
-
let hasAny = false
|
|
594
|
-
for (const key of Object.keys(counts)) {
|
|
595
|
-
const value = counts[key]
|
|
596
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) continue
|
|
597
|
-
hasAny = true
|
|
598
|
-
byStatus[key] = value
|
|
599
|
-
total += value
|
|
600
|
-
}
|
|
601
|
-
if (!hasAny) return null
|
|
602
|
-
return { total, byStatus, active: typeof byStatus.active === 'number' ? byStatus.active : 0 }
|
|
603
|
-
}
|
|
604
|
-
function taskCountsOf(tasks) {
|
|
605
|
-
if (Array.isArray(tasks)) {
|
|
606
|
-
const counts = { total: tasks.length, done: 0, open: 0, dropped: 0 }
|
|
607
|
-
for (const task of tasks) {
|
|
608
|
-
const status = isPlainObject(task) ? task.status : undefined
|
|
609
|
-
if (status === 'done') counts.done += 1
|
|
610
|
-
else if (status === 'dropped') counts.dropped += 1
|
|
611
|
-
else counts.open += 1
|
|
612
|
-
}
|
|
613
|
-
return counts
|
|
614
|
-
}
|
|
615
|
-
if (isPlainObject(tasks)) {
|
|
616
|
-
const total = typeof tasks.total === 'number' ? tasks.total : null
|
|
617
|
-
if (total === null) return null
|
|
618
|
-
const done = typeof tasks.done === 'number' ? tasks.done : 0
|
|
619
|
-
const open = typeof tasks.open === 'number' ? tasks.open : Math.max(0, total - done - (typeof tasks.dropped === 'number' ? tasks.dropped : 0))
|
|
620
|
-
return { total, done, open, dropped: typeof tasks.dropped === 'number' ? tasks.dropped : 0 }
|
|
621
|
-
}
|
|
622
|
-
return null
|
|
623
|
-
}
|
|
624
|
-
function toneForStatus(status) {
|
|
625
|
-
if (typeof status !== 'string') return 'none'
|
|
626
|
-
if (/^(merged|completed|clean|done|fixed|ok)$/.test(status)) return 'ok'
|
|
627
|
-
if (/^(blocked|abandoned|failed)$/.test(status) || /^p1-/.test(status)) return 'error'
|
|
628
|
-
if (/^(paused|stale|missing|p2-)/.test(status)) return 'warn'
|
|
629
|
-
return 'idle'
|
|
630
|
-
}
|
|
631
|
-
function toneForPayload(payload) {
|
|
632
|
-
if (!isPlainObject(payload)) return 'none'
|
|
633
|
-
if (payload.ok === false) return 'error'
|
|
634
|
-
if (blockersOf(payload.blockers).length > 0) return 'error'
|
|
635
|
-
if (payload.ok === true) return 'ok'
|
|
636
|
-
if (isPlainObject(payload.review)) return toneForStatus(payload.review.status)
|
|
637
|
-
if (isPlainObject(payload.mission)) return toneForStatus(payload.mission.status)
|
|
638
|
-
if (isPlainObject(payload.review) === false && payload.review === null) return 'idle'
|
|
639
|
-
return 'idle'
|
|
640
|
-
}
|
|
641
|
-
function checkEntries(checks) {
|
|
642
|
-
if (!Array.isArray(checks)) return []
|
|
643
|
-
const out = []
|
|
644
|
-
for (const entry of checks) {
|
|
645
|
-
if (typeof entry === 'string') {
|
|
646
|
-
const text = entry.trim()
|
|
647
|
-
if (text !== '') out.push({ name: truncate(text, 80), ok: null, code: null, detail: '' })
|
|
648
|
-
continue
|
|
649
|
-
}
|
|
650
|
-
if (!isPlainObject(entry)) continue
|
|
651
|
-
const name =
|
|
652
|
-
typeof entry.name === 'string' && entry.name !== ''
|
|
653
|
-
? entry.name
|
|
654
|
-
: typeof entry.id === 'string' && entry.id !== ''
|
|
655
|
-
? entry.id
|
|
656
|
-
: 'check'
|
|
657
|
-
const code = typeof entry.exitCode === 'number' ? entry.exitCode : null
|
|
658
|
-
const ok =
|
|
659
|
-
code !== null
|
|
660
|
-
? code === 0
|
|
661
|
-
: entry.status === 'pass'
|
|
662
|
-
? true
|
|
663
|
-
: entry.status === 'fail'
|
|
664
|
-
? false
|
|
665
|
-
: entry.ok === true
|
|
666
|
-
? true
|
|
667
|
-
: entry.ok === false
|
|
668
|
-
? false
|
|
669
|
-
: null
|
|
670
|
-
out.push({
|
|
671
|
-
name: truncate(name, 80),
|
|
672
|
-
ok,
|
|
673
|
-
code,
|
|
674
|
-
detail: typeof entry.detail === 'string' ? entry.detail : typeof entry.evidence === 'string' ? entry.evidence : '',
|
|
675
|
-
})
|
|
676
|
-
}
|
|
677
|
-
return out
|
|
678
|
-
}
|
|
679
|
-
function checkText(t, entry) {
|
|
680
|
-
const base =
|
|
681
|
-
entry.code !== null
|
|
682
|
-
? translate(t, entry.ok ? 'card.checkOk' : 'card.checkFail', {
|
|
683
|
-
name: entry.name,
|
|
684
|
-
code: entry.code,
|
|
685
|
-
})
|
|
686
|
-
: entry.name
|
|
687
|
-
return entry.detail ? truncate(base + ': ' + entry.detail, 120) : base
|
|
688
|
-
}
|
|
689
|
-
function checkSummary(t, checks) {
|
|
690
|
-
const entries = checkEntries(checks)
|
|
691
|
-
if (entries.length === 0) return ''
|
|
692
|
-
const passed = entries.filter((entry) => entry.ok === true).length
|
|
693
|
-
const known = entries.filter((entry) => entry.ok !== null).length
|
|
694
|
-
if (known > 0) return translate(t, 'card.checksCount', { passed, total: known })
|
|
695
|
-
return String(entries.length)
|
|
696
|
-
}
|
|
697
|
-
function missionLine(t, mission) {
|
|
698
|
-
if (!isPlainObject(mission)) return ''
|
|
699
|
-
const id = typeof mission.id === 'string' ? mission.id : translate(t, 'card.missionMissing')
|
|
700
|
-
const status = typeof mission.status === 'string' ? mission.status : translate(t, 'card.unknown')
|
|
701
|
-
const parts = [translate(t, 'card.missionLine', { id, status })]
|
|
702
|
-
const tasks = taskCountsOf(mission.tasks)
|
|
703
|
-
if (tasks !== null && tasks.total > 0) {
|
|
704
|
-
parts.push(translate(t, 'card.tasks', { done: tasks.done, total: tasks.total }))
|
|
705
|
-
}
|
|
706
|
-
if (missionBlocksOf(mission.blockers).length > 0) {
|
|
707
|
-
parts.push(String(missionBlocksOf(mission.blockers).length) + ' ⚠')
|
|
708
|
-
}
|
|
709
|
-
return parts.join(translate(t, 'card.sep'))
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// ---------------------------------------------------------------------
|
|
713
|
-
// Inline style helpers for generated rows.
|
|
714
|
-
// ---------------------------------------------------------------------
|
|
715
|
-
function rowElement(t, key, row) {
|
|
716
|
-
const style =
|
|
717
|
-
row.tone === 'error'
|
|
718
|
-
? Object.assign({}, ST.row, ST.rowError)
|
|
719
|
-
: row.tone === 'warn'
|
|
720
|
-
? Object.assign({}, ST.row, ST.rowWarn)
|
|
721
|
-
: row.tone === 'ok'
|
|
722
|
-
? Object.assign({}, ST.row, ST.rowOk)
|
|
723
|
-
: ST.row
|
|
724
|
-
return h('div', { key, style }, row.text)
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// ---------------------------------------------------------------------
|
|
728
|
-
// Tool cards. Owner props supplied by the Chat tool-call tree:
|
|
729
|
-
// callId, toolName, phase ('preparing' | 'start' | 'result'), block,
|
|
730
|
-
// cwd, home, openFile, loadImage, inspect, useDisclosure — plus the
|
|
731
|
-
// session-scope standard kit (useSession/useSessions/useChat/…).
|
|
732
|
-
// `phase === 'result'` means `block` is the settled `tool/result` block.
|
|
733
|
-
// ---------------------------------------------------------------------
|
|
734
|
-
function cardModel(toolName, payload, t) {
|
|
735
|
-
const rows = []
|
|
736
|
-
if (!isPlainObject(payload)) {
|
|
737
|
-
return { tone: 'ok', headline: translate(t, 'card.done'), rows }
|
|
738
|
-
}
|
|
739
|
-
if (payload.__error === true) {
|
|
740
|
-
const error = isPlainObject(payload.error) ? payload.error : {}
|
|
741
|
-
const name = typeof error.name === 'string' ? error.name : translate(t, 'card.error')
|
|
742
|
-
const code = typeof error.code === 'string' ? error.code : ''
|
|
743
|
-
const message = typeof error.message === 'string' ? error.message : ''
|
|
744
|
-
const line = truncate([name, code, message].filter((part) => part !== '').join(': '))
|
|
745
|
-
return { tone: 'error', headline: translate(t, 'card.failed'), rows: [{ text: line, tone: 'error' }] }
|
|
746
|
-
}
|
|
747
|
-
if (typeof payload.text === 'string' && payload.text.trim() !== '') {
|
|
748
|
-
return { tone: 'ok', headline: firstLine(payload.text), rows }
|
|
749
|
-
}
|
|
750
|
-
const blockers = blockersOf(payload.blockers)
|
|
751
|
-
let tone = toneForPayload(payload)
|
|
752
|
-
let headline = translate(t, 'card.done')
|
|
753
|
-
|
|
754
|
-
if (toolName === 'tower_status') {
|
|
755
|
-
const counts = countsOf(payload)
|
|
756
|
-
const openTasks = typeof payload.openTasks === 'number' ? payload.openTasks : null
|
|
757
|
-
if (counts !== null) {
|
|
758
|
-
headline = translate(t, 'card.statusHeadline', { total: counts.total, active: counts.active })
|
|
759
|
-
}
|
|
760
|
-
if (openTasks !== null) {
|
|
761
|
-
rows.push({
|
|
762
|
-
text: openTasks > 0 ? translate(t, 'card.openTasks', { count: openTasks }) : translate(t, 'card.noOpenTasks'),
|
|
763
|
-
tone: openTasks > 0 ? 'warn' : 'ok',
|
|
764
|
-
})
|
|
765
|
-
}
|
|
766
|
-
if (typeof payload.roster === 'number' || typeof payload.findings === 'number') {
|
|
767
|
-
rows.push({
|
|
768
|
-
text: translate(t, 'card.statusDetail', {
|
|
769
|
-
roster: typeof payload.roster === 'number' ? payload.roster : '?',
|
|
770
|
-
findings: typeof payload.findings === 'number' ? payload.findings : '?',
|
|
771
|
-
revision: typeof payload.revision === 'number' ? payload.revision : '?',
|
|
772
|
-
}),
|
|
773
|
-
})
|
|
774
|
-
}
|
|
775
|
-
if (Array.isArray(payload.missions)) {
|
|
776
|
-
for (const mission of payload.missions.slice(0, 3)) {
|
|
777
|
-
const line = missionLine(t, mission)
|
|
778
|
-
if (line !== '') rows.push({ text: line, tone: toneForStatus(isPlainObject(mission) ? mission.status : undefined) })
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
if (isPlainObject(payload.detail) && isPlainObject(payload.detail.mission)) {
|
|
782
|
-
const line = missionLine(t, payload.detail.mission)
|
|
783
|
-
if (line !== '') rows.push({ text: line, tone: toneForStatus(payload.detail.mission.status) })
|
|
784
|
-
}
|
|
785
|
-
if (headline === translate(t, 'card.done')) {
|
|
786
|
-
headline = translate(t, 'card.statusHeadline', { total: 0, active: 0 })
|
|
787
|
-
}
|
|
788
|
-
} else if (toolName === 'tower_roster') {
|
|
789
|
-
const roster = Array.isArray(payload.roster) ? payload.roster : []
|
|
790
|
-
let live = 0
|
|
791
|
-
let stale = 0
|
|
792
|
-
let missing = 0
|
|
793
|
-
let other = 0
|
|
794
|
-
for (const entry of roster) {
|
|
795
|
-
if (!isPlainObject(entry)) continue
|
|
796
|
-
const activity = entry.activity
|
|
797
|
-
if (activity === 'running') live += 1
|
|
798
|
-
else if (entry.stale === true) stale += 1
|
|
799
|
-
else if (activity === 'missing' || activity === 'unknown') missing += 1
|
|
800
|
-
else other += 1
|
|
801
|
-
}
|
|
802
|
-
headline = translate(t, 'card.rosterHeadline', { live, total: roster.length })
|
|
803
|
-
tone = live > 0 ? 'ok' : stale > 0 ? 'warn' : 'idle'
|
|
804
|
-
if (stale > 0) rows.push({ text: translate(t, 'card.staleCount', { count: stale }), tone: 'warn' })
|
|
805
|
-
if (missing + other > 0) rows.push({ text: translate(t, 'card.otherCount', { count: missing + other }) })
|
|
806
|
-
for (const entry of roster.slice(0, 4)) {
|
|
807
|
-
if (!isPlainObject(entry)) continue
|
|
808
|
-
rows.push({
|
|
809
|
-
text: translate(t, 'card.agentLine', {
|
|
810
|
-
name: typeof entry.name === 'string' ? entry.name : '?',
|
|
811
|
-
role: typeof entry.role === 'string' ? entry.role : '?',
|
|
812
|
-
mission: typeof entry.missionId === 'string' ? entry.missionId : '?',
|
|
813
|
-
activity: typeof entry.activity === 'string' ? entry.activity : '?',
|
|
814
|
-
}),
|
|
815
|
-
tone: entry.stale === true ? 'warn' : entry.activity === 'running' ? 'ok' : undefined,
|
|
816
|
-
})
|
|
817
|
-
}
|
|
818
|
-
if (isPlainObject(payload.parentStatus)) {
|
|
819
|
-
const unavailable = Object.keys(payload.parentStatus).filter((key) => payload.parentStatus[key] !== 'ok')
|
|
820
|
-
if (unavailable.length > 0) {
|
|
821
|
-
rows.push({ text: 'parent catalog unavailable: ' + unavailable.length, tone: 'warn' })
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
} else if (toolName === 'tower_spawn') {
|
|
825
|
-
headline = translate(t, 'card.spawnLine', {
|
|
826
|
-
role: typeof payload.role === 'string' ? payload.role : '?',
|
|
827
|
-
name: typeof payload.name === 'string' ? payload.name : '?',
|
|
828
|
-
mission: typeof payload.missionId === 'string' ? payload.missionId : '?',
|
|
829
|
-
})
|
|
830
|
-
tone = 'ok'
|
|
831
|
-
if (isPlainObject(payload.worktree) && typeof payload.worktree.branch === 'string') {
|
|
832
|
-
rows.push({ text: translate(t, 'card.worktreeLine', { branch: payload.worktree.branch }) })
|
|
833
|
-
}
|
|
834
|
-
if (payload.created === false) rows.push({ text: 'reused worktree' })
|
|
835
|
-
if (typeof payload.agentId === 'string') rows.push({ text: truncate(payload.agentId, 60) })
|
|
836
|
-
if (payload.timedOut === true) rows.push({ text: 'timed out', tone: 'warn' })
|
|
837
|
-
} else if (toolName === 'tower_mission') {
|
|
838
|
-
const mission = isPlainObject(payload.mission) ? payload.mission : null
|
|
839
|
-
headline = mission !== null ? missionLine(t, mission) : translate(t, 'card.done')
|
|
840
|
-
tone = mission !== null ? toneForStatus(mission.status) : 'ok'
|
|
841
|
-
if (mission !== null) {
|
|
842
|
-
const tasks = taskCountsOf(mission.tasks)
|
|
843
|
-
if (tasks !== null) rows.push({ text: translate(t, 'card.tasks', { done: tasks.done, total: tasks.total }) })
|
|
844
|
-
for (const blocker of missionBlocksOf(mission.blockers).slice(0, 3)) {
|
|
845
|
-
rows.push({ text: truncate(blocker), tone: 'error' })
|
|
846
|
-
}
|
|
847
|
-
if (typeof mission.reviewPolicy === 'string') {
|
|
848
|
-
rows.push({ text: translate(t, 'card.reviewPolicy', { policy: mission.reviewPolicy }) })
|
|
849
|
-
}
|
|
850
|
-
if (isPlainObject(mission.owner) && typeof mission.owner.name === 'string') {
|
|
851
|
-
rows.push({ text: translate(t, 'card.owner', { owner: mission.owner.name }) })
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
} else if (toolName === 'tower_complete') {
|
|
855
|
-
const mission = isPlainObject(payload.mission) ? payload.mission : null
|
|
856
|
-
headline = translate(t, 'card.completeLine', {
|
|
857
|
-
mission: mission !== null && typeof mission.id === 'string' ? mission.id : '?',
|
|
858
|
-
commits: typeof payload.commits === 'number' ? payload.commits : 0,
|
|
859
|
-
})
|
|
860
|
-
tone = 'ok'
|
|
861
|
-
if (typeof payload.unread === 'number' && payload.unread > 0) {
|
|
862
|
-
rows.push({ text: translate(t, 'card.unread', { count: payload.unread }), tone: 'warn' })
|
|
863
|
-
}
|
|
864
|
-
if (mission !== null && isPlainObject(mission.completionReport)) {
|
|
865
|
-
const checks = checkEntries(mission.completionReport.checks)
|
|
866
|
-
if (checks.length > 0) {
|
|
867
|
-
rows.push({ text: translate(t, 'card.checks') + ': ' + checkSummary(t, checks) })
|
|
868
|
-
for (const entry of checks.slice(0, 3)) rows.push({ text: checkText(t, entry), tone: entry.ok === false ? 'error' : 'ok' })
|
|
869
|
-
}
|
|
870
|
-
if (typeof mission.completionReport.report === 'string') {
|
|
871
|
-
rows.push({ text: firstLine(mission.completionReport.report) })
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
} else if (toolName === 'tower_review') {
|
|
875
|
-
const review = isPlainObject(payload.review) ? payload.review : {}
|
|
876
|
-
headline = translate(t, 'card.reviewLine', {
|
|
877
|
-
status: typeof review.status === 'string' ? review.status : '?',
|
|
878
|
-
merge: typeof review.mergeVerdict === 'string' ? review.mergeVerdict : '?',
|
|
879
|
-
round: typeof payload.round === 'number' ? payload.round : typeof review.round === 'number' ? review.round : '?',
|
|
880
|
-
})
|
|
881
|
-
tone = toneForStatus(review.status)
|
|
882
|
-
const checks = checkEntries(review.checks)
|
|
883
|
-
if (checks.length > 0) {
|
|
884
|
-
rows.push({ text: translate(t, 'card.checks') + ': ' + checks.slice(0, 4).map((entry) => checkText(t, entry)).join(translate(t, 'card.sep')) })
|
|
885
|
-
}
|
|
886
|
-
if (typeof review.findings === 'string' && review.findings.trim() !== '' && review.findings.trim().toLowerCase() !== 'none') {
|
|
887
|
-
rows.push({ text: firstLine(review.findings), tone: 'warn' })
|
|
888
|
-
}
|
|
889
|
-
if (typeof review.decision === 'string' && review.decision.trim() !== '') {
|
|
890
|
-
rows.push({ text: firstLine(review.decision) })
|
|
891
|
-
}
|
|
892
|
-
if (typeof payload.branchTip === 'string') rows.push({ text: 'tip ' + shortCommit(payload.branchTip) })
|
|
893
|
-
} else if (toolName === 'tower_preflight') {
|
|
894
|
-
headline =
|
|
895
|
-
payload.ok === true
|
|
896
|
-
? translate(t, 'card.mergeReady')
|
|
897
|
-
: translate(t, 'card.mergeBlocked', { count: blockers.length })
|
|
898
|
-
tone = payload.ok === true ? 'ok' : 'error'
|
|
899
|
-
if (Array.isArray(payload.changedFiles)) {
|
|
900
|
-
rows.push({ text: 'changed files ' + payload.changedFiles.length })
|
|
901
|
-
}
|
|
902
|
-
if (isPlainObject(payload.review)) {
|
|
903
|
-
rows.push({
|
|
904
|
-
text: translate(t, 'card.reviewLine', {
|
|
905
|
-
status: typeof payload.review.status === 'string' ? payload.review.status : '?',
|
|
906
|
-
merge: typeof payload.review.mergeVerdict === 'string' ? payload.review.mergeVerdict : '?',
|
|
907
|
-
round: typeof payload.review.round === 'number' ? payload.review.round : '?',
|
|
908
|
-
}),
|
|
909
|
-
tone: toneForStatus(payload.review.status),
|
|
910
|
-
})
|
|
911
|
-
}
|
|
912
|
-
} else if (toolName === 'tower_merge') {
|
|
913
|
-
if (payload.ok === true) {
|
|
914
|
-
headline = payload.alreadyMerged === true
|
|
915
|
-
? translate(t, 'card.alreadyMerged')
|
|
916
|
-
: translate(t, 'card.merged', { commit: shortCommit(payload.mergedCommit) })
|
|
917
|
-
tone = 'ok'
|
|
918
|
-
} else {
|
|
919
|
-
headline = translate(t, 'card.mergeBlocked', { count: blockers.length })
|
|
920
|
-
tone = 'error'
|
|
921
|
-
}
|
|
922
|
-
if (payload.recoveredFromFailure === true) rows.push({ text: 'recorded after a failed merge attempt', tone: 'warn' })
|
|
923
|
-
if (Array.isArray(payload.warnings)) {
|
|
924
|
-
for (const warning of payload.warnings.slice(0, 2)) rows.push({ text: truncate(warning), tone: 'warn' })
|
|
925
|
-
}
|
|
926
|
-
if (typeof payload.baseTip === 'string') rows.push({ text: 'base ' + shortCommit(payload.baseTip) })
|
|
927
|
-
} else if (toolName === 'tower_teardown') {
|
|
928
|
-
const removed = Array.isArray(payload.removed) ? payload.removed.length : 0
|
|
929
|
-
const decisions = Array.isArray(payload.decisions) ? payload.decisions : []
|
|
930
|
-
const kept = decisions.filter((entry) => isPlainObject(entry) && entry.action === 'keep').length
|
|
931
|
-
headline = translate(t, 'card.teardownLine', { removed, kept })
|
|
932
|
-
tone = payload.ok === false ? 'error' : 'ok'
|
|
933
|
-
if (payload.dryRun === true) rows.push({ text: translate(t, 'card.dryRun') })
|
|
934
|
-
for (const decision of decisions.slice(0, 3)) {
|
|
935
|
-
if (!isPlainObject(decision)) continue
|
|
936
|
-
const reason = typeof decision.reason === 'string' ? ' · ' + truncate(decision.reason, 60) : ''
|
|
937
|
-
rows.push({
|
|
938
|
-
text: (typeof decision.action === 'string' ? decision.action : '?') + ' ' + (typeof decision.missionId === 'string' ? decision.missionId : '?') + reason,
|
|
939
|
-
tone: decision.action === 'keep' ? 'warn' : 'ok',
|
|
940
|
-
})
|
|
941
|
-
}
|
|
942
|
-
} else if (toolName === 'tower_interrupt') {
|
|
943
|
-
tone = payload.interrupted === true ? 'warn' : 'idle'
|
|
944
|
-
headline =
|
|
945
|
-
payload.interrupted === true
|
|
946
|
-
? translate(t, 'card.interruptYes', { name: isPlainObject(payload.entry) && typeof payload.entry.name === 'string' ? payload.entry.name : '?' })
|
|
947
|
-
: translate(t, 'card.interruptNo', { name: isPlainObject(payload.entry) && typeof payload.entry.name === 'string' ? payload.entry.name : '?' })
|
|
948
|
-
if (typeof payload.previousActivity === 'string') {
|
|
949
|
-
rows.push({ text: translate(t, 'card.previousActivity', { activity: payload.previousActivity }) })
|
|
950
|
-
}
|
|
951
|
-
if (typeof payload.reason === 'string' && payload.reason !== '') rows.push({ text: truncate(payload.reason) })
|
|
952
|
-
} else if (toolName === 'tower_doctor') {
|
|
953
|
-
const checks = checkEntries(payload.checks)
|
|
954
|
-
const passed = checks.filter((entry) => entry.ok === true).length
|
|
955
|
-
const warned = checks.filter((entry) => entry.ok === null).length
|
|
956
|
-
const failed = checks.filter((entry) => entry.ok === false).length
|
|
957
|
-
headline = translate(t, 'card.doctorLine', { status: payload.ok === true ? 'ok' : 'issues' })
|
|
958
|
-
tone = payload.ok === true ? 'ok' : failed > 0 ? 'error' : 'warn'
|
|
959
|
-
if (checks.length > 0) {
|
|
960
|
-
rows.push({
|
|
961
|
-
text: translate(t, 'card.doctorChecks', { pass: passed, warn: warned, fail: failed }),
|
|
962
|
-
tone: failed > 0 ? 'error' : warned > 0 ? 'warn' : 'ok',
|
|
963
|
-
})
|
|
964
|
-
for (const entry of checks.filter((candidate) => candidate.ok !== true).slice(0, 4)) {
|
|
965
|
-
rows.push({ text: checkText(t, entry), tone: entry.ok === false ? 'error' : 'warn' })
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
} else if (toolName === 'tower_batch') {
|
|
969
|
-
const summary = isPlainObject(payload.summary)
|
|
970
|
-
? payload.summary
|
|
971
|
-
: isPlainObject(payload.batch)
|
|
972
|
-
? payload.batch
|
|
973
|
-
: null
|
|
974
|
-
const status =
|
|
975
|
-
summary !== null && typeof summary.status === 'string'
|
|
976
|
-
? summary.status
|
|
977
|
-
: payload.ok === true
|
|
978
|
-
? 'ok'
|
|
979
|
-
: 'issues'
|
|
980
|
-
headline = translate(t, 'card.batchLine', { status })
|
|
981
|
-
tone = payload.ok === true ? 'ok' : 'warn'
|
|
982
|
-
if (summary !== null) {
|
|
983
|
-
if (Array.isArray(summary.members)) {
|
|
984
|
-
rows.push({
|
|
985
|
-
text: translate(t, 'card.batchMembers', {
|
|
986
|
-
count: summary.members.length,
|
|
987
|
-
status: typeof summary.status === 'string' ? summary.status : '?',
|
|
988
|
-
}),
|
|
989
|
-
})
|
|
990
|
-
}
|
|
991
|
-
if (Array.isArray(summary.missingChecks) && summary.missingChecks.length > 0) {
|
|
992
|
-
rows.push({ text: translate(t, 'card.batchMissing', { count: summary.missingChecks.length }), tone: 'warn' })
|
|
993
|
-
}
|
|
994
|
-
if (Array.isArray(summary.pendingChecks) && summary.pendingChecks.length > 0) {
|
|
995
|
-
rows.push({ text: translate(t, 'card.batchMissing', { count: summary.pendingChecks.length }), tone: 'warn' })
|
|
996
|
-
}
|
|
997
|
-
if (Array.isArray(summary.failedChecks) && summary.failedChecks.length > 0) {
|
|
998
|
-
rows.push({ text: translate(t, 'card.batchFailed', { count: summary.failedChecks.length }), tone: 'error' })
|
|
999
|
-
}
|
|
1000
|
-
const checks = checkEntries(summary.checks)
|
|
1001
|
-
if (checks.length > 0) {
|
|
1002
|
-
rows.push({
|
|
1003
|
-
text: translate(t, 'card.checks') + ': ' + checkSummary(t, checks),
|
|
1004
|
-
tone: checks.some((entry) => entry.ok === false) ? 'error' : 'ok',
|
|
1005
|
-
})
|
|
1006
|
-
}
|
|
1007
|
-
} else if (Array.isArray(payload.batches)) {
|
|
1008
|
-
rows.push({ text: 'batches ' + payload.batches.length })
|
|
1009
|
-
}
|
|
1010
|
-
} else {
|
|
1011
|
-
tone = toneForPayload(payload)
|
|
1012
|
-
}
|
|
82
|
+
const SURFACE_CHUNKS = [
|
|
83
|
+
'./client.locales.js',
|
|
84
|
+
'./client.styles.js',
|
|
85
|
+
'./client.helpers.js',
|
|
86
|
+
'./client.cards.js',
|
|
87
|
+
'./client.toolview.js',
|
|
88
|
+
'./client.denied.js',
|
|
89
|
+
'./client.mode.js',
|
|
90
|
+
'./client.dock.js',
|
|
91
|
+
'./client.overlay.js',
|
|
92
|
+
]
|
|
1013
93
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
rows.push({ text: translate(t, 'card.warnings') + ': ' + payload.warnings.slice(0, 2).map((item) => truncate(item, 60)).join(translate(t, 'card.sep')), tone: 'warn' })
|
|
1021
|
-
}
|
|
1022
|
-
const counts = countsOf(payload)
|
|
1023
|
-
if (counts !== null) {
|
|
1024
|
-
rows.push({ text: translate(t, 'card.statusHeadline', { total: counts.total, active: counts.active }) })
|
|
1025
|
-
}
|
|
1026
|
-
if (Array.isArray(payload.missions)) {
|
|
1027
|
-
for (const mission of payload.missions.slice(0, 3)) {
|
|
1028
|
-
const line = missionLine(t, mission)
|
|
1029
|
-
if (line !== '') rows.push({ text: line, tone: toneForStatus(isPlainObject(mission) ? mission.status : undefined) })
|
|
1030
|
-
}
|
|
94
|
+
return {
|
|
95
|
+
inject: ['slots', 'locale'],
|
|
96
|
+
async apply(ctx) {
|
|
97
|
+
diagnostics.applied = true
|
|
98
|
+
if (typeof require.async !== 'function') {
|
|
99
|
+
throw new Error('dsh-tower-ui: requires the DSH client module system (require.async)')
|
|
1031
100
|
}
|
|
1032
|
-
}
|
|
1033
101
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
102
|
+
// Load every package-local surface chunk once. The entry cannot require
|
|
103
|
+
// a sibling synchronously, so the split surfaces arrive before the first
|
|
104
|
+
// registration and a missing chunk fails the client half loudly.
|
|
105
|
+
const loaded = await Promise.all(SURFACE_CHUNKS.map((file) => require.async(file)))
|
|
106
|
+
const [
|
|
107
|
+
locales,
|
|
108
|
+
styles,
|
|
109
|
+
helpersModule,
|
|
110
|
+
cardsModule,
|
|
111
|
+
toolviewModule,
|
|
112
|
+
deniedModule,
|
|
113
|
+
modeModule,
|
|
114
|
+
dockModule,
|
|
115
|
+
overlayModule,
|
|
116
|
+
] = loaded
|
|
1039
117
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
const failed = Boolean(props && props.block && (props.block.isError === true || isPlainObject(props.block.error)))
|
|
1049
|
-
const previous = React.useRef ? React.useRef(null) : { current: null }
|
|
1050
|
-
React.useEffect(
|
|
1051
|
-
function () {
|
|
1052
|
-
const before = previous.current
|
|
1053
|
-
previous.current = { phase, callId }
|
|
1054
|
-
if (!isTowerTool(toolName)) return
|
|
1055
|
-
const sameCall = before !== null && before.callId === callId
|
|
1056
|
-
if (phase !== 'result') {
|
|
1057
|
-
if (!sameCall || before.phase !== phase) publishNotice({ tool: toolName, running: true })
|
|
1058
|
-
} else if (sameCall && before.phase !== 'result') {
|
|
1059
|
-
publishNotice({ tool: toolName, running: false, ok: failed !== true })
|
|
1060
|
-
}
|
|
1061
|
-
},
|
|
1062
|
-
[phase, callId, toolName, failed],
|
|
1063
|
-
)
|
|
1064
|
-
}
|
|
118
|
+
const { translate, toolLabel, seatT, EN, ZH } = locales
|
|
119
|
+
const { ST, dotStyle } = styles
|
|
120
|
+
const helpers = helpersModule.createHelpers(translate)
|
|
121
|
+
const mode = modeModule.createModeStore(diagnostics, helpers)
|
|
122
|
+
const env = { React, h, translate, toolLabel, seatT, ST, dotStyle, helpers, mode }
|
|
123
|
+
env.modeModelFromLegacy = modeModule.modeModelFromLegacy
|
|
124
|
+
env.cards = cardsModule.createCards(env)
|
|
125
|
+
const overlay = overlayModule.createOverlay(env)
|
|
1065
126
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
: cardModel(toolName, payload, t)
|
|
1077
|
-
const title =
|
|
1078
|
-
toolLabel(t, toolName) +
|
|
1079
|
-
' · ' +
|
|
1080
|
-
(running ? translate(t, 'card.running') : model.headline)
|
|
1081
|
-
return h(
|
|
1082
|
-
'div',
|
|
1083
|
-
{
|
|
1084
|
-
'data-tower-toolview': toolName === '' ? 'tower_unknown' : toolName,
|
|
1085
|
-
'data-tower-phase': phase,
|
|
1086
|
-
style: ST.card,
|
|
1087
|
-
title,
|
|
1088
|
-
},
|
|
1089
|
-
h(
|
|
1090
|
-
'div',
|
|
1091
|
-
{ style: ST.header },
|
|
1092
|
-
h('span', { 'aria-hidden': true, style: dotStyle(running ? 'idle' : model.tone) }),
|
|
1093
|
-
h('span', { style: ST.title }, translate(t, 'card.title')),
|
|
1094
|
-
h('span', { style: ST.dim }, '·'),
|
|
1095
|
-
h('span', { style: ST.title }, toolLabel(t, toolName)),
|
|
1096
|
-
h('span', { style: running ? ST.dim : model.tone === 'error' ? ST.rowError : ST.dim }, truncate(model.headline, 100)),
|
|
1097
|
-
),
|
|
1098
|
-
model.rows.slice(0, 6).map(function (row, index) {
|
|
1099
|
-
return rowElement(t, index, row)
|
|
1100
|
-
}),
|
|
1101
|
-
)
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
// ---------------------------------------------------------------------
|
|
1105
|
-
// Dynamic denied-tool rows (#12). These entries exist only while the
|
|
1106
|
-
// displayed session is in Tower mode, registered at DENIED_PRIORITY so the
|
|
1107
|
-
// native toolviews (priority 0) return the moment the mode turns off. A
|
|
1108
|
-
// real guard denial renders the distinct `Tower mode · disabled` row;
|
|
1109
|
-
// anything else landing in these slots while the mode is on gets a
|
|
1110
|
-
// defensive generic row instead of the native view.
|
|
1111
|
-
// ---------------------------------------------------------------------
|
|
1112
|
-
function denialMessageOf(block) {
|
|
1113
|
-
if (!isPlainObject(block)) return null
|
|
1114
|
-
const candidates = []
|
|
1115
|
-
if (isPlainObject(block.error) && typeof block.error.message === 'string') candidates.push(block.error.message)
|
|
1116
|
-
const text = textOfBlock(block)
|
|
1117
|
-
if (text !== '') candidates.push(text)
|
|
1118
|
-
if (typeof block.text === 'string') candidates.push(block.text)
|
|
1119
|
-
for (const raw of candidates) {
|
|
1120
|
-
const message = normalizeDenialText(raw)
|
|
1121
|
-
if (message !== null) return message
|
|
1122
|
-
}
|
|
1123
|
-
return null
|
|
1124
|
-
}
|
|
1125
|
-
function normalizeDenialText(raw) {
|
|
1126
|
-
let value = String(raw === undefined || raw === null ? '' : raw).trim()
|
|
1127
|
-
if (value === '') return null
|
|
1128
|
-
if (value[0] === '{') {
|
|
1129
|
-
try {
|
|
1130
|
-
const parsed = JSON.parse(value)
|
|
1131
|
-
if (isPlainObject(parsed) && typeof parsed.message === 'string') value = parsed.message.trim()
|
|
1132
|
-
} catch (error) {
|
|
1133
|
-
/* keep the raw text */
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
value = value.replace(/^Error:\s*/i, '')
|
|
1137
|
-
const lower = value.toLowerCase()
|
|
1138
|
-
if (!lower.includes('tower mode')) return null
|
|
1139
|
-
const specific =
|
|
1140
|
-
lower.includes('is disabled while tower mode is active') ||
|
|
1141
|
-
lower.includes('goals cannot be resumed while tower mode is active')
|
|
1142
|
-
if (!specific && !lower.includes('/tower off')) return null
|
|
1143
|
-
return truncate(value, 400)
|
|
1144
|
-
}
|
|
1145
|
-
function deniedNames(configured) {
|
|
1146
|
-
const source = Array.isArray(configured) && configured.length > 0 ? configured : DEFAULT_DENIED_TOOLS
|
|
1147
|
-
const names = []
|
|
1148
|
-
for (const entry of source) {
|
|
1149
|
-
if (typeof entry !== 'string') continue
|
|
1150
|
-
const name = entry.trim()
|
|
1151
|
-
if (name === '' || names.includes(name)) continue
|
|
1152
|
-
names.push(name)
|
|
1153
|
-
}
|
|
1154
|
-
// update_goal is denied only for action:"resume", but the guard special
|
|
1155
|
-
// case exists regardless of the configured denyTools list.
|
|
1156
|
-
if (!names.includes('update_goal')) names.push('update_goal')
|
|
1157
|
-
return names
|
|
1158
|
-
}
|
|
1159
|
-
function DeniedToolRow(props) {
|
|
1160
|
-
const t = seatT(props)
|
|
1161
|
-
const toolName = props && typeof props.toolName === 'string' ? props.toolName : ''
|
|
1162
|
-
const phase = props && typeof props.phase === 'string' ? props.phase : 'start'
|
|
1163
|
-
if (phase !== 'result') {
|
|
1164
|
-
return h(
|
|
1165
|
-
'div',
|
|
1166
|
-
{ 'data-tower-toolview': toolName, 'data-tower-denied-view': 'running', style: ST.denyCard },
|
|
1167
|
-
h(
|
|
1168
|
-
'div',
|
|
1169
|
-
{ style: ST.header },
|
|
1170
|
-
h('span', { 'aria-hidden': true, style: dotStyle('idle') }),
|
|
1171
|
-
h('span', { style: ST.denyLabel }, translate(t, 'deny.label')),
|
|
1172
|
-
h('span', { style: ST.dim }, '·'),
|
|
1173
|
-
h('span', { style: ST.title }, toolName),
|
|
1174
|
-
h('span', { style: ST.dim }, translate(t, 'deny.running')),
|
|
1175
|
-
),
|
|
1176
|
-
)
|
|
1177
|
-
}
|
|
1178
|
-
const denial = denialMessageOf(props ? props.block : undefined)
|
|
1179
|
-
if (denial !== null) {
|
|
1180
|
-
return h(
|
|
1181
|
-
'div',
|
|
1182
|
-
{
|
|
1183
|
-
'data-tower-toolview': toolName,
|
|
1184
|
-
'data-tower-denied-view': 'denied',
|
|
1185
|
-
role: 'note',
|
|
1186
|
-
style: ST.denyCard,
|
|
1187
|
-
title: denial,
|
|
1188
|
-
},
|
|
1189
|
-
h(
|
|
1190
|
-
'div',
|
|
1191
|
-
{ style: ST.header },
|
|
1192
|
-
h('span', { 'aria-hidden': true, style: dotStyle('warn') }),
|
|
1193
|
-
h('span', { style: ST.denyLabel }, translate(t, 'deny.label')),
|
|
1194
|
-
h('span', { style: ST.dim }, '·'),
|
|
1195
|
-
h('span', { style: ST.title }, toolName),
|
|
1196
|
-
),
|
|
1197
|
-
h('div', { style: ST.row }, denial),
|
|
1198
|
-
h('div', { style: ST.denyHint }, translate(t, 'deny.hint')),
|
|
1199
|
-
)
|
|
1200
|
-
}
|
|
1201
|
-
// Defensive fallback: the slot is ours only while Tower mode is on, so a
|
|
1202
|
-
// non-denial result here has no native view to delegate to.
|
|
1203
|
-
const payload = parsePayload(props ? props.block : undefined)
|
|
1204
|
-
const error =
|
|
1205
|
-
isPlainObject(props) && isPlainObject(props.block) && isPlainObject(props.block.error) ? props.block.error : null
|
|
1206
|
-
let text = translate(t, 'deny.generic', { tool: toolName })
|
|
1207
|
-
let tone = 'ok'
|
|
1208
|
-
if (isPlainObject(payload) && payload.__error === true) {
|
|
1209
|
-
tone = 'error'
|
|
1210
|
-
const reason =
|
|
1211
|
-
error !== null && typeof error.message === 'string'
|
|
1212
|
-
? error.message
|
|
1213
|
-
: error !== null && typeof error.code === 'string'
|
|
1214
|
-
? error.code
|
|
1215
|
-
: 'error'
|
|
1216
|
-
text = translate(t, 'deny.error') + ': ' + firstLine(reason)
|
|
1217
|
-
} else if (isPlainObject(payload) && typeof payload.text === 'string' && payload.text.trim() !== '') {
|
|
1218
|
-
text = translate(t, 'deny.generic', { tool: toolName }) + translate(t, 'card.sep') + firstLine(payload.text)
|
|
1219
|
-
}
|
|
1220
|
-
return h(
|
|
1221
|
-
'div',
|
|
1222
|
-
{ 'data-tower-toolview': toolName, 'data-tower-denied-view': 'generic', style: ST.denyCard },
|
|
1223
|
-
h(
|
|
1224
|
-
'div',
|
|
1225
|
-
{ style: ST.header },
|
|
1226
|
-
h('span', { 'aria-hidden': true, style: dotStyle(tone) }),
|
|
1227
|
-
h('span', { style: ST.title }, translate(t, 'card.title')),
|
|
1228
|
-
h('span', { style: ST.dim }, '·'),
|
|
1229
|
-
h('span', { style: ST.title }, toolName),
|
|
1230
|
-
h('span', { style: tone === 'error' ? ST.rowError : ST.dim }, truncate(text, 120)),
|
|
1231
|
-
),
|
|
1232
|
-
)
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
// ---------------------------------------------------------------------
|
|
1236
|
-
// Composer dock: a history-derived Tower-mode chip while the displayed
|
|
1237
|
-
// session is in Tower mode; otherwise the static `/tower` hint until this
|
|
1238
|
-
// conversation has a Tower tool result or a running Tower call, then a
|
|
1239
|
-
// compact latest summary. Everything is derived only from the `useChat`
|
|
1240
|
-
// session-standard selector hook (the durable Chat legacy projection).
|
|
1241
|
-
// ---------------------------------------------------------------------
|
|
1242
|
-
function towerCommandAction(args) {
|
|
1243
|
-
const raw = String(args === undefined || args === null ? '' : args).trim()
|
|
1244
|
-
if (/^reload$/i.test(raw)) return null
|
|
1245
|
-
if (/^base\s+\S+\s*$/i.test(raw)) return null
|
|
1246
|
-
const match = /^(on|off)(?:\s+[\s\S]*)?$/i.exec(raw)
|
|
1247
|
-
if (match) return match[1].toLowerCase()
|
|
1248
|
-
// Bare `/tower` and `/tower <message>` both mean "ensure on".
|
|
1249
|
-
return 'on'
|
|
1250
|
-
}
|
|
1251
|
-
function repoNameOf(value) {
|
|
1252
|
-
if (typeof value !== 'string' || value === '') return null
|
|
1253
|
-
let cleaned = value.replace(/[\\/]+$/, '')
|
|
1254
|
-
if (/[\\/]\.git$/i.test(cleaned)) cleaned = cleaned.replace(/[\\/]\.git$/i, '')
|
|
1255
|
-
const index = Math.max(cleaned.lastIndexOf('/'), cleaned.lastIndexOf('\\'))
|
|
1256
|
-
const name = index >= 0 ? cleaned.slice(index + 1) : cleaned
|
|
1257
|
-
return name === '' ? null : name
|
|
1258
|
-
}
|
|
1259
|
-
// Fold the durable history the dock can see into one Tower-mode model. The
|
|
1260
|
-
// last recognized event wins; `/tower reload|base` and failed commands do
|
|
1261
|
-
// not change the stance. This is a loaded-history-window inference, not a
|
|
1262
|
-
// Host read: entries older than the window yield `known: false`.
|
|
1263
|
-
function modeModelFromLegacy(legacy) {
|
|
1264
|
-
const model = {
|
|
1265
|
-
known: false,
|
|
1266
|
-
on: null,
|
|
1267
|
-
denied: null,
|
|
1268
|
-
repo: null,
|
|
1269
|
-
lock: null,
|
|
1270
|
-
lastBy: null,
|
|
1271
|
-
lastReason: null,
|
|
1272
|
-
source: null,
|
|
1273
|
-
}
|
|
1274
|
-
const nodes = isPlainObject(legacy) && Array.isArray(legacy.nodes) ? legacy.nodes : []
|
|
1275
|
-
for (const node of nodes) {
|
|
1276
|
-
if (!isPlainObject(node)) continue
|
|
1277
|
-
if (node.kind === 'command') {
|
|
1278
|
-
if (node.name !== 'tower') continue
|
|
1279
|
-
const outcome = isPlainObject(node.outcome) ? node.outcome : null
|
|
1280
|
-
if (outcome !== null && outcome.kind === 'error') continue
|
|
1281
|
-
const action = towerCommandAction(node.args)
|
|
1282
|
-
if (action === null) continue
|
|
1283
|
-
model.known = true
|
|
1284
|
-
model.on = action === 'on'
|
|
1285
|
-
model.source = 'command'
|
|
1286
|
-
model.lastBy = 'user'
|
|
1287
|
-
model.lastReason = null
|
|
1288
|
-
continue
|
|
1289
|
-
}
|
|
1290
|
-
const toolName = isPlainObject(node.call) ? node.call.name : null
|
|
1291
|
-
if (toolName === 'tower_mode') {
|
|
1292
|
-
const payload = parsePayload(node)
|
|
1293
|
-
if (!isPlainObject(payload) || payload.__error === true) continue
|
|
1294
|
-
if (payload.mode === 'on' || payload.mode === 'off') {
|
|
1295
|
-
model.known = true
|
|
1296
|
-
model.on = payload.mode === 'on'
|
|
1297
|
-
model.source = 'tower_mode'
|
|
1298
|
-
}
|
|
1299
|
-
if (Array.isArray(payload.deniedWhileActive)) {
|
|
1300
|
-
const names = []
|
|
1301
|
-
for (const entry of payload.deniedWhileActive) {
|
|
1302
|
-
if (typeof entry !== 'string') continue
|
|
1303
|
-
const name = entry.trim()
|
|
1304
|
-
if (name !== '' && !names.includes(name)) names.push(name)
|
|
1305
|
-
}
|
|
1306
|
-
if (names.length > 0) model.denied = names
|
|
1307
|
-
}
|
|
1308
|
-
const workspace = isPlainObject(payload.workspaceLock) ? payload.workspaceLock : null
|
|
1309
|
-
if (workspace !== null) {
|
|
1310
|
-
model.lock = { enabled: workspace.enabled === true, held: workspace.held === true }
|
|
1311
|
-
const name = repoNameOf(workspace.commonDir)
|
|
1312
|
-
if (name !== null) model.repo = name
|
|
1313
|
-
}
|
|
1314
|
-
if (isPlainObject(payload.lastChange)) {
|
|
1315
|
-
if (typeof payload.lastChange.by === 'string') model.lastBy = payload.lastChange.by
|
|
1316
|
-
if (typeof payload.lastChange.reason === 'string' && payload.lastChange.reason.trim() !== '') {
|
|
1317
|
-
model.lastReason = payload.lastChange.reason.trim()
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
continue
|
|
1321
|
-
}
|
|
1322
|
-
if (toolName === 'tower_control') {
|
|
1323
|
-
const payload = parsePayload(node)
|
|
1324
|
-
if (!isPlainObject(payload) || payload.__error === true || payload.ok === false) continue
|
|
1325
|
-
const mode =
|
|
1326
|
-
payload.mode === 'on' || payload.mode === 'off'
|
|
1327
|
-
? payload.mode
|
|
1328
|
-
: payload.action === 'enter'
|
|
1329
|
-
? 'on'
|
|
1330
|
-
: payload.action === 'exit'
|
|
1331
|
-
? 'off'
|
|
1332
|
-
: null
|
|
1333
|
-
if (mode === null) continue
|
|
1334
|
-
model.known = true
|
|
1335
|
-
model.on = mode === 'on'
|
|
1336
|
-
model.source = 'tower_control'
|
|
1337
|
-
if (typeof payload.changedBy === 'string') model.lastBy = payload.changedBy
|
|
1338
|
-
if (typeof payload.reason === 'string' && payload.reason.trim() !== '') model.lastReason = payload.reason.trim()
|
|
1339
|
-
continue
|
|
1340
|
-
}
|
|
1341
|
-
if (toolName === 'tower_status') {
|
|
1342
|
-
const payload = parsePayload(node)
|
|
1343
|
-
if (isPlainObject(payload)) {
|
|
1344
|
-
const name = repoNameOf(payload.repoRoot)
|
|
1345
|
-
if (name !== null) model.repo = name
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
return model
|
|
1350
|
-
}
|
|
1351
|
-
const EMPTY_MODE = {
|
|
1352
|
-
writer: null,
|
|
1353
|
-
known: false,
|
|
1354
|
-
on: null,
|
|
1355
|
-
denied: null,
|
|
1356
|
-
repo: null,
|
|
1357
|
-
lock: null,
|
|
1358
|
-
lastBy: null,
|
|
1359
|
-
lastReason: null,
|
|
1360
|
-
source: null,
|
|
1361
|
-
}
|
|
1362
|
-
// Module-local mode observable. The dock (session scope) publishes the
|
|
1363
|
-
// stance it derived from history; the plugin uses it only to add/remove
|
|
1364
|
-
// the dynamic denied-tool toolviews. It never holds or fetches Host state.
|
|
1365
|
-
let modeValue = Object.assign({}, EMPTY_MODE)
|
|
1366
|
-
const modeListeners = new Set()
|
|
1367
|
-
let modeWriterSeq = 0
|
|
1368
|
-
function nextDockWriter() {
|
|
1369
|
-
modeWriterSeq += 1
|
|
1370
|
-
return 'dock-' + modeWriterSeq
|
|
1371
|
-
}
|
|
1372
|
-
function modeSnapshot() {
|
|
1373
|
-
return modeValue
|
|
1374
|
-
}
|
|
1375
|
-
function subscribeMode(listener) {
|
|
1376
|
-
modeListeners.add(listener)
|
|
1377
|
-
return function () {
|
|
1378
|
-
modeListeners.delete(listener)
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
function sameMode(left, right) {
|
|
1382
|
-
return (
|
|
1383
|
-
left.known === right.known &&
|
|
1384
|
-
left.on === right.on &&
|
|
1385
|
-
JSON.stringify(left.denied) === JSON.stringify(right.denied) &&
|
|
1386
|
-
left.repo === right.repo &&
|
|
1387
|
-
JSON.stringify(left.lock) === JSON.stringify(right.lock) &&
|
|
1388
|
-
left.lastBy === right.lastBy &&
|
|
1389
|
-
left.source === right.source
|
|
1390
|
-
)
|
|
1391
|
-
}
|
|
1392
|
-
function notifyMode() {
|
|
1393
|
-
for (const listener of Array.from(modeListeners)) {
|
|
1394
|
-
try {
|
|
1395
|
-
listener()
|
|
1396
|
-
} catch (error) {
|
|
1397
|
-
/* a broken registration reconciler must not break the dock */
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
function publishMode(writer, next) {
|
|
1402
|
-
const value = {
|
|
1403
|
-
writer,
|
|
1404
|
-
known: next.known === true,
|
|
1405
|
-
on: next.known === true && next.on === true,
|
|
1406
|
-
denied: Array.isArray(next.denied) ? next.denied.slice() : null,
|
|
1407
|
-
repo: typeof next.repo === 'string' && next.repo !== '' ? next.repo : null,
|
|
1408
|
-
lock: isPlainObject(next.lock) ? { enabled: next.lock.enabled === true, held: next.lock.held === true } : null,
|
|
1409
|
-
lastBy: typeof next.lastBy === 'string' && next.lastBy !== '' ? next.lastBy : null,
|
|
1410
|
-
lastReason: typeof next.lastReason === 'string' && next.lastReason !== '' ? next.lastReason : null,
|
|
1411
|
-
source: typeof next.source === 'string' ? next.source : null,
|
|
1412
|
-
}
|
|
1413
|
-
diagnostics.mode = { known: value.known, on: value.on, denied: value.denied }
|
|
1414
|
-
if (sameMode(value, modeValue)) {
|
|
1415
|
-
modeValue = value
|
|
1416
|
-
return
|
|
1417
|
-
}
|
|
1418
|
-
modeValue = value
|
|
1419
|
-
notifyMode()
|
|
1420
|
-
}
|
|
1421
|
-
function clearMode(writer) {
|
|
1422
|
-
if (modeValue.writer !== writer) return
|
|
1423
|
-
const empty = Object.assign({}, EMPTY_MODE, { writer })
|
|
1424
|
-
if (sameMode(empty, modeValue)) {
|
|
1425
|
-
modeValue = empty
|
|
1426
|
-
return
|
|
1427
|
-
}
|
|
1428
|
-
modeValue = empty
|
|
1429
|
-
diagnostics.mode = { known: false, on: null, denied: null }
|
|
1430
|
-
notifyMode()
|
|
1431
|
-
}
|
|
1432
|
-
function renderDockHint(t) {
|
|
1433
|
-
return h(
|
|
1434
|
-
'div',
|
|
1435
|
-
{
|
|
1436
|
-
'data-tower-ui': 'dock-hint',
|
|
1437
|
-
title: translate(t, 'dock.hint.title'),
|
|
1438
|
-
style: ST.hint,
|
|
1439
|
-
},
|
|
1440
|
-
h('span', { 'aria-hidden': true }, '⌘'),
|
|
1441
|
-
h('span', null, '/tower'),
|
|
1442
|
-
)
|
|
1443
|
-
}
|
|
1444
|
-
function latestTowerSignal(legacy) {
|
|
1445
|
-
if (!isPlainObject(legacy)) return null
|
|
1446
|
-
const running = Array.isArray(legacy.runningCalls) ? legacy.runningCalls : []
|
|
1447
|
-
for (let index = running.length - 1; index >= 0; index -= 1) {
|
|
1448
|
-
const block = running[index]
|
|
1449
|
-
if (isPlainObject(block) && isTowerTool(block.name)) return { name: block.name, phase: 'start', block }
|
|
1450
|
-
}
|
|
1451
|
-
const nodes = Array.isArray(legacy.nodes) ? legacy.nodes : []
|
|
1452
|
-
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
1453
|
-
const block = nodes[index]
|
|
1454
|
-
const name = isPlainObject(block) && isPlainObject(block.call) ? block.call.name : undefined
|
|
1455
|
-
if (isTowerTool(name)) return { name, phase: 'result', block }
|
|
1456
|
-
}
|
|
1457
|
-
return null
|
|
1458
|
-
}
|
|
1459
|
-
function dockResultModel(toolName, payload) {
|
|
1460
|
-
if (!isPlainObject(payload)) return { tool: toolName, running: false, tone: 'none', key: 'dock.generic' }
|
|
1461
|
-
if (payload.__error === true) return { tool: toolName, running: false, tone: 'error', key: 'dock.generic' }
|
|
1462
|
-
if (typeof payload.text === 'string') {
|
|
1463
|
-
return { tool: toolName, running: false, tone: 'idle', key: 'dock.generic' }
|
|
1464
|
-
}
|
|
1465
|
-
const blockers = blockersOf(payload.blockers)
|
|
1466
|
-
if (toolName === 'tower_status') {
|
|
1467
|
-
const counts = countsOf(payload)
|
|
1468
|
-
const open = typeof payload.openTasks === 'number' ? payload.openTasks : 0
|
|
1469
|
-
if (counts !== null) {
|
|
1470
|
-
return { tool: toolName, running: false, tone: open > 0 ? 'warn' : 'ok', key: 'dock.statusCounts', params: { total: counts.total, active: counts.active, open } }
|
|
1471
|
-
}
|
|
1472
|
-
return { tool: toolName, running: false, tone: 'ok', key: 'dock.generic' }
|
|
1473
|
-
}
|
|
1474
|
-
if (toolName === 'tower_roster') {
|
|
1475
|
-
const roster = Array.isArray(payload.roster) ? payload.roster : []
|
|
1476
|
-
let live = 0
|
|
1477
|
-
for (const entry of roster) if (isPlainObject(entry) && entry.activity === 'running') live += 1
|
|
1478
|
-
return { tool: toolName, running: false, tone: live > 0 ? 'ok' : 'idle', key: 'dock.rosterCounts', params: { live, total: roster.length } }
|
|
1479
|
-
}
|
|
1480
|
-
if (toolName === 'tower_review' && isPlainObject(payload.review)) {
|
|
1481
|
-
return {
|
|
1482
|
-
tool: toolName,
|
|
1483
|
-
running: false,
|
|
1484
|
-
tone: toneForStatus(payload.review.status),
|
|
1485
|
-
key: 'dock.review',
|
|
1486
|
-
params: {
|
|
1487
|
-
status: typeof payload.review.status === 'string' ? payload.review.status : '?',
|
|
1488
|
-
merge: typeof payload.review.mergeVerdict === 'string' ? payload.review.mergeVerdict : '?',
|
|
1489
|
-
},
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
if (toolName === 'tower_preflight') {
|
|
1493
|
-
return payload.ok === true
|
|
1494
|
-
? { tool: toolName, running: false, tone: 'ok', key: 'dock.ready' }
|
|
1495
|
-
: { tool: toolName, running: false, tone: 'error', key: 'dock.blocked', params: { count: blockers.length } }
|
|
1496
|
-
}
|
|
1497
|
-
if (toolName === 'tower_merge') {
|
|
1498
|
-
return payload.ok === true
|
|
1499
|
-
? { tool: toolName, running: false, tone: 'ok', key: 'dock.merged', params: { commit: shortCommit(payload.mergedCommit) } }
|
|
1500
|
-
: { tool: toolName, running: false, tone: 'error', key: 'dock.blocked', params: { count: blockers.length } }
|
|
1501
|
-
}
|
|
1502
|
-
if (toolName === 'tower_complete' && isPlainObject(payload.mission)) {
|
|
1503
|
-
return {
|
|
1504
|
-
tool: toolName,
|
|
1505
|
-
running: false,
|
|
1506
|
-
tone: 'ok',
|
|
1507
|
-
key: 'dock.complete',
|
|
1508
|
-
params: { mission: typeof payload.mission.id === 'string' ? payload.mission.id : '?', commits: typeof payload.commits === 'number' ? payload.commits : 0 },
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
if (toolName === 'tower_mission' && isPlainObject(payload.mission)) {
|
|
1512
|
-
return {
|
|
1513
|
-
tool: toolName,
|
|
1514
|
-
running: false,
|
|
1515
|
-
tone: toneForStatus(payload.mission.status),
|
|
1516
|
-
key: 'dock.mission',
|
|
1517
|
-
params: { id: typeof payload.mission.id === 'string' ? payload.mission.id : '?', status: typeof payload.mission.status === 'string' ? payload.mission.status : '?' },
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
if (toolName === 'tower_spawn') {
|
|
1521
|
-
return {
|
|
1522
|
-
tool: toolName,
|
|
1523
|
-
running: false,
|
|
1524
|
-
tone: 'ok',
|
|
1525
|
-
key: 'dock.spawn',
|
|
1526
|
-
params: {
|
|
1527
|
-
role: typeof payload.role === 'string' ? payload.role : '?',
|
|
1528
|
-
name: typeof payload.name === 'string' ? payload.name : '?',
|
|
1529
|
-
mission: typeof payload.missionId === 'string' ? payload.missionId : '?',
|
|
1530
|
-
},
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
if (toolName === 'tower_teardown') {
|
|
1534
|
-
return {
|
|
1535
|
-
tool: toolName,
|
|
1536
|
-
running: false,
|
|
1537
|
-
tone: 'ok',
|
|
1538
|
-
key: 'dock.removed',
|
|
1539
|
-
params: { count: Array.isArray(payload.removed) ? payload.removed.length : 0 },
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
if (toolName === 'tower_interrupt') {
|
|
1543
|
-
const name = isPlainObject(payload.entry) && typeof payload.entry.name === 'string' ? payload.entry.name : '?'
|
|
1544
|
-
return payload.interrupted === true
|
|
1545
|
-
? { tool: toolName, running: false, tone: 'warn', key: 'dock.interrupt', params: { name } }
|
|
1546
|
-
: { tool: toolName, running: false, tone: 'idle', key: 'dock.generic' }
|
|
1547
|
-
}
|
|
1548
|
-
if (toolName === 'tower_doctor') {
|
|
1549
|
-
return {
|
|
1550
|
-
tool: toolName,
|
|
1551
|
-
running: false,
|
|
1552
|
-
tone: payload.ok === true ? 'ok' : 'warn',
|
|
1553
|
-
key: 'dock.doctor',
|
|
1554
|
-
params: { status: payload.ok === true ? 'ok' : 'issues' },
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
if (toolName === 'tower_batch') {
|
|
1558
|
-
const summary = isPlainObject(payload.summary) ? payload.summary : isPlainObject(payload.batch) ? payload.batch : null
|
|
1559
|
-
const failed = summary !== null && Array.isArray(summary.failedChecks) ? summary.failedChecks.length : 0
|
|
1560
|
-
const pending = summary !== null && Array.isArray(summary.pendingChecks) ? summary.pendingChecks.length : 0
|
|
1561
|
-
const tone = payload.ok === true && failed === 0 && pending === 0 ? 'ok' : failed > 0 ? 'error' : 'warn'
|
|
1562
|
-
return {
|
|
1563
|
-
tool: toolName,
|
|
1564
|
-
running: false,
|
|
1565
|
-
tone,
|
|
1566
|
-
key: 'dock.batch',
|
|
1567
|
-
params: { status: summary !== null && typeof summary.status === 'string' ? summary.status : payload.ok === true ? 'ok' : 'issues' },
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
return { tool: toolName, running: false, tone: 'idle', key: 'dock.generic' }
|
|
1571
|
-
}
|
|
1572
|
-
function dockSelector(snapshot) {
|
|
1573
|
-
try {
|
|
1574
|
-
const legacy = snapshot ? snapshot.legacy : undefined
|
|
1575
|
-
const mode = modeModelFromLegacy(legacy)
|
|
1576
|
-
let summary = null
|
|
1577
|
-
// While Tower mode is on the chip is the dock's message; the latest
|
|
1578
|
-
// tool summary stays available again after /tower off.
|
|
1579
|
-
if (!(mode.known === true && mode.on === true)) {
|
|
1580
|
-
const signal = latestTowerSignal(legacy)
|
|
1581
|
-
if (signal !== null) {
|
|
1582
|
-
summary =
|
|
1583
|
-
signal.phase !== 'result'
|
|
1584
|
-
? { tool: signal.name, running: true, tone: 'idle' }
|
|
1585
|
-
: dockResultModel(signal.name, parsePayload(signal.block))
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
return JSON.stringify({
|
|
1589
|
-
mode,
|
|
1590
|
-
summary,
|
|
1591
|
-
// A Tower tool result with no recognized mode event means the loaded
|
|
1592
|
-
// history window cannot prove the stance. Surface that explicitly
|
|
1593
|
-
// instead of pretending the session is off.
|
|
1594
|
-
uncertain: mode.known !== true && summary !== null && summary.running !== true,
|
|
1595
|
-
})
|
|
1596
|
-
} catch (error) {
|
|
1597
|
-
return ''
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
function modeStorageKey(sessionId) {
|
|
1601
|
-
return 'dsh-tower-ui:mode:' + String(sessionId)
|
|
1602
|
-
}
|
|
1603
|
-
function readStoredMode(sessionId) {
|
|
1604
|
-
if (typeof sessionId !== 'string' || sessionId === '') return null
|
|
1605
|
-
try {
|
|
1606
|
-
const raw = globalThis.localStorage && globalThis.localStorage.getItem(modeStorageKey(sessionId))
|
|
1607
|
-
if (!raw) return null
|
|
1608
|
-
const parsed = JSON.parse(raw)
|
|
1609
|
-
if (!isPlainObject(parsed) || typeof parsed.at !== 'number') return null
|
|
1610
|
-
if (Date.now() - parsed.at > 7 * 24 * 60 * 60 * 1000) return null
|
|
1611
|
-
return parsed
|
|
1612
|
-
} catch {
|
|
1613
|
-
return null
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
function writeStoredMode(sessionId, mode) {
|
|
1617
|
-
if (typeof sessionId !== 'string' || sessionId === '') return
|
|
1618
|
-
try {
|
|
1619
|
-
if (!globalThis.localStorage) return
|
|
1620
|
-
if (mode.known === true && (mode.on === true || mode.on === false)) {
|
|
1621
|
-
globalThis.localStorage.setItem(modeStorageKey(sessionId), JSON.stringify({
|
|
1622
|
-
on: mode.on,
|
|
1623
|
-
denied: mode.denied ?? null,
|
|
1624
|
-
repo: mode.repo ?? null,
|
|
1625
|
-
lock: mode.lock ?? null,
|
|
1626
|
-
lastBy: mode.lastBy ?? null,
|
|
1627
|
-
lastReason: mode.lastReason ?? null,
|
|
1628
|
-
at: Date.now(),
|
|
1629
|
-
}))
|
|
1630
|
-
}
|
|
1631
|
-
} catch {
|
|
1632
|
-
// storage is best effort; the history-derived model remains the source
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
function modeTooltip(t, mode) {
|
|
1636
|
-
const parts = [translate(t, 'dock.mode.tooltip')]
|
|
1637
|
-
if (mode.source === 'stored') parts.push(translate(t, 'dock.mode.lastKnown'))
|
|
1638
|
-
if (typeof mode.repo === 'string' && mode.repo !== '') parts.push(translate(t, 'dock.mode.repo', { repo: mode.repo }))
|
|
1639
|
-
if (isPlainObject(mode.lock)) {
|
|
1640
|
-
parts.push(translate(t, mode.lock.held === true ? 'dock.mode.lockHeld' : 'dock.mode.lockFree'))
|
|
1641
|
-
}
|
|
1642
|
-
if (typeof mode.lastBy === 'string' && mode.lastBy !== '') parts.push(translate(t, 'dock.mode.lastBy', { by: mode.lastBy }))
|
|
1643
|
-
return parts.join(translate(t, 'card.sep'))
|
|
1644
|
-
}
|
|
1645
|
-
function renderModeChip(t, mode) {
|
|
1646
|
-
// Non-interactive on purpose: the existing inputActions.submit() submits
|
|
1647
|
-
// the composer draft, so a programmatic /tower off would clobber what the
|
|
1648
|
-
// user is typing. The tooltip is the actionable affordance instead.
|
|
1649
|
-
return h(
|
|
1650
|
-
'div',
|
|
1651
|
-
{
|
|
1652
|
-
'data-tower-ui': 'dock-mode',
|
|
1653
|
-
'data-tower-mode': mode.on === true ? 'on' : 'off',
|
|
1654
|
-
role: 'status',
|
|
1655
|
-
'aria-live': 'polite',
|
|
1656
|
-
title: modeTooltip(t, mode),
|
|
1657
|
-
style: ST.modeChip,
|
|
1658
|
-
},
|
|
1659
|
-
h('span', { 'aria-hidden': true, style: dotStyle('ok') }),
|
|
1660
|
-
h('span', { style: ST.modeChipLabel }, translate(t, 'card.title')),
|
|
1661
|
-
h('span', { style: ST.dim }, '·'),
|
|
1662
|
-
h('span', { style: ST.modeChipOn }, translate(t, 'dock.mode.onLabel')),
|
|
1663
|
-
)
|
|
1664
|
-
}
|
|
1665
|
-
function renderModeUnknown(t) {
|
|
1666
|
-
return h(
|
|
1667
|
-
'div',
|
|
1668
|
-
{
|
|
1669
|
-
'data-tower-ui': 'dock-mode',
|
|
1670
|
-
'data-tower-mode': 'unknown',
|
|
1671
|
-
role: 'status',
|
|
1672
|
-
'aria-live': 'polite',
|
|
1673
|
-
title: translate(t, 'dock.mode.unknownTooltip'),
|
|
1674
|
-
style: ST.modeChip,
|
|
1675
|
-
},
|
|
1676
|
-
h('span', { 'aria-hidden': true, style: dotStyle('idle') }),
|
|
1677
|
-
h('span', { style: ST.modeChipLabel }, translate(t, 'card.title')),
|
|
1678
|
-
h('span', { style: ST.dim }, '·'),
|
|
1679
|
-
h('span', { style: ST.modeChipOn }, translate(t, 'dock.mode.unknownLabel')),
|
|
1680
|
-
)
|
|
1681
|
-
}
|
|
1682
|
-
function TowerDock(props) {
|
|
1683
|
-
const t = seatT(props)
|
|
1684
|
-
if (!props || typeof props.useChat !== 'function') return renderDockHint(t)
|
|
1685
|
-
return h(TowerDockLive, Object.assign({}, props, { t }))
|
|
1686
|
-
}
|
|
1687
|
-
function TowerDockLive(props) {
|
|
1688
|
-
const t = seatT(props)
|
|
1689
|
-
const raw = props.useChat(dockSelector)
|
|
1690
|
-
let model = null
|
|
1691
|
-
if (typeof raw === 'string' && raw !== '') {
|
|
1692
|
-
try {
|
|
1693
|
-
model = JSON.parse(raw)
|
|
1694
|
-
} catch (error) {
|
|
1695
|
-
model = null
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
const modeFromHistory = isPlainObject(model) && isPlainObject(model.mode) ? model.mode : { known: false, on: null }
|
|
1699
|
-
const stored = modeFromHistory.known !== true ? readStoredMode(props.sessionId) : null
|
|
1700
|
-
const mode = stored
|
|
1701
|
-
? Object.assign({}, modeFromHistory, stored, { known: true, source: 'stored' })
|
|
1702
|
-
: modeFromHistory
|
|
1703
|
-
writeStoredMode(props.sessionId, modeFromHistory)
|
|
1704
|
-
const writerState = React.useState(function () {
|
|
1705
|
-
return nextDockWriter()
|
|
1706
|
-
})
|
|
1707
|
-
const writer = writerState[0]
|
|
1708
|
-
const modeKey = JSON.stringify(mode)
|
|
1709
|
-
React.useEffect(
|
|
1710
|
-
function () {
|
|
1711
|
-
// Publish only the history-derived stance. A stored last-known value
|
|
1712
|
-
// is display-only: it must not shadow native toolviews when the Host
|
|
1713
|
-
// stance is actually unknown.
|
|
1714
|
-
publishMode(writer, modeFromHistory)
|
|
1715
|
-
return function () {
|
|
1716
|
-
clearMode(writer)
|
|
1717
|
-
}
|
|
1718
|
-
},
|
|
1719
|
-
[JSON.stringify(modeFromHistory)],
|
|
1720
|
-
)
|
|
1721
|
-
if (mode.known === true && mode.on === true) return renderModeChip(t, mode)
|
|
1722
|
-
if (isPlainObject(model) && model.uncertain === true) return renderModeUnknown(t)
|
|
1723
|
-
const summary = isPlainObject(model) ? model.summary : null
|
|
1724
|
-
if (!isPlainObject(summary)) return renderDockHint(t)
|
|
1725
|
-
const text = summary.running === true
|
|
1726
|
-
? translate(t, 'dock.running', { tool: toolLabel(t, summary.tool) })
|
|
1727
|
-
: summary.key
|
|
1728
|
-
? translate(t, summary.key, summary.params)
|
|
1729
|
-
: translate(t, 'dock.generic')
|
|
1730
|
-
const tone = summary.running === true ? 'idle' : summary.tone
|
|
1731
|
-
return h(
|
|
1732
|
-
'div',
|
|
1733
|
-
{ 'data-tower-ui': 'dock', style: ST.dock, title: toolLabel(t, summary.tool) + translate(t, 'card.sep') + text },
|
|
1734
|
-
h('span', { 'aria-hidden': true, style: dotStyle(tone) }),
|
|
1735
|
-
h('span', { style: ST.dockTool }, toolLabel(t, summary.tool)),
|
|
1736
|
-
h('span', { style: ST.dim }, '·'),
|
|
1737
|
-
h('span', { style: ST.dockText }, text),
|
|
1738
|
-
)
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
// ---------------------------------------------------------------------
|
|
1742
|
-
// shell.overlay: transient status strip. Root-scoped, so it receives no
|
|
1743
|
-
// chat hooks; the chat-driven toolview publishes into this local
|
|
1744
|
-
// observable and the strip entry injects it. Auto-dismiss is a single
|
|
1745
|
-
// timer owned by `apply`'s ctx.effect.
|
|
1746
|
-
// ---------------------------------------------------------------------
|
|
1747
|
-
let noticeValue = null
|
|
1748
|
-
const noticeListeners = new Set()
|
|
1749
|
-
function noticeSnapshot() {
|
|
1750
|
-
return noticeValue
|
|
1751
|
-
}
|
|
1752
|
-
function subscribeNotice(listener) {
|
|
1753
|
-
noticeListeners.add(listener)
|
|
1754
|
-
return function () {
|
|
1755
|
-
noticeListeners.delete(listener)
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
function setNotice(next) {
|
|
1759
|
-
if (Object.is(next, noticeValue)) return
|
|
1760
|
-
noticeValue = next
|
|
1761
|
-
for (const listener of Array.from(noticeListeners)) {
|
|
1762
|
-
try {
|
|
1763
|
-
listener()
|
|
1764
|
-
} catch (error) {
|
|
1765
|
-
/* a broken subscriber must not break the strip */
|
|
127
|
+
// One auto-dismiss timer for the overlay strip, owned by ctx.effect.
|
|
128
|
+
let dismissTimer = null
|
|
129
|
+
function publishNotice(notice) {
|
|
130
|
+
if (!helpers.isPlainObject(notice)) return
|
|
131
|
+
overlay.setNotice(Object.assign({}, notice, { seq: Date.now() }))
|
|
132
|
+
if (dismissTimer !== null) clearTimeout(dismissTimer)
|
|
133
|
+
dismissTimer = setTimeout(function () {
|
|
134
|
+
dismissTimer = null
|
|
135
|
+
overlay.setNotice(null)
|
|
136
|
+
}, NOTICE_MS)
|
|
1766
137
|
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
function TowerStatusStrip(props) {
|
|
1772
|
-
const t = seatT(props)
|
|
1773
|
-
if (!props || typeof props.useTowerNotice !== 'function') return null
|
|
1774
|
-
const notice = props.useTowerNotice(function (value) {
|
|
1775
|
-
return value
|
|
1776
|
-
})
|
|
1777
|
-
if (!isPlainObject(notice)) return null
|
|
1778
|
-
const running = notice.running === true
|
|
1779
|
-
const failed = notice.ok === false
|
|
1780
|
-
const tone = running ? 'idle' : failed ? 'error' : 'ok'
|
|
1781
|
-
const text = translate(t, running ? 'strip.started' : failed ? 'strip.failed' : 'strip.finished', {
|
|
1782
|
-
tool: toolLabel(t, notice.tool),
|
|
1783
|
-
})
|
|
1784
|
-
return h(
|
|
1785
|
-
'div',
|
|
1786
|
-
{ 'data-tower-ui': 'status-strip', role: 'status', 'aria-live': 'polite', style: ST.stripWrap },
|
|
1787
|
-
h('div', { style: ST.stripCard }, h('span', { 'aria-hidden': true, style: dotStyle(tone) }), h('span', null, text)),
|
|
1788
|
-
)
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
// ---------------------------------------------------------------------
|
|
1792
|
-
// Plugin body.
|
|
1793
|
-
// ---------------------------------------------------------------------
|
|
1794
|
-
return {
|
|
1795
|
-
inject: ['slots', 'locale'],
|
|
1796
|
-
apply(ctx) {
|
|
1797
|
-
diagnostics.applied = true
|
|
138
|
+
env.notice = { source: overlay.source, publish: publishNotice }
|
|
139
|
+
const toolview = toolviewModule.createToolview(env)
|
|
140
|
+
const denied = deniedModule.createDenied(env)
|
|
141
|
+
const dock = dockModule.createDock(env)
|
|
1798
142
|
|
|
1799
143
|
// Locale dictionaries for the slot entries' bound `t` seat.
|
|
1800
144
|
guard('locale', function () {
|
|
@@ -1805,18 +149,6 @@ window.__ModuleLoader__.load({
|
|
|
1805
149
|
'tower-ui: dictionaries',
|
|
1806
150
|
)
|
|
1807
151
|
})
|
|
1808
|
-
|
|
1809
|
-
// One auto-dismiss timer for the overlay strip, owned by ctx.effect.
|
|
1810
|
-
let dismissTimer = null
|
|
1811
|
-
publishNotice = function (notice) {
|
|
1812
|
-
if (!isPlainObject(notice)) return
|
|
1813
|
-
setNotice(Object.assign({}, notice, { seq: Date.now() }))
|
|
1814
|
-
if (dismissTimer !== null) clearTimeout(dismissTimer)
|
|
1815
|
-
dismissTimer = setTimeout(function () {
|
|
1816
|
-
dismissTimer = null
|
|
1817
|
-
setNotice(null)
|
|
1818
|
-
}, NOTICE_MS)
|
|
1819
|
-
}
|
|
1820
152
|
guard('timer', function () {
|
|
1821
153
|
return ctx.effect(
|
|
1822
154
|
function () {
|
|
@@ -1833,7 +165,7 @@ window.__ModuleLoader__.load({
|
|
|
1833
165
|
|
|
1834
166
|
// 1. Keyed per-tool cards. `key` is the wire tool name selected by the
|
|
1835
167
|
// Chat tool-call tree through `entryKey`.
|
|
1836
|
-
for (const tool of TOOL_NAMES) {
|
|
168
|
+
for (const tool of helpers.TOOL_NAMES) {
|
|
1837
169
|
guard('toolview:' + tool, function () {
|
|
1838
170
|
return ctx.slots.inject(
|
|
1839
171
|
'tool.call.toolview',
|
|
@@ -1844,7 +176,7 @@ window.__ModuleLoader__.load({
|
|
|
1844
176
|
key: tool,
|
|
1845
177
|
locale: NS,
|
|
1846
178
|
},
|
|
1847
|
-
TowerToolCard,
|
|
179
|
+
toolview.TowerToolCard,
|
|
1848
180
|
)
|
|
1849
181
|
if (!diagnostics.toolviews.includes(tool)) diagnostics.toolviews.push(tool)
|
|
1850
182
|
recordSlot('tool.call.toolview', { key: tool })
|
|
@@ -1866,7 +198,7 @@ window.__ModuleLoader__.load({
|
|
|
1866
198
|
order: 6,
|
|
1867
199
|
locale: NS,
|
|
1868
200
|
},
|
|
1869
|
-
TowerDock,
|
|
201
|
+
dock.TowerDock,
|
|
1870
202
|
)
|
|
1871
203
|
recordSlot('conversation.composer.dock', { id: 'tower-hint', order: 6 })
|
|
1872
204
|
return dispose
|
|
@@ -1886,10 +218,10 @@ window.__ModuleLoader__.load({
|
|
|
1886
218
|
order: 20,
|
|
1887
219
|
locale: NS,
|
|
1888
220
|
inject: function () {
|
|
1889
|
-
return { hooks: { towerNotice:
|
|
221
|
+
return { hooks: { towerNotice: overlay.source } }
|
|
1890
222
|
},
|
|
1891
223
|
},
|
|
1892
|
-
TowerStatusStrip,
|
|
224
|
+
overlay.TowerStatusStrip,
|
|
1893
225
|
)
|
|
1894
226
|
recordSlot('shell.overlay', { id: 'tower-status-strip', order: 20 })
|
|
1895
227
|
return dispose
|
|
@@ -1909,9 +241,9 @@ window.__ModuleLoader__.load({
|
|
|
1909
241
|
const disposers = new Map()
|
|
1910
242
|
let currentKey = null
|
|
1911
243
|
const reconcile = function () {
|
|
1912
|
-
const
|
|
1913
|
-
const active =
|
|
1914
|
-
const names = active ? deniedNames(
|
|
244
|
+
const currentMode = mode.modeSnapshot()
|
|
245
|
+
const active = currentMode.known === true && currentMode.on === true
|
|
246
|
+
const names = active ? denied.deniedNames(currentMode.denied) : []
|
|
1915
247
|
const nextKey = names.join('\n')
|
|
1916
248
|
if (nextKey === currentKey) return
|
|
1917
249
|
currentKey = nextKey
|
|
@@ -1940,7 +272,7 @@ window.__ModuleLoader__.load({
|
|
|
1940
272
|
priority: DENIED_PRIORITY,
|
|
1941
273
|
locale: NS,
|
|
1942
274
|
},
|
|
1943
|
-
DeniedToolRow,
|
|
275
|
+
denied.DeniedToolRow,
|
|
1944
276
|
),
|
|
1945
277
|
)
|
|
1946
278
|
} catch (error) {
|
|
@@ -1953,7 +285,7 @@ window.__ModuleLoader__.load({
|
|
|
1953
285
|
diagnostics.denied = { active, keys: names.slice() }
|
|
1954
286
|
recordSlot('tool.call.toolview', { denied: names.slice(), priority: DENIED_PRIORITY })
|
|
1955
287
|
}
|
|
1956
|
-
const unsubscribe = subscribeMode(reconcile)
|
|
288
|
+
const unsubscribe = mode.subscribeMode(reconcile)
|
|
1957
289
|
reconcile()
|
|
1958
290
|
return function () {
|
|
1959
291
|
try {
|