dsh-tower 2.1.0-beta.1 → 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 +154 -0
- package/COMPATIBILITY.md +6 -1
- package/CONTRIBUTING.md +71 -5
- package/PRINCIPLES.md +145 -0
- package/README.en.md +14 -5
- package/README.md +14 -5
- package/SECURITY.md +6 -0
- package/adapters/archive.js +279 -0
- package/adapters/caseSensitivity.js +66 -0
- package/adapters/default.js +51 -8
- package/{lib → adapters}/git.js +1 -1
- package/{lib → adapters}/leases.js +15 -6
- package/{lib → adapters}/legacy.js +20 -6
- package/adapters/persistence.js +543 -0
- package/adapters/stateFile.js +99 -0
- package/adapters/store-fs.js +28 -1
- package/adapters/store.js +204 -0
- package/{lib → adapters}/tests.js +15 -7
- package/adapters/vcs-git.js +15 -1
- package/{lib → adapters}/workspaceLock.js +1 -1
- package/{lib → adapters}/worktreeOwnership.js +18 -7
- package/app/archive.js +188 -0
- package/app/batch.js +9 -10
- package/app/board.js +4 -4
- package/app/checkCompletion.js +23 -11
- package/app/enrichedMissions.js +2 -4
- package/app/findings.js +6 -7
- package/app/gate.js +32 -15
- package/app/init.js +59 -22
- package/app/merge.js +64 -51
- package/app/messages.js +7 -8
- package/app/mission.js +7 -5
- package/app/plan.js +20 -9
- package/app/provision.js +12 -14
- package/app/read.js +65 -6
- package/app/rebase.js +14 -16
- package/app/recording.js +7 -8
- package/app/review.js +17 -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 +28 -8
- package/app/tower.js +121 -39
- 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/domain.md +38 -0
- package/docs/agents/issue-tracker.md +46 -0
- package/docs/agents/triage-labels.md +15 -0
- package/docs/agents/workflow.md +51 -0
- package/docs/architecture/overview.md +170 -37
- package/docs/guide/hot-reload.md +1 -1
- package/docs/guide/install-upgrade-migration.md +18 -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 +3 -3
- package/docs/reference/config.generated.md +5 -0
- package/docs/reference/config.md +4 -0
- package/docs/reference/doctor.md +104 -8
- package/docs/reference/errors.generated.md +41 -35
- package/docs/reference/errors.md +54 -40
- package/docs/reference/events.md +38 -6
- package/docs/reference/surface-budget.md +128 -0
- package/docs/reference/tools.generated.md +9 -8
- package/docs/reference/tools.md +9 -8
- package/docs/zh/guide/hot-reload.md +1 -1
- package/docs/zh/guide/install-upgrade-migration.md +13 -1
- package/docs/zh/reference/blockers.md +3 -3
- package/docs/zh/reference/config.md +4 -0
- package/docs/zh/reference/doctor.md +63 -2
- package/docs/zh/reference/errors.md +4 -2
- package/docs/zh/reference/tools.md +6 -6
- package/domain/batch.js +2 -2
- package/domain/config/schema.js +51 -4
- package/domain/contracts/outputs.js +33 -3
- package/domain/contracts/tool-inputs/missions.js +2 -1
- package/domain/contracts/tool-inputs/ops.js +1 -0
- package/domain/errors.js +297 -40
- package/domain/events.js +207 -0
- package/domain/integration/gate.js +41 -12
- package/domain/integration/init.js +40 -4
- package/domain/integration/merge.js +35 -0
- package/domain/lockBudget.js +30 -0
- package/domain/mission.js +1 -1
- package/domain/missions/completion.js +72 -5
- package/domain/missions/lifecycle.js +11 -4
- package/domain/missions/plan.js +14 -0
- package/domain/missions/review.js +90 -7
- package/domain/planning.js +6 -3
- package/domain/read/views.js +17 -1
- package/domain/report.js +9 -1
- package/domain/scope.js +83 -11
- package/domain/state/archive.js +257 -0
- package/domain/state/migrations.js +145 -0
- package/domain/state/schema.js +6 -0
- package/domain/state.js +154 -4
- package/domain/summary.js +2 -0
- package/domain/surface.js +248 -0
- package/host/doctor.js +555 -0
- 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 +64 -19
- 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 +3 -4
- 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 +4 -7
- 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 +7 -6
- package/tools/preflight.js +3 -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 +16 -8
- package/tools/teardown.js +3 -4
- package/tools/test.js +2 -2
- package/types/index.d.ts +35 -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/doctor.js +0 -270
- package/lib/persistence.js +0 -349
- package/lib/store.js +0 -118
- /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
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// dsh-tower-ui 0.2.2 — the composer dock surface (#11 + latest summary).
|
|
2
|
+
//
|
|
3
|
+
// The chip is history-derived; the dock only reads the `useChat` legacy slice
|
|
4
|
+
// and never polls or fetches Host state. Package-local chunk (#71).
|
|
5
|
+
window.__ModuleLoader__.load({
|
|
6
|
+
id: 'dsh-tower-ui',
|
|
7
|
+
chunk: 'client.dock.js',
|
|
8
|
+
factory() {
|
|
9
|
+
function createDock(env) {
|
|
10
|
+
const { React, h, translate, toolLabel, seatT, ST, dotStyle, helpers, mode } = env
|
|
11
|
+
const { modeModelFromLegacy } = env
|
|
12
|
+
const {
|
|
13
|
+
isPlainObject,
|
|
14
|
+
parsePayload,
|
|
15
|
+
isTowerTool,
|
|
16
|
+
countsOf,
|
|
17
|
+
toneForStatus,
|
|
18
|
+
shortCommit,
|
|
19
|
+
blockersOf,
|
|
20
|
+
} = helpers
|
|
21
|
+
const { nextDockWriter, publishMode, clearMode } = mode
|
|
22
|
+
|
|
23
|
+
function renderDockHint(t) {
|
|
24
|
+
return h(
|
|
25
|
+
'div',
|
|
26
|
+
{
|
|
27
|
+
'data-tower-ui': 'dock-hint',
|
|
28
|
+
title: translate(t, 'dock.hint.title'),
|
|
29
|
+
style: ST.hint,
|
|
30
|
+
},
|
|
31
|
+
h('span', { 'aria-hidden': true }, '⌘'),
|
|
32
|
+
h('span', null, '/tower'),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
function latestTowerSignal(legacy) {
|
|
36
|
+
if (!isPlainObject(legacy)) return null
|
|
37
|
+
const running = Array.isArray(legacy.runningCalls) ? legacy.runningCalls : []
|
|
38
|
+
for (let index = running.length - 1; index >= 0; index -= 1) {
|
|
39
|
+
const block = running[index]
|
|
40
|
+
if (isPlainObject(block) && isTowerTool(block.name)) return { name: block.name, phase: 'start', block }
|
|
41
|
+
}
|
|
42
|
+
const nodes = Array.isArray(legacy.nodes) ? legacy.nodes : []
|
|
43
|
+
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
44
|
+
const block = nodes[index]
|
|
45
|
+
const name = isPlainObject(block) && isPlainObject(block.call) ? block.call.name : undefined
|
|
46
|
+
if (isTowerTool(name)) return { name, phase: 'result', block }
|
|
47
|
+
}
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
function dockResultModel(toolName, payload) {
|
|
51
|
+
if (!isPlainObject(payload)) return { tool: toolName, running: false, tone: 'none', key: 'dock.generic' }
|
|
52
|
+
if (payload.__error === true) return { tool: toolName, running: false, tone: 'error', key: 'dock.generic' }
|
|
53
|
+
if (typeof payload.text === 'string') {
|
|
54
|
+
return { tool: toolName, running: false, tone: 'idle', key: 'dock.generic' }
|
|
55
|
+
}
|
|
56
|
+
const blockers = blockersOf(payload.blockers)
|
|
57
|
+
if (toolName === 'tower_status') {
|
|
58
|
+
const counts = countsOf(payload)
|
|
59
|
+
const open = typeof payload.openTasks === 'number' ? payload.openTasks : 0
|
|
60
|
+
if (counts !== null) {
|
|
61
|
+
return { tool: toolName, running: false, tone: open > 0 ? 'warn' : 'ok', key: 'dock.statusCounts', params: { total: counts.total, active: counts.active, open } }
|
|
62
|
+
}
|
|
63
|
+
return { tool: toolName, running: false, tone: 'ok', key: 'dock.generic' }
|
|
64
|
+
}
|
|
65
|
+
if (toolName === 'tower_roster') {
|
|
66
|
+
const roster = Array.isArray(payload.roster) ? payload.roster : []
|
|
67
|
+
let live = 0
|
|
68
|
+
for (const entry of roster) if (isPlainObject(entry) && entry.activity === 'running') live += 1
|
|
69
|
+
return { tool: toolName, running: false, tone: live > 0 ? 'ok' : 'idle', key: 'dock.rosterCounts', params: { live, total: roster.length } }
|
|
70
|
+
}
|
|
71
|
+
if (toolName === 'tower_review' && isPlainObject(payload.review)) {
|
|
72
|
+
return {
|
|
73
|
+
tool: toolName,
|
|
74
|
+
running: false,
|
|
75
|
+
tone: toneForStatus(payload.review.status),
|
|
76
|
+
key: 'dock.review',
|
|
77
|
+
params: {
|
|
78
|
+
status: typeof payload.review.status === 'string' ? payload.review.status : '?',
|
|
79
|
+
merge: typeof payload.review.mergeVerdict === 'string' ? payload.review.mergeVerdict : '?',
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (toolName === 'tower_preflight') {
|
|
84
|
+
return payload.ok === true
|
|
85
|
+
? { tool: toolName, running: false, tone: 'ok', key: 'dock.ready' }
|
|
86
|
+
: { tool: toolName, running: false, tone: 'error', key: 'dock.blocked', params: { count: blockers.length } }
|
|
87
|
+
}
|
|
88
|
+
if (toolName === 'tower_merge') {
|
|
89
|
+
return payload.ok === true
|
|
90
|
+
? { tool: toolName, running: false, tone: 'ok', key: 'dock.merged', params: { commit: shortCommit(payload.mergedCommit) } }
|
|
91
|
+
: { tool: toolName, running: false, tone: 'error', key: 'dock.blocked', params: { count: blockers.length } }
|
|
92
|
+
}
|
|
93
|
+
if (toolName === 'tower_complete' && isPlainObject(payload.mission)) {
|
|
94
|
+
return {
|
|
95
|
+
tool: toolName,
|
|
96
|
+
running: false,
|
|
97
|
+
tone: 'ok',
|
|
98
|
+
key: 'dock.complete',
|
|
99
|
+
params: { mission: typeof payload.mission.id === 'string' ? payload.mission.id : '?', commits: typeof payload.commits === 'number' ? payload.commits : 0 },
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (toolName === 'tower_mission' && isPlainObject(payload.mission)) {
|
|
103
|
+
return {
|
|
104
|
+
tool: toolName,
|
|
105
|
+
running: false,
|
|
106
|
+
tone: toneForStatus(payload.mission.status),
|
|
107
|
+
key: 'dock.mission',
|
|
108
|
+
params: { id: typeof payload.mission.id === 'string' ? payload.mission.id : '?', status: typeof payload.mission.status === 'string' ? payload.mission.status : '?' },
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (toolName === 'tower_spawn') {
|
|
112
|
+
return {
|
|
113
|
+
tool: toolName,
|
|
114
|
+
running: false,
|
|
115
|
+
tone: 'ok',
|
|
116
|
+
key: 'dock.spawn',
|
|
117
|
+
params: {
|
|
118
|
+
role: typeof payload.role === 'string' ? payload.role : '?',
|
|
119
|
+
name: typeof payload.name === 'string' ? payload.name : '?',
|
|
120
|
+
mission: typeof payload.missionId === 'string' ? payload.missionId : '?',
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (toolName === 'tower_teardown') {
|
|
125
|
+
return {
|
|
126
|
+
tool: toolName,
|
|
127
|
+
running: false,
|
|
128
|
+
tone: 'ok',
|
|
129
|
+
key: 'dock.removed',
|
|
130
|
+
params: { count: Array.isArray(payload.removed) ? payload.removed.length : 0 },
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (toolName === 'tower_interrupt') {
|
|
134
|
+
const name = isPlainObject(payload.entry) && typeof payload.entry.name === 'string' ? payload.entry.name : '?'
|
|
135
|
+
return payload.interrupted === true
|
|
136
|
+
? { tool: toolName, running: false, tone: 'warn', key: 'dock.interrupt', params: { name } }
|
|
137
|
+
: { tool: toolName, running: false, tone: 'idle', key: 'dock.generic' }
|
|
138
|
+
}
|
|
139
|
+
if (toolName === 'tower_doctor') {
|
|
140
|
+
return {
|
|
141
|
+
tool: toolName,
|
|
142
|
+
running: false,
|
|
143
|
+
tone: payload.ok === true ? 'ok' : 'warn',
|
|
144
|
+
key: 'dock.doctor',
|
|
145
|
+
params: { status: payload.ok === true ? 'ok' : 'issues' },
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (toolName === 'tower_batch') {
|
|
149
|
+
const summary = isPlainObject(payload.summary) ? payload.summary : isPlainObject(payload.batch) ? payload.batch : null
|
|
150
|
+
const failed = summary !== null && Array.isArray(summary.failedChecks) ? summary.failedChecks.length : 0
|
|
151
|
+
const pending = summary !== null && Array.isArray(summary.pendingChecks) ? summary.pendingChecks.length : 0
|
|
152
|
+
const tone = payload.ok === true && failed === 0 && pending === 0 ? 'ok' : failed > 0 ? 'error' : 'warn'
|
|
153
|
+
return {
|
|
154
|
+
tool: toolName,
|
|
155
|
+
running: false,
|
|
156
|
+
tone,
|
|
157
|
+
key: 'dock.batch',
|
|
158
|
+
params: { status: summary !== null && typeof summary.status === 'string' ? summary.status : payload.ok === true ? 'ok' : 'issues' },
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { tool: toolName, running: false, tone: 'idle', key: 'dock.generic' }
|
|
162
|
+
}
|
|
163
|
+
function dockSelector(snapshot) {
|
|
164
|
+
try {
|
|
165
|
+
const legacy = snapshot ? snapshot.legacy : undefined
|
|
166
|
+
const modeModel = modeModelFromLegacy(legacy, helpers)
|
|
167
|
+
let summary = null
|
|
168
|
+
// While the chip is on it is the dock's message; the summary returns
|
|
169
|
+
// after /tower off.
|
|
170
|
+
if (!(modeModel.known === true && modeModel.on === true)) {
|
|
171
|
+
const signal = latestTowerSignal(legacy)
|
|
172
|
+
if (signal !== null) {
|
|
173
|
+
summary =
|
|
174
|
+
signal.phase !== 'result'
|
|
175
|
+
? { tool: signal.name, running: true, tone: 'idle' }
|
|
176
|
+
: dockResultModel(signal.name, parsePayload(signal.block))
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return JSON.stringify({
|
|
180
|
+
mode: modeModel,
|
|
181
|
+
summary,
|
|
182
|
+
// A Tower result with no mode event cannot prove the stance; surface
|
|
183
|
+
// that explicitly instead of pretending the session is off.
|
|
184
|
+
uncertain: modeModel.known !== true && summary !== null && summary.running !== true,
|
|
185
|
+
})
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return ''
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function modeStorageKey(sessionId) {
|
|
191
|
+
return 'dsh-tower-ui:mode:' + String(sessionId)
|
|
192
|
+
}
|
|
193
|
+
function readStoredMode(sessionId) {
|
|
194
|
+
if (typeof sessionId !== 'string' || sessionId === '') return null
|
|
195
|
+
try {
|
|
196
|
+
const raw = globalThis.localStorage && globalThis.localStorage.getItem(modeStorageKey(sessionId))
|
|
197
|
+
if (!raw) return null
|
|
198
|
+
const parsed = JSON.parse(raw)
|
|
199
|
+
if (!isPlainObject(parsed) || typeof parsed.at !== 'number') return null
|
|
200
|
+
if (Date.now() - parsed.at > 7 * 24 * 60 * 60 * 1000) return null
|
|
201
|
+
return parsed
|
|
202
|
+
} catch {
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function writeStoredMode(sessionId, current) {
|
|
207
|
+
if (typeof sessionId !== 'string' || sessionId === '') return
|
|
208
|
+
try {
|
|
209
|
+
if (!globalThis.localStorage) return
|
|
210
|
+
if (current.known === true && (current.on === true || current.on === false)) {
|
|
211
|
+
globalThis.localStorage.setItem(modeStorageKey(sessionId), JSON.stringify({
|
|
212
|
+
on: current.on,
|
|
213
|
+
denied: current.denied ?? null,
|
|
214
|
+
repo: current.repo ?? null,
|
|
215
|
+
lock: current.lock ?? null,
|
|
216
|
+
lastBy: current.lastBy ?? null,
|
|
217
|
+
lastReason: current.lastReason ?? null,
|
|
218
|
+
at: Date.now(),
|
|
219
|
+
}))
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
// storage is best effort; the history-derived model remains the source
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function modeTooltip(t, modeModel) {
|
|
226
|
+
const parts = [translate(t, 'dock.mode.tooltip')]
|
|
227
|
+
if (modeModel.source === 'stored') parts.push(translate(t, 'dock.mode.lastKnown'))
|
|
228
|
+
if (typeof modeModel.repo === 'string' && modeModel.repo !== '') parts.push(translate(t, 'dock.mode.repo', { repo: modeModel.repo }))
|
|
229
|
+
if (isPlainObject(modeModel.lock)) {
|
|
230
|
+
parts.push(translate(t, modeModel.lock.held === true ? 'dock.mode.lockHeld' : 'dock.mode.lockFree'))
|
|
231
|
+
}
|
|
232
|
+
if (typeof modeModel.lastBy === 'string' && modeModel.lastBy !== '') parts.push(translate(t, 'dock.mode.lastBy', { by: modeModel.lastBy }))
|
|
233
|
+
return parts.join(translate(t, 'card.sep'))
|
|
234
|
+
}
|
|
235
|
+
function renderModeChip(t, modeModel) {
|
|
236
|
+
// Non-interactive on purpose: inputActions.submit() would submit (and
|
|
237
|
+
// clobber) the composer draft; the tooltip is the affordance instead.
|
|
238
|
+
return h(
|
|
239
|
+
'div',
|
|
240
|
+
{
|
|
241
|
+
'data-tower-ui': 'dock-mode',
|
|
242
|
+
'data-tower-mode': modeModel.on === true ? 'on' : 'off',
|
|
243
|
+
role: 'status',
|
|
244
|
+
'aria-live': 'polite',
|
|
245
|
+
title: modeTooltip(t, modeModel),
|
|
246
|
+
style: ST.modeChip,
|
|
247
|
+
},
|
|
248
|
+
h('span', { 'aria-hidden': true, style: dotStyle('ok') }),
|
|
249
|
+
h('span', { style: ST.modeChipLabel }, translate(t, 'card.title')),
|
|
250
|
+
h('span', { style: ST.dim }, '·'),
|
|
251
|
+
h('span', { style: ST.modeChipOn }, translate(t, 'dock.mode.onLabel')),
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
function renderModeUnknown(t) {
|
|
255
|
+
return h(
|
|
256
|
+
'div',
|
|
257
|
+
{
|
|
258
|
+
'data-tower-ui': 'dock-mode',
|
|
259
|
+
'data-tower-mode': 'unknown',
|
|
260
|
+
role: 'status',
|
|
261
|
+
'aria-live': 'polite',
|
|
262
|
+
title: translate(t, 'dock.mode.unknownTooltip'),
|
|
263
|
+
style: ST.modeChip,
|
|
264
|
+
},
|
|
265
|
+
h('span', { 'aria-hidden': true, style: dotStyle('idle') }),
|
|
266
|
+
h('span', { style: ST.modeChipLabel }, translate(t, 'card.title')),
|
|
267
|
+
h('span', { style: ST.dim }, '·'),
|
|
268
|
+
h('span', { style: ST.modeChipOn }, translate(t, 'dock.mode.unknownLabel')),
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
function TowerDock(props) {
|
|
272
|
+
const t = seatT(props)
|
|
273
|
+
if (!props || typeof props.useChat !== 'function') return renderDockHint(t)
|
|
274
|
+
return h(TowerDockLive, Object.assign({}, props, { t }))
|
|
275
|
+
}
|
|
276
|
+
function TowerDockLive(props) {
|
|
277
|
+
const t = seatT(props)
|
|
278
|
+
const raw = props.useChat(dockSelector)
|
|
279
|
+
let model = null
|
|
280
|
+
if (typeof raw === 'string' && raw !== '') {
|
|
281
|
+
try {
|
|
282
|
+
model = JSON.parse(raw)
|
|
283
|
+
} catch (error) {
|
|
284
|
+
model = null
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const modeFromHistory = isPlainObject(model) && isPlainObject(model.mode) ? model.mode : { known: false, on: null }
|
|
288
|
+
const stored = modeFromHistory.known !== true ? readStoredMode(props.sessionId) : null
|
|
289
|
+
const current = stored
|
|
290
|
+
? Object.assign({}, modeFromHistory, stored, { known: true, source: 'stored' })
|
|
291
|
+
: modeFromHistory
|
|
292
|
+
writeStoredMode(props.sessionId, modeFromHistory)
|
|
293
|
+
const writerState = React.useState(function () {
|
|
294
|
+
return nextDockWriter()
|
|
295
|
+
})
|
|
296
|
+
const writer = writerState[0]
|
|
297
|
+
React.useEffect(
|
|
298
|
+
function () {
|
|
299
|
+
// Publish only the history-derived stance. A stored last-known value
|
|
300
|
+
// is display-only: it must not shadow native toolviews.
|
|
301
|
+
publishMode(writer, modeFromHistory)
|
|
302
|
+
return function () {
|
|
303
|
+
clearMode(writer)
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
[JSON.stringify(modeFromHistory)],
|
|
307
|
+
)
|
|
308
|
+
if (current.known === true && current.on === true) return renderModeChip(t, current)
|
|
309
|
+
if (isPlainObject(model) && model.uncertain === true) return renderModeUnknown(t)
|
|
310
|
+
const summary = isPlainObject(model) ? model.summary : null
|
|
311
|
+
if (!isPlainObject(summary)) return renderDockHint(t)
|
|
312
|
+
const text = summary.running === true
|
|
313
|
+
? translate(t, 'dock.running', { tool: toolLabel(t, summary.tool) })
|
|
314
|
+
: summary.key
|
|
315
|
+
? translate(t, summary.key, summary.params)
|
|
316
|
+
: translate(t, 'dock.generic')
|
|
317
|
+
const tone = summary.running === true ? 'idle' : summary.tone
|
|
318
|
+
return h(
|
|
319
|
+
'div',
|
|
320
|
+
{ 'data-tower-ui': 'dock', style: ST.dock, title: toolLabel(t, summary.tool) + translate(t, 'card.sep') + text },
|
|
321
|
+
h('span', { 'aria-hidden': true, style: dotStyle(tone) }),
|
|
322
|
+
h('span', { style: ST.dockTool }, toolLabel(t, summary.tool)),
|
|
323
|
+
h('span', { style: ST.dim }, '·'),
|
|
324
|
+
h('span', { style: ST.dockText }, text),
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return { TowerDock }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return { createDock }
|
|
332
|
+
},
|
|
333
|
+
})
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// dsh-tower-ui 0.2.2 — tool vocabulary and the defensive payload helpers.
|
|
2
|
+
//
|
|
3
|
+
// Every helper is defensive: a tool result is durable user-facing data and may
|
|
4
|
+
// be an older shape, a partial stream, an error, or plain text instead of
|
|
5
|
+
// JSON. Package-local chunk loaded by client.js (#71); the translation helpers
|
|
6
|
+
// are injected once so the module stays free of top-level state.
|
|
7
|
+
window.__ModuleLoader__.load({
|
|
8
|
+
id: 'dsh-tower-ui',
|
|
9
|
+
chunk: 'client.helpers.js',
|
|
10
|
+
factory() {
|
|
11
|
+
const MAX_TEXT = 150
|
|
12
|
+
const TOOL_NAMES = [
|
|
13
|
+
'tower_status',
|
|
14
|
+
'tower_roster',
|
|
15
|
+
'tower_spawn',
|
|
16
|
+
'tower_mission',
|
|
17
|
+
'tower_complete',
|
|
18
|
+
'tower_review',
|
|
19
|
+
'tower_preflight',
|
|
20
|
+
'tower_merge',
|
|
21
|
+
'tower_teardown',
|
|
22
|
+
'tower_interrupt',
|
|
23
|
+
'tower_doctor',
|
|
24
|
+
'tower_batch',
|
|
25
|
+
]
|
|
26
|
+
const TOOL_SET = new Set(TOOL_NAMES)
|
|
27
|
+
// Mirrors host/config.js DEFAULT_DENY_TOOLS; overridden by the
|
|
28
|
+
// `deniedWhileActive` list of the latest `tower_mode` output when present.
|
|
29
|
+
const DEFAULT_DENIED_TOOLS = [
|
|
30
|
+
'create_goal',
|
|
31
|
+
'todo_write',
|
|
32
|
+
'team_task_create',
|
|
33
|
+
'team_task_update',
|
|
34
|
+
'team_task_reopen',
|
|
35
|
+
'team_task_list',
|
|
36
|
+
'spawn_teammate',
|
|
37
|
+
'wait_agent',
|
|
38
|
+
'workflow',
|
|
39
|
+
'ralph',
|
|
40
|
+
'subagent',
|
|
41
|
+
'subagent_fork',
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
function createHelpers(translate) {
|
|
45
|
+
function isTowerTool(name) {
|
|
46
|
+
return typeof name === 'string' && TOOL_SET.has(name)
|
|
47
|
+
}
|
|
48
|
+
function truncate(value, max) {
|
|
49
|
+
const text = String(value === undefined || value === null ? '' : value)
|
|
50
|
+
.replace(/\s+/g, ' ')
|
|
51
|
+
.trim()
|
|
52
|
+
const limit = max === undefined ? MAX_TEXT : max
|
|
53
|
+
return text.length > limit ? text.slice(0, limit - 1) + '…' : text
|
|
54
|
+
}
|
|
55
|
+
function firstLine(value, max) {
|
|
56
|
+
const text = String(value === undefined || value === null ? '' : value)
|
|
57
|
+
const line = text.split('\n')[0]
|
|
58
|
+
return truncate(line, max)
|
|
59
|
+
}
|
|
60
|
+
function shortCommit(value) {
|
|
61
|
+
if (typeof value !== 'string' || value === '') return ''
|
|
62
|
+
return value.slice(0, 7)
|
|
63
|
+
}
|
|
64
|
+
function isPlainObject(value) {
|
|
65
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
66
|
+
}
|
|
67
|
+
function textOfBlock(block) {
|
|
68
|
+
if (!isPlainObject(block)) return ''
|
|
69
|
+
const parts = []
|
|
70
|
+
if (Array.isArray(block.content)) {
|
|
71
|
+
for (const item of block.content) {
|
|
72
|
+
if (!isPlainObject(item)) continue
|
|
73
|
+
if (typeof item.text === 'string') parts.push(item.text)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (parts.length === 0 && typeof block.text === 'string') parts.push(block.text)
|
|
77
|
+
return parts.join('\n')
|
|
78
|
+
}
|
|
79
|
+
function errorOfBlock(block) {
|
|
80
|
+
if (!isPlainObject(block)) return null
|
|
81
|
+
if (isPlainObject(block.error)) return block.error
|
|
82
|
+
if (block.isError === true) return { name: 'ToolError', code: 'error' }
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
function parsePayload(block) {
|
|
86
|
+
if (!isPlainObject(block)) return null
|
|
87
|
+
const error = errorOfBlock(block)
|
|
88
|
+
if (error !== null) return { __error: true, error }
|
|
89
|
+
const text = textOfBlock(block).trim()
|
|
90
|
+
if (text === '') return null
|
|
91
|
+
if (text[0] === '{' || text[0] === '[') {
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(text)
|
|
94
|
+
if (isPlainObject(parsed)) return parsed
|
|
95
|
+
if (Array.isArray(parsed)) return { items: parsed }
|
|
96
|
+
} catch (error) {
|
|
97
|
+
/* fall through to text */
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { text }
|
|
101
|
+
}
|
|
102
|
+
function blockersOf(value) {
|
|
103
|
+
if (!Array.isArray(value)) return []
|
|
104
|
+
const out = []
|
|
105
|
+
for (const entry of value) {
|
|
106
|
+
if (typeof entry === 'string' && entry.trim() !== '') out.push(entry.trim())
|
|
107
|
+
else if (isPlainObject(entry)) {
|
|
108
|
+
const code = typeof entry.code === 'string' ? entry.code : 'blocker'
|
|
109
|
+
const detail = typeof entry.detail === 'string' ? entry.detail : ''
|
|
110
|
+
out.push(detail === '' ? code : code + ': ' + detail)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out
|
|
114
|
+
}
|
|
115
|
+
function missionBlocksOf(value) {
|
|
116
|
+
if (!Array.isArray(value)) return []
|
|
117
|
+
return value.filter((entry) => typeof entry === 'string' && entry.trim() !== '').map((entry) => entry.trim())
|
|
118
|
+
}
|
|
119
|
+
function countsOf(payload) {
|
|
120
|
+
if (!isPlainObject(payload) || !isPlainObject(payload.counts)) return null
|
|
121
|
+
const counts = payload.counts
|
|
122
|
+
let total = 0
|
|
123
|
+
const byStatus = {}
|
|
124
|
+
let hasAny = false
|
|
125
|
+
for (const key of Object.keys(counts)) {
|
|
126
|
+
const value = counts[key]
|
|
127
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) continue
|
|
128
|
+
hasAny = true
|
|
129
|
+
byStatus[key] = value
|
|
130
|
+
total += value
|
|
131
|
+
}
|
|
132
|
+
if (!hasAny) return null
|
|
133
|
+
return { total, byStatus, active: typeof byStatus.active === 'number' ? byStatus.active : 0 }
|
|
134
|
+
}
|
|
135
|
+
function taskCountsOf(tasks) {
|
|
136
|
+
if (Array.isArray(tasks)) {
|
|
137
|
+
const counts = { total: tasks.length, done: 0, open: 0, dropped: 0 }
|
|
138
|
+
for (const task of tasks) {
|
|
139
|
+
const status = isPlainObject(task) ? task.status : undefined
|
|
140
|
+
if (status === 'done') counts.done += 1
|
|
141
|
+
else if (status === 'dropped') counts.dropped += 1
|
|
142
|
+
else counts.open += 1
|
|
143
|
+
}
|
|
144
|
+
return counts
|
|
145
|
+
}
|
|
146
|
+
if (isPlainObject(tasks)) {
|
|
147
|
+
const total = typeof tasks.total === 'number' ? tasks.total : null
|
|
148
|
+
if (total === null) return null
|
|
149
|
+
const done = typeof tasks.done === 'number' ? tasks.done : 0
|
|
150
|
+
const open = typeof tasks.open === 'number' ? tasks.open : Math.max(0, total - done - (typeof tasks.dropped === 'number' ? tasks.dropped : 0))
|
|
151
|
+
return { total, done, open, dropped: typeof tasks.dropped === 'number' ? tasks.dropped : 0 }
|
|
152
|
+
}
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
function toneForStatus(status) {
|
|
156
|
+
if (typeof status !== 'string') return 'none'
|
|
157
|
+
if (/^(merged|completed|clean|done|fixed|ok)$/.test(status)) return 'ok'
|
|
158
|
+
if (/^(blocked|abandoned|failed)$/.test(status) || /^p1-/.test(status)) return 'error'
|
|
159
|
+
if (/^(paused|stale|missing|p2-)/.test(status)) return 'warn'
|
|
160
|
+
return 'idle'
|
|
161
|
+
}
|
|
162
|
+
function toneForPayload(payload) {
|
|
163
|
+
if (!isPlainObject(payload)) return 'none'
|
|
164
|
+
if (payload.ok === false) return 'error'
|
|
165
|
+
if (blockersOf(payload.blockers).length > 0) return 'error'
|
|
166
|
+
if (payload.ok === true) return 'ok'
|
|
167
|
+
if (isPlainObject(payload.review)) return toneForStatus(payload.review.status)
|
|
168
|
+
if (isPlainObject(payload.mission)) return toneForStatus(payload.mission.status)
|
|
169
|
+
if (isPlainObject(payload.review) === false && payload.review === null) return 'idle'
|
|
170
|
+
return 'idle'
|
|
171
|
+
}
|
|
172
|
+
function checkEntries(checks) {
|
|
173
|
+
if (!Array.isArray(checks)) return []
|
|
174
|
+
const out = []
|
|
175
|
+
for (const entry of checks) {
|
|
176
|
+
if (typeof entry === 'string') {
|
|
177
|
+
const text = entry.trim()
|
|
178
|
+
if (text !== '') out.push({ name: truncate(text, 80), ok: null, code: null, detail: '' })
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
if (!isPlainObject(entry)) continue
|
|
182
|
+
const name =
|
|
183
|
+
typeof entry.name === 'string' && entry.name !== ''
|
|
184
|
+
? entry.name
|
|
185
|
+
: typeof entry.id === 'string' && entry.id !== ''
|
|
186
|
+
? entry.id
|
|
187
|
+
: 'check'
|
|
188
|
+
const code = typeof entry.exitCode === 'number' ? entry.exitCode : null
|
|
189
|
+
const ok =
|
|
190
|
+
code !== null
|
|
191
|
+
? code === 0
|
|
192
|
+
: entry.status === 'pass'
|
|
193
|
+
? true
|
|
194
|
+
: entry.status === 'fail'
|
|
195
|
+
? false
|
|
196
|
+
: entry.ok === true
|
|
197
|
+
? true
|
|
198
|
+
: entry.ok === false
|
|
199
|
+
? false
|
|
200
|
+
: null
|
|
201
|
+
out.push({
|
|
202
|
+
name: truncate(name, 80),
|
|
203
|
+
ok,
|
|
204
|
+
code,
|
|
205
|
+
detail: typeof entry.detail === 'string' ? entry.detail : typeof entry.evidence === 'string' ? entry.evidence : '',
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
return out
|
|
209
|
+
}
|
|
210
|
+
function checkText(t, entry) {
|
|
211
|
+
const base =
|
|
212
|
+
entry.code !== null
|
|
213
|
+
? translate(t, entry.ok ? 'card.checkOk' : 'card.checkFail', {
|
|
214
|
+
name: entry.name,
|
|
215
|
+
code: entry.code,
|
|
216
|
+
})
|
|
217
|
+
: entry.name
|
|
218
|
+
return entry.detail ? truncate(base + ': ' + entry.detail, 120) : base
|
|
219
|
+
}
|
|
220
|
+
function checkSummary(t, checks) {
|
|
221
|
+
const entries = checkEntries(checks)
|
|
222
|
+
if (entries.length === 0) return ''
|
|
223
|
+
const passed = entries.filter((entry) => entry.ok === true).length
|
|
224
|
+
const known = entries.filter((entry) => entry.ok !== null).length
|
|
225
|
+
if (known > 0) return translate(t, 'card.checksCount', { passed, total: known })
|
|
226
|
+
return String(entries.length)
|
|
227
|
+
}
|
|
228
|
+
function missionLine(t, mission) {
|
|
229
|
+
if (!isPlainObject(mission)) return ''
|
|
230
|
+
const id = typeof mission.id === 'string' ? mission.id : translate(t, 'card.missionMissing')
|
|
231
|
+
const status = typeof mission.status === 'string' ? mission.status : translate(t, 'card.unknown')
|
|
232
|
+
const parts = [translate(t, 'card.missionLine', { id, status })]
|
|
233
|
+
const tasks = taskCountsOf(mission.tasks)
|
|
234
|
+
if (tasks !== null && tasks.total > 0) {
|
|
235
|
+
parts.push(translate(t, 'card.tasks', { done: tasks.done, total: tasks.total }))
|
|
236
|
+
}
|
|
237
|
+
if (missionBlocksOf(mission.blockers).length > 0) {
|
|
238
|
+
parts.push(String(missionBlocksOf(mission.blockers).length) + ' ⚠')
|
|
239
|
+
}
|
|
240
|
+
return parts.join(translate(t, 'card.sep'))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
MAX_TEXT,
|
|
245
|
+
TOOL_NAMES,
|
|
246
|
+
TOOL_SET,
|
|
247
|
+
DEFAULT_DENIED_TOOLS,
|
|
248
|
+
isTowerTool,
|
|
249
|
+
truncate,
|
|
250
|
+
firstLine,
|
|
251
|
+
shortCommit,
|
|
252
|
+
isPlainObject,
|
|
253
|
+
textOfBlock,
|
|
254
|
+
errorOfBlock,
|
|
255
|
+
parsePayload,
|
|
256
|
+
blockersOf,
|
|
257
|
+
missionBlocksOf,
|
|
258
|
+
countsOf,
|
|
259
|
+
taskCountsOf,
|
|
260
|
+
toneForStatus,
|
|
261
|
+
toneForPayload,
|
|
262
|
+
checkEntries,
|
|
263
|
+
checkText,
|
|
264
|
+
checkSummary,
|
|
265
|
+
missionLine,
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { MAX_TEXT, TOOL_NAMES, TOOL_SET, DEFAULT_DENIED_TOOLS, createHelpers }
|
|
270
|
+
},
|
|
271
|
+
})
|