martty 0.2.11

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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +118 -0
  3. package/bin/dsh-tui.js +68 -0
  4. package/cordis.patch.yml +30 -0
  5. package/creator/cordis.patch.yml +6 -0
  6. package/creator/package.json +10 -0
  7. package/lib/acp-client-events.js +65 -0
  8. package/lib/acp-client.js +114 -0
  9. package/lib/acp-host.js +24 -0
  10. package/lib/acp-session-config.js +376 -0
  11. package/lib/acp-session-plan.js +196 -0
  12. package/lib/acp-session-stats.js +239 -0
  13. package/lib/agent.js +64 -0
  14. package/lib/boot.js +119 -0
  15. package/lib/client-process.js +11 -0
  16. package/lib/client-run.js +379 -0
  17. package/lib/cordis-protocol.js +51 -0
  18. package/lib/creator-overlay.js +77 -0
  19. package/lib/demo-skin.js +79 -0
  20. package/lib/ember.js +20 -0
  21. package/lib/index.js +226 -0
  22. package/lib/inspect.js +971 -0
  23. package/lib/jsonrpc-line-transport.js +155 -0
  24. package/lib/mux.js +281 -0
  25. package/lib/palettes/default.json +44 -0
  26. package/lib/palettes/ember.json +44 -0
  27. package/lib/plan-view.js +92 -0
  28. package/lib/profile-acp-client.js +11 -0
  29. package/lib/right-demo.js +55 -0
  30. package/lib/runner.js +94 -0
  31. package/lib/spawn-tui.js +179 -0
  32. package/lib/stats-view.js +90 -0
  33. package/lib/tui-commands.js +144 -0
  34. package/lib/tui-overlay.js +252 -0
  35. package/lib/tui-slots.js +351 -0
  36. package/lib/tui-theme.js +463 -0
  37. package/package.json +83 -0
  38. package/skills/tui-plugin-development/SKILL.md +172 -0
  39. package/vendor/darwin-arm64/dsh-tui +0 -0
  40. package/vendor/darwin-x64/dsh-tui +0 -0
  41. package/vendor/linux-arm64/dsh-tui +0 -0
  42. package/vendor/linux-x64/dsh-tui +0 -0
  43. package/vendor/win32-x64/dsh-tui.exe +0 -0
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Newline-delimited JSON-RPC 2.0 over caller-owned streams.
3
+ *
4
+ * Local copy of the wire the native TUI speaks. Published `dsh` does not put
5
+ * `@deepseek-ai/dsh-sdk-protocol` on the profile module fallback, and listing
6
+ * that package as an npm dependency makes `dsh plugin add` warn about its
7
+ * unmet peers (profile `autoInstallPeers: false`).
8
+ */
9
+
10
+ import { StringDecoder } from 'node:string_decoder'
11
+
12
+ export class JsonRpcLineTransport {
13
+ constructor(input, output) {
14
+ this.input = input
15
+ this.output = output
16
+ this.buffer = ''
17
+ this.decoder = new StringDecoder('utf8')
18
+ this.started = false
19
+ this.requestHandler = undefined
20
+ this.notificationHandler = undefined
21
+ this.pending = new Map()
22
+ }
23
+
24
+ start() {
25
+ if (this.started) return
26
+ this.started = true
27
+ this.input.on('data', this.onData)
28
+ this.input.on('error', this.onInputError)
29
+ this.input.on('end', this.onInputEnd)
30
+ }
31
+
32
+ close() {
33
+ this.input.off('data', this.onData)
34
+ this.input.off('error', this.onInputError)
35
+ this.input.off('end', this.onInputEnd)
36
+ this.failPending(new Error('JSON-RPC transport closed'))
37
+ }
38
+
39
+ onRequest(handler) {
40
+ this.requestHandler = handler
41
+ }
42
+
43
+ onNotification(handler) {
44
+ this.notificationHandler = handler
45
+ }
46
+
47
+ notify(method, params) {
48
+ this.write(
49
+ params === undefined
50
+ ? { jsonrpc: '2.0', method }
51
+ : { jsonrpc: '2.0', method, params },
52
+ )
53
+ }
54
+
55
+ flush() {
56
+ return new Promise((resolve, reject) => {
57
+ this.output.write('', (error) => {
58
+ if (error) reject(error)
59
+ else resolve()
60
+ })
61
+ })
62
+ }
63
+
64
+ onData = (chunk) => {
65
+ this.buffer += typeof chunk === 'string' ? chunk : this.decoder.write(chunk)
66
+ this.drainLines()
67
+ }
68
+
69
+ drainLines() {
70
+ for (;;) {
71
+ const newline = this.buffer.indexOf('\n')
72
+ if (newline < 0) break
73
+ const line = this.buffer.slice(0, newline).trim()
74
+ this.buffer = this.buffer.slice(newline + 1)
75
+ if (!line) continue
76
+ void this.handleLine(line)
77
+ }
78
+ }
79
+
80
+ onInputError = (error) => {
81
+ this.failPending(error)
82
+ }
83
+
84
+ onInputEnd = () => {
85
+ this.buffer += this.decoder.end()
86
+ this.drainLines()
87
+ this.failPending(new Error('JSON-RPC input closed'))
88
+ }
89
+
90
+ async handleLine(line) {
91
+ let message
92
+ try {
93
+ message = JSON.parse(line)
94
+ } catch {
95
+ return
96
+ }
97
+ if (!message || typeof message !== 'object') return
98
+ const id = message.id
99
+ const method = message.method
100
+ if ((typeof id === 'string' || typeof id === 'number') && typeof method === 'string') {
101
+ await this.handleIncomingRequest(id, method, objectParams(message.params))
102
+ return
103
+ }
104
+ if (typeof id === 'string' || typeof id === 'number') {
105
+ this.handleIncomingResponse(id, message)
106
+ return
107
+ }
108
+ if (typeof method === 'string') {
109
+ this.notificationHandler?.(method, objectParams(message.params))
110
+ }
111
+ }
112
+
113
+ async handleIncomingRequest(id, method, params) {
114
+ const handler = this.requestHandler
115
+ if (!handler) {
116
+ this.write({ jsonrpc: '2.0', id, error: { code: -32601, message: `method not found: ${method}` } })
117
+ return
118
+ }
119
+ try {
120
+ const result = await handler(method, params)
121
+ this.write({ jsonrpc: '2.0', id, result })
122
+ } catch (error) {
123
+ this.write({
124
+ jsonrpc: '2.0',
125
+ id,
126
+ error: { code: -32603, message: error instanceof Error ? error.message : String(error) },
127
+ })
128
+ }
129
+ }
130
+
131
+ handleIncomingResponse(id, frame) {
132
+ const pending = this.pending.get(id)
133
+ if (!pending) return
134
+ this.pending.delete(id)
135
+ if (frame.error && typeof frame.error === 'object') {
136
+ pending.reject(new Error(typeof frame.error.message === 'string' ? frame.error.message : 'JSON-RPC error'))
137
+ return
138
+ }
139
+ pending.resolve(frame.result)
140
+ }
141
+
142
+ write(message) {
143
+ this.output.write(`${JSON.stringify(message)}\n`)
144
+ }
145
+
146
+ failPending(error) {
147
+ const pending = [...this.pending.values()]
148
+ this.pending.clear()
149
+ for (const waiter of pending) waiter.reject(error)
150
+ }
151
+ }
152
+
153
+ function objectParams(params) {
154
+ return params && typeof params === 'object' && !Array.isArray(params) ? params : {}
155
+ }
package/lib/mux.js ADDED
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Byte mux between an ACP agent and the Rust ACP client.
3
+ *
4
+ * spawn-tui connection: `output` is Node→TUI (Rust reads fd 3), `input` is
5
+ * TUI→Node (Rust writes fd 4). Agent stdout feeds `output` except Client-plane
6
+ * Cordis Client-plane extensions; Rust outgoing on `input` goes
7
+ * to agent stdin except compositor paint methods. Palette notifications are
8
+ * injected toward Rust only. Node-originated JSON-RPC ids stay
9
+ * off the painter.
10
+ */
11
+
12
+ import { CORDIS_CAPABILITY, CORDIS_METHODS, readCordisCapability } from './cordis-protocol.js'
13
+
14
+ const COMPOSITOR_METHODS = Object.freeze(new Set([
15
+ CORDIS_METHODS.themeUpdate,
16
+ CORDIS_METHODS.themeRemove,
17
+ CORDIS_METHODS.themeSelected,
18
+ CORDIS_METHODS.slotsUpdate,
19
+ CORDIS_METHODS.commandsUpdate,
20
+ CORDIS_METHODS.overlayUpdate,
21
+ CORDIS_METHODS.commandInvoke,
22
+ CORDIS_METHODS.overlayEvent,
23
+ CORDIS_METHODS.sessionConfigSet,
24
+ ]))
25
+
26
+ /** Agent → TUI Node extras. Not compositor paint; Rust never sees these. */
27
+ export const HOST_METHODS = Object.freeze(new Set([
28
+ CORDIS_METHODS.inspectQuery,
29
+ CORDIS_METHODS.inspectQueryResolved,
30
+ CORDIS_METHODS.requestRun,
31
+ CORDIS_METHODS.requestRunResolved,
32
+ CORDIS_METHODS.userRun,
33
+ CORDIS_METHODS.pluginRetract,
34
+ ]))
35
+
36
+ /**
37
+ * @param {{ jsonrpc?: unknown, method?: unknown, id?: unknown }} message
38
+ * @returns {boolean}
39
+ */
40
+ export function isCompositorMessage(message) {
41
+ return typeof message.method === 'string' && COMPOSITOR_METHODS.has(message.method)
42
+ }
43
+
44
+ /**
45
+ * @param {{ jsonrpc?: unknown, method?: unknown }} message
46
+ * @returns {boolean}
47
+ */
48
+ export function isHostMessage(message) {
49
+ return typeof message.method === 'string' && HOST_METHODS.has(message.method)
50
+ }
51
+
52
+ /**
53
+ * Stamp `clientCapabilities._meta.dsh.cordis` so dsh-acp forwards Client jobs.
54
+ * Zed never sets this; unknown agents ignore the extra field.
55
+ * @param {object} message
56
+ * @returns {object}
57
+ */
58
+ export function advertiseCordis(message) {
59
+ if (message.method !== 'initialize') return message
60
+ const params = message.params !== null && typeof message.params === 'object' && !Array.isArray(message.params)
61
+ ? { ...message.params }
62
+ : {}
63
+ const caps = params.clientCapabilities !== null && typeof params.clientCapabilities === 'object'
64
+ && !Array.isArray(params.clientCapabilities)
65
+ ? { ...params.clientCapabilities }
66
+ : {}
67
+ const meta = caps._meta !== null && typeof caps._meta === 'object' && !Array.isArray(caps._meta)
68
+ ? { ...caps._meta }
69
+ : {}
70
+ const dsh = meta.dsh !== null && typeof meta.dsh === 'object' && !Array.isArray(meta.dsh)
71
+ ? { ...meta.dsh }
72
+ : {}
73
+ dsh.cordis = { ...CORDIS_CAPABILITY }
74
+ meta.dsh = dsh
75
+ delete meta.tuiInspect
76
+ caps._meta = meta
77
+ params.clientCapabilities = caps
78
+ return { ...message, params }
79
+ }
80
+
81
+ /**
82
+ * @param {import('node:stream').Writable} dest
83
+ * @param {unknown} value
84
+ */
85
+ export function writeJsonLine(dest, value) {
86
+ dest.write(`${JSON.stringify(value)}\n`)
87
+ }
88
+
89
+ /**
90
+ * Pipe NDJSON, invoking `onLine` for each complete line (trimmed, non-empty).
91
+ * @param {import('node:stream').Readable} source
92
+ * @param {(line: string) => void} onLine
93
+ */
94
+ export function onJsonLines(source, onLine) {
95
+ let buffer = ''
96
+ source.on('data', (chunk) => {
97
+ buffer += chunk.toString('utf8')
98
+ for (;;) {
99
+ const newline = buffer.indexOf('\n')
100
+ if (newline < 0) break
101
+ const line = buffer.slice(0, newline).trim()
102
+ buffer = buffer.slice(newline + 1)
103
+ if (line.length === 0) continue
104
+ onLine(line)
105
+ }
106
+ })
107
+ }
108
+
109
+ /**
110
+ * @param {{
111
+ * agent: { stdin: import('node:stream').Writable, stdout: import('node:stream').Readable },
112
+ * tui: { input: import('node:stream').Writable, output: import('node:stream').Readable },
113
+ * onCompositor?: (message: object) => unknown | Promise<unknown>,
114
+ * onHost?: (message: object) => void,
115
+ * onCordisReady?: (capability: { protocol: number }) => void,
116
+ * onAcp?: (direction: 'client' | 'agent', message: object) => void,
117
+ * }} opts
118
+ * @returns {{
119
+ * notifyTui: (method: string, params?: object) => void,
120
+ * requestAgent: (method: string, params?: object) => Promise<unknown>,
121
+ * requestTui: (method: string, params?: object) => Promise<unknown>,
122
+ * }}
123
+ */
124
+ export function muxAcpAndCompositor(opts) {
125
+ const { agent, tui, onCompositor, onHost, onCordisReady, onAcp } = opts
126
+ for (const stream of [agent.stdin, agent.stdout, tui.input, tui.output]) {
127
+ stream.on?.('error', () => {})
128
+ }
129
+
130
+ /** @type {Map<string, { resolve: (value: unknown) => void, reject: (error: Error) => void }>} */
131
+ const pendingAgent = new Map()
132
+ const pendingTui = new Map()
133
+ let nextHostId = 0
134
+ let nextTuiId = 0
135
+ /** @type {unknown} */
136
+ let initializeId
137
+ /** @type {{ protocol: number } | null} */
138
+ let agentCordis = null
139
+
140
+ onJsonLines(agent.stdout, (line) => {
141
+ let message
142
+ try {
143
+ message = JSON.parse(line)
144
+ } catch {
145
+ tui.output.write(`${line}\n`)
146
+ return
147
+ }
148
+ if (message && typeof message === 'object' && pendingAgent.has(message.id)) {
149
+ const waiter = pendingAgent.get(message.id)
150
+ pendingAgent.delete(message.id)
151
+ if (message.error !== undefined) {
152
+ const detail = message.error && typeof message.error === 'object' ? message.error.message : undefined
153
+ waiter.reject(new Error(typeof detail === 'string' ? detail : 'ACP extension request failed'))
154
+ } else {
155
+ waiter.resolve(message.result)
156
+ }
157
+ return
158
+ }
159
+ if (isHostMessage(message)) {
160
+ if (agentCordis !== null) onHost?.(message)
161
+ return
162
+ }
163
+ onAcp?.('agent', message)
164
+ tui.output.write(`${line}\n`)
165
+ if (initializeId !== undefined && message && typeof message === 'object' && message.id === initializeId) {
166
+ initializeId = undefined
167
+ agentCordis = readCordisCapability(message.result)
168
+ if (agentCordis !== null) onCordisReady?.(agentCordis)
169
+ }
170
+ })
171
+
172
+ onJsonLines(tui.input, (line) => {
173
+ let message
174
+ try {
175
+ message = JSON.parse(line)
176
+ } catch {
177
+ agent.stdin.write(`${line}\n`)
178
+ return
179
+ }
180
+ if (message && typeof message === 'object' && pendingTui.has(message.id)) {
181
+ const waiter = pendingTui.get(message.id)
182
+ pendingTui.delete(message.id)
183
+ if (message.error !== undefined) {
184
+ const detail = message.error && typeof message.error === 'object' ? message.error.message : undefined
185
+ waiter.reject(new Error(typeof detail === 'string' ? detail : 'TUI compositor request failed'))
186
+ } else {
187
+ waiter.resolve(message.result)
188
+ }
189
+ return
190
+ }
191
+ if (isCompositorMessage(message)) {
192
+ const hasId = message.id !== undefined
193
+ if (onCompositor === undefined) {
194
+ if (hasId) {
195
+ writeJsonLine(tui.output, {
196
+ jsonrpc: '2.0',
197
+ id: message.id,
198
+ error: { code: -32601, message: 'Method not found' },
199
+ })
200
+ }
201
+ return
202
+ }
203
+ Promise.resolve()
204
+ .then(() => onCompositor(message))
205
+ .then((result) => {
206
+ if (hasId) writeJsonLine(tui.output, { jsonrpc: '2.0', id: message.id, result: result ?? null })
207
+ })
208
+ .catch((error) => {
209
+ if (!hasId) return
210
+ writeJsonLine(tui.output, {
211
+ jsonrpc: '2.0',
212
+ id: message.id,
213
+ error: {
214
+ code: -32000,
215
+ message: error instanceof Error ? error.message : String(error),
216
+ },
217
+ })
218
+ })
219
+ return
220
+ }
221
+ if (
222
+ typeof message.method === 'string'
223
+ && message.method.startsWith('_dsh/cordis/')
224
+ && agentCordis === null
225
+ ) {
226
+ if (message.id !== undefined) {
227
+ writeJsonLine(tui.output, {
228
+ jsonrpc: '2.0',
229
+ id: message.id,
230
+ error: { code: -32601, message: 'agent has not advertised _dsh/cordis' },
231
+ })
232
+ }
233
+ return
234
+ }
235
+ const outgoing = advertiseCordis(message)
236
+ if (outgoing.method === 'initialize' && outgoing.id !== undefined) {
237
+ initializeId = outgoing.id
238
+ }
239
+ onAcp?.('client', outgoing)
240
+ writeJsonLine(agent.stdin, outgoing)
241
+ })
242
+
243
+ return {
244
+ notifyTui(method, params) {
245
+ writeJsonLine(
246
+ tui.output,
247
+ params === undefined ? { jsonrpc: '2.0', method } : { jsonrpc: '2.0', method, params },
248
+ )
249
+ },
250
+ requestAgent(method, params) {
251
+ if (method.startsWith('_dsh/cordis/') && agentCordis === null) {
252
+ return Promise.reject(new Error('agent has not advertised _dsh/cordis'))
253
+ }
254
+ const id = `tui-host-${++nextHostId}`
255
+ return new Promise((resolve, reject) => {
256
+ pendingAgent.set(id, { resolve, reject })
257
+ writeJsonLine(
258
+ agent.stdin,
259
+ params === undefined
260
+ ? { jsonrpc: '2.0', id, method }
261
+ : { jsonrpc: '2.0', id, method, params },
262
+ )
263
+ })
264
+ },
265
+ requestTui(method, params) {
266
+ if (typeof method !== 'string' || !method.startsWith('_dsh/cordis/tui/')) {
267
+ return Promise.reject(new Error('requestTui only accepts _dsh/cordis/tui/* methods'))
268
+ }
269
+ const id = `tui-client-${++nextTuiId}`
270
+ return new Promise((resolve, reject) => {
271
+ pendingTui.set(id, { resolve, reject })
272
+ writeJsonLine(
273
+ tui.output,
274
+ params === undefined
275
+ ? { jsonrpc: '2.0', id, method }
276
+ : { jsonrpc: '2.0', id, method, params },
277
+ )
278
+ })
279
+ },
280
+ }
281
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "id": "default",
3
+ "label": "Default",
4
+ "dark": {
5
+ "bg": "#0F1115",
6
+ "surface": "#151517",
7
+ "panel": "#1B1B1C",
8
+ "fg": "#F9FAFB",
9
+ "fg_secondary": "#CFD3D6",
10
+ "fg_tertiary": "#979DA6",
11
+ "caption": "#81858C",
12
+ "brand": "#5686FE",
13
+ "brand_soft": "#679EFE",
14
+ "bubble_bg": "#1B1B1C",
15
+ "bubble_fg": "#F1F3F5",
16
+ "border": "#2C2C2E",
17
+ "code_bg": "#151517",
18
+ "ok": "#4ED17E",
19
+ "warn": "#F7AD31",
20
+ "err": "#F25A5A",
21
+ "hint": "#6C7A96",
22
+ "chip_bg": "#2C2C2E"
23
+ },
24
+ "light": {
25
+ "bg": "#FFFFFF",
26
+ "surface": "#F9FAFB",
27
+ "panel": "#F5F6F7",
28
+ "fg": "#0F1115",
29
+ "fg_secondary": "#43454A",
30
+ "fg_tertiary": "#61666B",
31
+ "caption": "#ADB2B8",
32
+ "brand": "#4176E6",
33
+ "brand_soft": "#5686FE",
34
+ "bubble_bg": "#F1F3F5",
35
+ "bubble_fg": "#0F1115",
36
+ "border": "#E1E5EE",
37
+ "code_bg": "#F5F6F7",
38
+ "ok": "#22C55E",
39
+ "warn": "#DD8629",
40
+ "err": "#EC1313",
41
+ "hint": "#546078",
42
+ "chip_bg": "#EBEEF2"
43
+ }
44
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "id": "ember",
3
+ "label": "Ember",
4
+ "dark": {
5
+ "bg": "#16100E",
6
+ "surface": "#201612",
7
+ "panel": "#2A1C16",
8
+ "fg": "#FFF4E6",
9
+ "fg_secondary": "#D2B4A0",
10
+ "fg_tertiary": "#A0826E",
11
+ "caption": "#78645A",
12
+ "brand": "#F78C3C",
13
+ "brand_soft": "#FFAA5A",
14
+ "bubble_bg": "#30201A",
15
+ "bubble_fg": "#FFF4E6",
16
+ "border": "#463028",
17
+ "code_bg": "#1C1210",
18
+ "ok": "#78BE6E",
19
+ "warn": "#F7AD31",
20
+ "err": "#F25A5A",
21
+ "hint": "#C48C64",
22
+ "chip_bg": "#463028"
23
+ },
24
+ "light": {
25
+ "bg": "#FFF6EE",
26
+ "surface": "#FFE8D6",
27
+ "panel": "#FFDCC4",
28
+ "fg": "#2A1810",
29
+ "fg_secondary": "#5A3C2E",
30
+ "fg_tertiary": "#7A5848",
31
+ "caption": "#9A7868",
32
+ "brand": "#D96A1E",
33
+ "brand_soft": "#E88838",
34
+ "bubble_bg": "#FFE0C8",
35
+ "bubble_fg": "#2A1810",
36
+ "border": "#E8C4A8",
37
+ "code_bg": "#FFE8D6",
38
+ "ok": "#2F8F44",
39
+ "warn": "#DD8629",
40
+ "err": "#EC1313",
41
+ "hint": "#A06038",
42
+ "chip_bg": "#FFD4B0"
43
+ }
44
+ }
@@ -0,0 +1,92 @@
1
+ /** Built-in Client Plugin: ACP Plan dock plus a `/plan-view` fallback modal. */
2
+
3
+ export const name = 'plan-view'
4
+ export const inject = ['acpSessionPlan', 'tuiSlots', 'tuiCommands', 'tuiOverlay']
5
+
6
+ export function apply(ctx) {
7
+ let current = ctx.acpSessionPlan.current()
8
+ let panel
9
+
10
+ const stopSlot = ctx.tuiSlots.inject('conversation.input.dock', () => {
11
+ panel = ctx.tuiSlots.register(
12
+ { name: 'conversation.input.dock', id: 'plan-view' },
13
+ dockNodes(current),
14
+ )
15
+ return () => panel.dispose()
16
+ })
17
+ const stopPlan = ctx.acpSessionPlan.subscribe((snapshot) => {
18
+ current = snapshot.plans.at(-1) ?? null
19
+ panel?.update(dockNodes(current))
20
+ })
21
+ const stopCommand = ctx.tuiCommands.register({
22
+ name: 'plan-view',
23
+ description: 'Open the current ACP plan',
24
+ }, async () => {
25
+ current = ctx.acpSessionPlan.current()
26
+ ctx.tuiOverlay.openView({
27
+ id: 'plan-view',
28
+ title: 'Plan',
29
+ nodes: viewNodes(current),
30
+ })
31
+ })
32
+
33
+ return () => {
34
+ stopCommand?.()
35
+ stopPlan?.()
36
+ stopSlot?.()
37
+ }
38
+ }
39
+
40
+ function dockNodes(plan) {
41
+ if (plan === null) return []
42
+ if (plan.kind === 'items') {
43
+ const total = plan.entries.length
44
+ if (total === 0) return []
45
+ const completed = plan.entries.filter((entry) => entry.status === 'completed').length
46
+ const focus = plan.entries.find((entry) => entry.status === 'in_progress')
47
+ ?? plan.entries.find((entry) => entry.status !== 'completed')
48
+ ?? plan.entries.at(-1)
49
+ return [{
50
+ id: 'summary',
51
+ kind: 'generic',
52
+ title: `Plan · ${completed}/${total}${focus ? ` · ${focus.content}` : ''}`,
53
+ body: '',
54
+ action: { kind: 'command', name: 'plan-view', args: '' },
55
+ ...(completed === total ? { status: 'ok' } : { status: 'running' }),
56
+ }]
57
+ }
58
+ return [{
59
+ id: 'summary',
60
+ kind: 'generic',
61
+ title: plan.kind === 'file' ? `Plan · ${plan.uri}` : 'Plan · available',
62
+ body: '',
63
+ action: { kind: 'command', name: 'plan-view', args: '' },
64
+ status: 'running',
65
+ }]
66
+ }
67
+
68
+ function viewNodes(plan) {
69
+ if (plan === null) {
70
+ return [{ id: 'empty', kind: 'notice', level: 'info', text: 'No active plan' }]
71
+ }
72
+ if (plan.kind === 'markdown') {
73
+ return [{ id: 'content', kind: 'markdown', text: plan.content }]
74
+ }
75
+ if (plan.kind === 'file') {
76
+ return [{ id: 'file', kind: 'notice', level: 'info', text: plan.uri }]
77
+ }
78
+ return plan.entries.map((entry, index) => ({
79
+ id: `step-${index + 1}`,
80
+ kind: 'generic',
81
+ title: entry.content,
82
+ body: entry.priority ? `priority · ${entry.priority}` : '',
83
+ ...nodeStatus(entry.status),
84
+ }))
85
+ }
86
+
87
+ function nodeStatus(status) {
88
+ if (status === 'completed') return { status: 'ok' }
89
+ if (status === 'in_progress') return { status: 'running' }
90
+ if (status === 'cancelled' || status === 'failed') return { status: 'err' }
91
+ return {}
92
+ }
@@ -0,0 +1,11 @@
1
+ /** Profile sibling that selects the linked ACP + Creator dependency stack. */
2
+
3
+ import { apply as applyAcpClient } from './acp-client.js'
4
+ import { resolveStackedAgent } from './agent.js'
5
+
6
+ export const name = 'dsh-tui-profile-acp-client'
7
+ export const inject = []
8
+
9
+ export function apply(ctx) {
10
+ return applyAcpClient(ctx, { agent: resolveStackedAgent() })
11
+ }
@@ -0,0 +1,55 @@
1
+ /** A deliberately rich `chrome.right` gallery; not mounted by default. */
2
+
3
+ export const name = 'tui-right-slot-gallery'
4
+ export const inject = ['tuiSlots']
5
+
6
+ export function apply(ctx) {
7
+ ctx.tuiSlots.inject('chrome.right', () => {
8
+ const panel = ctx.tuiSlots.register(
9
+ { name: 'chrome.right', id: 'slot-gallery', order: 10 },
10
+ [
11
+ {
12
+ id: 'mission-control',
13
+ kind: 'group',
14
+ title: 'Mission Control',
15
+ tone: 'brand',
16
+ children: [
17
+ {
18
+ id: 'intro',
19
+ kind: 'markdown',
20
+ text: '**Live plugin canvas**\n\nAnything below is owned by one sibling Cordis plugin.',
21
+ },
22
+ {
23
+ id: 'tests',
24
+ kind: 'generic',
25
+ title: 'Regression suite',
26
+ body: '247 checks passed',
27
+ status: 'ok',
28
+ },
29
+ {
30
+ id: 'deploy',
31
+ kind: 'terminal',
32
+ title: 'Preview deploy',
33
+ body: '$ dsh plugin run slot-gallery\nmounted chrome.right\nwatching source…',
34
+ exit: 0,
35
+ },
36
+ {
37
+ id: 'patch',
38
+ kind: 'diff',
39
+ title: 'Live patch',
40
+ path: 'client/plugin.js',
41
+ unified: '@@ -1 +1 @@\n-static card\n+live TuiNode tree',
42
+ },
43
+ ],
44
+ },
45
+ {
46
+ id: 'hint',
47
+ kind: 'notice',
48
+ level: 'info',
49
+ text: 'panel.update(nodes) replaces this view immediately; unload removes the rail.',
50
+ },
51
+ ],
52
+ )
53
+ return panel.dispose
54
+ })
55
+ }