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,247 @@
1
+ // dsh-tower-ui 0.2.2 — locale dictionaries and the translation helpers.
2
+ //
3
+ // Package-local chunk: client.js loads it once through require.async() before
4
+ // the first slot registration. The Client locale service owns activation and
5
+ // language; each slot registration's `locale` option supplies the bound `t`
6
+ // seat, and `EN` is the fallback for a missing seat (#71).
7
+ window.__ModuleLoader__.load({
8
+ id: 'dsh-tower-ui',
9
+ chunk: 'client.locales.js',
10
+ factory() {
11
+ const TOOL_LABEL_KEYS = {
12
+ tower_status: 'tool.status',
13
+ tower_roster: 'tool.roster',
14
+ tower_spawn: 'tool.spawn',
15
+ tower_mission: 'tool.mission',
16
+ tower_complete: 'tool.complete',
17
+ tower_review: 'tool.review',
18
+ tower_preflight: 'tool.preflight',
19
+ tower_merge: 'tool.merge',
20
+ tower_teardown: 'tool.teardown',
21
+ tower_interrupt: 'tool.interrupt',
22
+ tower_doctor: 'tool.doctor',
23
+ tower_batch: 'tool.batch',
24
+ }
25
+
26
+ const EN = {
27
+ 'card.title': 'Tower',
28
+ 'card.running': 'running…',
29
+ 'card.done': 'done',
30
+ 'card.failed': 'failed',
31
+ 'card.error': 'Error',
32
+ 'card.blockers': 'Blockers',
33
+ 'card.noBlockers': 'no blockers',
34
+ 'card.checks': 'Checks',
35
+ 'card.warnings': 'Warnings',
36
+ 'card.checkOk': '{name} ✓',
37
+ 'card.checkFail': '{name} (exit {code})',
38
+ 'card.checksCount': '{passed}/{total} checks passed',
39
+ 'card.tasks': 'tasks {done}/{total}',
40
+ 'card.openTasks': '{count} open task(s)',
41
+ 'card.noOpenTasks': 'no open tasks',
42
+ 'card.statusHeadline': '{total} missions · {active} active',
43
+ 'card.statusDetail': 'roster {roster} · findings {findings} · rev {revision}',
44
+ 'card.rosterHeadline': '{live} live / {total} agents',
45
+ 'card.staleCount': '{count} stale',
46
+ 'card.otherCount': '{count} inactive',
47
+ 'card.agentLine': '{name} · {role} · {mission} · {activity}',
48
+ 'card.reviewLine': '{status} · merge {merge} · round {round}',
49
+ 'card.mergeReady': 'ready to merge',
50
+ 'card.mergeBlocked': '{count} blocker(s)',
51
+ 'card.merged': 'merged {commit}',
52
+ 'card.alreadyMerged': 'already merged',
53
+ 'card.spawnLine': '{role} {name} → {mission}',
54
+ 'card.worktreeLine': 'worktree {branch}',
55
+ 'card.completeLine': '{mission} completed · {commits} commit(s)',
56
+ 'card.unread': '{count} unread',
57
+ 'card.interruptYes': 'interrupted {name}',
58
+ 'card.interruptNo': '{name} is not running',
59
+ 'card.previousActivity': 'previous activity {activity}',
60
+ 'card.teardownLine': '{removed} removed · {kept} kept',
61
+ 'card.dryRun': 'dry run',
62
+ 'card.doctorLine': 'doctor {status}',
63
+ 'card.doctorChecks': '{pass} pass · {warn} warn · {fail} fail',
64
+ 'card.batchLine': 'batch {status}',
65
+ 'card.batchMembers': '{count} member(s) · batch {status}',
66
+ 'card.batchMissing': '{count} missing check(s)',
67
+ 'card.batchFailed': '{count} failed check(s)',
68
+ 'card.missionLine': '{id} {status}',
69
+ 'card.missionMissing': 'mission',
70
+ 'card.reviewPolicy': 'review {policy}',
71
+ 'card.owner': 'owner {owner}',
72
+ 'card.unknown': 'unknown',
73
+ 'card.summary': '{text}',
74
+ 'card.sep': ' · ',
75
+ 'tool.status': 'Status',
76
+ 'tool.roster': 'Roster',
77
+ 'tool.spawn': 'Spawn',
78
+ 'tool.mission': 'Mission',
79
+ 'tool.complete': 'Complete',
80
+ 'tool.review': 'Review',
81
+ 'tool.preflight': 'Preflight',
82
+ 'tool.merge': 'Merge',
83
+ 'tool.teardown': 'Teardown',
84
+ 'tool.interrupt': 'Interrupt',
85
+ 'tool.doctor': 'Doctor',
86
+ 'tool.batch': 'Batch',
87
+ 'dock.hint.title': '/tower · /tower on [message] · /tower off [message]',
88
+ 'dock.running': '{tool} running…',
89
+ 'dock.statusCounts': '{total} missions · {active} active · {open} open',
90
+ 'dock.rosterCounts': '{live} live / {total} agents',
91
+ 'dock.review': 'review {status} · {merge}',
92
+ 'dock.ready': 'ready to merge',
93
+ 'dock.blocked': '{count} blocker(s)',
94
+ 'dock.merged': 'merged {commit}',
95
+ 'dock.complete': '{mission} completed · {commits} commit(s)',
96
+ 'dock.spawn': '{role} {name} → {mission}',
97
+ 'dock.removed': '{count} worktree(s) removed',
98
+ 'dock.interrupt': '{name} interrupted',
99
+ 'dock.mission': '{id} {status}',
100
+ 'dock.preflight': 'preflight {status}',
101
+ 'dock.batch': 'batch {status}',
102
+ 'dock.doctor': 'doctor {status}',
103
+ 'dock.generic': 'done',
104
+ 'dock.mode.onLabel': 'on',
105
+ 'dock.mode.unknownLabel': 'mode unknown',
106
+ 'dock.mode.unknownTooltip': 'The loaded history has Tower activity but no mode event. Run /tower or tower_mode to confirm the current stance.',
107
+ 'dock.mode.tooltip': 'Tower mode is on for this session. Run /tower off in the composer to leave Tower mode.',
108
+ 'dock.mode.lastKnown': 'last known from this browser session; run /tower or tower_mode to confirm',
109
+ 'dock.mode.repo': 'repo {repo}',
110
+ 'dock.mode.lockHeld': 'workspace lock held',
111
+ 'dock.mode.lockFree': 'workspace lock free',
112
+ 'dock.mode.lastBy': 'last change by {by}',
113
+ 'deny.label': 'Tower mode · disabled',
114
+ 'deny.hint': 'Run /tower off in the composer to use this tool again.',
115
+ 'deny.running': 'Tower mode blocks this tool while it is active',
116
+ 'deny.generic': '{tool} (Tower mode active)',
117
+ 'deny.error': 'Tool error while Tower mode is active',
118
+ 'strip.started': 'Tower · {tool} started',
119
+ 'strip.finished': 'Tower · {tool} finished',
120
+ 'strip.failed': 'Tower · {tool} failed',
121
+ }
122
+ const ZH = {
123
+ 'card.title': 'Tower',
124
+ 'card.running': '运行中…',
125
+ 'card.done': '已完成',
126
+ 'card.failed': '失败',
127
+ 'card.error': '错误',
128
+ 'card.blockers': '阻塞项',
129
+ 'card.noBlockers': '无阻塞',
130
+ 'card.checks': '检查',
131
+ 'card.warnings': '警告',
132
+ 'card.checkOk': '{name} ✓',
133
+ 'card.checkFail': '{name}(退出码 {code})',
134
+ 'card.checksCount': '通过 {passed}/{total} 项检查',
135
+ 'card.tasks': '任务 {done}/{total}',
136
+ 'card.openTasks': '{count} 个未完成任务',
137
+ 'card.noOpenTasks': '无未完成任务',
138
+ 'card.statusHeadline': '{total} 个任务线 · {active} 活跃',
139
+ 'card.statusDetail': '名册 {roster} · 发现 {findings} · 修订 {revision}',
140
+ 'card.rosterHeadline': '{live} 个运行中 / 共 {total} 个',
141
+ 'card.staleCount': '{count} 个疑似停滞',
142
+ 'card.otherCount': '{count} 个非活跃',
143
+ 'card.agentLine': '{name} · {role} · {mission} · {activity}',
144
+ 'card.reviewLine': '{status} · 合并建议 {merge} · 第 {round} 轮',
145
+ 'card.mergeReady': '可以合并',
146
+ 'card.mergeBlocked': '{count} 个阻塞',
147
+ 'card.merged': '已合并 {commit}',
148
+ 'card.alreadyMerged': '已合并(补记录)',
149
+ 'card.spawnLine': '{role} {name} → {mission}',
150
+ 'card.worktreeLine': '工作树 {branch}',
151
+ 'card.completeLine': '{mission} 已完成 · {commits} 个提交',
152
+ 'card.unread': '{count} 条未读',
153
+ 'card.interruptYes': '已中断 {name}',
154
+ 'card.interruptNo': '{name} 未在运行',
155
+ 'card.previousActivity': '先前活动 {activity}',
156
+ 'card.teardownLine': '移除 {removed} · 保留 {kept}',
157
+ 'card.dryRun': '演练',
158
+ 'card.doctorLine': '体检 {status}',
159
+ 'card.doctorChecks': '通过 {pass} · 警告 {warn} · 失败 {fail}',
160
+ 'card.batchLine': '批量 {status}',
161
+ 'card.batchMembers': '{count} 个成员 · 批次 {status}',
162
+ 'card.batchMissing': '{count} 项缺失检查',
163
+ 'card.batchFailed': '{count} 项失败检查',
164
+ 'card.missionLine': '{id} {status}',
165
+ 'card.missionMissing': '任务线',
166
+ 'card.reviewPolicy': '评审 {policy}',
167
+ 'card.owner': '负责人 {owner}',
168
+ 'card.unknown': '未知',
169
+ 'card.summary': '{text}',
170
+ 'card.sep': ' · ',
171
+ 'tool.status': '状态',
172
+ 'tool.roster': '名册',
173
+ 'tool.spawn': '派发',
174
+ 'tool.mission': '任务线',
175
+ 'tool.complete': '完成',
176
+ 'tool.review': '评审',
177
+ 'tool.preflight': '预检',
178
+ 'tool.merge': '合并',
179
+ 'tool.teardown': '清理',
180
+ 'tool.interrupt': '中断',
181
+ 'tool.doctor': '体检',
182
+ 'tool.batch': '批量',
183
+ 'dock.hint.title': '/tower · /tower on [message] · /tower off [message]',
184
+ 'dock.running': '{tool} 运行中…',
185
+ 'dock.statusCounts': '{total} 个任务线 · {active} 活跃 · {open} 未完成',
186
+ 'dock.rosterCounts': '{live} 个运行中 / 共 {total} 个',
187
+ 'dock.review': '评审 {status} · {merge}',
188
+ 'dock.ready': '可以合并',
189
+ 'dock.blocked': '{count} 个阻塞',
190
+ 'dock.merged': '已合并 {commit}',
191
+ 'dock.complete': '{mission} 已完成 · {commits} 个提交',
192
+ 'dock.spawn': '{role} {name} → {mission}',
193
+ 'dock.removed': '移除 {count} 个工作树',
194
+ 'dock.interrupt': '已中断 {name}',
195
+ 'dock.mission': '{id} {status}',
196
+ 'dock.preflight': '预检 {status}',
197
+ 'dock.batch': '批量 {status}',
198
+ 'dock.doctor': '体检 {status}',
199
+ 'dock.generic': '已完成',
200
+ 'dock.mode.onLabel': '已开启',
201
+ 'dock.mode.unknownLabel': '模式未知',
202
+ 'dock.mode.unknownTooltip': '已加载的历史中有 Tower 活动,但没有模式事件。执行 /tower 或 tower_mode 以确认当前状态。',
203
+ 'dock.mode.tooltip': '此会话已开启 Tower 模式。在输入框中执行 /tower off 可退出。',
204
+ 'dock.mode.lastKnown': '来自此浏览器会话的最后已知状态;请执行 /tower 或 tower_mode 确认',
205
+ 'dock.mode.repo': '仓库 {repo}',
206
+ 'dock.mode.lockHeld': '工作区锁已持有',
207
+ 'dock.mode.lockFree': '工作区锁空闲',
208
+ 'dock.mode.lastBy': '最近变更者 {by}',
209
+ 'deny.label': 'Tower 模式 · 已禁用',
210
+ 'deny.hint': '在输入框中执行 /tower off 后即可重新使用该工具。',
211
+ 'deny.running': 'Tower 模式启用期间该工具被禁用',
212
+ 'deny.generic': '{tool}(Tower 模式启用中)',
213
+ 'deny.error': 'Tower 模式启用期间的工具错误',
214
+ 'strip.started': 'Tower · {tool} 开始',
215
+ 'strip.finished': 'Tower · {tool} 完成',
216
+ 'strip.failed': 'Tower · {tool} 失败',
217
+ }
218
+
219
+ function fill(template, params) {
220
+ if (typeof template !== 'string') return String(template)
221
+ if (params === undefined || params === null) return template
222
+ return template.replace(/\{(\w+)\}/g, (match, name) =>
223
+ Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match,
224
+ )
225
+ }
226
+ function translate(t, key, params) {
227
+ if (typeof t === 'function') {
228
+ try {
229
+ const value = t(key, params)
230
+ if (typeof value === 'string') return value
231
+ } catch (error) {
232
+ /* fall through to the bundled English dictionary */
233
+ }
234
+ }
235
+ return fill(EN[key] !== undefined ? EN[key] : key, params)
236
+ }
237
+ function seatT(props) {
238
+ return props && typeof props.t === 'function' ? props.t : null
239
+ }
240
+ function toolLabel(t, name) {
241
+ if (typeof name !== 'string' || name === '') return translate(t, 'card.title')
242
+ return translate(t, TOOL_LABEL_KEYS[name] || 'card.title')
243
+ }
244
+
245
+ return { EN, ZH, fill, translate, seatT, toolLabel }
246
+ },
247
+ })
@@ -0,0 +1,206 @@
1
+ // dsh-tower-ui 0.2.2 — the history-derived Tower-mode model and observable.
2
+ //
3
+ // The dock folds the durable history into one stance model; the store publishes
4
+ // it so the denied-tool toolviews can be added/disposed. No Host state is read
5
+ // or fetched. Package-local chunk (#71).
6
+ window.__ModuleLoader__.load({
7
+ id: 'dsh-tower-ui',
8
+ chunk: 'client.mode.js',
9
+ factory() {
10
+ function towerCommandAction(args) {
11
+ const raw = String(args === undefined || args === null ? '' : args).trim()
12
+ if (/^reload$/i.test(raw)) return null
13
+ if (/^base\s+\S+\s*$/i.test(raw)) return null
14
+ const match = /^(on|off)(?:\s+[\s\S]*)?$/i.exec(raw)
15
+ if (match) return match[1].toLowerCase()
16
+ // Bare `/tower` and `/tower <message>` both mean "ensure on".
17
+ return 'on'
18
+ }
19
+ function repoNameOf(value) {
20
+ if (typeof value !== 'string' || value === '') return null
21
+ let cleaned = value.replace(/[\\/]+$/, '')
22
+ if (/[\\/]\.git$/i.test(cleaned)) cleaned = cleaned.replace(/[\\/]\.git$/i, '')
23
+ const index = Math.max(cleaned.lastIndexOf('/'), cleaned.lastIndexOf('\\'))
24
+ const name = index >= 0 ? cleaned.slice(index + 1) : cleaned
25
+ return name === '' ? null : name
26
+ }
27
+ // Fold the visible history into one model: the last recognized event wins,
28
+ // and `/tower reload|base` or a failed command leaves the stance unchanged.
29
+ function modeModelFromLegacy(legacy, helpers) {
30
+ const { isPlainObject, parsePayload } = helpers
31
+ const model = {
32
+ known: false,
33
+ on: null,
34
+ denied: null,
35
+ repo: null,
36
+ lock: null,
37
+ lastBy: null,
38
+ lastReason: null,
39
+ source: null,
40
+ }
41
+ const nodes = isPlainObject(legacy) && Array.isArray(legacy.nodes) ? legacy.nodes : []
42
+ for (const node of nodes) {
43
+ if (!isPlainObject(node)) continue
44
+ if (node.kind === 'command') {
45
+ if (node.name !== 'tower') continue
46
+ const outcome = isPlainObject(node.outcome) ? node.outcome : null
47
+ if (outcome !== null && outcome.kind === 'error') continue
48
+ const action = towerCommandAction(node.args)
49
+ if (action === null) continue
50
+ model.known = true
51
+ model.on = action === 'on'
52
+ model.source = 'command'
53
+ model.lastBy = 'user'
54
+ model.lastReason = null
55
+ continue
56
+ }
57
+ const toolName = isPlainObject(node.call) ? node.call.name : null
58
+ if (toolName === 'tower_mode') {
59
+ const payload = parsePayload(node)
60
+ if (!isPlainObject(payload) || payload.__error === true) continue
61
+ if (payload.mode === 'on' || payload.mode === 'off') {
62
+ model.known = true
63
+ model.on = payload.mode === 'on'
64
+ model.source = 'tower_mode'
65
+ }
66
+ if (Array.isArray(payload.deniedWhileActive)) {
67
+ const names = []
68
+ for (const entry of payload.deniedWhileActive) {
69
+ if (typeof entry !== 'string') continue
70
+ const name = entry.trim()
71
+ if (name !== '' && !names.includes(name)) names.push(name)
72
+ }
73
+ if (names.length > 0) model.denied = names
74
+ }
75
+ const workspace = isPlainObject(payload.workspaceLock) ? payload.workspaceLock : null
76
+ if (workspace !== null) {
77
+ model.lock = { enabled: workspace.enabled === true, held: workspace.held === true }
78
+ const name = repoNameOf(workspace.commonDir)
79
+ if (name !== null) model.repo = name
80
+ }
81
+ if (isPlainObject(payload.lastChange)) {
82
+ if (typeof payload.lastChange.by === 'string') model.lastBy = payload.lastChange.by
83
+ if (typeof payload.lastChange.reason === 'string' && payload.lastChange.reason.trim() !== '') {
84
+ model.lastReason = payload.lastChange.reason.trim()
85
+ }
86
+ }
87
+ continue
88
+ }
89
+ if (toolName === 'tower_control') {
90
+ const payload = parsePayload(node)
91
+ if (!isPlainObject(payload) || payload.__error === true || payload.ok === false) continue
92
+ const mode =
93
+ payload.mode === 'on' || payload.mode === 'off'
94
+ ? payload.mode
95
+ : payload.action === 'enter'
96
+ ? 'on'
97
+ : payload.action === 'exit'
98
+ ? 'off'
99
+ : null
100
+ if (mode === null) continue
101
+ model.known = true
102
+ model.on = mode === 'on'
103
+ model.source = 'tower_control'
104
+ if (typeof payload.changedBy === 'string') model.lastBy = payload.changedBy
105
+ if (typeof payload.reason === 'string' && payload.reason.trim() !== '') model.lastReason = payload.reason.trim()
106
+ continue
107
+ }
108
+ if (toolName === 'tower_status') {
109
+ const payload = parsePayload(node)
110
+ if (isPlainObject(payload)) {
111
+ const name = repoNameOf(payload.repoRoot)
112
+ if (name !== null) model.repo = name
113
+ }
114
+ }
115
+ }
116
+ return model
117
+ }
118
+ const EMPTY_MODE = {
119
+ writer: null,
120
+ known: false,
121
+ on: null,
122
+ denied: null,
123
+ repo: null,
124
+ lock: null,
125
+ lastBy: null,
126
+ lastReason: null,
127
+ source: null,
128
+ }
129
+ function createModeStore(diagnostics, helpers) {
130
+ const { isPlainObject } = helpers
131
+ // All mutable client state lives in this closure, never at module scope
132
+ // (client-ambient).
133
+ const state = { value: Object.assign({}, EMPTY_MODE), seq: 0 }
134
+ const listeners = new Set()
135
+
136
+ function nextDockWriter() {
137
+ state.seq += 1
138
+ return 'dock-' + state.seq
139
+ }
140
+ function modeSnapshot() {
141
+ return state.value
142
+ }
143
+ function subscribeMode(listener) {
144
+ listeners.add(listener)
145
+ return function () {
146
+ listeners.delete(listener)
147
+ }
148
+ }
149
+ function sameMode(left, right) {
150
+ return (
151
+ left.known === right.known &&
152
+ left.on === right.on &&
153
+ JSON.stringify(left.denied) === JSON.stringify(right.denied) &&
154
+ left.repo === right.repo &&
155
+ JSON.stringify(left.lock) === JSON.stringify(right.lock) &&
156
+ left.lastBy === right.lastBy &&
157
+ left.source === right.source
158
+ )
159
+ }
160
+ function notifyMode() {
161
+ for (const listener of Array.from(listeners)) {
162
+ try {
163
+ listener()
164
+ } catch (error) {
165
+ /* a broken registration reconciler must not break the dock */
166
+ }
167
+ }
168
+ }
169
+ function publishMode(writer, next) {
170
+ const value = {
171
+ writer,
172
+ known: next.known === true,
173
+ on: next.known === true && next.on === true,
174
+ denied: Array.isArray(next.denied) ? next.denied.slice() : null,
175
+ repo: typeof next.repo === 'string' && next.repo !== '' ? next.repo : null,
176
+ lock: isPlainObject(next.lock) ? { enabled: next.lock.enabled === true, held: next.lock.held === true } : null,
177
+ lastBy: typeof next.lastBy === 'string' && next.lastBy !== '' ? next.lastBy : null,
178
+ lastReason: typeof next.lastReason === 'string' && next.lastReason !== '' ? next.lastReason : null,
179
+ source: typeof next.source === 'string' ? next.source : null,
180
+ }
181
+ diagnostics.mode = { known: value.known, on: value.on, denied: value.denied }
182
+ if (sameMode(value, state.value)) {
183
+ state.value = value
184
+ return
185
+ }
186
+ state.value = value
187
+ notifyMode()
188
+ }
189
+ function clearMode(writer) {
190
+ if (state.value.writer !== writer) return
191
+ const empty = Object.assign({}, EMPTY_MODE, { writer })
192
+ if (sameMode(empty, state.value)) {
193
+ state.value = empty
194
+ return
195
+ }
196
+ state.value = empty
197
+ diagnostics.mode = { known: false, on: null, denied: null }
198
+ notifyMode()
199
+ }
200
+
201
+ return { nextDockWriter, modeSnapshot, subscribeMode, publishMode, clearMode, sameMode }
202
+ }
203
+
204
+ return { EMPTY_MODE, towerCommandAction, repoNameOf, modeModelFromLegacy, createModeStore }
205
+ },
206
+ })
@@ -0,0 +1,64 @@
1
+ // dsh-tower-ui 0.2.2 — the transient shell.overlay status strip.
2
+ //
3
+ // Root-scoped (no chat hooks), so the toolview publishes a notice into this
4
+ // observable and the strip injects it; the timer is owned by apply's ctx.effect.
5
+ // Package-local chunk (#71).
6
+ window.__ModuleLoader__.load({
7
+ id: 'dsh-tower-ui',
8
+ chunk: 'client.overlay.js',
9
+ factory() {
10
+ function createOverlay(env) {
11
+ const { h, translate, toolLabel, seatT, ST, dotStyle, helpers } = env
12
+ const { isPlainObject } = helpers
13
+ // Mutable state lives in this closure; the module has no top-level state.
14
+ const state = { value: null }
15
+ const listeners = new Set()
16
+
17
+ function snapshot() {
18
+ return state.value
19
+ }
20
+ function subscribe(listener) {
21
+ listeners.add(listener)
22
+ return function () {
23
+ listeners.delete(listener)
24
+ }
25
+ }
26
+ function setNotice(next) {
27
+ if (Object.is(next, state.value)) return
28
+ state.value = next
29
+ for (const listener of Array.from(listeners)) {
30
+ try {
31
+ listener()
32
+ } catch (error) {
33
+ /* a broken subscriber must not break the strip */
34
+ }
35
+ }
36
+ }
37
+ const source = { getSnapshot: snapshot, subscribe: subscribe }
38
+
39
+ function TowerStatusStrip(props) {
40
+ const t = seatT(props)
41
+ if (!props || typeof props.useTowerNotice !== 'function') return null
42
+ const notice = props.useTowerNotice(function (value) {
43
+ return value
44
+ })
45
+ if (!isPlainObject(notice)) return null
46
+ const running = notice.running === true
47
+ const failed = notice.ok === false
48
+ const tone = running ? 'idle' : failed ? 'error' : 'ok'
49
+ const text = translate(t, running ? 'strip.started' : failed ? 'strip.failed' : 'strip.finished', {
50
+ tool: toolLabel(t, notice.tool),
51
+ })
52
+ return h(
53
+ 'div',
54
+ { 'data-tower-ui': 'status-strip', role: 'status', 'aria-live': 'polite', style: ST.stripWrap },
55
+ h('div', { style: ST.stripCard }, h('span', { 'aria-hidden': true, style: dotStyle(tone) }, null), h('span', null, text)),
56
+ )
57
+ }
58
+
59
+ return { source, setNotice, TowerStatusStrip }
60
+ }
61
+
62
+ return { createOverlay }
63
+ },
64
+ })
@@ -0,0 +1,151 @@
1
+ // dsh-tower-ui 0.2.2 — the inline style table.
2
+ //
3
+ // Theme references are restricted to `--dsw-alias-*` tokens. Chunk (#71).
4
+ window.__ModuleLoader__.load({
5
+ id: 'dsh-tower-ui',
6
+ chunk: 'client.styles.js',
7
+ factory() {
8
+ const TONE = {
9
+ ok: 'var(--dsw-alias-state-success-primary)',
10
+ warn: 'var(--dsw-alias-state-warn-primary)',
11
+ error: 'var(--dsw-alias-state-error-primary)',
12
+ idle: 'var(--dsw-alias-state-idle-primary)',
13
+ none: 'var(--dsw-alias-label-tertiary)',
14
+ }
15
+ const ST = {
16
+ card: {
17
+ display: 'flex',
18
+ flexDirection: 'column',
19
+ gap: '4px',
20
+ minWidth: 0,
21
+ maxWidth: '100%',
22
+ boxSizing: 'border-box',
23
+ margin: '2px 0 2px 4px',
24
+ padding: '8px 10px',
25
+ border: '0.5px solid var(--dsw-alias-border-l1)',
26
+ borderRadius: '8px',
27
+ background: 'var(--dsw-alias-bg-layer-1)',
28
+ color: 'var(--dsw-alias-label-secondary)',
29
+ fontSize: '12px',
30
+ lineHeight: '18px',
31
+ },
32
+ header: { display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0 },
33
+ title: { color: 'var(--dsw-alias-label-primary)', flex: 'none' },
34
+ dim: {
35
+ color: 'var(--dsw-alias-label-tertiary)',
36
+ minWidth: 0,
37
+ overflow: 'hidden',
38
+ textOverflow: 'ellipsis',
39
+ whiteSpace: 'nowrap',
40
+ },
41
+ row: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
42
+ rowError: { color: 'var(--dsw-alias-state-error-primary)' },
43
+ rowWarn: { color: 'var(--dsw-alias-state-warn-primary)' },
44
+ rowOk: { color: 'var(--dsw-alias-state-success-primary)' },
45
+ hint: {
46
+ display: 'inline-flex',
47
+ alignItems: 'center',
48
+ gap: '4px',
49
+ padding: '0 6px',
50
+ fontSize: '11px',
51
+ lineHeight: '18px',
52
+ color: 'inherit',
53
+ opacity: 0.55,
54
+ userSelect: 'none',
55
+ whiteSpace: 'nowrap',
56
+ },
57
+ dock: {
58
+ display: 'inline-flex',
59
+ alignItems: 'center',
60
+ gap: '6px',
61
+ minWidth: 0,
62
+ maxWidth: '100%',
63
+ padding: '1px 8px',
64
+ fontSize: '11px',
65
+ lineHeight: '18px',
66
+ color: 'var(--dsw-alias-label-tertiary)',
67
+ whiteSpace: 'nowrap',
68
+ },
69
+ dockTool: { color: 'var(--dsw-alias-label-secondary)', flex: 'none' },
70
+ dockText: {
71
+ minWidth: 0,
72
+ overflow: 'hidden',
73
+ textOverflow: 'ellipsis',
74
+ color: 'var(--dsw-alias-label-tertiary)',
75
+ },
76
+ stripWrap: {
77
+ position: 'fixed',
78
+ left: '50%',
79
+ bottom: '16px',
80
+ transform: 'translateX(-50%)',
81
+ display: 'flex',
82
+ justifyContent: 'center',
83
+ pointerEvents: 'none',
84
+ zIndex: 9100,
85
+ maxWidth: 'calc(100vw - 32px)',
86
+ },
87
+ stripCard: {
88
+ pointerEvents: 'none',
89
+ display: 'inline-flex',
90
+ alignItems: 'center',
91
+ gap: '6px',
92
+ maxWidth: '100%',
93
+ padding: '4px 10px',
94
+ border: '0.5px solid var(--dsw-alias-border-l1)',
95
+ borderRadius: '999px',
96
+ background: 'var(--dsw-alias-bg-overlay)',
97
+ color: 'var(--dsw-alias-label-secondary)',
98
+ fontSize: '12px',
99
+ lineHeight: '18px',
100
+ whiteSpace: 'nowrap',
101
+ overflow: 'hidden',
102
+ textOverflow: 'ellipsis',
103
+ },
104
+ modeChip: {
105
+ display: 'inline-flex',
106
+ alignItems: 'center',
107
+ gap: '6px',
108
+ padding: '0 8px',
109
+ border: '0.5px solid var(--dsw-alias-border-l2)',
110
+ borderRadius: '999px',
111
+ background: 'var(--dsw-alias-bg-layer-1)',
112
+ color: 'var(--dsw-alias-label-secondary)',
113
+ fontSize: '11px',
114
+ lineHeight: '18px',
115
+ whiteSpace: 'nowrap',
116
+ userSelect: 'none',
117
+ },
118
+ modeChipLabel: { color: 'var(--dsw-alias-label-primary)' },
119
+ modeChipOn: { color: 'var(--dsw-alias-state-success-primary)' },
120
+ denyCard: {
121
+ display: 'flex',
122
+ flexDirection: 'column',
123
+ gap: '4px',
124
+ minWidth: 0,
125
+ maxWidth: '100%',
126
+ boxSizing: 'border-box',
127
+ margin: '2px 0 2px 4px',
128
+ padding: '8px 10px',
129
+ border: '0.5px solid var(--dsw-alias-border-l2)',
130
+ borderRadius: '8px',
131
+ background: 'var(--dsw-alias-bg-layer-1)',
132
+ color: 'var(--dsw-alias-label-secondary)',
133
+ fontSize: '12px',
134
+ lineHeight: '18px',
135
+ },
136
+ denyLabel: { color: 'var(--dsw-alias-state-warn-primary)', flex: 'none' },
137
+ denyHint: { color: 'var(--dsw-alias-label-tertiary)' },
138
+ }
139
+ function dotStyle(tone) {
140
+ return {
141
+ width: '6px',
142
+ height: '6px',
143
+ flex: 'none',
144
+ borderRadius: '999px',
145
+ background: TONE[tone] || TONE.none,
146
+ }
147
+ }
148
+
149
+ return { TONE, ST, dotStyle }
150
+ },
151
+ })