martty 0.2.15-beta.1 → 0.2.16
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/README.md +3 -2
- package/lib/boot.js +21 -1
- package/lib/client-run.js +19 -2
- package/lib/deepseek-logo.js +23 -56
- package/lib/index.js +10 -4
- package/lib/inspect.js +74 -8
- package/lib/martty-preset.js +24 -0
- package/lib/tui-commands.js +62 -3
- package/lib/tui-overlay.js +86 -2
- package/lib/tui-presets.js +242 -0
- package/lib/tui-slots.js +30 -0
- package/package.json +4 -2
- package/skills/tui-plugin-development/SKILL.md +9 -1
- 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/README.md
CHANGED
|
@@ -85,8 +85,9 @@ dsh-tui --demo-skin
|
|
|
85
85
|
platform-native modifier bindings, mouse selection, and inline-expanded tools.
|
|
86
86
|
- Dark/light themes, clipboard routing for local, tmux, and SSH sessions, plus
|
|
87
87
|
the optional `/liang` pixel companion.
|
|
88
|
-
-
|
|
89
|
-
Harness
|
|
88
|
+
- Persistent UI Presets selected with the native `/ui` picker (or `/ui <id>`): builtin Martty and the classic
|
|
89
|
+
DeepSeek Harness composition, both assembled from independent welcome Hero
|
|
90
|
+
and information slots without writing to the transcript.
|
|
90
91
|
- A root `chrome.right` plugin rail for validated TuiNode trees, with live
|
|
91
92
|
update/unload and Client inspect support for Creator-authored plugins.
|
|
92
93
|
- Lifecycle-owned local commands and native slider overlays, plus transactions
|
package/lib/boot.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { apply as applyAcpClient } from './acp-client.js'
|
|
10
|
+
import { homedir } from 'node:os'
|
|
11
|
+
import path from 'node:path'
|
|
10
12
|
import { apply as applyCordisClientRunner } from './inspect.js'
|
|
11
13
|
import { apply as applyShell } from './index.js'
|
|
12
14
|
import { resolveStackedAgent } from './agent.js'
|
|
@@ -14,6 +16,8 @@ import { apply as applySlots } from './tui-slots.js'
|
|
|
14
16
|
import { apply as applyTheme } from './tui-theme.js'
|
|
15
17
|
import { apply as applyCommands } from './tui-commands.js'
|
|
16
18
|
import { apply as applyOverlay } from './tui-overlay.js'
|
|
19
|
+
import { apply as applyPresets, inject as presetsInject } from './tui-presets.js'
|
|
20
|
+
import { apply as applyMarttyPreset, inject as marttyPresetInject } from './martty-preset.js'
|
|
17
21
|
import { apply as applyPlanView, inject as planViewInject } from './plan-view.js'
|
|
18
22
|
import { apply as applyStatsView, inject as statsViewInject } from './stats-view.js'
|
|
19
23
|
import { apply as applySessionStatus, inject as sessionStatusInject } from './acp-session-status.js'
|
|
@@ -32,11 +36,16 @@ export async function bootClient(options = {}) {
|
|
|
32
36
|
const acpConfig = options.stream !== undefined
|
|
33
37
|
? { stream: options.stream }
|
|
34
38
|
: { agent: options.agent ?? resolveStackedAgent() }
|
|
39
|
+
const presetConfig = {
|
|
40
|
+
settingsPath: options.settingsPath ?? uiSettingsPath(options.extraArgs ?? []),
|
|
41
|
+
}
|
|
35
42
|
if (typeof ctx.plugin === 'function') {
|
|
36
43
|
await ctx.plugin({ name: 'tui-theme', inject: [], apply: applyTheme })
|
|
37
44
|
await ctx.plugin({ name: 'tui-slots', inject: [], apply: applySlots })
|
|
38
45
|
await ctx.plugin({ name: 'tui-commands', inject: [], apply: applyCommands })
|
|
39
46
|
await ctx.plugin({ name: 'tui-overlay', inject: [], apply: applyOverlay })
|
|
47
|
+
await ctx.plugin({ name: 'tui-presets', inject: presetsInject, apply: applyPresets }, presetConfig)
|
|
48
|
+
await ctx.plugin({ name: 'martty-preset', inject: marttyPresetInject, apply: applyMarttyPreset })
|
|
40
49
|
await ctx.plugin({ name: 'acp-client', inject: [], apply: applyAcpClient }, acpConfig)
|
|
41
50
|
await ctx.plugin({ name: 'plan-view', inject: planViewInject, apply: applyPlanView })
|
|
42
51
|
await ctx.plugin({ name: 'stats-view', inject: statsViewInject, apply: applyStatsView })
|
|
@@ -46,7 +55,7 @@ export async function bootClient(options = {}) {
|
|
|
46
55
|
await ctx.plugin({
|
|
47
56
|
name: 'tui-cordis-client-runner',
|
|
48
57
|
inject: [
|
|
49
|
-
'tuiTheme', 'tuiSlots', 'tuiCommands', 'tuiOverlay', 'acpSessionConfig',
|
|
58
|
+
'tuiTheme', 'tuiPresets', 'tuiSlots', 'tuiCommands', 'tuiOverlay', 'acpSessionConfig',
|
|
50
59
|
'acpSessionPlan', 'acpSessionStats', 'acpSessionStatus',
|
|
51
60
|
],
|
|
52
61
|
apply: applyCordisClientRunner,
|
|
@@ -67,6 +76,8 @@ export async function bootClient(options = {}) {
|
|
|
67
76
|
applySlots(ctx)
|
|
68
77
|
applyCommands(ctx)
|
|
69
78
|
applyOverlay(ctx)
|
|
79
|
+
applyPresets(ctx, presetConfig)
|
|
80
|
+
applyMarttyPreset(ctx)
|
|
70
81
|
applyAcpClient(ctx, acpConfig)
|
|
71
82
|
applyPlanView(ctx)
|
|
72
83
|
applyStatsView(ctx)
|
|
@@ -79,6 +90,15 @@ export async function bootClient(options = {}) {
|
|
|
79
90
|
return ctx
|
|
80
91
|
}
|
|
81
92
|
|
|
93
|
+
export function uiSettingsPath(extraArgs = []) {
|
|
94
|
+
for (let index = 0; index < extraArgs.length; index += 1) {
|
|
95
|
+
if (extraArgs[index] === '--session-root' && typeof extraArgs[index + 1] === 'string') {
|
|
96
|
+
return path.join(extraArgs[index + 1], 'dsh-tui-settings.json')
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return path.join(homedir(), '.dsh-tui', 'sessions', 'dsh-tui-settings.json')
|
|
100
|
+
}
|
|
101
|
+
|
|
82
102
|
/**
|
|
83
103
|
* Parse `--agent` / `--agent-arg` from argv. Remaining flags pass through to Rust.
|
|
84
104
|
* `--agent` is also forwarded via {@link painterArgs} so Terminal Auth can
|
package/lib/client-run.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Evaluate a Cordis `code.client` function body on the TUI client tree.
|
|
3
3
|
*
|
|
4
4
|
* Same closure convention as the web runner: the source is an async function
|
|
5
|
-
* body that returns a plugin. Open services include `tuiTheme`, `tuiSlots`,
|
|
5
|
+
* body that returns a plugin. Open services include `tuiTheme`, `tuiPresets`, `tuiSlots`,
|
|
6
6
|
* `acpSessionConfig`, `acpSessionPlan`, `acpSessionStats`, `acpSessionStatus`,
|
|
7
7
|
* and lifecycle-owned `timer`; `host.call` reaches this
|
|
8
8
|
* Package's Host half.
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
const ALLOWED_INJECT = new Set([
|
|
13
13
|
'tuiTheme',
|
|
14
|
+
'tuiPresets',
|
|
14
15
|
'tuiSlots',
|
|
15
16
|
'tuiCommands',
|
|
16
17
|
'tuiOverlay',
|
|
@@ -86,7 +87,7 @@ export async function applyClientHalf(clientCode, env) {
|
|
|
86
87
|
}
|
|
87
88
|
if (!ALLOWED_INJECT.has(name)) {
|
|
88
89
|
throw new Error(
|
|
89
|
-
`TUI Client inject "${name}" is not open. Open services: tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan, acpSessionStats, acpSessionStatus, timer.`,
|
|
90
|
+
`TUI Client inject "${name}" is not open. Open services: tuiTheme, tuiPresets, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan, acpSessionStats, acpSessionStatus, timer.`,
|
|
90
91
|
)
|
|
91
92
|
}
|
|
92
93
|
}
|
|
@@ -133,6 +134,7 @@ function normalizePlugin(value) {
|
|
|
133
134
|
|
|
134
135
|
function restrictedCtx(env, own, inject) {
|
|
135
136
|
const sourceTheme = env.tuiTheme
|
|
137
|
+
const sourcePresets = env.tuiPresets
|
|
136
138
|
const sourceSlots = env.tuiSlots
|
|
137
139
|
const sourceCommands = env.tuiCommands
|
|
138
140
|
const sourceOverlay = env.tuiOverlay
|
|
@@ -183,6 +185,19 @@ function restrictedCtx(env, own, inject) {
|
|
|
183
185
|
return sourceSlots.list()
|
|
184
186
|
},
|
|
185
187
|
}
|
|
188
|
+
const tuiPresets = sourcePresets === undefined
|
|
189
|
+
? undefined
|
|
190
|
+
: {
|
|
191
|
+
register(options, mount) {
|
|
192
|
+
return own(sourcePresets.register(options, mount))
|
|
193
|
+
},
|
|
194
|
+
list() {
|
|
195
|
+
return sourcePresets.list()
|
|
196
|
+
},
|
|
197
|
+
active() {
|
|
198
|
+
return sourcePresets.active()
|
|
199
|
+
},
|
|
200
|
+
}
|
|
186
201
|
const tuiCommands = sourceCommands === undefined
|
|
187
202
|
? undefined
|
|
188
203
|
: {
|
|
@@ -279,6 +294,7 @@ function restrictedCtx(env, own, inject) {
|
|
|
279
294
|
}
|
|
280
295
|
return {
|
|
281
296
|
tuiTheme,
|
|
297
|
+
tuiPresets,
|
|
282
298
|
tuiSlots,
|
|
283
299
|
tuiCommands,
|
|
284
300
|
tuiOverlay,
|
|
@@ -291,6 +307,7 @@ function restrictedCtx(env, own, inject) {
|
|
|
291
307
|
timeout: timer?.timeout,
|
|
292
308
|
get(name) {
|
|
293
309
|
if (name === 'tuiTheme') return tuiTheme
|
|
310
|
+
if (name === 'tuiPresets') return tuiPresets
|
|
294
311
|
if (name === 'tuiSlots') return tuiSlots
|
|
295
312
|
if (name === 'tuiCommands') return tuiCommands
|
|
296
313
|
if (name === 'tuiOverlay') return tuiOverlay
|
package/lib/deepseek-logo.js
CHANGED
|
@@ -1,62 +1,29 @@
|
|
|
1
|
-
/** Built-in Client Plugin: the classic DeepSeek Harness
|
|
1
|
+
/** Built-in Client Plugin: the classic DeepSeek Harness UI Preset. */
|
|
2
2
|
|
|
3
3
|
export const name = 'deepseek-logo'
|
|
4
|
-
export const inject = ['
|
|
5
|
-
|
|
6
|
-
const WHALE_LG = [
|
|
7
|
-
' ▄▄▄▄ ▄▄▄███ █▄',
|
|
8
|
-
' ▄▄█████████████ ███▄▄ ▄█',
|
|
9
|
-
' ▄█████████████████▄▄ █████▄██████',
|
|
10
|
-
' ▄█████████████████████▄ ▀██████████',
|
|
11
|
-
' ▄████████████████████████▄ ▀████▀▀▀',
|
|
12
|
-
' ██▀ ▀▀▀██████████▀▀█████▄████',
|
|
13
|
-
' ███ ▀███████▀▄ ▀████████',
|
|
14
|
-
' ███ ▀███████ ██████▀',
|
|
15
|
-
' ████ ▀██████████████',
|
|
16
|
-
' ████ ▀████████████',
|
|
17
|
-
' ████▄ ▄▄▄ █████████▀',
|
|
18
|
-
' ▀████▄ ███▄▄ ▀██████▄',
|
|
19
|
-
' ▀█████████████▄▄▄████████',
|
|
20
|
-
' ▀▀████████████▀▀',
|
|
21
|
-
' ▀▀▀▀▀▀',
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
const WORDMARK_SMALL = [
|
|
25
|
-
' ___ ___ ___ ___ ___ ___ ___ _ __',
|
|
26
|
-
'| \\| __| __| _ \\/ __| __| __| |/ /',
|
|
27
|
-
'| |) | _|| _|| _/\\__ \\ _|| _|| \' < ',
|
|
28
|
-
'|___/|___|___|_| |___/___|___|_|\\_\\',
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
export function deepseekLogoMarkdown() {
|
|
32
|
-
return [
|
|
33
|
-
'## DeepSeek Harness',
|
|
34
|
-
'',
|
|
35
|
-
'```text',
|
|
36
|
-
...WHALE_LG,
|
|
37
|
-
'',
|
|
38
|
-
...WORDMARK_SMALL,
|
|
39
|
-
'H A R N E S S',
|
|
40
|
-
'```',
|
|
41
|
-
'',
|
|
42
|
-
'_Into the Unknown_',
|
|
43
|
-
].join('\n')
|
|
44
|
-
}
|
|
4
|
+
export const inject = ['tuiPresets', 'tuiSlots']
|
|
45
5
|
|
|
46
6
|
export function apply(ctx) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
7
|
+
return ctx.tuiPresets.register({ id: 'deepseek', label: 'DeepSeek' }, () => {
|
|
8
|
+
const hero = ctx.tuiSlots.register(
|
|
9
|
+
{ name: 'welcome.hero', id: 'deepseek-hero' },
|
|
10
|
+
[
|
|
11
|
+
{ id: 'logo', kind: 'logo', name: 'deepseek' },
|
|
12
|
+
{
|
|
13
|
+
id: 'hint',
|
|
14
|
+
kind: 'text',
|
|
15
|
+
text: 'Into the Unknown',
|
|
16
|
+
tone: 'fg_tertiary',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
)
|
|
20
|
+
const info = ctx.tuiSlots.register(
|
|
21
|
+
{ name: 'welcome.info', id: 'deepseek-info' },
|
|
22
|
+
[{ id: 'info', kind: 'welcomeinfo' }],
|
|
23
|
+
)
|
|
24
|
+
return () => {
|
|
25
|
+
info.dispose()
|
|
26
|
+
hero.dispose()
|
|
27
|
+
}
|
|
60
28
|
})
|
|
61
|
-
return () => stopCommand?.()
|
|
62
29
|
}
|
package/lib/index.js
CHANGED
|
@@ -184,6 +184,7 @@ export async function applyShell(ctx, options = {}) {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
if (!connection.muxed) {
|
|
187
|
+
let republishCompositorState = () => {}
|
|
187
188
|
const mux = muxAcpAndCompositor({
|
|
188
189
|
agent,
|
|
189
190
|
tui: connection,
|
|
@@ -191,6 +192,7 @@ export async function applyShell(ctx, options = {}) {
|
|
|
191
192
|
clientRunner.onHost(message)
|
|
192
193
|
},
|
|
193
194
|
onCordisReady() {
|
|
195
|
+
republishCompositorState()
|
|
194
196
|
clientRunner.sync()
|
|
195
197
|
},
|
|
196
198
|
onAcp(direction, message) {
|
|
@@ -213,10 +215,14 @@ export async function applyShell(ctx, options = {}) {
|
|
|
213
215
|
throw new Error(`unsupported Cordis TUI method: ${String(message.method)}`)
|
|
214
216
|
},
|
|
215
217
|
})
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
const notifyTui = (method, params) => mux.notifyTui(method, params)
|
|
219
|
+
republishCompositorState = () => {
|
|
220
|
+
handle.bindNotify(notifyTui)
|
|
221
|
+
slots.bindNotify(notifyTui)
|
|
222
|
+
commands.bindNotify(notifyTui)
|
|
223
|
+
overlay.bindNotify(notifyTui)
|
|
224
|
+
}
|
|
225
|
+
republishCompositorState()
|
|
220
226
|
clientRunner.bindTransport(mux.requestAgent)
|
|
221
227
|
sessionConfig.bindTransport(mux.requestTui)
|
|
222
228
|
connection.resume?.()
|
package/lib/inspect.js
CHANGED
|
@@ -9,7 +9,7 @@ import { CORDIS_METHODS } from './cordis-protocol.js'
|
|
|
9
9
|
|
|
10
10
|
export const name = 'tui-cordis-client-runner'
|
|
11
11
|
export const inject = [
|
|
12
|
-
'tuiTheme', 'tuiSlots', 'tuiCommands', 'tuiOverlay', 'acpSessionConfig',
|
|
12
|
+
'tuiTheme', 'tuiPresets', 'tuiSlots', 'tuiCommands', 'tuiOverlay', 'acpSessionConfig',
|
|
13
13
|
'acpSessionPlan', 'acpSessionStats', 'acpSessionStatus',
|
|
14
14
|
]
|
|
15
15
|
|
|
@@ -135,9 +135,43 @@ export function themeInspectProvider(tuiTheme) {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/** Describe saved compositions of several independent TUI contributions. */
|
|
139
|
+
export function uiPresetInspectProvider(tuiPresets) {
|
|
140
|
+
return {
|
|
141
|
+
manifest: {
|
|
142
|
+
id: 'UiPresets',
|
|
143
|
+
description:
|
|
144
|
+
'Saved UI compositions. One preset mounts several UI contributions as one lifecycle.',
|
|
145
|
+
methods: [{
|
|
146
|
+
name: 'list',
|
|
147
|
+
description: 'List registered UI Presets and the composition registration contract.',
|
|
148
|
+
inputSchema: EMPTY_INPUT,
|
|
149
|
+
outputSchema: ANY_OUTPUT,
|
|
150
|
+
}],
|
|
151
|
+
},
|
|
152
|
+
query(method) {
|
|
153
|
+
if (method !== 'list') throw new Error(`unknown UiPresets inspect method "${method}"`)
|
|
154
|
+
return {
|
|
155
|
+
active: tuiPresets.active(),
|
|
156
|
+
presets: tuiPresets.list(),
|
|
157
|
+
apply: {
|
|
158
|
+
inject: ['tuiPresets'],
|
|
159
|
+
register: {
|
|
160
|
+
call: 'ctx.tuiPresets.register({ id, label }, mount)',
|
|
161
|
+
mount:
|
|
162
|
+
'A synchronous callback that mounts multiple UI contributions and returns one disposer.',
|
|
163
|
+
},
|
|
164
|
+
selection: '/ui <id>',
|
|
165
|
+
persistence: 'The selected id survives restart in dsh-tui-settings.json.',
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
138
172
|
const TUI_NODE_KINDS = Object.freeze([
|
|
139
|
-
'
|
|
140
|
-
'image', 'notice', 'unknown',
|
|
173
|
+
'ascii', 'logo', 'welcomeinfo', 'text', 'group', 'markdown', 'reasoning',
|
|
174
|
+
'user', 'generic', 'terminal', 'diff', 'image', 'notice', 'unknown',
|
|
141
175
|
])
|
|
142
176
|
|
|
143
177
|
/** Describe the root TUI shell slots available to a dynamic Client half. */
|
|
@@ -168,7 +202,8 @@ export function slotInspectProvider(tuiSlots) {
|
|
|
168
202
|
update: 'const panel = register(...); panel.update(nextNodes)',
|
|
169
203
|
dispose: 'Return panel.dispose from inject; plugin unload removes the pane immediately.',
|
|
170
204
|
note:
|
|
171
|
-
'
|
|
205
|
+
'welcome.hero and welcome.info are independent single root regions; '
|
|
206
|
+
+ 'chrome.right is a root list slot; conversation.input.dock is the additive row above '
|
|
172
207
|
+ 'the composer; conversation.composer.dock is the additive compact row below it. '
|
|
173
208
|
+ 'Node ids are stable inside one contribution; '
|
|
174
209
|
+ 'the compositor namespaces them by contribution id. Compose group/markdown/reasoning/'
|
|
@@ -225,6 +260,27 @@ export function commandInspectProvider(tuiCommands) {
|
|
|
225
260
|
slashPrefix: false,
|
|
226
261
|
},
|
|
227
262
|
description: { type: 'string', minLength: 1 },
|
|
263
|
+
input: {
|
|
264
|
+
type: 'object',
|
|
265
|
+
required: ['hint'],
|
|
266
|
+
additionalProperties: false,
|
|
267
|
+
properties: {
|
|
268
|
+
hint: { type: 'string', minLength: 1 },
|
|
269
|
+
options: {
|
|
270
|
+
type: 'array',
|
|
271
|
+
items: {
|
|
272
|
+
type: 'object',
|
|
273
|
+
required: ['value'],
|
|
274
|
+
additionalProperties: false,
|
|
275
|
+
properties: {
|
|
276
|
+
value: { type: 'string', minLength: 1 },
|
|
277
|
+
label: { type: 'string' },
|
|
278
|
+
description: { type: 'string' },
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
228
284
|
},
|
|
229
285
|
},
|
|
230
286
|
handler: {
|
|
@@ -242,6 +298,12 @@ export function commandInspectProvider(tuiCommands) {
|
|
|
242
298
|
type: 'function',
|
|
243
299
|
role: 'dispose',
|
|
244
300
|
idempotent: true,
|
|
301
|
+
methods: {
|
|
302
|
+
update: {
|
|
303
|
+
arguments: [{ name: 'patch', type: 'object' }],
|
|
304
|
+
description: 'Refresh description or input completion metadata',
|
|
305
|
+
},
|
|
306
|
+
},
|
|
245
307
|
},
|
|
246
308
|
},
|
|
247
309
|
list: {
|
|
@@ -633,6 +695,7 @@ async function mountClientHalf(
|
|
|
633
695
|
pluginId,
|
|
634
696
|
clientCode,
|
|
635
697
|
tuiTheme,
|
|
698
|
+
tuiPresets,
|
|
636
699
|
tuiSlots,
|
|
637
700
|
tuiCommands,
|
|
638
701
|
tuiOverlay,
|
|
@@ -646,7 +709,7 @@ async function mountClientHalf(
|
|
|
646
709
|
if (typeof ctx.plugin !== 'function') {
|
|
647
710
|
return applyClientHalf(clientCode, {
|
|
648
711
|
pluginId,
|
|
649
|
-
tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
712
|
+
tuiTheme, tuiPresets, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
650
713
|
acpSessionStats, acpSessionStatus, timer, invoke,
|
|
651
714
|
})
|
|
652
715
|
}
|
|
@@ -658,7 +721,7 @@ async function mountClientHalf(
|
|
|
658
721
|
apply: async () => {
|
|
659
722
|
applied = await applyClientHalf(clientCode, {
|
|
660
723
|
pluginId,
|
|
661
|
-
tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
724
|
+
tuiTheme, tuiPresets, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
662
725
|
acpSessionStats, acpSessionStatus, timer, invoke,
|
|
663
726
|
})
|
|
664
727
|
return applied.dispose
|
|
@@ -697,12 +760,13 @@ async function mountClientHalf(
|
|
|
697
760
|
*/
|
|
698
761
|
export function attachTuiClient(opts) {
|
|
699
762
|
const {
|
|
700
|
-
ctx, tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
763
|
+
ctx, tuiTheme, tuiPresets, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
701
764
|
acpSessionStats, acpSessionStatus,
|
|
702
765
|
requestAgent,
|
|
703
766
|
} = opts
|
|
704
767
|
const providers = [
|
|
705
768
|
themeInspectProvider(tuiTheme),
|
|
769
|
+
...(tuiPresets === undefined ? [] : [uiPresetInspectProvider(tuiPresets)]),
|
|
706
770
|
...(tuiSlots === undefined ? [] : [slotInspectProvider(tuiSlots)]),
|
|
707
771
|
...(tuiCommands === undefined ? [] : [commandInspectProvider(tuiCommands)]),
|
|
708
772
|
...(tuiOverlay === undefined ? [] : [overlayInspectProvider(tuiOverlay)]),
|
|
@@ -801,6 +865,7 @@ export function attachTuiClient(opts) {
|
|
|
801
865
|
request.pluginId,
|
|
802
866
|
typeof code === 'string' ? code : '',
|
|
803
867
|
tuiTheme,
|
|
868
|
+
tuiPresets,
|
|
804
869
|
tuiSlots,
|
|
805
870
|
tuiCommands,
|
|
806
871
|
tuiOverlay,
|
|
@@ -970,6 +1035,7 @@ export function attachTuiClient(opts) {
|
|
|
970
1035
|
/** Mount the TUI counterpart of the Web Cordis Client runner. */
|
|
971
1036
|
export function apply(ctx) {
|
|
972
1037
|
const tuiTheme = ctx.tuiTheme ?? ctx.get?.('tuiTheme')
|
|
1038
|
+
const tuiPresets = ctx.tuiPresets ?? ctx.get?.('tuiPresets')
|
|
973
1039
|
const tuiSlots = ctx.tuiSlots ?? ctx.get?.('tuiSlots')
|
|
974
1040
|
const tuiCommands = ctx.tuiCommands ?? ctx.get?.('tuiCommands')
|
|
975
1041
|
const tuiOverlay = ctx.tuiOverlay ?? ctx.get?.('tuiOverlay')
|
|
@@ -982,7 +1048,7 @@ export function apply(ctx) {
|
|
|
982
1048
|
bindTransport(requestAgent) {
|
|
983
1049
|
client?.dispose()
|
|
984
1050
|
client = attachTuiClient({
|
|
985
|
-
ctx, tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
1051
|
+
ctx, tuiTheme, tuiPresets, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
986
1052
|
acpSessionStats, acpSessionStatus,
|
|
987
1053
|
requestAgent,
|
|
988
1054
|
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Built-in Martty UI Preset. */
|
|
2
|
+
|
|
3
|
+
export const name = 'martty-preset'
|
|
4
|
+
export const inject = ['tuiPresets', 'tuiSlots']
|
|
5
|
+
|
|
6
|
+
export function apply(ctx) {
|
|
7
|
+
return ctx.tuiPresets.register({ id: 'default', label: 'Martty' }, () => {
|
|
8
|
+
const hero = ctx.tuiSlots.register(
|
|
9
|
+
{ name: 'welcome.hero', id: 'martty-hero' },
|
|
10
|
+
[
|
|
11
|
+
{ id: 'logo', kind: 'logo', name: 'martty' },
|
|
12
|
+
{ id: 'hint', kind: 'text', text: 'https://martty.sh', tone: 'fg_tertiary' },
|
|
13
|
+
],
|
|
14
|
+
)
|
|
15
|
+
const info = ctx.tuiSlots.register(
|
|
16
|
+
{ name: 'welcome.info', id: 'martty-info' },
|
|
17
|
+
[{ id: 'info', kind: 'welcomeinfo' }],
|
|
18
|
+
)
|
|
19
|
+
return () => {
|
|
20
|
+
info.dispose()
|
|
21
|
+
hero.dispose()
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
}
|
package/lib/tui-commands.js
CHANGED
|
@@ -42,14 +42,60 @@ function validateCommand(options) {
|
|
|
42
42
|
if (typeof options.description !== 'string' || options.description.length === 0) {
|
|
43
43
|
throw new Error('tuiCommands.register: description must be a non-empty string')
|
|
44
44
|
}
|
|
45
|
-
const extra = Object.keys(options).filter((key) =>
|
|
45
|
+
const extra = Object.keys(options).filter((key) => !['name', 'description', 'input'].includes(key))
|
|
46
46
|
if (extra.length > 0) {
|
|
47
47
|
throw new Error(`tuiCommands.register: unknown option field(s) ${extra.join(', ')}`)
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
const command = {
|
|
50
50
|
name: options.name,
|
|
51
51
|
description: options.description,
|
|
52
52
|
}
|
|
53
|
+
if (options.input !== undefined) command.input = validateInput(options.input)
|
|
54
|
+
return command
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateInput(input) {
|
|
58
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
59
|
+
throw new Error('tuiCommands.register: input must be an object')
|
|
60
|
+
}
|
|
61
|
+
const extra = Object.keys(input).filter((key) => !['hint', 'options'].includes(key))
|
|
62
|
+
if (extra.length > 0) {
|
|
63
|
+
throw new Error(`tuiCommands.register: unknown input field(s) ${extra.join(', ')}`)
|
|
64
|
+
}
|
|
65
|
+
if (typeof input.hint !== 'string' || input.hint.length === 0) {
|
|
66
|
+
throw new Error('tuiCommands.register: input.hint must be a non-empty string')
|
|
67
|
+
}
|
|
68
|
+
if (input.options !== undefined && !Array.isArray(input.options)) {
|
|
69
|
+
throw new Error('tuiCommands.register: input.options must be an array')
|
|
70
|
+
}
|
|
71
|
+
const normalized = { hint: input.hint }
|
|
72
|
+
if (input.options !== undefined) {
|
|
73
|
+
normalized.options = input.options.map((option) => {
|
|
74
|
+
if (option === null || typeof option !== 'object' || Array.isArray(option)) {
|
|
75
|
+
throw new Error('tuiCommands.register: each input option must be an object')
|
|
76
|
+
}
|
|
77
|
+
const unknown = Object.keys(option)
|
|
78
|
+
.filter((key) => !['value', 'label', 'description'].includes(key))
|
|
79
|
+
if (unknown.length > 0) {
|
|
80
|
+
throw new Error(`tuiCommands.register: unknown input option field(s) ${unknown.join(', ')}`)
|
|
81
|
+
}
|
|
82
|
+
if (typeof option.value !== 'string' || option.value.length === 0) {
|
|
83
|
+
throw new Error('tuiCommands.register: input option value must be a non-empty string')
|
|
84
|
+
}
|
|
85
|
+
if (option.label !== undefined && typeof option.label !== 'string') {
|
|
86
|
+
throw new Error('tuiCommands.register: input option label must be a string')
|
|
87
|
+
}
|
|
88
|
+
if (option.description !== undefined && typeof option.description !== 'string') {
|
|
89
|
+
throw new Error('tuiCommands.register: input option description must be a string')
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
value: option.value,
|
|
93
|
+
...(option.label === undefined ? {} : { label: option.label }),
|
|
94
|
+
...(option.description === undefined ? {} : { description: option.description }),
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
return normalized
|
|
53
99
|
}
|
|
54
100
|
|
|
55
101
|
/**
|
|
@@ -101,11 +147,24 @@ export function installTuiCommands(ctx, options = {}) {
|
|
|
101
147
|
? effectCtx.effect(setup, `tuiCommands.register(${JSON.stringify(command.name)})`)
|
|
102
148
|
: setup()
|
|
103
149
|
let disposed = false
|
|
104
|
-
|
|
150
|
+
const dispose = () => {
|
|
105
151
|
if (disposed) return
|
|
106
152
|
disposed = true
|
|
107
153
|
return release?.()
|
|
108
154
|
}
|
|
155
|
+
dispose.update = (patch) => {
|
|
156
|
+
if (disposed) return
|
|
157
|
+
if (patch === null || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
158
|
+
throw new Error('tuiCommands.register: update patch must be an object')
|
|
159
|
+
}
|
|
160
|
+
const unknown = Object.keys(patch).filter((key) => !['description', 'input'].includes(key))
|
|
161
|
+
if (unknown.length > 0) {
|
|
162
|
+
throw new Error(`tuiCommands.register: unknown update field(s) ${unknown.join(', ')}`)
|
|
163
|
+
}
|
|
164
|
+
entry.command = validateCommand({ ...entry.command, ...patch, name: entry.command.name })
|
|
165
|
+
if (entry.active) publish()
|
|
166
|
+
}
|
|
167
|
+
return dispose
|
|
109
168
|
}
|
|
110
169
|
|
|
111
170
|
async function dispatch(params) {
|
package/lib/tui-overlay.js
CHANGED
|
@@ -19,6 +19,10 @@ class TuiOverlayService extends Service {
|
|
|
19
19
|
return this.core.openSlider(options, handlers)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
openSelect(options, handlers) {
|
|
23
|
+
return this.core.openSelect(options, handlers)
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
openView(options, handlers) {
|
|
23
27
|
return this.core.openView(options, handlers)
|
|
24
28
|
}
|
|
@@ -116,6 +120,50 @@ function validateView(input) {
|
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
122
|
|
|
123
|
+
function validateSelect(input) {
|
|
124
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
125
|
+
throw new Error('tuiOverlay.openSelect: options must be an object')
|
|
126
|
+
}
|
|
127
|
+
if (typeof input.id !== 'string' || input.id.length === 0) {
|
|
128
|
+
throw new Error('tuiOverlay.openSelect: id must be a non-empty string')
|
|
129
|
+
}
|
|
130
|
+
if (typeof input.title !== 'string' || input.title.length === 0) {
|
|
131
|
+
throw new Error('tuiOverlay.openSelect: title must be a non-empty string')
|
|
132
|
+
}
|
|
133
|
+
if (!Array.isArray(input.options) || input.options.length === 0) {
|
|
134
|
+
throw new Error('tuiOverlay.openSelect: options must be a non-empty array')
|
|
135
|
+
}
|
|
136
|
+
const values = new Set()
|
|
137
|
+
const options = input.options.map((option, index) => {
|
|
138
|
+
if (option === null || typeof option !== 'object' || Array.isArray(option)) {
|
|
139
|
+
throw new Error(`tuiOverlay.openSelect: options[${index}] must be an object`)
|
|
140
|
+
}
|
|
141
|
+
if (typeof option.value !== 'string' || option.value.length === 0) {
|
|
142
|
+
throw new Error(`tuiOverlay.openSelect: options[${index}].value must be a non-empty string`)
|
|
143
|
+
}
|
|
144
|
+
if (values.has(option.value)) {
|
|
145
|
+
throw new Error(`tuiOverlay.openSelect: duplicate value "${option.value}"`)
|
|
146
|
+
}
|
|
147
|
+
values.add(option.value)
|
|
148
|
+
if (typeof option.label !== 'string' || option.label.length === 0) {
|
|
149
|
+
throw new Error(`tuiOverlay.openSelect: options[${index}].label must be a non-empty string`)
|
|
150
|
+
}
|
|
151
|
+
if (option.description !== undefined && typeof option.description !== 'string') {
|
|
152
|
+
throw new Error(`tuiOverlay.openSelect: options[${index}].description must be a string`)
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
value: option.value,
|
|
156
|
+
label: option.label,
|
|
157
|
+
...(option.description === undefined ? {} : { description: option.description }),
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
const value = input.value === undefined ? options[0].value : input.value
|
|
161
|
+
if (typeof value !== 'string' || !values.has(value)) {
|
|
162
|
+
throw new Error('tuiOverlay.openSelect: value must match one option')
|
|
163
|
+
}
|
|
164
|
+
return { kind: 'select', id: input.id, title: input.title, value, options }
|
|
165
|
+
}
|
|
166
|
+
|
|
119
167
|
/**
|
|
120
168
|
* @param {object} ctx
|
|
121
169
|
* @param {{ notify?: (method: string, params: object) => void }} [options]
|
|
@@ -164,6 +212,17 @@ export function installTuiOverlay(ctx, options = {}) {
|
|
|
164
212
|
return controllerFor(entry)
|
|
165
213
|
}
|
|
166
214
|
|
|
215
|
+
function openSelect(options, handlers = {}) {
|
|
216
|
+
if (current !== null) {
|
|
217
|
+
throw new Error(`tuiOverlay.openSelect: overlay "${current.overlay.id}" is already open`)
|
|
218
|
+
}
|
|
219
|
+
const select = validateSelect(options)
|
|
220
|
+
const entry = { overlay: select, handlers, closed: false }
|
|
221
|
+
current = entry
|
|
222
|
+
publish(structuredClone(select))
|
|
223
|
+
return controllerFor(entry)
|
|
224
|
+
}
|
|
225
|
+
|
|
167
226
|
function openView(options, handlers = {}) {
|
|
168
227
|
const view = validateView(options)
|
|
169
228
|
if (current !== null) {
|
|
@@ -205,6 +264,31 @@ export function installTuiOverlay(ctx, options = {}) {
|
|
|
205
264
|
return handler?.()
|
|
206
265
|
}
|
|
207
266
|
|
|
267
|
+
if (entry.overlay.kind === 'select') {
|
|
268
|
+
if (!['change', 'submit', 'cancel'].includes(params.event)) {
|
|
269
|
+
throw new Error(`tuiOverlay.dispatch: unknown select event "${String(params.event)}"`)
|
|
270
|
+
}
|
|
271
|
+
if (params.event !== 'cancel') {
|
|
272
|
+
if (typeof params.value !== 'string'
|
|
273
|
+
|| !entry.overlay.options.some((option) => option.value === params.value)) {
|
|
274
|
+
throw new Error('tuiOverlay.dispatch: select value is not an option')
|
|
275
|
+
}
|
|
276
|
+
entry.overlay.value = params.value
|
|
277
|
+
}
|
|
278
|
+
if (params.event === 'change') {
|
|
279
|
+
return entry.handlers.onChange?.(entry.overlay.value)
|
|
280
|
+
}
|
|
281
|
+
const handler = params.event === 'submit'
|
|
282
|
+
? entry.handlers.onSubmit
|
|
283
|
+
: entry.handlers.onCancel
|
|
284
|
+
entry.closed = true
|
|
285
|
+
if (current === entry) {
|
|
286
|
+
current = null
|
|
287
|
+
publish(null)
|
|
288
|
+
}
|
|
289
|
+
return handler?.(entry.overlay.value)
|
|
290
|
+
}
|
|
291
|
+
|
|
208
292
|
const slider = entry.overlay
|
|
209
293
|
const value = finite(params.value, 'event.value')
|
|
210
294
|
if (value < slider.min || value > slider.max) {
|
|
@@ -239,10 +323,10 @@ export function installTuiOverlay(ctx, options = {}) {
|
|
|
239
323
|
return current === null ? null : structuredClone(current.overlay)
|
|
240
324
|
}
|
|
241
325
|
|
|
242
|
-
const core = { openSlider, openView, dispatch, active, bindNotify }
|
|
326
|
+
const core = { openSlider, openSelect, openView, dispatch, active, bindNotify }
|
|
243
327
|
const service = typeof ctx.provide === 'function'
|
|
244
328
|
? new TuiOverlayService(ctx, core)
|
|
245
|
-
: { openSlider, openView, dispatch, active, bindNotify }
|
|
329
|
+
: { openSlider, openSelect, openView, dispatch, active, bindNotify }
|
|
246
330
|
if (typeof ctx.provide !== 'function') ctx.tuiOverlay = service
|
|
247
331
|
return service
|
|
248
332
|
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/** UI Presets compose several UI plugin contributions into one saved choice. */
|
|
2
|
+
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { Service } from '@deepseek-ai/cordis'
|
|
6
|
+
|
|
7
|
+
export const name = 'tui-presets'
|
|
8
|
+
export const inject = ['tuiCommands', 'tuiOverlay']
|
|
9
|
+
|
|
10
|
+
const ID = /^[a-z0-9][a-z0-9-]*$/
|
|
11
|
+
|
|
12
|
+
class TuiPresetsService extends Service {
|
|
13
|
+
constructor(ctx, core) {
|
|
14
|
+
super(ctx, 'tuiPresets')
|
|
15
|
+
this.core = core
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
register(options, mount) {
|
|
19
|
+
return this.core.register(options, mount)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
activate(id) {
|
|
23
|
+
return this.core.activate(id)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
list() {
|
|
27
|
+
return this.core.list()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
active() {
|
|
31
|
+
return this.core.active()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readSettings(settingsPath) {
|
|
36
|
+
if (typeof settingsPath !== 'string' || settingsPath.length === 0) return {}
|
|
37
|
+
try {
|
|
38
|
+
const value = JSON.parse(readFileSync(settingsPath, 'utf8'))
|
|
39
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
40
|
+
} catch {
|
|
41
|
+
return {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writePreferred(settingsPath, id) {
|
|
46
|
+
if (typeof settingsPath !== 'string' || settingsPath.length === 0) return
|
|
47
|
+
const settings = readSettings(settingsPath)
|
|
48
|
+
settings.uiPreset = id
|
|
49
|
+
mkdirSync(path.dirname(settingsPath), { recursive: true })
|
|
50
|
+
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function releaseOf(value) {
|
|
54
|
+
if (typeof value === 'function') return value
|
|
55
|
+
if (value !== null && typeof value === 'object' && typeof value.dispose === 'function') {
|
|
56
|
+
return () => value.dispose()
|
|
57
|
+
}
|
|
58
|
+
return () => {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function installTuiPresets(ctx, options = {}) {
|
|
62
|
+
const entries = new Map()
|
|
63
|
+
const settingsPath = options.settingsPath
|
|
64
|
+
const saved = readSettings(settingsPath).uiPreset
|
|
65
|
+
let preferredId = typeof saved === 'string' && ID.test(saved) ? saved : 'default'
|
|
66
|
+
let activeEntry
|
|
67
|
+
let releaseActive
|
|
68
|
+
let presetSelector
|
|
69
|
+
let disposed = false
|
|
70
|
+
|
|
71
|
+
function mount(entry) {
|
|
72
|
+
const mounted = entry.mount()
|
|
73
|
+
if (mounted !== undefined && typeof mounted?.then === 'function') {
|
|
74
|
+
throw new Error('tuiPresets: preset mounts must be synchronous')
|
|
75
|
+
}
|
|
76
|
+
return releaseOf(mounted)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function switchTo(entry) {
|
|
80
|
+
if (activeEntry === entry) return
|
|
81
|
+
const previous = activeEntry
|
|
82
|
+
const releasePrevious = releaseActive
|
|
83
|
+
releasePrevious?.()
|
|
84
|
+
activeEntry = undefined
|
|
85
|
+
releaseActive = undefined
|
|
86
|
+
try {
|
|
87
|
+
releaseActive = mount(entry)
|
|
88
|
+
activeEntry = entry
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (previous !== undefined) {
|
|
91
|
+
try {
|
|
92
|
+
releaseActive = mount(previous)
|
|
93
|
+
activeEntry = previous
|
|
94
|
+
} catch {
|
|
95
|
+
activeEntry = undefined
|
|
96
|
+
releaseActive = undefined
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
throw error
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function activate(id, persist = true) {
|
|
104
|
+
if (disposed) throw new Error('tuiPresets: registry is disposed')
|
|
105
|
+
if (typeof id !== 'string' || !ID.test(id)) {
|
|
106
|
+
throw new Error('tuiPresets.activate: id must be a lowercase preset identifier')
|
|
107
|
+
}
|
|
108
|
+
const entry = entries.get(id)
|
|
109
|
+
if (entry === undefined) {
|
|
110
|
+
throw new Error(`tuiPresets.activate: preset "${id}" is not registered`)
|
|
111
|
+
}
|
|
112
|
+
switchTo(entry)
|
|
113
|
+
if (persist) {
|
|
114
|
+
preferredId = id
|
|
115
|
+
writePreferred(settingsPath, id)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function fallback() {
|
|
120
|
+
const next = entries.get(preferredId) ?? entries.get('default') ?? entries.values().next().value
|
|
121
|
+
if (next !== undefined) switchTo(next)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function refreshCommandInput() {
|
|
125
|
+
stopCommand?.update?.({
|
|
126
|
+
input: {
|
|
127
|
+
hint: 'preset',
|
|
128
|
+
options: list().map((entry) => ({
|
|
129
|
+
value: entry.id,
|
|
130
|
+
label: entry.label,
|
|
131
|
+
})),
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function register(options, mountPreset) {
|
|
137
|
+
if (disposed) throw new Error('tuiPresets: registry is disposed')
|
|
138
|
+
if (options === null || typeof options !== 'object' || Array.isArray(options)) {
|
|
139
|
+
throw new Error('tuiPresets.register: options must be an object')
|
|
140
|
+
}
|
|
141
|
+
if (typeof options.id !== 'string' || !ID.test(options.id)) {
|
|
142
|
+
throw new Error('tuiPresets.register: id must be a lowercase preset identifier')
|
|
143
|
+
}
|
|
144
|
+
if (typeof options.label !== 'string' || options.label.length === 0) {
|
|
145
|
+
throw new Error('tuiPresets.register: label must be a non-empty string')
|
|
146
|
+
}
|
|
147
|
+
const extras = Object.keys(options).filter((key) => !['id', 'label'].includes(key))
|
|
148
|
+
if (extras.length > 0) {
|
|
149
|
+
throw new Error(`tuiPresets.register: unknown option(s) ${extras.join(', ')}`)
|
|
150
|
+
}
|
|
151
|
+
if (typeof mountPreset !== 'function') {
|
|
152
|
+
throw new Error('tuiPresets.register: mount must be a function')
|
|
153
|
+
}
|
|
154
|
+
if (entries.has(options.id)) {
|
|
155
|
+
throw new Error(`tuiPresets.register: preset "${options.id}" is already registered`)
|
|
156
|
+
}
|
|
157
|
+
const entry = { id: options.id, label: options.label, mount: mountPreset }
|
|
158
|
+
entries.set(entry.id, entry)
|
|
159
|
+
refreshCommandInput()
|
|
160
|
+
if (activeEntry === undefined || entry.id === preferredId) fallback()
|
|
161
|
+
|
|
162
|
+
let stopped = false
|
|
163
|
+
return () => {
|
|
164
|
+
if (stopped) return
|
|
165
|
+
stopped = true
|
|
166
|
+
if (entries.get(entry.id) !== entry) return
|
|
167
|
+
const wasActive = activeEntry === entry
|
|
168
|
+
if (wasActive) {
|
|
169
|
+
releaseActive?.()
|
|
170
|
+
activeEntry = undefined
|
|
171
|
+
releaseActive = undefined
|
|
172
|
+
}
|
|
173
|
+
entries.delete(entry.id)
|
|
174
|
+
refreshCommandInput()
|
|
175
|
+
if (wasActive) fallback()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function list() {
|
|
180
|
+
return [...entries.values()].map(({ id, label }) => ({ id, label }))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function active() {
|
|
184
|
+
return activeEntry?.id
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const stopCommand = ctx.tuiCommands.register({
|
|
188
|
+
name: 'ui',
|
|
189
|
+
description: 'Switch UI preset',
|
|
190
|
+
}, async (args = '') => {
|
|
191
|
+
const id = args.trim().toLowerCase()
|
|
192
|
+
if (id.length === 0) {
|
|
193
|
+
const choices = list()
|
|
194
|
+
presetSelector?.close()
|
|
195
|
+
presetSelector = ctx.tuiOverlay.openSelect({
|
|
196
|
+
id: 'ui-preset',
|
|
197
|
+
title: 'UI preset',
|
|
198
|
+
value: active(),
|
|
199
|
+
options: choices.map((entry) => ({
|
|
200
|
+
value: entry.id,
|
|
201
|
+
label: entry.label,
|
|
202
|
+
})),
|
|
203
|
+
}, {
|
|
204
|
+
onSubmit(value) {
|
|
205
|
+
presetSelector = undefined
|
|
206
|
+
activate(value)
|
|
207
|
+
},
|
|
208
|
+
onCancel() {
|
|
209
|
+
presetSelector = undefined
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
activate(id)
|
|
215
|
+
})
|
|
216
|
+
refreshCommandInput()
|
|
217
|
+
|
|
218
|
+
function dispose() {
|
|
219
|
+
if (disposed) return
|
|
220
|
+
disposed = true
|
|
221
|
+
stopCommand?.()
|
|
222
|
+
presetSelector?.close()
|
|
223
|
+
presetSelector = undefined
|
|
224
|
+
releaseActive?.()
|
|
225
|
+
activeEntry = undefined
|
|
226
|
+
releaseActive = undefined
|
|
227
|
+
entries.clear()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const core = { register, activate, list, active, dispose }
|
|
231
|
+
const service = typeof ctx.provide === 'function'
|
|
232
|
+
? new TuiPresetsService(ctx, core)
|
|
233
|
+
: core
|
|
234
|
+
service.dispose = dispose
|
|
235
|
+
if (typeof ctx.provide !== 'function') ctx.tuiPresets = service
|
|
236
|
+
return service
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function apply(ctx, options) {
|
|
240
|
+
const service = installTuiPresets(ctx, options)
|
|
241
|
+
return () => service.dispose()
|
|
242
|
+
}
|
package/lib/tui-slots.js
CHANGED
|
@@ -10,12 +10,16 @@ export const name = 'tui-slots'
|
|
|
10
10
|
export const inject = []
|
|
11
11
|
|
|
12
12
|
export const SLOT_NAMES = Object.freeze([
|
|
13
|
+
'welcome.hero',
|
|
14
|
+
'welcome.info',
|
|
13
15
|
'chrome.right',
|
|
14
16
|
'conversation.input.dock',
|
|
15
17
|
'conversation.composer.dock',
|
|
16
18
|
])
|
|
17
19
|
|
|
18
20
|
const SLOT_DEFINITIONS = Object.freeze({
|
|
21
|
+
'welcome.hero': Object.freeze({ kind: 'single', scope: 'root' }),
|
|
22
|
+
'welcome.info': Object.freeze({ kind: 'single', scope: 'root' }),
|
|
19
23
|
'chrome.right': Object.freeze({ kind: 'list', scope: 'root' }),
|
|
20
24
|
'conversation.input.dock': Object.freeze({ kind: 'list', scope: 'session' }),
|
|
21
25
|
'conversation.composer.dock': Object.freeze({ kind: 'list', scope: 'session' }),
|
|
@@ -52,6 +56,10 @@ const THEME_TOKENS = new Set([
|
|
|
52
56
|
])
|
|
53
57
|
|
|
54
58
|
const NODE_FIELDS = Object.freeze({
|
|
59
|
+
ascii: { required: ['id', 'kind', 'lines'], optional: ['tone'] },
|
|
60
|
+
logo: { required: ['id', 'kind', 'name'], optional: [] },
|
|
61
|
+
welcomeinfo: { required: ['id', 'kind'], optional: [] },
|
|
62
|
+
text: { required: ['id', 'kind', 'text'], optional: ['tone'] },
|
|
55
63
|
group: { required: ['id', 'kind', 'children'], optional: ['title', 'tone'] },
|
|
56
64
|
markdown: { required: ['id', 'kind', 'text'], optional: ['streaming'] },
|
|
57
65
|
reasoning: { required: ['id', 'kind', 'text', 'done'], optional: ['seconds'] },
|
|
@@ -113,6 +121,28 @@ function validateNode(node, path, ids) {
|
|
|
113
121
|
ids.add(node.id)
|
|
114
122
|
|
|
115
123
|
switch (node.kind) {
|
|
124
|
+
case 'ascii':
|
|
125
|
+
if (!Array.isArray(node.lines) || node.lines.length === 0) {
|
|
126
|
+
throw new Error(`tuiSlots: ${path}.lines must be a non-empty array`)
|
|
127
|
+
}
|
|
128
|
+
node.lines.forEach((line, index) => string(line, `${path}.lines[${index}]`))
|
|
129
|
+
if (node.tone !== undefined && !THEME_TOKENS.has(node.tone)) {
|
|
130
|
+
throw new Error(`tuiSlots: ${path}.tone must be a theme token`)
|
|
131
|
+
}
|
|
132
|
+
break
|
|
133
|
+
case 'logo':
|
|
134
|
+
if (!['martty', 'deepseek'].includes(node.name)) {
|
|
135
|
+
throw new Error(`tuiSlots: ${path}.name must be a built-in logo primitive`)
|
|
136
|
+
}
|
|
137
|
+
break
|
|
138
|
+
case 'welcomeinfo':
|
|
139
|
+
break
|
|
140
|
+
case 'text':
|
|
141
|
+
string(node.text, `${path}.text`)
|
|
142
|
+
if (node.tone !== undefined && !THEME_TOKENS.has(node.tone)) {
|
|
143
|
+
throw new Error(`tuiSlots: ${path}.tone must be a theme token`)
|
|
144
|
+
}
|
|
145
|
+
break
|
|
116
146
|
case 'group':
|
|
117
147
|
optionalString(node.title, `${path}.title`)
|
|
118
148
|
if (node.tone !== undefined && !THEME_TOKENS.has(node.tone)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "martty",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "Terminal-native ACP client UI; Cordis client tree, any ACP agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"main": "lib/index.js",
|
|
16
16
|
"scripts": {
|
|
17
17
|
"pretest": "node --test ../scripts/workflow-release.test.mjs ../scripts/package-alias.test.mjs",
|
|
18
|
-
"test": "node --test ../scripts/package-native.test.mjs ../scripts/check-release-tag.test.mjs ../scripts/check-static-elf.test.mjs ../scripts/smoke-old-linux.test.mjs ../scripts/cargo-guard.test.mjs ../scripts/build-npm.test.mjs ../scripts/client-profile.test.mjs ../scripts/plugin-runner.test.mjs ../scripts/profile-link-resolution.test.mjs ../scripts/release.test.mjs ../scripts/jsonrpc-line-transport.test.mjs ../scripts/tui-theme.test.mjs ../scripts/tui-slots.test.mjs ../scripts/tui-commands.test.mjs ../scripts/tui-overlay.test.mjs ../scripts/mux.test.mjs ../scripts/acp-client.test.mjs ../scripts/acp-client-events.test.mjs ../scripts/acp-session-config.test.mjs ../scripts/acp-session-plan.test.mjs ../scripts/acp-session-stats.test.mjs ../scripts/acp-session-status.test.mjs ../scripts/plan-view.test.mjs ../scripts/stats-view.test.mjs ../scripts/status-view.test.mjs ../scripts/deepseek-logo.test.mjs ../scripts/runner.test.mjs ../scripts/inspect.test.mjs ../scripts/creator-overlay.test.mjs ../scripts/real-agent-e2e.test.mjs"
|
|
18
|
+
"test": "node --test ../scripts/package-native.test.mjs ../scripts/check-release-tag.test.mjs ../scripts/check-static-elf.test.mjs ../scripts/smoke-old-linux.test.mjs ../scripts/cargo-guard.test.mjs ../scripts/build-npm.test.mjs ../scripts/client-profile.test.mjs ../scripts/plugin-runner.test.mjs ../scripts/profile-link-resolution.test.mjs ../scripts/release.test.mjs ../scripts/jsonrpc-line-transport.test.mjs ../scripts/tui-theme.test.mjs ../scripts/tui-presets.test.mjs ../scripts/tui-slots.test.mjs ../scripts/tui-commands.test.mjs ../scripts/tui-overlay.test.mjs ../scripts/mux.test.mjs ../scripts/acp-client.test.mjs ../scripts/acp-client-events.test.mjs ../scripts/acp-session-config.test.mjs ../scripts/acp-session-plan.test.mjs ../scripts/acp-session-stats.test.mjs ../scripts/acp-session-status.test.mjs ../scripts/plan-view.test.mjs ../scripts/stats-view.test.mjs ../scripts/status-view.test.mjs ../scripts/deepseek-logo.test.mjs ../scripts/runner.test.mjs ../scripts/inspect.test.mjs ../scripts/creator-overlay.test.mjs ../scripts/real-agent-e2e.test.mjs"
|
|
19
19
|
},
|
|
20
20
|
"publishConfig": {
|
|
21
21
|
"access": "public",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
".": "./lib/index.js",
|
|
26
26
|
"./ember": "./lib/ember.js",
|
|
27
27
|
"./theme": "./lib/tui-theme.js",
|
|
28
|
+
"./presets": "./lib/tui-presets.js",
|
|
28
29
|
"./slots": "./lib/tui-slots.js",
|
|
29
30
|
"./commands": "./lib/tui-commands.js",
|
|
30
31
|
"./overlay": "./lib/tui-overlay.js",
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
"./stats-view": "./lib/stats-view.js",
|
|
36
37
|
"./status-view": "./lib/status-view.js",
|
|
37
38
|
"./deepseek-logo": "./lib/deepseek-logo.js",
|
|
39
|
+
"./martty-preset": "./lib/martty-preset.js",
|
|
38
40
|
"./right-demo": "./lib/right-demo.js",
|
|
39
41
|
"./acp-client": "./lib/acp-client.js",
|
|
40
42
|
"./acp-client-events": "./lib/acp-client-events.js",
|
|
@@ -56,6 +56,7 @@ Use the narrowest capability:
|
|
|
56
56
|
| Need | Inspect family | Runtime service |
|
|
57
57
|
| --- | --- | --- |
|
|
58
58
|
| Palette registration/activation | `Theme` | `tuiTheme` |
|
|
59
|
+
| Saved composition of several UI contributions | `UiPresets` | `tuiPresets` |
|
|
59
60
|
| Persistent terminal content | `Slots` | `tuiSlots` |
|
|
60
61
|
| Local slash command | `Commands` | `tuiCommands` |
|
|
61
62
|
| Transient slider or node view | `Overlay` | `tuiOverlay` |
|
|
@@ -85,10 +86,17 @@ compose only in the Plugin code. The services do not imply one another:
|
|
|
85
86
|
in that one Plugin. Never read/subscribe to active theme or implement a
|
|
86
87
|
theme condition yourself. Immediate preview uses the inspected registration
|
|
87
88
|
option and enters the same Plugin seat.
|
|
89
|
+
- **UI Presets:** use `tuiPresets.register({ id, label }, mount)` when the user
|
|
90
|
+
wants one saved `/ui` choice to mount several UI contributions together.
|
|
91
|
+
The synchronous mount callback returns one disposer. A UI Preset composes
|
|
92
|
+
Theme, slots, pet, or other UI Plugins; it is not another name for Theme.
|
|
88
93
|
- **Commands:** register a local slash command. Registration publishes slash
|
|
89
94
|
completion and keeps invocation out of the ACP prompt. Its availability is
|
|
90
95
|
the registration's lifetime; Commands does not know about themes, overlays,
|
|
91
|
-
config categories, or Slots.
|
|
96
|
+
config categories, or Slots. Optional `input: { hint, options }` metadata
|
|
97
|
+
reuses the same upward slash menu after `/name `; each option has `value`
|
|
98
|
+
plus optional `label` and `description`. Call the returned disposer’s
|
|
99
|
+
`update({ input })` method when a dynamic candidate catalog changes.
|
|
92
100
|
- **Slots:** register persistent native `TuiNode` content only when the user
|
|
93
101
|
asked for persistent shell UI. Use stable node ids and update the existing
|
|
94
102
|
contribution instead of creating parallel panels. Select the live seat from
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/vendor/linux-x64/dsh-tui
CHANGED
|
Binary file
|
|
Binary file
|