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,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live standard ACP Session configuration on the Client tree.
|
|
3
|
+
*
|
|
4
|
+
* The mux observes the existing ACP setup/update traffic. Mutations are sent
|
|
5
|
+
* to the native ACP client, which remains the sole owner of
|
|
6
|
+
* `session/set_config_option` and therefore also folds response-only state.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Service } from '@deepseek-ai/cordis'
|
|
10
|
+
import { CORDIS_METHODS, CORDIS_PROTOCOL } from './cordis-protocol.js'
|
|
11
|
+
|
|
12
|
+
export const name = 'acp-session-config'
|
|
13
|
+
export const inject = []
|
|
14
|
+
|
|
15
|
+
const SETUP_METHODS = new Set(['session/new', 'session/load'])
|
|
16
|
+
|
|
17
|
+
class AcpSessionConfigService extends Service {
|
|
18
|
+
constructor(ctx, core) {
|
|
19
|
+
super(ctx, 'acpSessionConfig')
|
|
20
|
+
this.core = core
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
list() {
|
|
24
|
+
return this.core.list()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
current(id) {
|
|
28
|
+
return this.core.current(id)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
byCategory(category) {
|
|
32
|
+
return this.core.byCategory(category)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
transaction(selector) {
|
|
36
|
+
return this.core.transaction(selector)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
set(id, value) {
|
|
40
|
+
return this.core.set(id, value)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
subscribe(listener) {
|
|
44
|
+
return this.core.subscribe(this.ctx, listener)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
bindTransport(requestTui) {
|
|
48
|
+
return this.core.bindTransport(requestTui)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
observeClient(message) {
|
|
52
|
+
return this.core.observeClient(message)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
observeAgent(message) {
|
|
56
|
+
return this.core.observeAgent(message)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Install the `ctx.acpSessionConfig` standard ACP state service. */
|
|
61
|
+
export function installAcpSessionConfig(ctx) {
|
|
62
|
+
let sessionId
|
|
63
|
+
let options = []
|
|
64
|
+
let requestTui
|
|
65
|
+
const pending = new Map()
|
|
66
|
+
const listeners = new Set()
|
|
67
|
+
|
|
68
|
+
function list() {
|
|
69
|
+
return cloneJson(options)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function current(id) {
|
|
73
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
74
|
+
throw new Error('acpSessionConfig.current: id must be a non-empty string')
|
|
75
|
+
}
|
|
76
|
+
const option = options.find((candidate) => candidate?.id === id)
|
|
77
|
+
return cloneJson(option?.currentValue ?? option?.current_value)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function byCategory(category) {
|
|
81
|
+
if (typeof category !== 'string' || category.length === 0) {
|
|
82
|
+
throw new Error('acpSessionConfig.byCategory: category must be a non-empty string')
|
|
83
|
+
}
|
|
84
|
+
return cloneJson(options.filter((candidate) => candidate?.category === category))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function snapshot() {
|
|
88
|
+
return { sessionId, options: list() }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function publish(nextSessionId, nextOptions) {
|
|
92
|
+
if (typeof nextSessionId === 'string' && nextSessionId.length > 0) {
|
|
93
|
+
sessionId = nextSessionId
|
|
94
|
+
}
|
|
95
|
+
if (!Array.isArray(nextOptions)) return
|
|
96
|
+
options = cloneJson(nextOptions)
|
|
97
|
+
const next = snapshot()
|
|
98
|
+
for (const listener of [...listeners]) listener(next)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function subscribe(effectCtx, listener) {
|
|
102
|
+
if (typeof listener !== 'function') {
|
|
103
|
+
throw new Error('acpSessionConfig.subscribe: listener must be a function')
|
|
104
|
+
}
|
|
105
|
+
const setup = () => {
|
|
106
|
+
listeners.add(listener)
|
|
107
|
+
return () => listeners.delete(listener)
|
|
108
|
+
}
|
|
109
|
+
const release = typeof effectCtx?.effect === 'function'
|
|
110
|
+
? effectCtx.effect(setup, 'acpSessionConfig.subscribe')
|
|
111
|
+
: setup()
|
|
112
|
+
let disposed = false
|
|
113
|
+
return () => {
|
|
114
|
+
if (disposed) return
|
|
115
|
+
disposed = true
|
|
116
|
+
return release?.()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function observeClient(message) {
|
|
121
|
+
if (!isObject(message) || message.id === undefined || typeof message.method !== 'string') return
|
|
122
|
+
if (SETUP_METHODS.has(message.method)) {
|
|
123
|
+
const requested = message.method === 'session/load'
|
|
124
|
+
? readString(message.params, 'sessionId', 'session_id')
|
|
125
|
+
: undefined
|
|
126
|
+
pending.set(message.id, { kind: 'setup', sessionId: requested })
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
if (message.method === 'session/set_config_option') {
|
|
130
|
+
pending.set(message.id, {
|
|
131
|
+
kind: 'config',
|
|
132
|
+
sessionId: readString(message.params, 'sessionId', 'session_id') ?? sessionId,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function observeAgent(message) {
|
|
138
|
+
if (!isObject(message)) return
|
|
139
|
+
if (message.id !== undefined && pending.has(message.id)) {
|
|
140
|
+
const tracked = pending.get(message.id)
|
|
141
|
+
pending.delete(message.id)
|
|
142
|
+
if (message.error !== undefined || !isObject(message.result)) return
|
|
143
|
+
const bound = readString(message.result, 'sessionId', 'session_id') ?? tracked.sessionId
|
|
144
|
+
const next = readOptions(message.result)
|
|
145
|
+
if (tracked.kind === 'setup') {
|
|
146
|
+
sessionId = bound
|
|
147
|
+
publish(bound, next ?? [])
|
|
148
|
+
} else if (bound === undefined || bound === sessionId) {
|
|
149
|
+
publish(bound, next)
|
|
150
|
+
}
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
if (message.method !== 'session/update' || !isObject(message.params)) return
|
|
154
|
+
const updatedSession = readString(message.params, 'sessionId', 'session_id')
|
|
155
|
+
if (sessionId !== undefined && updatedSession !== sessionId) return
|
|
156
|
+
const update = message.params.update
|
|
157
|
+
if (!isObject(update)) return
|
|
158
|
+
const type = readString(update, 'sessionUpdate', 'session_update')
|
|
159
|
+
if (type !== 'config_option_update') return
|
|
160
|
+
publish(updatedSession, readOptions(update))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function bindTransport(nextRequestTui) {
|
|
164
|
+
if (typeof nextRequestTui !== 'function') {
|
|
165
|
+
throw new Error('acpSessionConfig.bindTransport: requestTui must be a function')
|
|
166
|
+
}
|
|
167
|
+
requestTui = nextRequestTui
|
|
168
|
+
return () => {
|
|
169
|
+
if (requestTui === nextRequestTui) requestTui = undefined
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function set(id, value) {
|
|
174
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
175
|
+
throw new Error('acpSessionConfig.set: id must be a non-empty string')
|
|
176
|
+
}
|
|
177
|
+
if (sessionId === undefined) {
|
|
178
|
+
throw new Error('acpSessionConfig.set: no ACP Session is active')
|
|
179
|
+
}
|
|
180
|
+
if (typeof requestTui !== 'function') {
|
|
181
|
+
throw new Error('acpSessionConfig.set: native ACP transport is unavailable')
|
|
182
|
+
}
|
|
183
|
+
const option = options.find((candidate) => candidate?.id === id)
|
|
184
|
+
if (option === undefined) {
|
|
185
|
+
throw new Error(`acpSessionConfig.set: option "${id}" is not advertised by the current Session`)
|
|
186
|
+
}
|
|
187
|
+
validateValue(option, value)
|
|
188
|
+
const result = await requestTui(CORDIS_METHODS.sessionConfigSet, {
|
|
189
|
+
protocol: CORDIS_PROTOCOL,
|
|
190
|
+
sessionId,
|
|
191
|
+
configId: id,
|
|
192
|
+
value,
|
|
193
|
+
})
|
|
194
|
+
if (!isObject(result)) {
|
|
195
|
+
throw new Error('acpSessionConfig.set: native ACP client returned an invalid response')
|
|
196
|
+
}
|
|
197
|
+
const resultSession = readString(result, 'sessionId', 'session_id') ?? sessionId
|
|
198
|
+
publish(resultSession, readOptions(result))
|
|
199
|
+
return list()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function transaction(selector) {
|
|
203
|
+
const option = resolveTransactionOption(selector, options)
|
|
204
|
+
const original = cloneJson(option.currentValue ?? option.current_value)
|
|
205
|
+
if (original === undefined) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`acpSessionConfig.transaction: option "${option.id}" has no current value to restore`,
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let desired = cloneJson(original)
|
|
212
|
+
let applied = cloneJson(original)
|
|
213
|
+
let chain = Promise.resolve(list())
|
|
214
|
+
let finalizing
|
|
215
|
+
let settled = false
|
|
216
|
+
|
|
217
|
+
const write = async (value) => {
|
|
218
|
+
const result = await set(option.id, value)
|
|
219
|
+
applied = cloneJson(value)
|
|
220
|
+
return result
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const preview = (value) => {
|
|
224
|
+
if (settled) return Promise.resolve(list())
|
|
225
|
+
if (finalizing) return finalizing
|
|
226
|
+
if (Object.is(value, desired)) return chain
|
|
227
|
+
desired = cloneJson(value)
|
|
228
|
+
const next = cloneJson(value)
|
|
229
|
+
const task = () => write(next)
|
|
230
|
+
chain = chain.then(task, task)
|
|
231
|
+
return chain
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const finish = (value) => {
|
|
235
|
+
if (settled) return finalizing ?? Promise.resolve(list())
|
|
236
|
+
if (finalizing) return finalizing
|
|
237
|
+
desired = cloneJson(value)
|
|
238
|
+
const settle = async () => {
|
|
239
|
+
if (!Object.is(applied, desired)) await write(desired)
|
|
240
|
+
settled = true
|
|
241
|
+
return list()
|
|
242
|
+
}
|
|
243
|
+
finalizing = chain.then(settle, settle).catch((error) => {
|
|
244
|
+
finalizing = undefined
|
|
245
|
+
throw error
|
|
246
|
+
})
|
|
247
|
+
return finalizing
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
option: cloneJson(option),
|
|
252
|
+
original: cloneJson(original),
|
|
253
|
+
value() {
|
|
254
|
+
return cloneJson(desired)
|
|
255
|
+
},
|
|
256
|
+
preview,
|
|
257
|
+
commit(value = desired) {
|
|
258
|
+
return finish(value)
|
|
259
|
+
},
|
|
260
|
+
rollback() {
|
|
261
|
+
return finish(original)
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const core = {
|
|
267
|
+
list,
|
|
268
|
+
current,
|
|
269
|
+
byCategory,
|
|
270
|
+
transaction,
|
|
271
|
+
set,
|
|
272
|
+
subscribe,
|
|
273
|
+
bindTransport,
|
|
274
|
+
observeClient,
|
|
275
|
+
observeAgent,
|
|
276
|
+
}
|
|
277
|
+
const service = typeof ctx.provide === 'function'
|
|
278
|
+
? new AcpSessionConfigService(ctx, core)
|
|
279
|
+
: {
|
|
280
|
+
list,
|
|
281
|
+
current,
|
|
282
|
+
byCategory,
|
|
283
|
+
transaction,
|
|
284
|
+
set,
|
|
285
|
+
subscribe(listener) {
|
|
286
|
+
return subscribe(ctx, listener)
|
|
287
|
+
},
|
|
288
|
+
bindTransport,
|
|
289
|
+
observeClient,
|
|
290
|
+
observeAgent,
|
|
291
|
+
}
|
|
292
|
+
if (typeof ctx.provide !== 'function') ctx.acpSessionConfig = service
|
|
293
|
+
return service
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function resolveTransactionOption(selector, options) {
|
|
297
|
+
if (!isObject(selector)) {
|
|
298
|
+
throw new Error('acpSessionConfig.transaction: selector must be an object')
|
|
299
|
+
}
|
|
300
|
+
const hasCategory = typeof selector.category === 'string' && selector.category.length > 0
|
|
301
|
+
const hasId = typeof selector.id === 'string' && selector.id.length > 0
|
|
302
|
+
if (hasCategory === hasId) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
'acpSessionConfig.transaction: selector must contain exactly one non-empty category or id',
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
const matches = hasCategory
|
|
308
|
+
? options.filter((candidate) => candidate?.category === selector.category)
|
|
309
|
+
: options.filter((candidate) => candidate?.id === selector.id)
|
|
310
|
+
if (matches.length !== 1) {
|
|
311
|
+
const key = hasCategory ? `category "${selector.category}"` : `id "${selector.id}"`
|
|
312
|
+
throw new Error(
|
|
313
|
+
`acpSessionConfig.transaction: expected exactly one option for ${key}, found ${matches.length}`,
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
return cloneJson(matches[0])
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function validateValue(option, value) {
|
|
320
|
+
const kind = option.type
|
|
321
|
+
const current = option.currentValue ?? option.current_value
|
|
322
|
+
if (kind === 'boolean' || typeof current === 'boolean') {
|
|
323
|
+
if (typeof value !== 'boolean') {
|
|
324
|
+
throw new Error(`acpSessionConfig.set: option "${option.id}" requires a boolean value`)
|
|
325
|
+
}
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
if (typeof value !== 'string') {
|
|
329
|
+
throw new Error(`acpSessionConfig.set: option "${option.id}" requires a string value id`)
|
|
330
|
+
}
|
|
331
|
+
if (kind === 'select' && Array.isArray(option.options)) {
|
|
332
|
+
const values = selectValues(option.options)
|
|
333
|
+
if (!values.has(value)) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`acpSessionConfig.set: value "${value}" is not advertised for option "${option.id}"`,
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function selectValues(entries, out = new Set()) {
|
|
342
|
+
for (const entry of entries) {
|
|
343
|
+
if (!isObject(entry)) continue
|
|
344
|
+
const value = entry.value ?? entry.id
|
|
345
|
+
if (typeof value === 'string') out.add(value)
|
|
346
|
+
if (Array.isArray(entry.options)) selectValues(entry.options, out)
|
|
347
|
+
}
|
|
348
|
+
return out
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function readOptions(value) {
|
|
352
|
+
if (!isObject(value)) return undefined
|
|
353
|
+
const found = value.configOptions ?? value.config_options
|
|
354
|
+
return Array.isArray(found) ? found : undefined
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function readString(value, ...keys) {
|
|
358
|
+
if (!isObject(value)) return undefined
|
|
359
|
+
for (const key of keys) {
|
|
360
|
+
if (typeof value[key] === 'string' && value[key].length > 0) return value[key]
|
|
361
|
+
}
|
|
362
|
+
return undefined
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function isObject(value) {
|
|
366
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function cloneJson(value) {
|
|
370
|
+
if (value === undefined) return undefined
|
|
371
|
+
return structuredClone(value)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function apply(ctx) {
|
|
375
|
+
installAcpSessionConfig(ctx)
|
|
376
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/** Live structured Plan state folded from standard ACP Session updates. */
|
|
2
|
+
|
|
3
|
+
import { Service } from '@deepseek-ai/cordis'
|
|
4
|
+
|
|
5
|
+
export const name = 'acp-session-plan'
|
|
6
|
+
export const inject = []
|
|
7
|
+
|
|
8
|
+
const SETUP_METHODS = new Set(['session/new', 'session/load'])
|
|
9
|
+
|
|
10
|
+
class AcpSessionPlanService extends Service {
|
|
11
|
+
constructor(ctx, core) {
|
|
12
|
+
super(ctx, 'acpSessionPlan')
|
|
13
|
+
this.core = core
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
list() {
|
|
17
|
+
return this.core.list()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
current() {
|
|
21
|
+
return this.core.current()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
subscribe(listener) {
|
|
25
|
+
return this.core.subscribe(this.ctx, listener)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
observeClient(message) {
|
|
29
|
+
return this.core.observeClient(message)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
observeAgent(message) {
|
|
33
|
+
return this.core.observeAgent(message)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Install `ctx.acpSessionPlan`, backed only by standard ACP traffic. */
|
|
38
|
+
export function installAcpSessionPlan(ctx) {
|
|
39
|
+
let sessionId
|
|
40
|
+
const plans = new Map()
|
|
41
|
+
const pending = new Map()
|
|
42
|
+
const listeners = new Set()
|
|
43
|
+
|
|
44
|
+
function list() {
|
|
45
|
+
return cloneJson([...plans.values()])
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function current() {
|
|
49
|
+
return cloneJson([...plans.values()].at(-1) ?? null)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function snapshot() {
|
|
53
|
+
return { sessionId, plans: list() }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function publish() {
|
|
57
|
+
const next = snapshot()
|
|
58
|
+
for (const listener of [...listeners]) listener(next)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function reset(nextSessionId) {
|
|
62
|
+
sessionId = nextSessionId
|
|
63
|
+
plans.clear()
|
|
64
|
+
publish()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function subscribe(effectCtx, listener) {
|
|
68
|
+
if (typeof listener !== 'function') {
|
|
69
|
+
throw new Error('acpSessionPlan.subscribe: listener must be a function')
|
|
70
|
+
}
|
|
71
|
+
const setup = () => {
|
|
72
|
+
listeners.add(listener)
|
|
73
|
+
return () => listeners.delete(listener)
|
|
74
|
+
}
|
|
75
|
+
const release = typeof effectCtx?.effect === 'function'
|
|
76
|
+
? effectCtx.effect(setup, 'acpSessionPlan.subscribe')
|
|
77
|
+
: setup()
|
|
78
|
+
let disposed = false
|
|
79
|
+
return () => {
|
|
80
|
+
if (disposed) return
|
|
81
|
+
disposed = true
|
|
82
|
+
return release?.()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function observeClient(message) {
|
|
87
|
+
if (!isObject(message) || message.id === undefined || !SETUP_METHODS.has(message.method)) return
|
|
88
|
+
pending.set(message.id, {
|
|
89
|
+
sessionId: message.method === 'session/load'
|
|
90
|
+
? readString(message.params, 'sessionId', 'session_id')
|
|
91
|
+
: undefined,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function observeAgent(message) {
|
|
96
|
+
if (!isObject(message)) return
|
|
97
|
+
if (message.id !== undefined && pending.has(message.id)) {
|
|
98
|
+
const tracked = pending.get(message.id)
|
|
99
|
+
pending.delete(message.id)
|
|
100
|
+
if (message.error !== undefined || !isObject(message.result)) return
|
|
101
|
+
reset(readString(message.result, 'sessionId', 'session_id') ?? tracked.sessionId)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
if (message.method !== 'session/update' || !isObject(message.params)) return
|
|
105
|
+
const updatedSession = readString(message.params, 'sessionId', 'session_id')
|
|
106
|
+
if (sessionId !== undefined && updatedSession !== sessionId) return
|
|
107
|
+
if (sessionId === undefined && updatedSession !== undefined) sessionId = updatedSession
|
|
108
|
+
const update = message.params.update
|
|
109
|
+
if (!isObject(update)) return
|
|
110
|
+
const type = readString(update, 'sessionUpdate', 'session_update')
|
|
111
|
+
if (type === 'plan_removed') {
|
|
112
|
+
const id = readString(update, 'planId', 'plan_id')
|
|
113
|
+
if (id === undefined) plans.clear()
|
|
114
|
+
else plans.delete(id)
|
|
115
|
+
publish()
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
if (type !== 'plan' && type !== 'plan_update') return
|
|
119
|
+
const plan = normalizePlan(update)
|
|
120
|
+
if (plan === null) return
|
|
121
|
+
if (plan.kind === 'items' && plan.entries.length === 0) {
|
|
122
|
+
plans.delete(plan.id)
|
|
123
|
+
} else {
|
|
124
|
+
plans.delete(plan.id)
|
|
125
|
+
plans.set(plan.id, plan)
|
|
126
|
+
}
|
|
127
|
+
publish()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const core = { list, current, subscribe, observeClient, observeAgent }
|
|
131
|
+
const service = typeof ctx.provide === 'function'
|
|
132
|
+
? new AcpSessionPlanService(ctx, core)
|
|
133
|
+
: {
|
|
134
|
+
list,
|
|
135
|
+
current,
|
|
136
|
+
subscribe(listener) {
|
|
137
|
+
return subscribe(ctx, listener)
|
|
138
|
+
},
|
|
139
|
+
observeClient,
|
|
140
|
+
observeAgent,
|
|
141
|
+
}
|
|
142
|
+
if (typeof ctx.provide !== 'function') ctx.acpSessionPlan = service
|
|
143
|
+
return service
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function normalizePlan(update) {
|
|
147
|
+
const nested = isObject(update.plan) ? update.plan : update
|
|
148
|
+
const id = readString(nested, 'planId', 'plan_id')
|
|
149
|
+
?? readString(update, 'planId', 'plan_id')
|
|
150
|
+
?? 'default'
|
|
151
|
+
const declaredKind = readString(nested, 'type', 'kind')
|
|
152
|
+
if (declaredKind === 'items' || Array.isArray(nested.entries)) {
|
|
153
|
+
const entries = Array.isArray(nested.entries)
|
|
154
|
+
? nested.entries.map(normalizeEntry).filter((entry) => entry !== null)
|
|
155
|
+
: []
|
|
156
|
+
return { id, kind: 'items', entries }
|
|
157
|
+
}
|
|
158
|
+
if (declaredKind === 'markdown' || typeof nested.content === 'string') {
|
|
159
|
+
const content = typeof nested.content === 'string' ? nested.content : ''
|
|
160
|
+
return { id, kind: 'markdown', content }
|
|
161
|
+
}
|
|
162
|
+
if (declaredKind === 'file' || typeof nested.uri === 'string') {
|
|
163
|
+
const uri = typeof nested.uri === 'string' ? nested.uri : ''
|
|
164
|
+
return { id, kind: 'file', uri }
|
|
165
|
+
}
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizeEntry(value) {
|
|
170
|
+
if (!isObject(value) || typeof value.content !== 'string') return null
|
|
171
|
+
return {
|
|
172
|
+
content: value.content,
|
|
173
|
+
...(typeof value.priority === 'string' ? { priority: value.priority } : {}),
|
|
174
|
+
...(typeof value.status === 'string' ? { status: value.status } : {}),
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function readString(value, ...keys) {
|
|
179
|
+
if (!isObject(value)) return undefined
|
|
180
|
+
for (const key of keys) {
|
|
181
|
+
if (typeof value[key] === 'string' && value[key].length > 0) return value[key]
|
|
182
|
+
}
|
|
183
|
+
return undefined
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isObject(value) {
|
|
187
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function cloneJson(value) {
|
|
191
|
+
return value === undefined ? undefined : structuredClone(value)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function apply(ctx) {
|
|
195
|
+
installAcpSessionPlan(ctx)
|
|
196
|
+
}
|