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
package/lib/inspect.js
ADDED
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Client inspect + run plane: advertise Theme, answer queries, load
|
|
3
|
+
* `code.client` that injects `tuiTheme` / `tuiSlots`. Transport is ACP extras, not a
|
|
4
|
+
* cordis-acp plugin-id sync.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { applyClientHalf, createClientTimer } from './client-run.js'
|
|
8
|
+
import { CORDIS_METHODS } from './cordis-protocol.js'
|
|
9
|
+
|
|
10
|
+
export const name = 'tui-cordis-client-runner'
|
|
11
|
+
export const inject = [
|
|
12
|
+
'tuiTheme', 'tuiSlots', 'tuiCommands', 'tuiOverlay', 'acpSessionConfig',
|
|
13
|
+
'acpSessionPlan', 'acpSessionStats',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
const EMPTY_INPUT = Object.freeze({ type: 'object', properties: {}, additionalProperties: false })
|
|
17
|
+
const ANY_OUTPUT = Object.freeze({ description: 'JSON data owned by this inspect provider.' })
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {{ list: Function, active: Function, exportInspectTokens: Function }} tuiTheme
|
|
21
|
+
* @returns {{ manifest: object, query: Function }}
|
|
22
|
+
*/
|
|
23
|
+
export function themeInspectProvider(tuiTheme) {
|
|
24
|
+
return {
|
|
25
|
+
manifest: {
|
|
26
|
+
id: 'Theme',
|
|
27
|
+
description:
|
|
28
|
+
'TUI Theme Plugin seat and closed palette tokens. A Client Plugin registers its palette; '
|
|
29
|
+
+ '/theme loads or replaces that whole Plugin, including its other contributions.',
|
|
30
|
+
methods: [{
|
|
31
|
+
name: 'listTokens',
|
|
32
|
+
description:
|
|
33
|
+
'Return the closed token directory, mounted palettes, and how a Client half covers the TUI.',
|
|
34
|
+
inputSchema: EMPTY_INPUT,
|
|
35
|
+
outputSchema: ANY_OUTPUT,
|
|
36
|
+
}],
|
|
37
|
+
},
|
|
38
|
+
query(method) {
|
|
39
|
+
if (method !== 'listTokens') throw new Error(`unknown Theme inspect method "${method}"`)
|
|
40
|
+
const active = tuiTheme.active()
|
|
41
|
+
return {
|
|
42
|
+
tokens: tuiTheme.exportInspectTokens(),
|
|
43
|
+
palettes: tuiTheme.list().map((palette) => ({
|
|
44
|
+
id: palette.id,
|
|
45
|
+
label: palette.label,
|
|
46
|
+
active: palette.id === active,
|
|
47
|
+
})),
|
|
48
|
+
apply: {
|
|
49
|
+
inject: ['tuiTheme'],
|
|
50
|
+
register: {
|
|
51
|
+
call: 'ctx.tuiTheme.register(palette, options?)',
|
|
52
|
+
palette: {
|
|
53
|
+
required: ['id', 'label', 'dark', 'light'],
|
|
54
|
+
optional: ['background'],
|
|
55
|
+
background: {
|
|
56
|
+
source: {
|
|
57
|
+
variants: [
|
|
58
|
+
{ kind: 'file', path: 'absolute filesystem path to a PNG' },
|
|
59
|
+
{ kind: 'data', mediaType: 'image/png', base64: 'non-empty string' },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
fit: {
|
|
63
|
+
enum: ['cover', 'contain', 'stretch'],
|
|
64
|
+
default: 'cover',
|
|
65
|
+
},
|
|
66
|
+
anchor: {
|
|
67
|
+
x: 'finite number from 0 (left) to 1 (right)',
|
|
68
|
+
y: 'finite number from 0 (top) to 1 (bottom)',
|
|
69
|
+
default: { x: 0.5, y: 0.5 },
|
|
70
|
+
},
|
|
71
|
+
opacity: 'finite number from 0 to 1; default 1',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
options: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
required: ['palette'],
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
properties: {
|
|
79
|
+
palette: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
required: ['id', 'label', 'dark', 'light'],
|
|
82
|
+
description: 'Complete validated TUI palette pack.',
|
|
83
|
+
},
|
|
84
|
+
activate: {
|
|
85
|
+
type: 'boolean',
|
|
86
|
+
optional: true,
|
|
87
|
+
default: false,
|
|
88
|
+
description: 'Cover immediately and restore the actual previous palette on dispose.',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
returns: {
|
|
93
|
+
type: 'callable ThemeRegistration',
|
|
94
|
+
legacyCall: 'registration() disposes the registration',
|
|
95
|
+
properties: {
|
|
96
|
+
update: {
|
|
97
|
+
call: 'registration.update({ background })',
|
|
98
|
+
role: 'update-background',
|
|
99
|
+
effect: 'Updates this owned theme in place; null removes its background.',
|
|
100
|
+
},
|
|
101
|
+
dispose: {
|
|
102
|
+
call: 'registration.dispose()',
|
|
103
|
+
role: 'dispose',
|
|
104
|
+
idempotent: true,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
lifecycle: {
|
|
110
|
+
ownership:
|
|
111
|
+
'The palette and every command, overlay, slot, timer, transaction, and Host RPC registered '
|
|
112
|
+
+ 'by the same Client Plugin share one Cordis Fiber lifetime.',
|
|
113
|
+
preview:
|
|
114
|
+
'register(palette, { activate: true }) selects this Plugin run for immediate preview. '
|
|
115
|
+
+ 'The runner replaces the previous Theme Plugin as one lifecycle.',
|
|
116
|
+
},
|
|
117
|
+
note:
|
|
118
|
+
'Every pack supplies all 18 token names as #RRGGBB in both dark and light. '
|
|
119
|
+
+ 'A pack may additionally own one PNG background and update it through its registration. '
|
|
120
|
+
+ 'Register all features unconditionally in one Plugin. Do not inspect the active palette, '
|
|
121
|
+
+ 'subscribe to theme changes, add theme conditions to commands, call theme.overrideTokens, '
|
|
122
|
+
+ 'or write CSS variables.',
|
|
123
|
+
},
|
|
124
|
+
selection: {
|
|
125
|
+
command: '/theme <id>',
|
|
126
|
+
unit: 'Client Plugin',
|
|
127
|
+
unload: 'all Plugin contributions',
|
|
128
|
+
behavior:
|
|
129
|
+
'Selecting an entry starts its owning Theme Plugin and stops the previously selected one. '
|
|
130
|
+
+ 'Selecting default stops the current dynamic Theme Plugin.',
|
|
131
|
+
},
|
|
132
|
+
referencedTypes: [],
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const TUI_NODE_KINDS = Object.freeze([
|
|
139
|
+
'group', 'markdown', 'reasoning', 'user', 'generic', 'terminal', 'diff',
|
|
140
|
+
'image', 'notice', 'unknown',
|
|
141
|
+
])
|
|
142
|
+
|
|
143
|
+
/** Describe the root TUI shell slots available to a dynamic Client half. */
|
|
144
|
+
export function slotInspectProvider(tuiSlots) {
|
|
145
|
+
return {
|
|
146
|
+
manifest: {
|
|
147
|
+
id: 'Slots',
|
|
148
|
+
description:
|
|
149
|
+
'Terminal-native root shell slots. Plugins contribute validated TuiNode trees; '
|
|
150
|
+
+ 'they do not receive React, the TTY, screen coordinates, or Ratatui.',
|
|
151
|
+
methods: [{
|
|
152
|
+
name: 'list',
|
|
153
|
+
description: 'List declared TUI seats, current occupants, the registration API, and node kinds.',
|
|
154
|
+
inputSchema: EMPTY_INPUT,
|
|
155
|
+
outputSchema: ANY_OUTPUT,
|
|
156
|
+
}],
|
|
157
|
+
},
|
|
158
|
+
query(method) {
|
|
159
|
+
if (method !== 'list') throw new Error(`unknown Slots inspect method "${method}"`)
|
|
160
|
+
return {
|
|
161
|
+
slots: tuiSlots.list(),
|
|
162
|
+
nodeKinds: [...TUI_NODE_KINDS],
|
|
163
|
+
apply: {
|
|
164
|
+
inject: ['tuiSlots'],
|
|
165
|
+
wait: "ctx.tuiSlots.inject(slotName, () => { ... })",
|
|
166
|
+
register:
|
|
167
|
+
'ctx.tuiSlots.register({ name: slotName, id, order? }, TuiNode[])',
|
|
168
|
+
update: 'const panel = register(...); panel.update(nextNodes)',
|
|
169
|
+
dispose: 'Return panel.dispose from inject; plugin unload removes the pane immediately.',
|
|
170
|
+
note:
|
|
171
|
+
'chrome.right is a root list slot; conversation.input.dock is the additive row above '
|
|
172
|
+
+ 'the composer; conversation.composer.dock is the additive compact row below it. '
|
|
173
|
+
+ 'Node ids are stable inside one contribution; '
|
|
174
|
+
+ 'the compositor namespaces them by contribution id. Compose group/markdown/reasoning/'
|
|
175
|
+
+ 'user/generic/terminal/diff/image/notice/unknown nodes. Colors use theme tokens only.',
|
|
176
|
+
},
|
|
177
|
+
runtime: {
|
|
178
|
+
inject: ['timer'],
|
|
179
|
+
callHost: "await host.call('method', jsonArgs)",
|
|
180
|
+
poll: 'ctx.interval(() => void refresh(), milliseconds)',
|
|
181
|
+
note:
|
|
182
|
+
'Host code registers Package-private JSON handlers with harness.handle. '
|
|
183
|
+
+ 'Client code calls them with host.call. For polling, inject timer and use ctx.interval; '
|
|
184
|
+
+ 'the interval is cancelled automatically when the Plugin run stops.',
|
|
185
|
+
},
|
|
186
|
+
referencedTypes: ['TuiNode'],
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Describe lifecycle-owned local slash commands available to dynamic Client halves. */
|
|
193
|
+
export function commandInspectProvider(tuiCommands) {
|
|
194
|
+
return {
|
|
195
|
+
manifest: {
|
|
196
|
+
id: 'Commands',
|
|
197
|
+
description:
|
|
198
|
+
'TUI-local slash commands. Inspection is read-only; Client halves inject tuiCommands '
|
|
199
|
+
+ 'to register commands that never enter the agent prompt.',
|
|
200
|
+
methods: [{
|
|
201
|
+
name: 'list',
|
|
202
|
+
description:
|
|
203
|
+
'Return the current local command catalog and the register/list contract.',
|
|
204
|
+
inputSchema: EMPTY_INPUT,
|
|
205
|
+
outputSchema: ANY_OUTPUT,
|
|
206
|
+
}],
|
|
207
|
+
},
|
|
208
|
+
query(method) {
|
|
209
|
+
if (method !== 'list') throw new Error(`unknown Commands inspect method "${method}"`)
|
|
210
|
+
return {
|
|
211
|
+
commands: tuiCommands.list(),
|
|
212
|
+
api: {
|
|
213
|
+
service: 'tuiCommands',
|
|
214
|
+
inject: ['tuiCommands'],
|
|
215
|
+
register: {
|
|
216
|
+
call: 'ctx.tuiCommands.register(options, handler)',
|
|
217
|
+
options: {
|
|
218
|
+
type: 'object',
|
|
219
|
+
required: ['name', 'description'],
|
|
220
|
+
additionalProperties: false,
|
|
221
|
+
properties: {
|
|
222
|
+
name: {
|
|
223
|
+
type: 'string',
|
|
224
|
+
pattern: '^[a-z0-9][a-z0-9-]*$',
|
|
225
|
+
slashPrefix: false,
|
|
226
|
+
},
|
|
227
|
+
description: { type: 'string', minLength: 1 },
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
handler: {
|
|
231
|
+
arguments: [
|
|
232
|
+
{ name: 'args', type: 'string', default: '' },
|
|
233
|
+
{
|
|
234
|
+
name: 'command',
|
|
235
|
+
type: 'object',
|
|
236
|
+
properties: { name: { type: 'string' } },
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
async: true,
|
|
240
|
+
},
|
|
241
|
+
returns: {
|
|
242
|
+
type: 'function',
|
|
243
|
+
role: 'dispose',
|
|
244
|
+
idempotent: true,
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
list: {
|
|
248
|
+
call: 'ctx.tuiCommands.list()',
|
|
249
|
+
returns: { type: 'array', items: 'Command' },
|
|
250
|
+
},
|
|
251
|
+
lifecycle: {
|
|
252
|
+
ownership:
|
|
253
|
+
'The registration belongs to the Client Plugin run and is removed when that run stops, '
|
|
254
|
+
+ 'even if the returned disposer is ignored.',
|
|
255
|
+
invocation:
|
|
256
|
+
'Users type /<name> followed by optional text. The handler receives that text as args; '
|
|
257
|
+
+ 'the command is handled locally and is not sent as an ACP prompt.',
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
referencedTypes: ['Command'],
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Describe the native semantic overlay surface without opening visible UI. */
|
|
267
|
+
export function overlayInspectProvider(tuiOverlay) {
|
|
268
|
+
return {
|
|
269
|
+
manifest: {
|
|
270
|
+
id: 'Overlay',
|
|
271
|
+
description:
|
|
272
|
+
'Native semantic TUI overlays. Inspection only returns the active snapshot; '
|
|
273
|
+
+ 'Client halves inject tuiOverlay to open user-requested controls.',
|
|
274
|
+
methods: [{
|
|
275
|
+
name: 'active',
|
|
276
|
+
description: 'Return the active overlay snapshot and the slider lifecycle contract.',
|
|
277
|
+
inputSchema: EMPTY_INPUT,
|
|
278
|
+
outputSchema: ANY_OUTPUT,
|
|
279
|
+
}],
|
|
280
|
+
},
|
|
281
|
+
query(method) {
|
|
282
|
+
if (method !== 'active') throw new Error(`unknown Overlay inspect method "${method}"`)
|
|
283
|
+
return {
|
|
284
|
+
active: tuiOverlay.active(),
|
|
285
|
+
api: {
|
|
286
|
+
service: 'tuiOverlay',
|
|
287
|
+
inject: ['tuiOverlay'],
|
|
288
|
+
openSlider: {
|
|
289
|
+
call: 'ctx.tuiOverlay.openSlider(options, handlers?)',
|
|
290
|
+
options: {
|
|
291
|
+
type: 'object',
|
|
292
|
+
required: ['id', 'title', 'min', 'max', 'step', 'value'],
|
|
293
|
+
additionalProperties: false,
|
|
294
|
+
properties: {
|
|
295
|
+
id: { type: 'string', minLength: 1 },
|
|
296
|
+
title: { type: 'string', minLength: 1 },
|
|
297
|
+
min: { type: 'number', finite: true },
|
|
298
|
+
max: { type: 'number', finite: true, greaterThan: 'min' },
|
|
299
|
+
step: { type: 'number', finite: true, exclusiveMinimum: 0 },
|
|
300
|
+
value: { type: 'number', finite: true, clampedTo: ['min', 'max'] },
|
|
301
|
+
marks: {
|
|
302
|
+
type: 'array',
|
|
303
|
+
optional: true,
|
|
304
|
+
default: [],
|
|
305
|
+
items: {
|
|
306
|
+
type: 'object',
|
|
307
|
+
required: ['value', 'label'],
|
|
308
|
+
additionalProperties: false,
|
|
309
|
+
properties: {
|
|
310
|
+
value: { type: 'number', finite: true, range: ['min', 'max'] },
|
|
311
|
+
id: { type: 'string', minLength: 1, optional: true },
|
|
312
|
+
label: { type: 'string', minLength: 1 },
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
snapToMarks: {
|
|
317
|
+
type: 'boolean',
|
|
318
|
+
optional: true,
|
|
319
|
+
default: false,
|
|
320
|
+
requires: 'at least one mark when true',
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
handlers: {
|
|
325
|
+
onChange: {
|
|
326
|
+
optional: true,
|
|
327
|
+
async: true,
|
|
328
|
+
arguments: [{ name: 'value', type: 'number' }],
|
|
329
|
+
lifecycle: 'Runs after left/right changes; the slider remains open.',
|
|
330
|
+
},
|
|
331
|
+
onSubmit: {
|
|
332
|
+
optional: true,
|
|
333
|
+
async: true,
|
|
334
|
+
arguments: [
|
|
335
|
+
{ name: 'value', type: 'number' },
|
|
336
|
+
{ name: 'mark', type: 'SliderMark | undefined' },
|
|
337
|
+
{ name: 'controller', type: 'SliderController' },
|
|
338
|
+
],
|
|
339
|
+
lifecycle: 'Enter closes the slider before this handler runs.',
|
|
340
|
+
},
|
|
341
|
+
onCancel: {
|
|
342
|
+
optional: true,
|
|
343
|
+
async: true,
|
|
344
|
+
arguments: [
|
|
345
|
+
{ name: 'value', type: 'number' },
|
|
346
|
+
{ name: 'mark', type: 'SliderMark | undefined' },
|
|
347
|
+
{ name: 'controller', type: 'SliderController' },
|
|
348
|
+
],
|
|
349
|
+
lifecycle: 'Escape closes the slider before this handler runs.',
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
returns: {
|
|
353
|
+
type: 'SliderController',
|
|
354
|
+
properties: {
|
|
355
|
+
close: {
|
|
356
|
+
type: 'function',
|
|
357
|
+
role: 'close',
|
|
358
|
+
idempotent: true,
|
|
359
|
+
effect: 'Closes the active slider immediately.',
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
openView: {
|
|
365
|
+
call: 'ctx.tuiOverlay.openView(options, handlers?)',
|
|
366
|
+
options: {
|
|
367
|
+
type: 'object',
|
|
368
|
+
required: ['id', 'title', 'nodes'],
|
|
369
|
+
additionalProperties: false,
|
|
370
|
+
properties: {
|
|
371
|
+
id: { type: 'string', minLength: 1 },
|
|
372
|
+
title: { type: 'string', minLength: 1 },
|
|
373
|
+
nodes: { type: 'array', items: 'TuiNode' },
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
handlers: {
|
|
377
|
+
onSubmit: { optional: true, async: true, arguments: [] },
|
|
378
|
+
onCancel: { optional: true, async: true, arguments: [] },
|
|
379
|
+
},
|
|
380
|
+
returns: {
|
|
381
|
+
type: 'ViewController',
|
|
382
|
+
properties: {
|
|
383
|
+
close: { type: 'function', role: 'close', idempotent: true },
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
active: {
|
|
388
|
+
call: 'ctx.tuiOverlay.active()',
|
|
389
|
+
returns: 'Slider | View | null',
|
|
390
|
+
},
|
|
391
|
+
lifecycle: {
|
|
392
|
+
concurrency: 'Only one overlay may be active at a time.',
|
|
393
|
+
ownership:
|
|
394
|
+
'The overlay belongs to the Client Plugin run and closes automatically when that run stops, '
|
|
395
|
+
+ 'even if the returned controller is ignored.',
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
referencedTypes: ['Slider', 'SliderMark', 'SliderController', 'View', 'ViewController', 'TuiNode'],
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Describe the current standard ACP Session config-option snapshot and setter. */
|
|
405
|
+
export function configOptionsInspectProvider(acpSessionConfig) {
|
|
406
|
+
return {
|
|
407
|
+
manifest: {
|
|
408
|
+
id: 'ConfigOptions',
|
|
409
|
+
description:
|
|
410
|
+
'Current ACP Session config options. Values and choices come from the connected agent; '
|
|
411
|
+
+ 'Client halves inject acpSessionConfig to read, set, and subscribe without raw ACP access.',
|
|
412
|
+
methods: [{
|
|
413
|
+
name: 'list',
|
|
414
|
+
description:
|
|
415
|
+
'Return the live Session option snapshot and the ACP-backed read/set/subscribe contract.',
|
|
416
|
+
inputSchema: EMPTY_INPUT,
|
|
417
|
+
outputSchema: ANY_OUTPUT,
|
|
418
|
+
}],
|
|
419
|
+
},
|
|
420
|
+
query(method) {
|
|
421
|
+
if (method !== 'list') throw new Error(`unknown ConfigOptions inspect method "${method}"`)
|
|
422
|
+
return {
|
|
423
|
+
options: acpSessionConfig.list(),
|
|
424
|
+
api: {
|
|
425
|
+
service: 'acpSessionConfig',
|
|
426
|
+
inject: ['acpSessionConfig'],
|
|
427
|
+
list: {
|
|
428
|
+
call: 'ctx.acpSessionConfig.list()',
|
|
429
|
+
returns: 'ConfigOption[]',
|
|
430
|
+
},
|
|
431
|
+
byCategory: {
|
|
432
|
+
call: 'ctx.acpSessionConfig.byCategory(category)',
|
|
433
|
+
parameters: [{ name: 'category', type: 'string', minLength: 1 }],
|
|
434
|
+
returns: 'ConfigOption[]',
|
|
435
|
+
standardCategories: ['mode', 'model', 'model_config', 'thought_level'],
|
|
436
|
+
runtimeIdentity:
|
|
437
|
+
'SessionConfigOption.category is the standard ACP semantic identity. Resolve it '
|
|
438
|
+
+ 'against the current live snapshot on every invocation; ids remain agent-owned.',
|
|
439
|
+
},
|
|
440
|
+
transaction: {
|
|
441
|
+
call: 'ctx.acpSessionConfig.transaction({ category })',
|
|
442
|
+
selectors: [
|
|
443
|
+
{ shape: '{ category: string }', match: 'exactly one live standard category' },
|
|
444
|
+
{ shape: '{ id: string }', match: 'exactly one live agent-owned id' },
|
|
445
|
+
],
|
|
446
|
+
returns: {
|
|
447
|
+
option: 'the matched live ConfigOption snapshot',
|
|
448
|
+
original: 'the value captured when the transaction starts',
|
|
449
|
+
value: 'transaction.value() returns the latest requested value',
|
|
450
|
+
preview: 'await transaction.preview(value): serialized and deduplicated ACP writes',
|
|
451
|
+
commit: 'await transaction.commit(value?): keep the final value; first finalizer wins',
|
|
452
|
+
rollback: 'await transaction.rollback(): restore original; first finalizer wins',
|
|
453
|
+
},
|
|
454
|
+
preview:
|
|
455
|
+
'preview(value) serializes ACP writes and is deduplicated when value is unchanged.',
|
|
456
|
+
commit:
|
|
457
|
+
'commit(value?) keeps the requested value and prevents later lifecycle rollback.',
|
|
458
|
+
rollback:
|
|
459
|
+
'rollback() restores original. An unfinished transaction rolls back automatically '
|
|
460
|
+
+ 'when its owning Client Plugin run stops, updates, fails, or unloads.',
|
|
461
|
+
},
|
|
462
|
+
current: {
|
|
463
|
+
call: 'ctx.acpSessionConfig.current(id)',
|
|
464
|
+
parameters: [{ name: 'id', type: 'string', minLength: 1 }],
|
|
465
|
+
returns: 'string | boolean | undefined',
|
|
466
|
+
},
|
|
467
|
+
set: {
|
|
468
|
+
call: 'await ctx.acpSessionConfig.set(id, value)',
|
|
469
|
+
parameters: [
|
|
470
|
+
{ name: 'id', type: 'string', minLength: 1 },
|
|
471
|
+
{ name: 'value', type: 'string | boolean' },
|
|
472
|
+
],
|
|
473
|
+
returns: 'Promise<ConfigOption[]>',
|
|
474
|
+
transport: 'standard ACP session/set_config_option',
|
|
475
|
+
validation:
|
|
476
|
+
'The id must exist in the current snapshot. Select values must be advertised choices; '
|
|
477
|
+
+ 'boolean options require booleans.',
|
|
478
|
+
},
|
|
479
|
+
subscribe: {
|
|
480
|
+
call: 'ctx.acpSessionConfig.subscribe((snapshot) => { ... })',
|
|
481
|
+
listener: {
|
|
482
|
+
arguments: [{
|
|
483
|
+
name: 'snapshot',
|
|
484
|
+
type: '{ sessionId?: string, options: ConfigOption[] }',
|
|
485
|
+
}],
|
|
486
|
+
},
|
|
487
|
+
returns: { type: 'function', role: 'dispose', idempotent: true },
|
|
488
|
+
},
|
|
489
|
+
lifecycle: {
|
|
490
|
+
values:
|
|
491
|
+
'Read ids, kinds, current values, and legal choices from the current Session snapshot; '
|
|
492
|
+
+ 'never infer them from model metadata or Host defaults.',
|
|
493
|
+
semanticSelection:
|
|
494
|
+
'When the requested behavior matches an advertised standard ACP category, call '
|
|
495
|
+
+ 'byCategory(category) at runtime and require exactly one match. Use that live option id '
|
|
496
|
+
+ 'for current/set; never compile an inspect-time agent-owned id into portable Plugin code.',
|
|
497
|
+
ownership:
|
|
498
|
+
'Subscriptions belong to the Client Plugin run and are removed automatically on '
|
|
499
|
+
+ 'stop, update, rollback, or unload.',
|
|
500
|
+
updates:
|
|
501
|
+
'Successful set responses and later ACP config_option_update snapshots both refresh '
|
|
502
|
+
+ 'list/current and notify subscribers.',
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
referencedTypes: ['ConfigOption'],
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Describe structured Plan snapshots folded from standard ACP Session updates. */
|
|
512
|
+
export function planInspectProvider(acpSessionPlan) {
|
|
513
|
+
return {
|
|
514
|
+
manifest: {
|
|
515
|
+
id: 'Plans',
|
|
516
|
+
description:
|
|
517
|
+
'Current structured ACP Session plans. Client halves can render or react to plan updates '
|
|
518
|
+
+ 'without parsing raw ACP messages.',
|
|
519
|
+
methods: [{
|
|
520
|
+
name: 'current',
|
|
521
|
+
description: 'Return the active plan and the read/subscribe contract.',
|
|
522
|
+
inputSchema: EMPTY_INPUT,
|
|
523
|
+
outputSchema: ANY_OUTPUT,
|
|
524
|
+
}],
|
|
525
|
+
},
|
|
526
|
+
query(method) {
|
|
527
|
+
if (method !== 'current') throw new Error(`unknown Plans inspect method "${method}"`)
|
|
528
|
+
return {
|
|
529
|
+
current: acpSessionPlan.current(),
|
|
530
|
+
plans: acpSessionPlan.list(),
|
|
531
|
+
api: {
|
|
532
|
+
service: 'acpSessionPlan',
|
|
533
|
+
inject: ['acpSessionPlan'],
|
|
534
|
+
current: { call: 'ctx.acpSessionPlan.current()', returns: 'Plan | null' },
|
|
535
|
+
list: { call: 'ctx.acpSessionPlan.list()', returns: 'Plan[]' },
|
|
536
|
+
subscribe: {
|
|
537
|
+
call: 'ctx.acpSessionPlan.subscribe((snapshot) => { ... })',
|
|
538
|
+
listener: {
|
|
539
|
+
arguments: [{
|
|
540
|
+
name: 'snapshot',
|
|
541
|
+
type: '{ sessionId?: string, plans: Plan[] }',
|
|
542
|
+
}],
|
|
543
|
+
},
|
|
544
|
+
returns: { type: 'function', role: 'dispose', idempotent: true },
|
|
545
|
+
},
|
|
546
|
+
transport: 'standard ACP session/update plan, plan_update, and plan_removed',
|
|
547
|
+
},
|
|
548
|
+
referencedTypes: ['Plan'],
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Describe token totals and live prompt timings folded from standard ACP. */
|
|
555
|
+
export function statsInspectProvider(acpSessionStats) {
|
|
556
|
+
return {
|
|
557
|
+
manifest: {
|
|
558
|
+
id: 'Stats',
|
|
559
|
+
description:
|
|
560
|
+
'Current standard ACP token usage and client-observed prompt timings. '
|
|
561
|
+
+ 'Client halves can render stats without parsing raw ACP messages.',
|
|
562
|
+
methods: [{
|
|
563
|
+
name: 'current',
|
|
564
|
+
description: 'Return the current stats snapshot and the read/subscribe contract.',
|
|
565
|
+
inputSchema: EMPTY_INPUT,
|
|
566
|
+
outputSchema: ANY_OUTPUT,
|
|
567
|
+
}],
|
|
568
|
+
},
|
|
569
|
+
query(method) {
|
|
570
|
+
if (method !== 'current') throw new Error(`unknown Stats inspect method "${method}"`)
|
|
571
|
+
return {
|
|
572
|
+
current: acpSessionStats.current(),
|
|
573
|
+
api: {
|
|
574
|
+
service: 'acpSessionStats',
|
|
575
|
+
inject: ['acpSessionStats'],
|
|
576
|
+
current: {
|
|
577
|
+
call: 'ctx.acpSessionStats.current()',
|
|
578
|
+
returns: 'SessionStatsSnapshot',
|
|
579
|
+
},
|
|
580
|
+
subscribe: {
|
|
581
|
+
call: 'ctx.acpSessionStats.subscribe((snapshot) => { ... })',
|
|
582
|
+
returns: { type: 'function', role: 'dispose', idempotent: true },
|
|
583
|
+
},
|
|
584
|
+
transport: 'standard ACP prompt responses and session/update notifications',
|
|
585
|
+
},
|
|
586
|
+
referencedTypes: ['SessionStatsSnapshot'],
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function mountClientHalf(
|
|
593
|
+
ctx,
|
|
594
|
+
pluginId,
|
|
595
|
+
clientCode,
|
|
596
|
+
tuiTheme,
|
|
597
|
+
tuiSlots,
|
|
598
|
+
tuiCommands,
|
|
599
|
+
tuiOverlay,
|
|
600
|
+
acpSessionConfig,
|
|
601
|
+
acpSessionPlan,
|
|
602
|
+
acpSessionStats,
|
|
603
|
+
timer,
|
|
604
|
+
invoke,
|
|
605
|
+
) {
|
|
606
|
+
if (typeof ctx.plugin !== 'function') {
|
|
607
|
+
return applyClientHalf(clientCode, {
|
|
608
|
+
pluginId,
|
|
609
|
+
tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
610
|
+
acpSessionStats, timer, invoke,
|
|
611
|
+
})
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
let applied
|
|
615
|
+
const fiber = ctx.plugin({
|
|
616
|
+
name: `tui-dynamic:${pluginId}`,
|
|
617
|
+
inject: [],
|
|
618
|
+
apply: async () => {
|
|
619
|
+
applied = await applyClientHalf(clientCode, {
|
|
620
|
+
pluginId,
|
|
621
|
+
tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
622
|
+
acpSessionStats, timer, invoke,
|
|
623
|
+
})
|
|
624
|
+
return applied.dispose
|
|
625
|
+
},
|
|
626
|
+
})
|
|
627
|
+
try {
|
|
628
|
+
await fiber
|
|
629
|
+
} catch (error) {
|
|
630
|
+
await fiber.dispose()
|
|
631
|
+
throw error
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
waitingFor: applied.waitingFor,
|
|
635
|
+
dispose() {
|
|
636
|
+
applied.dispose()
|
|
637
|
+
return fiber.dispose()
|
|
638
|
+
},
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Bind inspect/run to the mux after the painter is up.
|
|
644
|
+
* @param {{
|
|
645
|
+
* ctx: object,
|
|
646
|
+
* tuiTheme: object,
|
|
647
|
+
* tuiSlots?: object,
|
|
648
|
+
* tuiCommands?: object,
|
|
649
|
+
* tuiOverlay?: object,
|
|
650
|
+
* acpSessionConfig?: object,
|
|
651
|
+
* acpSessionPlan?: object,
|
|
652
|
+
* acpSessionStats?: object,
|
|
653
|
+
* requestAgent: (method: string, params?: object) => Promise<unknown>,
|
|
654
|
+
* }} opts
|
|
655
|
+
* @returns {{ onHost: (message: object) => void }}
|
|
656
|
+
*/
|
|
657
|
+
export function attachTuiClient(opts) {
|
|
658
|
+
const {
|
|
659
|
+
ctx, tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
660
|
+
acpSessionStats,
|
|
661
|
+
requestAgent,
|
|
662
|
+
} = opts
|
|
663
|
+
const providers = [
|
|
664
|
+
themeInspectProvider(tuiTheme),
|
|
665
|
+
...(tuiSlots === undefined ? [] : [slotInspectProvider(tuiSlots)]),
|
|
666
|
+
...(tuiCommands === undefined ? [] : [commandInspectProvider(tuiCommands)]),
|
|
667
|
+
...(tuiOverlay === undefined ? [] : [overlayInspectProvider(tuiOverlay)]),
|
|
668
|
+
...(acpSessionConfig === undefined ? [] : [configOptionsInspectProvider(acpSessionConfig)]),
|
|
669
|
+
...(acpSessionPlan === undefined ? [] : [planInspectProvider(acpSessionPlan)]),
|
|
670
|
+
...(acpSessionStats === undefined ? [] : [statsInspectProvider(acpSessionStats)]),
|
|
671
|
+
]
|
|
672
|
+
const timer = createClientTimer()
|
|
673
|
+
/** @type {Map<string, () => void>} */
|
|
674
|
+
const loaded = new Map()
|
|
675
|
+
const pendingThemeSelections = new Map()
|
|
676
|
+
|
|
677
|
+
function sync() {
|
|
678
|
+
void requestAgent(CORDIS_METHODS.inspectSync, { providers: providers.map((provider) => provider.manifest) }).catch(() => {
|
|
679
|
+
// Agent has no TUI Client extras (Zed / non-dsh ACP). Stay a standard client.
|
|
680
|
+
})
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async function answerQuery(params) {
|
|
684
|
+
const request = params !== null && typeof params === 'object' ? params : {}
|
|
685
|
+
const requestId = request.requestId
|
|
686
|
+
const agentId = request.agentId
|
|
687
|
+
if (typeof requestId !== 'string' || typeof agentId !== 'string') return
|
|
688
|
+
let resolution
|
|
689
|
+
try {
|
|
690
|
+
const provider = providers.find((candidate) => candidate.manifest.id === request.provider)
|
|
691
|
+
if (provider === undefined) {
|
|
692
|
+
resolution = {
|
|
693
|
+
ok: false,
|
|
694
|
+
reason: 'provider-missing',
|
|
695
|
+
message: `Client inspect provider "${String(request.provider)}" is unavailable`,
|
|
696
|
+
}
|
|
697
|
+
} else {
|
|
698
|
+
const data = await provider.query(request.method, request.input)
|
|
699
|
+
resolution = { ok: true, data }
|
|
700
|
+
}
|
|
701
|
+
} catch (error) {
|
|
702
|
+
resolution = {
|
|
703
|
+
ok: false,
|
|
704
|
+
reason: 'provider-error',
|
|
705
|
+
message: error instanceof Error ? error.message : String(error),
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
await requestAgent(CORDIS_METHODS.inspectResolve, { agentId, requestId, resolution }).catch(() => {})
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function runClient(params, direct = false) {
|
|
712
|
+
const request = params !== null && typeof params === 'object' ? params : {}
|
|
713
|
+
const requestId = request.requestId
|
|
714
|
+
if (!direct && typeof requestId !== 'string') return
|
|
715
|
+
const settle = (resolution) => requestAgent(
|
|
716
|
+
direct ? CORDIS_METHODS.settleUserRun : CORDIS_METHODS.resolveRequestRun,
|
|
717
|
+
direct
|
|
718
|
+
? { agentId: request.agentId, pluginId: request.pluginId, resolution }
|
|
719
|
+
: { requestId, resolution },
|
|
720
|
+
)
|
|
721
|
+
let host = direct && typeof request.pluginRunId === 'string'
|
|
722
|
+
? { ok: true, pluginRunId: request.pluginRunId }
|
|
723
|
+
: undefined
|
|
724
|
+
const previousThemeOwner = tuiTheme.owner?.(tuiTheme.active())
|
|
725
|
+
try {
|
|
726
|
+
if (!direct) {
|
|
727
|
+
host = await requestAgent(CORDIS_METHODS.runHost, {
|
|
728
|
+
agentId: request.agentId,
|
|
729
|
+
pluginId: request.pluginId,
|
|
730
|
+
packageId: request.packageId,
|
|
731
|
+
mode: request.mode,
|
|
732
|
+
requestId,
|
|
733
|
+
approveFutureVersions: false,
|
|
734
|
+
})
|
|
735
|
+
}
|
|
736
|
+
if (host === null || typeof host !== 'object' || host.ok !== true) {
|
|
737
|
+
if (!direct) {
|
|
738
|
+
await settle({
|
|
739
|
+
ok: false,
|
|
740
|
+
reason: 'host-half-failed',
|
|
741
|
+
message: host && typeof host === 'object' && typeof host.message === 'string'
|
|
742
|
+
? host.message
|
|
743
|
+
: 'Host half failed',
|
|
744
|
+
...(typeof host?.pluginRunId === 'string' ? { pluginRunId: host.pluginRunId } : {}),
|
|
745
|
+
})
|
|
746
|
+
}
|
|
747
|
+
return
|
|
748
|
+
}
|
|
749
|
+
if (direct && request.hasClientHalf !== true) return
|
|
750
|
+
const source = await requestAgent(CORDIS_METHODS.getClientCode, {
|
|
751
|
+
agentId: request.agentId,
|
|
752
|
+
pluginId: request.pluginId,
|
|
753
|
+
pluginRunId: host.pluginRunId,
|
|
754
|
+
})
|
|
755
|
+
const code = source && typeof source === 'object' ? source.code : undefined
|
|
756
|
+
loaded.get(request.pluginId)?.()
|
|
757
|
+
const applied = await mountClientHalf(
|
|
758
|
+
ctx,
|
|
759
|
+
request.pluginId,
|
|
760
|
+
typeof code === 'string' ? code : '',
|
|
761
|
+
tuiTheme,
|
|
762
|
+
tuiSlots,
|
|
763
|
+
tuiCommands,
|
|
764
|
+
tuiOverlay,
|
|
765
|
+
acpSessionConfig,
|
|
766
|
+
acpSessionPlan,
|
|
767
|
+
acpSessionStats,
|
|
768
|
+
timer,
|
|
769
|
+
async (method, args) => {
|
|
770
|
+
const answered = await requestAgent(CORDIS_METHODS.pluginInvoke, {
|
|
771
|
+
agentId: request.agentId,
|
|
772
|
+
pluginId: request.pluginId,
|
|
773
|
+
pluginRunId: host.pluginRunId,
|
|
774
|
+
method,
|
|
775
|
+
args,
|
|
776
|
+
})
|
|
777
|
+
if (answered !== null && typeof answered === 'object' && answered.ok === true) {
|
|
778
|
+
return answered.value
|
|
779
|
+
}
|
|
780
|
+
const code = answered !== null && typeof answered === 'object' ? answered.code : undefined
|
|
781
|
+
const message = answered !== null && typeof answered === 'object' ? answered.message : undefined
|
|
782
|
+
throw new Error(
|
|
783
|
+
`host.call("${String(method)}") failed`
|
|
784
|
+
+ `${typeof code === 'string' ? ` (${code})` : ''}: `
|
|
785
|
+
+ `${typeof message === 'string' ? message : 'invalid Host response'}`,
|
|
786
|
+
)
|
|
787
|
+
},
|
|
788
|
+
)
|
|
789
|
+
loaded.set(request.pluginId, applied.dispose)
|
|
790
|
+
const selectedTheme = pendingThemeSelections.get(request.pluginId)
|
|
791
|
+
if (selectedTheme !== undefined && tuiTheme.isLoaded?.(selectedTheme) === true) {
|
|
792
|
+
pendingThemeSelections.delete(request.pluginId)
|
|
793
|
+
tuiTheme.activate(selectedTheme)
|
|
794
|
+
}
|
|
795
|
+
const selectedThemeOwner = tuiTheme.owner?.(tuiTheme.active())
|
|
796
|
+
if (selectedThemeOwner === request.pluginId
|
|
797
|
+
&& previousThemeOwner !== undefined
|
|
798
|
+
&& previousThemeOwner !== selectedThemeOwner) {
|
|
799
|
+
try {
|
|
800
|
+
const stopped = await requestAgent(CORDIS_METHODS.pluginStop, {
|
|
801
|
+
agentId: request.agentId,
|
|
802
|
+
pluginId: previousThemeOwner,
|
|
803
|
+
})
|
|
804
|
+
if (stopped?.ok === false) {
|
|
805
|
+
throw new Error(typeof stopped.message === 'string'
|
|
806
|
+
? stopped.message
|
|
807
|
+
: `failed to stop theme Plugin "${previousThemeOwner}"`)
|
|
808
|
+
}
|
|
809
|
+
} catch (error) {
|
|
810
|
+
if (loaded.get(request.pluginId) === applied.dispose) {
|
|
811
|
+
loaded.delete(request.pluginId)
|
|
812
|
+
applied.dispose()
|
|
813
|
+
}
|
|
814
|
+
throw error
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
await settle({
|
|
818
|
+
ok: true,
|
|
819
|
+
pluginRunId: host.pluginRunId,
|
|
820
|
+
waitingFor: applied.waitingFor,
|
|
821
|
+
})
|
|
822
|
+
} catch (error) {
|
|
823
|
+
if (!direct || (host !== null && typeof host === 'object' && host.ok === true)) {
|
|
824
|
+
await settle({
|
|
825
|
+
ok: false,
|
|
826
|
+
reason: 'client-half-failed',
|
|
827
|
+
message: error instanceof Error ? error.message : String(error),
|
|
828
|
+
...(typeof host?.pluginRunId === 'string' ? { pluginRunId: host.pluginRunId } : {}),
|
|
829
|
+
...(direct && typeof request.startedHere === 'boolean'
|
|
830
|
+
? { startedHere: request.startedHere }
|
|
831
|
+
: {}),
|
|
832
|
+
}).catch(() => {})
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function onHost(message) {
|
|
838
|
+
switch (message.method) {
|
|
839
|
+
case CORDIS_METHODS.inspectQuery:
|
|
840
|
+
return answerQuery(message.params)
|
|
841
|
+
case CORDIS_METHODS.requestRun:
|
|
842
|
+
return runClient(message.params)
|
|
843
|
+
case CORDIS_METHODS.userRun:
|
|
844
|
+
return runClient(message.params, true)
|
|
845
|
+
case CORDIS_METHODS.pluginRetract: {
|
|
846
|
+
const pluginId = message.params && typeof message.params === 'object'
|
|
847
|
+
? message.params.pluginId
|
|
848
|
+
: undefined
|
|
849
|
+
if (typeof pluginId === 'string') {
|
|
850
|
+
loaded.get(pluginId)?.()
|
|
851
|
+
loaded.delete(pluginId)
|
|
852
|
+
}
|
|
853
|
+
return
|
|
854
|
+
}
|
|
855
|
+
default:
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
async function selectTheme(params) {
|
|
860
|
+
const request = params !== null && typeof params === 'object' ? params : {}
|
|
861
|
+
if (request.protocol !== 0) throw new Error('unsupported theme selection payload')
|
|
862
|
+
if (typeof request.agentId !== 'string' || request.agentId.length === 0) {
|
|
863
|
+
throw new Error('theme selection needs agentId')
|
|
864
|
+
}
|
|
865
|
+
if (typeof request.id !== 'string' || request.id.length === 0) {
|
|
866
|
+
throw new Error('theme selection needs id')
|
|
867
|
+
}
|
|
868
|
+
if (!tuiTheme.list().some((entry) => entry.id === request.id)) {
|
|
869
|
+
throw new Error(`theme Plugin "${request.id}" is not registered`)
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const previousId = tuiTheme.active()
|
|
873
|
+
const previousOwner = tuiTheme.owner?.(previousId)
|
|
874
|
+
const targetOwner = tuiTheme.owner?.(request.id)
|
|
875
|
+
|
|
876
|
+
if (targetOwner === undefined || loaded.has(targetOwner)) {
|
|
877
|
+
tuiTheme.activate(request.id)
|
|
878
|
+
if (previousOwner !== undefined && previousOwner !== targetOwner) {
|
|
879
|
+
try {
|
|
880
|
+
const stopped = await requestAgent(CORDIS_METHODS.pluginStop, {
|
|
881
|
+
agentId: request.agentId,
|
|
882
|
+
pluginId: previousOwner,
|
|
883
|
+
})
|
|
884
|
+
if (stopped?.ok === false) {
|
|
885
|
+
throw new Error(typeof stopped.message === 'string'
|
|
886
|
+
? stopped.message
|
|
887
|
+
: `failed to stop theme Plugin "${previousOwner}"`)
|
|
888
|
+
}
|
|
889
|
+
} catch (error) {
|
|
890
|
+
if (tuiTheme.isLoaded?.(previousId) === true) tuiTheme.activate(previousId)
|
|
891
|
+
throw error
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return { ok: true, status: 'selected', id: request.id }
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
pendingThemeSelections.set(targetOwner, request.id)
|
|
898
|
+
try {
|
|
899
|
+
const started = await requestAgent(CORDIS_METHODS.pluginStart, {
|
|
900
|
+
agentId: request.agentId,
|
|
901
|
+
pluginId: targetOwner,
|
|
902
|
+
})
|
|
903
|
+
if (started?.ok === false) {
|
|
904
|
+
throw new Error(typeof started.message === 'string'
|
|
905
|
+
? started.message
|
|
906
|
+
: `failed to start theme Plugin "${targetOwner}"`)
|
|
907
|
+
}
|
|
908
|
+
if (loaded.has(targetOwner) && tuiTheme.isLoaded?.(request.id) === true) {
|
|
909
|
+
pendingThemeSelections.delete(targetOwner)
|
|
910
|
+
tuiTheme.activate(request.id)
|
|
911
|
+
}
|
|
912
|
+
return { ok: true, status: 'starting', id: request.id }
|
|
913
|
+
} catch (error) {
|
|
914
|
+
pendingThemeSelections.delete(targetOwner)
|
|
915
|
+
throw error
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function dispose() {
|
|
920
|
+
for (const release of loaded.values()) release()
|
|
921
|
+
loaded.clear()
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return { onHost, selectTheme, sync, dispose }
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/** Mount the TUI counterpart of the Web Cordis Client runner. */
|
|
928
|
+
export function apply(ctx) {
|
|
929
|
+
const tuiTheme = ctx.tuiTheme ?? ctx.get?.('tuiTheme')
|
|
930
|
+
const tuiSlots = ctx.tuiSlots ?? ctx.get?.('tuiSlots')
|
|
931
|
+
const tuiCommands = ctx.tuiCommands ?? ctx.get?.('tuiCommands')
|
|
932
|
+
const tuiOverlay = ctx.tuiOverlay ?? ctx.get?.('tuiOverlay')
|
|
933
|
+
const acpSessionConfig = ctx.acpSessionConfig ?? ctx.get?.('acpSessionConfig')
|
|
934
|
+
const acpSessionPlan = ctx.acpSessionPlan ?? ctx.get?.('acpSessionPlan')
|
|
935
|
+
const acpSessionStats = ctx.acpSessionStats ?? ctx.get?.('acpSessionStats')
|
|
936
|
+
let client
|
|
937
|
+
const service = {
|
|
938
|
+
bindTransport(requestAgent) {
|
|
939
|
+
client?.dispose()
|
|
940
|
+
client = attachTuiClient({
|
|
941
|
+
ctx, tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
942
|
+
acpSessionStats,
|
|
943
|
+
requestAgent,
|
|
944
|
+
})
|
|
945
|
+
const attached = client
|
|
946
|
+
return () => {
|
|
947
|
+
if (client !== attached) return
|
|
948
|
+
attached.dispose()
|
|
949
|
+
client = undefined
|
|
950
|
+
}
|
|
951
|
+
},
|
|
952
|
+
onHost(message) {
|
|
953
|
+
return client?.onHost(message)
|
|
954
|
+
},
|
|
955
|
+
selectTheme(params) {
|
|
956
|
+
if (client === undefined) throw new Error('TUI Cordis Client runner is not attached')
|
|
957
|
+
return client.selectTheme(params)
|
|
958
|
+
},
|
|
959
|
+
sync() {
|
|
960
|
+
client?.sync()
|
|
961
|
+
},
|
|
962
|
+
}
|
|
963
|
+
if (typeof ctx.provide === 'function') {
|
|
964
|
+
ctx.provide('tuiCordisClientRunner', service)
|
|
965
|
+
} else {
|
|
966
|
+
ctx.tuiCordisClientRunner = service
|
|
967
|
+
}
|
|
968
|
+
if (typeof ctx.effect === 'function') {
|
|
969
|
+
ctx.effect(() => () => client?.dispose(), 'tui-cordis-client-runner')
|
|
970
|
+
}
|
|
971
|
+
}
|