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.
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/bin/dsh-tui.js +68 -0
- package/cordis.patch.yml +30 -0
- package/creator/cordis.patch.yml +6 -0
- package/creator/package.json +10 -0
- package/lib/acp-client-events.js +65 -0
- package/lib/acp-client.js +114 -0
- package/lib/acp-host.js +24 -0
- package/lib/acp-session-config.js +376 -0
- package/lib/acp-session-plan.js +196 -0
- package/lib/acp-session-stats.js +239 -0
- package/lib/agent.js +64 -0
- package/lib/boot.js +119 -0
- package/lib/client-process.js +11 -0
- package/lib/client-run.js +379 -0
- package/lib/cordis-protocol.js +51 -0
- package/lib/creator-overlay.js +77 -0
- package/lib/demo-skin.js +79 -0
- package/lib/ember.js +20 -0
- package/lib/index.js +226 -0
- package/lib/inspect.js +971 -0
- package/lib/jsonrpc-line-transport.js +155 -0
- package/lib/mux.js +281 -0
- package/lib/palettes/default.json +44 -0
- package/lib/palettes/ember.json +44 -0
- package/lib/plan-view.js +92 -0
- package/lib/profile-acp-client.js +11 -0
- package/lib/right-demo.js +55 -0
- package/lib/runner.js +94 -0
- package/lib/spawn-tui.js +179 -0
- package/lib/stats-view.js +90 -0
- package/lib/tui-commands.js +144 -0
- package/lib/tui-overlay.js +252 -0
- package/lib/tui-slots.js +351 -0
- package/lib/tui-theme.js +463 -0
- package/package.json +83 -0
- package/skills/tui-plugin-development/SKILL.md +172 -0
- package/vendor/darwin-arm64/dsh-tui +0 -0
- package/vendor/darwin-x64/dsh-tui +0 -0
- package/vendor/linux-arm64/dsh-tui +0 -0
- package/vendor/linux-x64/dsh-tui +0 -0
- 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
|
+
}
|
package/lib/plan-view.js
ADDED
|
@@ -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
|
+
}
|