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.
Files changed (170) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/COMPATIBILITY.md +5 -1
  3. package/CONTRIBUTING.md +64 -5
  4. package/PRINCIPLES.md +145 -0
  5. package/README.en.md +7 -4
  6. package/README.md +7 -4
  7. package/SECURITY.md +6 -0
  8. package/{lib → adapters}/archive.js +1 -1
  9. package/{lib → adapters}/caseSensitivity.js +1 -1
  10. package/adapters/default.js +51 -10
  11. package/{lib → adapters}/git.js +1 -1
  12. package/{lib → adapters}/leases.js +1 -1
  13. package/{lib → adapters}/legacy.js +1 -1
  14. package/adapters/persistence.js +543 -0
  15. package/adapters/store-fs.js +28 -1
  16. package/adapters/store.js +204 -0
  17. package/{lib → adapters}/tests.js +9 -5
  18. package/adapters/vcs-git.js +15 -1
  19. package/{lib → adapters}/workspaceLock.js +1 -1
  20. package/{lib → adapters}/worktreeOwnership.js +1 -1
  21. package/app/archive.js +4 -6
  22. package/app/batch.js +9 -10
  23. package/app/board.js +2 -4
  24. package/app/checkCompletion.js +9 -10
  25. package/app/enrichedMissions.js +2 -4
  26. package/app/findings.js +6 -7
  27. package/app/gate.js +1 -2
  28. package/app/init.js +24 -29
  29. package/app/merge.js +58 -51
  30. package/app/messages.js +7 -8
  31. package/app/mission.js +5 -5
  32. package/app/plan.js +8 -5
  33. package/app/provision.js +12 -14
  34. package/app/read.js +6 -3
  35. package/app/rebase.js +14 -16
  36. package/app/recording.js +7 -8
  37. package/app/review.js +15 -8
  38. package/app/roster.js +14 -15
  39. package/{lib → app}/spawn-prechecks.js +4 -3
  40. package/app/spawn.js +12 -13
  41. package/app/teardown.js +8 -9
  42. package/app/tower.js +89 -35
  43. package/docs/README.md +6 -2
  44. package/docs/adr/0001-ports-adapters-as-data.md +49 -0
  45. package/docs/adr/0002-single-state-json.md +50 -0
  46. package/docs/adr/0003-allow-agent-control-default-true.md +48 -0
  47. package/docs/adr/0004-surface-budget.md +50 -0
  48. package/docs/adr/README.md +51 -0
  49. package/docs/agents/workflow.md +51 -0
  50. package/docs/architecture/overview.md +167 -36
  51. package/docs/guide/hot-reload.md +1 -1
  52. package/docs/guide/release.md +1 -1
  53. package/docs/operations/incident.md +82 -0
  54. package/docs/operations/runbook.md +12 -3
  55. package/docs/reference/blockers.md +2 -2
  56. package/docs/reference/config.generated.md +1 -0
  57. package/docs/reference/doctor.md +48 -2
  58. package/docs/reference/errors.generated.md +41 -37
  59. package/docs/reference/errors.md +53 -41
  60. package/docs/reference/events.md +38 -6
  61. package/docs/reference/surface-budget.md +128 -0
  62. package/docs/reference/tools.generated.md +5 -4
  63. package/docs/reference/tools.md +4 -4
  64. package/docs/zh/guide/hot-reload.md +1 -1
  65. package/docs/zh/reference/blockers.md +2 -2
  66. package/docs/zh/reference/doctor.md +29 -1
  67. package/docs/zh/reference/tools.md +4 -4
  68. package/domain/batch.js +2 -2
  69. package/domain/config/schema.js +25 -1
  70. package/domain/contracts/outputs.js +6 -2
  71. package/domain/contracts/tool-inputs/ops.js +1 -0
  72. package/domain/errors.js +297 -42
  73. package/domain/events.js +207 -0
  74. package/domain/integration/gate.js +17 -2
  75. package/domain/integration/merge.js +35 -0
  76. package/domain/lockBudget.js +30 -0
  77. package/domain/missions/completion.js +14 -0
  78. package/domain/missions/lifecycle.js +9 -3
  79. package/domain/missions/plan.js +14 -0
  80. package/domain/missions/review.js +90 -7
  81. package/domain/read/views.js +3 -0
  82. package/domain/state/archive.js +1 -1
  83. package/domain/state.js +1 -1
  84. package/domain/surface.js +248 -0
  85. package/{lib → host}/doctor.js +116 -9
  86. package/{lib → host}/modeStore.js +1 -1
  87. package/{lib → host}/tool-registry.js +106 -31
  88. package/host/tower.js +32 -0
  89. package/index.js +12 -9
  90. package/package.json +10 -3
  91. package/ports/archive.js +18 -0
  92. package/ports/caseSensitivity.js +15 -0
  93. package/ports/clock.js +10 -2
  94. package/ports/config.js +19 -0
  95. package/ports/contract.js +38 -0
  96. package/ports/events.js +24 -0
  97. package/ports/ids.js +10 -2
  98. package/ports/index.js +54 -2
  99. package/ports/ownership.js +17 -0
  100. package/ports/state.js +16 -0
  101. package/ports/store.js +52 -3
  102. package/ports/vcs.js +25 -4
  103. package/scripts/architecture-waivers.json +19 -0
  104. package/scripts/architecture.mjs +634 -23
  105. package/scripts/check-docs.mjs +8 -35
  106. package/scripts/check-pack.mjs +68 -39
  107. package/scripts/check-philosophy.mjs +266 -0
  108. package/scripts/domain-metrics.mjs +13 -4
  109. package/scripts/generate-config-reference.mjs +14 -1
  110. package/scripts/generate-error-reference.mjs +68 -14
  111. package/scripts/generate-events-reference.mjs +83 -0
  112. package/scripts/generate-tools-reference.mjs +11 -0
  113. package/scripts/issue-audit.mjs +2 -2
  114. package/scripts/lib/surface.mjs +87 -0
  115. package/scripts/pack-manifest.txt +58 -22
  116. package/scripts/regression-audit.mjs +164 -0
  117. package/scripts/release-check.mjs +1 -1
  118. package/scripts/surface-baseline.json +21 -0
  119. package/scripts/surface-baseline.mjs +120 -0
  120. package/tools/adopt.js +2 -3
  121. package/tools/batch.js +2 -3
  122. package/tools/complete.js +2 -3
  123. package/tools/diff.js +3 -3
  124. package/tools/doctor.js +7 -10
  125. package/tools/finding.js +2 -3
  126. package/tools/findings.js +3 -3
  127. package/tools/import-legacy.js +3 -5
  128. package/tools/inbox.js +3 -3
  129. package/tools/index.js +7 -7
  130. package/tools/init.js +3 -6
  131. package/tools/interrupt.js +2 -3
  132. package/tools/legacy-map.js +3 -5
  133. package/tools/merge.js +3 -3
  134. package/tools/mission.js +2 -3
  135. package/tools/plan.js +6 -5
  136. package/tools/preflight.js +2 -3
  137. package/tools/rebase.js +3 -3
  138. package/tools/recover.js +38 -13
  139. package/tools/reload.js +3 -6
  140. package/tools/resource.js +3 -5
  141. package/tools/review.js +3 -4
  142. package/tools/roster.js +2 -3
  143. package/tools/send.js +2 -2
  144. package/tools/spawn.js +8 -5
  145. package/tools/status.js +3 -3
  146. package/tools/teardown.js +2 -3
  147. package/tools/test.js +2 -2
  148. package/types/index.d.ts +1 -1
  149. package/ui-bundle/README.md +37 -3
  150. package/ui-bundle/client.cards.js +356 -0
  151. package/ui-bundle/client.denied.js +142 -0
  152. package/ui-bundle/client.dock.js +333 -0
  153. package/ui-bundle/client.helpers.js +271 -0
  154. package/ui-bundle/client.js +79 -1747
  155. package/ui-bundle/client.locales.js +247 -0
  156. package/ui-bundle/client.mode.js +206 -0
  157. package/ui-bundle/client.overlay.js +64 -0
  158. package/ui-bundle/client.styles.js +151 -0
  159. package/ui-bundle/client.toolview.js +82 -0
  160. package/ui-bundle/package.json +1 -0
  161. package/lib/persistence.js +0 -356
  162. package/lib/store.js +0 -133
  163. /package/{lib → adapters}/stateFile.js +0 -0
  164. /package/{lib → app}/briefing.js +0 -0
  165. /package/{lib → app}/spawn-support.js +0 -0
  166. /package/{lib → host}/compatibility.js +0 -0
  167. /package/{lib → host}/config.js +0 -0
  168. /package/{lib → host}/hmr.js +0 -0
  169. /package/{lib → host}/modeState.js +0 -0
  170. /package/{lib → host}/presets.js +0 -0
@@ -0,0 +1,356 @@
1
+ // dsh-tower-ui 0.2.2 — the per-tool card models (toolview surface).
2
+ //
3
+ // One model per Tower tool plus the shared fallbacks for foreign/older result
4
+ // shapes. Package-local chunk loaded by client.js (#71).
5
+ window.__ModuleLoader__.load({
6
+ id: 'dsh-tower-ui',
7
+ chunk: 'client.cards.js',
8
+ factory() {
9
+ function createCards(env) {
10
+ const { h, translate, ST, helpers } = env
11
+ const {
12
+ isPlainObject,
13
+ truncate,
14
+ firstLine,
15
+ blockersOf,
16
+ toneForPayload,
17
+ countsOf,
18
+ missionLine,
19
+ toneForStatus,
20
+ taskCountsOf,
21
+ missionBlocksOf,
22
+ checkEntries,
23
+ checkSummary,
24
+ checkText,
25
+ shortCommit,
26
+ TOOL_SET,
27
+ } = helpers
28
+
29
+ function rowElement(t, key, row) {
30
+ const style =
31
+ row.tone === 'error'
32
+ ? Object.assign({}, ST.row, ST.rowError)
33
+ : row.tone === 'warn'
34
+ ? Object.assign({}, ST.row, ST.rowWarn)
35
+ : row.tone === 'ok'
36
+ ? Object.assign({}, ST.row, ST.rowOk)
37
+ : ST.row
38
+ return h('div', { key, style }, row.text)
39
+ }
40
+
41
+ // Owner props supplied by the Chat tool-call tree: callId, toolName,
42
+ // phase ('preparing' | 'start' | 'result'), block, cwd, home, openFile,
43
+ // loadImage, inspect, useDisclosure — plus the session-scope standard kit.
44
+ // `phase === 'result'` means `block` is the settled `tool/result` block.
45
+ function cardModel(toolName, payload, t) {
46
+ const rows = []
47
+ if (!isPlainObject(payload)) {
48
+ return { tone: 'ok', headline: translate(t, 'card.done'), rows }
49
+ }
50
+ if (payload.__error === true) {
51
+ const error = isPlainObject(payload.error) ? payload.error : {}
52
+ const name = typeof error.name === 'string' ? error.name : translate(t, 'card.error')
53
+ const code = typeof error.code === 'string' ? error.code : ''
54
+ const message = typeof error.message === 'string' ? error.message : ''
55
+ const line = truncate([name, code, message].filter((part) => part !== '').join(': '))
56
+ return { tone: 'error', headline: translate(t, 'card.failed'), rows: [{ text: line, tone: 'error' }] }
57
+ }
58
+ if (typeof payload.text === 'string' && payload.text.trim() !== '') {
59
+ return { tone: 'ok', headline: firstLine(payload.text), rows }
60
+ }
61
+ const blockers = blockersOf(payload.blockers)
62
+ let tone = toneForPayload(payload)
63
+ let headline = translate(t, 'card.done')
64
+
65
+ if (toolName === 'tower_status') {
66
+ const counts = countsOf(payload)
67
+ const openTasks = typeof payload.openTasks === 'number' ? payload.openTasks : null
68
+ if (counts !== null) {
69
+ headline = translate(t, 'card.statusHeadline', { total: counts.total, active: counts.active })
70
+ }
71
+ if (openTasks !== null) {
72
+ rows.push({
73
+ text: openTasks > 0 ? translate(t, 'card.openTasks', { count: openTasks }) : translate(t, 'card.noOpenTasks'),
74
+ tone: openTasks > 0 ? 'warn' : 'ok',
75
+ })
76
+ }
77
+ if (typeof payload.roster === 'number' || typeof payload.findings === 'number') {
78
+ rows.push({
79
+ text: translate(t, 'card.statusDetail', {
80
+ roster: typeof payload.roster === 'number' ? payload.roster : '?',
81
+ findings: typeof payload.findings === 'number' ? payload.findings : '?',
82
+ revision: typeof payload.revision === 'number' ? payload.revision : '?',
83
+ }),
84
+ })
85
+ }
86
+ if (Array.isArray(payload.missions)) {
87
+ for (const mission of payload.missions.slice(0, 3)) {
88
+ const line = missionLine(t, mission)
89
+ if (line !== '') rows.push({ text: line, tone: toneForStatus(isPlainObject(mission) ? mission.status : undefined) })
90
+ }
91
+ }
92
+ if (isPlainObject(payload.detail) && isPlainObject(payload.detail.mission)) {
93
+ const line = missionLine(t, payload.detail.mission)
94
+ if (line !== '') rows.push({ text: line, tone: toneForStatus(payload.detail.mission.status) })
95
+ }
96
+ if (headline === translate(t, 'card.done')) {
97
+ headline = translate(t, 'card.statusHeadline', { total: 0, active: 0 })
98
+ }
99
+ } else if (toolName === 'tower_roster') {
100
+ const roster = Array.isArray(payload.roster) ? payload.roster : []
101
+ let live = 0
102
+ let stale = 0
103
+ let missing = 0
104
+ let other = 0
105
+ for (const entry of roster) {
106
+ if (!isPlainObject(entry)) continue
107
+ const activity = entry.activity
108
+ if (activity === 'running') live += 1
109
+ else if (entry.stale === true) stale += 1
110
+ else if (activity === 'missing' || activity === 'unknown') missing += 1
111
+ else other += 1
112
+ }
113
+ headline = translate(t, 'card.rosterHeadline', { live, total: roster.length })
114
+ tone = live > 0 ? 'ok' : stale > 0 ? 'warn' : 'idle'
115
+ if (stale > 0) rows.push({ text: translate(t, 'card.staleCount', { count: stale }), tone: 'warn' })
116
+ if (missing + other > 0) rows.push({ text: translate(t, 'card.otherCount', { count: missing + other }) })
117
+ for (const entry of roster.slice(0, 4)) {
118
+ if (!isPlainObject(entry)) continue
119
+ rows.push({
120
+ text: translate(t, 'card.agentLine', {
121
+ name: typeof entry.name === 'string' ? entry.name : '?',
122
+ role: typeof entry.role === 'string' ? entry.role : '?',
123
+ mission: typeof entry.missionId === 'string' ? entry.missionId : '?',
124
+ activity: typeof entry.activity === 'string' ? entry.activity : '?',
125
+ }),
126
+ tone: entry.stale === true ? 'warn' : entry.activity === 'running' ? 'ok' : undefined,
127
+ })
128
+ }
129
+ if (isPlainObject(payload.parentStatus)) {
130
+ const unavailable = Object.keys(payload.parentStatus).filter((key) => payload.parentStatus[key] !== 'ok')
131
+ if (unavailable.length > 0) {
132
+ rows.push({ text: 'parent catalog unavailable: ' + unavailable.length, tone: 'warn' })
133
+ }
134
+ }
135
+ } else if (toolName === 'tower_spawn') {
136
+ headline = translate(t, 'card.spawnLine', {
137
+ role: typeof payload.role === 'string' ? payload.role : '?',
138
+ name: typeof payload.name === 'string' ? payload.name : '?',
139
+ mission: typeof payload.missionId === 'string' ? payload.missionId : '?',
140
+ })
141
+ tone = 'ok'
142
+ if (isPlainObject(payload.worktree) && typeof payload.worktree.branch === 'string') {
143
+ rows.push({ text: translate(t, 'card.worktreeLine', { branch: payload.worktree.branch }) })
144
+ }
145
+ if (payload.created === false) rows.push({ text: 'reused worktree' })
146
+ if (typeof payload.agentId === 'string') rows.push({ text: truncate(payload.agentId, 60) })
147
+ if (payload.timedOut === true) rows.push({ text: 'timed out', tone: 'warn' })
148
+ } else if (toolName === 'tower_mission') {
149
+ const mission = isPlainObject(payload.mission) ? payload.mission : null
150
+ headline = mission !== null ? missionLine(t, mission) : translate(t, 'card.done')
151
+ tone = mission !== null ? toneForStatus(mission.status) : 'ok'
152
+ if (mission !== null) {
153
+ const tasks = taskCountsOf(mission.tasks)
154
+ if (tasks !== null) rows.push({ text: translate(t, 'card.tasks', { done: tasks.done, total: tasks.total }) })
155
+ for (const blocker of missionBlocksOf(mission.blockers).slice(0, 3)) {
156
+ rows.push({ text: truncate(blocker), tone: 'error' })
157
+ }
158
+ if (typeof mission.reviewPolicy === 'string') {
159
+ rows.push({ text: translate(t, 'card.reviewPolicy', { policy: mission.reviewPolicy }) })
160
+ }
161
+ if (isPlainObject(mission.owner) && typeof mission.owner.name === 'string') {
162
+ rows.push({ text: translate(t, 'card.owner', { owner: mission.owner.name }) })
163
+ }
164
+ }
165
+ } else if (toolName === 'tower_complete') {
166
+ const mission = isPlainObject(payload.mission) ? payload.mission : null
167
+ headline = translate(t, 'card.completeLine', {
168
+ mission: mission !== null && typeof mission.id === 'string' ? mission.id : '?',
169
+ commits: typeof payload.commits === 'number' ? payload.commits : 0,
170
+ })
171
+ tone = 'ok'
172
+ if (typeof payload.unread === 'number' && payload.unread > 0) {
173
+ rows.push({ text: translate(t, 'card.unread', { count: payload.unread }), tone: 'warn' })
174
+ }
175
+ if (mission !== null && isPlainObject(mission.completionReport)) {
176
+ const checks = checkEntries(mission.completionReport.checks)
177
+ if (checks.length > 0) {
178
+ rows.push({ text: translate(t, 'card.checks') + ': ' + checkSummary(t, checks) })
179
+ for (const entry of checks.slice(0, 3)) rows.push({ text: checkText(t, entry), tone: entry.ok === false ? 'error' : 'ok' })
180
+ }
181
+ if (typeof mission.completionReport.report === 'string') {
182
+ rows.push({ text: firstLine(mission.completionReport.report) })
183
+ }
184
+ }
185
+ } else if (toolName === 'tower_review') {
186
+ const review = isPlainObject(payload.review) ? payload.review : {}
187
+ headline = translate(t, 'card.reviewLine', {
188
+ status: typeof review.status === 'string' ? review.status : '?',
189
+ merge: typeof review.mergeVerdict === 'string' ? review.mergeVerdict : '?',
190
+ round: typeof payload.round === 'number' ? payload.round : typeof review.round === 'number' ? review.round : '?',
191
+ })
192
+ tone = toneForStatus(review.status)
193
+ const checks = checkEntries(review.checks)
194
+ if (checks.length > 0) {
195
+ rows.push({ text: translate(t, 'card.checks') + ': ' + checks.slice(0, 4).map((entry) => checkText(t, entry)).join(translate(t, 'card.sep')) })
196
+ }
197
+ if (typeof review.findings === 'string' && review.findings.trim() !== '' && review.findings.trim().toLowerCase() !== 'none') {
198
+ rows.push({ text: firstLine(review.findings), tone: 'warn' })
199
+ }
200
+ if (typeof review.decision === 'string' && review.decision.trim() !== '') {
201
+ rows.push({ text: firstLine(review.decision) })
202
+ }
203
+ if (typeof payload.branchTip === 'string') rows.push({ text: 'tip ' + shortCommit(payload.branchTip) })
204
+ } else if (toolName === 'tower_preflight') {
205
+ headline =
206
+ payload.ok === true
207
+ ? translate(t, 'card.mergeReady')
208
+ : translate(t, 'card.mergeBlocked', { count: blockers.length })
209
+ tone = payload.ok === true ? 'ok' : 'error'
210
+ if (Array.isArray(payload.changedFiles)) {
211
+ rows.push({ text: 'changed files ' + payload.changedFiles.length })
212
+ }
213
+ if (isPlainObject(payload.review)) {
214
+ rows.push({
215
+ text: translate(t, 'card.reviewLine', {
216
+ status: typeof payload.review.status === 'string' ? payload.review.status : '?',
217
+ merge: typeof payload.review.mergeVerdict === 'string' ? payload.review.mergeVerdict : '?',
218
+ round: typeof payload.review.round === 'number' ? payload.review.round : '?',
219
+ }),
220
+ tone: toneForStatus(payload.review.status),
221
+ })
222
+ }
223
+ } else if (toolName === 'tower_merge') {
224
+ if (payload.ok === true) {
225
+ headline = payload.alreadyMerged === true
226
+ ? translate(t, 'card.alreadyMerged')
227
+ : translate(t, 'card.merged', { commit: shortCommit(payload.mergedCommit) })
228
+ tone = 'ok'
229
+ } else {
230
+ headline = translate(t, 'card.mergeBlocked', { count: blockers.length })
231
+ tone = 'error'
232
+ }
233
+ if (payload.recoveredFromFailure === true) rows.push({ text: 'recorded after a failed merge attempt', tone: 'warn' })
234
+ if (Array.isArray(payload.warnings)) {
235
+ for (const warning of payload.warnings.slice(0, 2)) rows.push({ text: truncate(warning), tone: 'warn' })
236
+ }
237
+ if (typeof payload.baseTip === 'string') rows.push({ text: 'base ' + shortCommit(payload.baseTip) })
238
+ } else if (toolName === 'tower_teardown') {
239
+ const removed = Array.isArray(payload.removed) ? payload.removed.length : 0
240
+ const decisions = Array.isArray(payload.decisions) ? payload.decisions : []
241
+ const kept = decisions.filter((entry) => isPlainObject(entry) && entry.action === 'keep').length
242
+ headline = translate(t, 'card.teardownLine', { removed, kept })
243
+ tone = payload.ok === false ? 'error' : 'ok'
244
+ if (payload.dryRun === true) rows.push({ text: translate(t, 'card.dryRun') })
245
+ for (const decision of decisions.slice(0, 3)) {
246
+ if (!isPlainObject(decision)) continue
247
+ const reason = typeof decision.reason === 'string' ? ' · ' + truncate(decision.reason, 60) : ''
248
+ rows.push({
249
+ text: (typeof decision.action === 'string' ? decision.action : '?') + ' ' + (typeof decision.missionId === 'string' ? decision.missionId : '?') + reason,
250
+ tone: decision.action === 'keep' ? 'warn' : 'ok',
251
+ })
252
+ }
253
+ } else if (toolName === 'tower_interrupt') {
254
+ tone = payload.interrupted === true ? 'warn' : 'idle'
255
+ headline =
256
+ payload.interrupted === true
257
+ ? translate(t, 'card.interruptYes', { name: isPlainObject(payload.entry) && typeof payload.entry.name === 'string' ? payload.entry.name : '?' })
258
+ : translate(t, 'card.interruptNo', { name: isPlainObject(payload.entry) && typeof payload.entry.name === 'string' ? payload.entry.name : '?' })
259
+ if (typeof payload.previousActivity === 'string') {
260
+ rows.push({ text: translate(t, 'card.previousActivity', { activity: payload.previousActivity }) })
261
+ }
262
+ if (typeof payload.reason === 'string' && payload.reason !== '') rows.push({ text: truncate(payload.reason) })
263
+ } else if (toolName === 'tower_doctor') {
264
+ const checks = checkEntries(payload.checks)
265
+ const passed = checks.filter((entry) => entry.ok === true).length
266
+ const warned = checks.filter((entry) => entry.ok === null).length
267
+ const failed = checks.filter((entry) => entry.ok === false).length
268
+ headline = translate(t, 'card.doctorLine', { status: payload.ok === true ? 'ok' : 'issues' })
269
+ tone = payload.ok === true ? 'ok' : failed > 0 ? 'error' : 'warn'
270
+ if (checks.length > 0) {
271
+ rows.push({
272
+ text: translate(t, 'card.doctorChecks', { pass: passed, warn: warned, fail: failed }),
273
+ tone: failed > 0 ? 'error' : warned > 0 ? 'warn' : 'ok',
274
+ })
275
+ for (const entry of checks.filter((candidate) => candidate.ok !== true).slice(0, 4)) {
276
+ rows.push({ text: checkText(t, entry), tone: entry.ok === false ? 'error' : 'warn' })
277
+ }
278
+ }
279
+ } else if (toolName === 'tower_batch') {
280
+ const summary = isPlainObject(payload.summary)
281
+ ? payload.summary
282
+ : isPlainObject(payload.batch)
283
+ ? payload.batch
284
+ : null
285
+ const status =
286
+ summary !== null && typeof summary.status === 'string'
287
+ ? summary.status
288
+ : payload.ok === true
289
+ ? 'ok'
290
+ : 'issues'
291
+ headline = translate(t, 'card.batchLine', { status })
292
+ tone = payload.ok === true ? 'ok' : 'warn'
293
+ if (summary !== null) {
294
+ if (Array.isArray(summary.members)) {
295
+ rows.push({
296
+ text: translate(t, 'card.batchMembers', {
297
+ count: summary.members.length,
298
+ status: typeof summary.status === 'string' ? summary.status : '?',
299
+ }),
300
+ })
301
+ }
302
+ if (Array.isArray(summary.missingChecks) && summary.missingChecks.length > 0) {
303
+ rows.push({ text: translate(t, 'card.batchMissing', { count: summary.missingChecks.length }), tone: 'warn' })
304
+ }
305
+ if (Array.isArray(summary.pendingChecks) && summary.pendingChecks.length > 0) {
306
+ rows.push({ text: translate(t, 'card.batchMissing', { count: summary.pendingChecks.length }), tone: 'warn' })
307
+ }
308
+ if (Array.isArray(summary.failedChecks) && summary.failedChecks.length > 0) {
309
+ rows.push({ text: translate(t, 'card.batchFailed', { count: summary.failedChecks.length }), tone: 'error' })
310
+ }
311
+ const checks = checkEntries(summary.checks)
312
+ if (checks.length > 0) {
313
+ rows.push({
314
+ text: translate(t, 'card.checks') + ': ' + checkSummary(t, checks),
315
+ tone: checks.some((entry) => entry.ok === false) ? 'error' : 'ok',
316
+ })
317
+ }
318
+ } else if (Array.isArray(payload.batches)) {
319
+ rows.push({ text: 'batches ' + payload.batches.length })
320
+ }
321
+ } else {
322
+ tone = toneForPayload(payload)
323
+ }
324
+
325
+ if (!TOOL_SET.has(toolName)) {
326
+ const checks = checkEntries(payload.checks)
327
+ if (checks.length > 0) {
328
+ rows.push({ text: translate(t, 'card.checks') + ': ' + checkSummary(t, checks), tone: checks.some((entry) => entry.ok === false) ? 'error' : 'ok' })
329
+ }
330
+ if (Array.isArray(payload.warnings) && payload.warnings.length > 0) {
331
+ rows.push({ text: translate(t, 'card.warnings') + ': ' + payload.warnings.slice(0, 2).map((item) => truncate(item, 60)).join(translate(t, 'card.sep')), tone: 'warn' })
332
+ }
333
+ const counts = countsOf(payload)
334
+ if (counts !== null) {
335
+ rows.push({ text: translate(t, 'card.statusHeadline', { total: counts.total, active: counts.active }) })
336
+ }
337
+ if (Array.isArray(payload.missions)) {
338
+ for (const mission of payload.missions.slice(0, 3)) {
339
+ const line = missionLine(t, mission)
340
+ if (line !== '') rows.push({ text: line, tone: toneForStatus(isPlainObject(mission) ? mission.status : undefined) })
341
+ }
342
+ }
343
+ }
344
+
345
+ for (const blocker of blockers.slice(0, 3)) {
346
+ rows.push({ text: truncate(blocker), tone: 'error' })
347
+ }
348
+ return { tone, headline, rows }
349
+ }
350
+
351
+ return { rowElement, cardModel }
352
+ }
353
+
354
+ return { createCards }
355
+ },
356
+ })
@@ -0,0 +1,142 @@
1
+ // dsh-tower-ui 0.2.2 — the mode-aware denied-tool row surface (#12).
2
+ //
3
+ // The entries exist only while the session is in Tower mode and register at a
4
+ // negative priority so the native toolviews return when the mode turns off.
5
+ // Package-local chunk (#71).
6
+ window.__ModuleLoader__.load({
7
+ id: 'dsh-tower-ui',
8
+ chunk: 'client.denied.js',
9
+ factory() {
10
+ function createDenied(env) {
11
+ const { h, translate, seatT, ST, dotStyle, helpers } = env
12
+ const { isPlainObject, textOfBlock, truncate, firstLine, parsePayload, DEFAULT_DENIED_TOOLS } = helpers
13
+
14
+ function denialMessageOf(block) {
15
+ if (!isPlainObject(block)) return null
16
+ const candidates = []
17
+ if (isPlainObject(block.error) && typeof block.error.message === 'string') candidates.push(block.error.message)
18
+ const text = textOfBlock(block)
19
+ if (text !== '') candidates.push(text)
20
+ if (typeof block.text === 'string') candidates.push(block.text)
21
+ for (const raw of candidates) {
22
+ const message = normalizeDenialText(raw)
23
+ if (message !== null) return message
24
+ }
25
+ return null
26
+ }
27
+ function normalizeDenialText(raw) {
28
+ let value = String(raw === undefined || raw === null ? '' : raw).trim()
29
+ if (value === '') return null
30
+ if (value[0] === '{') {
31
+ try {
32
+ const parsed = JSON.parse(value)
33
+ if (isPlainObject(parsed) && typeof parsed.message === 'string') value = parsed.message.trim()
34
+ } catch (error) {
35
+ /* keep the raw text */
36
+ }
37
+ }
38
+ value = value.replace(/^Error:\s*/i, '')
39
+ const lower = value.toLowerCase()
40
+ if (!lower.includes('tower mode')) return null
41
+ const specific =
42
+ lower.includes('is disabled while tower mode is active') ||
43
+ lower.includes('goals cannot be resumed while tower mode is active')
44
+ if (!specific && !lower.includes('/tower off')) return null
45
+ return truncate(value, 400)
46
+ }
47
+ function deniedNames(configured) {
48
+ const source = Array.isArray(configured) && configured.length > 0 ? configured : DEFAULT_DENIED_TOOLS
49
+ const names = []
50
+ for (const entry of source) {
51
+ if (typeof entry !== 'string') continue
52
+ const name = entry.trim()
53
+ if (name === '' || names.includes(name)) continue
54
+ names.push(name)
55
+ }
56
+ // update_goal is denied only for action:"resume", but the guard special
57
+ // case exists regardless of the configured denyTools list.
58
+ if (!names.includes('update_goal')) names.push('update_goal')
59
+ return names
60
+ }
61
+ function DeniedToolRow(props) {
62
+ const t = seatT(props)
63
+ const toolName = props && typeof props.toolName === 'string' ? props.toolName : ''
64
+ const phase = props && typeof props.phase === 'string' ? props.phase : 'start'
65
+ if (phase !== 'result') {
66
+ return h(
67
+ 'div',
68
+ { 'data-tower-toolview': toolName, 'data-tower-denied-view': 'running', style: ST.denyCard },
69
+ h(
70
+ 'div',
71
+ { style: ST.header },
72
+ h('span', { 'aria-hidden': true, style: dotStyle('idle') }),
73
+ h('span', { style: ST.denyLabel }, translate(t, 'deny.label')),
74
+ h('span', { style: ST.dim }, '·'),
75
+ h('span', { style: ST.title }, toolName),
76
+ h('span', { style: ST.dim }, translate(t, 'deny.running')),
77
+ ),
78
+ )
79
+ }
80
+ const denial = denialMessageOf(props ? props.block : undefined)
81
+ if (denial !== null) {
82
+ return h(
83
+ 'div',
84
+ {
85
+ 'data-tower-toolview': toolName,
86
+ 'data-tower-denied-view': 'denied',
87
+ role: 'note',
88
+ style: ST.denyCard,
89
+ title: denial,
90
+ },
91
+ h(
92
+ 'div',
93
+ { style: ST.header },
94
+ h('span', { 'aria-hidden': true, style: dotStyle('warn') }),
95
+ h('span', { style: ST.denyLabel }, translate(t, 'deny.label')),
96
+ h('span', { style: ST.dim }, '·'),
97
+ h('span', { style: ST.title }, toolName),
98
+ ),
99
+ h('div', { style: ST.row }, denial),
100
+ h('div', { style: ST.denyHint }, translate(t, 'deny.hint')),
101
+ )
102
+ }
103
+ // Defensive fallback: the slot is ours only while Tower mode is on, so
104
+ // a non-denial result here has no native view to delegate to.
105
+ const payload = parsePayload(props ? props.block : undefined)
106
+ const error =
107
+ isPlainObject(props) && isPlainObject(props.block) && isPlainObject(props.block.error) ? props.block.error : null
108
+ let text = translate(t, 'deny.generic', { tool: toolName })
109
+ let tone = 'ok'
110
+ if (isPlainObject(payload) && payload.__error === true) {
111
+ tone = 'error'
112
+ const reason =
113
+ error !== null && typeof error.message === 'string'
114
+ ? error.message
115
+ : error !== null && typeof error.code === 'string'
116
+ ? error.code
117
+ : 'error'
118
+ text = translate(t, 'deny.error') + ': ' + firstLine(reason)
119
+ } else if (isPlainObject(payload) && typeof payload.text === 'string' && payload.text.trim() !== '') {
120
+ text = translate(t, 'deny.generic', { tool: toolName }) + translate(t, 'card.sep') + firstLine(payload.text)
121
+ }
122
+ return h(
123
+ 'div',
124
+ { 'data-tower-toolview': toolName, 'data-tower-denied-view': 'generic', style: ST.denyCard },
125
+ h(
126
+ 'div',
127
+ { style: ST.header },
128
+ h('span', { 'aria-hidden': true, style: dotStyle(tone) }),
129
+ h('span', { style: ST.title }, translate(t, 'card.title')),
130
+ h('span', { style: ST.dim }, '·'),
131
+ h('span', { style: ST.title }, toolName),
132
+ h('span', { style: tone === 'error' ? ST.rowError : ST.dim }, truncate(text, 120)),
133
+ ),
134
+ )
135
+ }
136
+
137
+ return { denialMessageOf, normalizeDenialText, deniedNames, DeniedToolRow }
138
+ }
139
+
140
+ return { createDenied }
141
+ },
142
+ })