martty 0.2.16 → 0.2.17
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 +14 -0
- package/cordis.patch.yml +3 -0
- package/lib/boot.js +110 -7
- package/lib/client-process.js +2 -1
- package/lib/client-run.js +10 -0
- package/lib/creator-overlay.js +166 -1
- package/lib/index.js +15 -2
- package/lib/inspect.js +40 -21
- package/lib/runner.js +5 -2
- package/lib/tui-client-plugin-registry.js +70 -0
- package/lib/tui-local-plugins.js +213 -0
- package/lib/tui-plugin-store.js +197 -0
- package/lib/tui-theme.js +41 -9
- package/package.json +4 -3
- package/skills/tui-plugin-development/SKILL.md +33 -13
- 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
|
@@ -57,6 +57,20 @@ The package carries ACP as a runtime dependency and exports its Creator Host
|
|
|
57
57
|
overlay internally. The profile bundle mounts both on the Host Base tree;
|
|
58
58
|
neither enters the Client tree. Creator adds TUI plugin guidance to the
|
|
59
59
|
existing `cordis` preset and does not use ACP for skill registration.
|
|
60
|
+
Creator-authored UI Presets and Theme Plugins preview as process-local dynamic
|
|
61
|
+
Packages, then persist explicitly under `$MARTTY_HOME/plugins` through
|
|
62
|
+
`tui_plugin_save`; `tui_plugin_read` resumes authoring after restart.
|
|
63
|
+
|
|
64
|
+
`MARTTY_HOME` resolves explicitly first, then to `$DSH_HOME/.martty`, then to
|
|
65
|
+
`~/.martty`. UI choices live in `$MARTTY_HOME/settings.json`. On first use,
|
|
66
|
+
legacy Creator artifacts and settings are copied forward without deleting or
|
|
67
|
+
overwriting the old files.
|
|
68
|
+
|
|
69
|
+
Third-party TUI plugins remain ordinary installed packages. Their Host-side
|
|
70
|
+
registrar contributes an absolute Client module entry to `tuiClientPlugins`;
|
|
71
|
+
the Host runner serializes only that directory into the separate Client
|
|
72
|
+
process. Package entries and Creator artifacts share one Client lifecycle
|
|
73
|
+
manager, with installed packages winning same-id conflicts.
|
|
60
74
|
|
|
61
75
|
Node and Rust use inherited pipes on Unix and an authenticated loopback TCP
|
|
62
76
|
socket on Windows. This compositor channel carries theme/render data only;
|
package/cordis.patch.yml
CHANGED
package/lib/boot.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { apply as applyAcpClient } from './acp-client.js'
|
|
10
|
+
import { constants, copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
10
11
|
import { homedir } from 'node:os'
|
|
11
12
|
import path from 'node:path'
|
|
12
13
|
import { apply as applyCordisClientRunner } from './inspect.js'
|
|
@@ -23,12 +24,17 @@ import { apply as applyStatsView, inject as statsViewInject } from './stats-view
|
|
|
23
24
|
import { apply as applySessionStatus, inject as sessionStatusInject } from './acp-session-status.js'
|
|
24
25
|
import { apply as applyStatusView, inject as statusViewInject } from './status-view.js'
|
|
25
26
|
import { apply as applyDeepseekLogo, inject as deepseekLogoInject } from './deepseek-logo.js'
|
|
27
|
+
import { createTuiPluginStore, marttyHome } from './tui-plugin-store.js'
|
|
28
|
+
import { installTuiLocalPlugins } from './tui-local-plugins.js'
|
|
26
29
|
|
|
27
30
|
/**
|
|
28
31
|
* @param {object} [options]
|
|
29
32
|
* @param {{ command: string, args?: string[] }} [options.agent]
|
|
30
33
|
* @param {{ stdin: import('node:stream').Writable, stdout: import('node:stream').Readable, child?: import('node:child_process').ChildProcess }} [options.stream]
|
|
31
34
|
* @param {{ stdin: number | 'inherit', stdout: number | 'inherit' }} [options.tty]
|
|
35
|
+
* @param {string} [options.settingsPath]
|
|
36
|
+
* @param {string} [options.artifactRoot]
|
|
37
|
+
* @param {Array<{ id: string, kind: 'theme' | 'ui-preset', entry: string }>} [options.packagePlugins]
|
|
32
38
|
*/
|
|
33
39
|
export async function bootClient(options = {}) {
|
|
34
40
|
const { Context } = await import('@deepseek-ai/cordis')
|
|
@@ -36,11 +42,13 @@ export async function bootClient(options = {}) {
|
|
|
36
42
|
const acpConfig = options.stream !== undefined
|
|
37
43
|
? { stream: options.stream }
|
|
38
44
|
: { agent: options.agent ?? resolveStackedAgent() }
|
|
39
|
-
const
|
|
40
|
-
|
|
45
|
+
const settingsPath = options.settingsPath ?? uiSettingsPath(options.extraArgs ?? [])
|
|
46
|
+
if (options.settingsPath === undefined) {
|
|
47
|
+
migrateLegacyUiSettings(settingsPath, legacyUiSettingsPaths(options.extraArgs ?? []))
|
|
41
48
|
}
|
|
49
|
+
const presetConfig = { settingsPath }
|
|
42
50
|
if (typeof ctx.plugin === 'function') {
|
|
43
|
-
await ctx.plugin({ name: 'tui-theme', inject: [], apply: applyTheme })
|
|
51
|
+
await ctx.plugin({ name: 'tui-theme', inject: [], apply: applyTheme }, presetConfig)
|
|
44
52
|
await ctx.plugin({ name: 'tui-slots', inject: [], apply: applySlots })
|
|
45
53
|
await ctx.plugin({ name: 'tui-commands', inject: [], apply: applyCommands })
|
|
46
54
|
await ctx.plugin({ name: 'tui-overlay', inject: [], apply: applyOverlay })
|
|
@@ -52,6 +60,20 @@ export async function bootClient(options = {}) {
|
|
|
52
60
|
await ctx.plugin({ name: 'acp-session-status', inject: sessionStatusInject, apply: applySessionStatus })
|
|
53
61
|
await ctx.plugin({ name: 'status-view', inject: statusViewInject, apply: applyStatusView })
|
|
54
62
|
await ctx.plugin({ name: 'deepseek-logo', inject: deepseekLogoInject, apply: applyDeepseekLogo })
|
|
63
|
+
const localPlugins = installTuiLocalPlugins(ctx, {
|
|
64
|
+
store: createTuiPluginStore({ root: options.artifactRoot }),
|
|
65
|
+
packages: options.packagePlugins ?? [],
|
|
66
|
+
tuiTheme: ctx.get('tuiTheme'),
|
|
67
|
+
tuiPresets: ctx.get('tuiPresets'),
|
|
68
|
+
tuiSlots: ctx.get('tuiSlots'),
|
|
69
|
+
tuiCommands: ctx.get('tuiCommands'),
|
|
70
|
+
tuiOverlay: ctx.get('tuiOverlay'),
|
|
71
|
+
acpSessionConfig: ctx.get('acpSessionConfig'),
|
|
72
|
+
acpSessionPlan: ctx.get('acpSessionPlan'),
|
|
73
|
+
acpSessionStats: ctx.get('acpSessionStats'),
|
|
74
|
+
acpSessionStatus: ctx.get('acpSessionStatus'),
|
|
75
|
+
})
|
|
76
|
+
await localPlugins.discover()
|
|
55
77
|
await ctx.plugin({
|
|
56
78
|
name: 'tui-cordis-client-runner',
|
|
57
79
|
inject: [
|
|
@@ -72,7 +94,7 @@ export async function bootClient(options = {}) {
|
|
|
72
94
|
{ extraArgs: options.extraArgs ?? [], tty: options.tty },
|
|
73
95
|
)
|
|
74
96
|
} else {
|
|
75
|
-
applyTheme(ctx)
|
|
97
|
+
applyTheme(ctx, presetConfig)
|
|
76
98
|
applySlots(ctx)
|
|
77
99
|
applyCommands(ctx)
|
|
78
100
|
applyOverlay(ctx)
|
|
@@ -84,19 +106,100 @@ export async function bootClient(options = {}) {
|
|
|
84
106
|
applySessionStatus(ctx)
|
|
85
107
|
applyStatusView(ctx)
|
|
86
108
|
applyDeepseekLogo(ctx)
|
|
109
|
+
const localPlugins = installTuiLocalPlugins(ctx, {
|
|
110
|
+
store: createTuiPluginStore({ root: options.artifactRoot }),
|
|
111
|
+
packages: options.packagePlugins ?? [],
|
|
112
|
+
tuiTheme: ctx.tuiTheme,
|
|
113
|
+
tuiPresets: ctx.tuiPresets,
|
|
114
|
+
tuiSlots: ctx.tuiSlots,
|
|
115
|
+
tuiCommands: ctx.tuiCommands,
|
|
116
|
+
tuiOverlay: ctx.tuiOverlay,
|
|
117
|
+
acpSessionConfig: ctx.acpSessionConfig,
|
|
118
|
+
acpSessionPlan: ctx.acpSessionPlan,
|
|
119
|
+
acpSessionStats: ctx.acpSessionStats,
|
|
120
|
+
acpSessionStatus: ctx.acpSessionStatus,
|
|
121
|
+
})
|
|
122
|
+
await localPlugins.discover()
|
|
87
123
|
applyCordisClientRunner(ctx)
|
|
88
124
|
await applyShell(ctx, { extraArgs: options.extraArgs ?? [], tty: options.tty })
|
|
89
125
|
}
|
|
90
126
|
return ctx
|
|
91
127
|
}
|
|
92
128
|
|
|
93
|
-
|
|
129
|
+
/** Parse the Host registry snapshot passed across the process boundary. */
|
|
130
|
+
export function parseClientPluginsEnv(value) {
|
|
131
|
+
if (value === undefined || value === '') return []
|
|
132
|
+
let parsed
|
|
133
|
+
try {
|
|
134
|
+
parsed = JSON.parse(value)
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw new Error(`dsh-tui: invalid installed Client plugin registry JSON: ${error.message}`)
|
|
137
|
+
}
|
|
138
|
+
if (!Array.isArray(parsed)) {
|
|
139
|
+
throw new Error('dsh-tui: installed Client plugin registry must be an array')
|
|
140
|
+
}
|
|
141
|
+
return parsed.map((entry, index) => {
|
|
142
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
143
|
+
throw new Error(`dsh-tui: installed Client plugin entry ${index} must be an object`)
|
|
144
|
+
}
|
|
145
|
+
if (typeof entry.id !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(entry.id)) {
|
|
146
|
+
throw new Error(`dsh-tui: installed Client plugin entry ${index} has an invalid id`)
|
|
147
|
+
}
|
|
148
|
+
if (!['theme', 'ui-preset'].includes(entry.kind)) {
|
|
149
|
+
throw new Error(`dsh-tui: installed Client plugin entry ${index} has an invalid kind`)
|
|
150
|
+
}
|
|
151
|
+
if (typeof entry.entry !== 'string') {
|
|
152
|
+
throw new Error(`dsh-tui: installed Client plugin entry ${index} has no file entry`)
|
|
153
|
+
}
|
|
154
|
+
let url
|
|
155
|
+
try {
|
|
156
|
+
url = new URL(entry.entry)
|
|
157
|
+
} catch {
|
|
158
|
+
throw new Error(`dsh-tui: installed Client plugin entry ${index} has an invalid file entry`)
|
|
159
|
+
}
|
|
160
|
+
if (url.protocol !== 'file:') {
|
|
161
|
+
throw new Error(`dsh-tui: installed Client plugin entry ${index} must use a file URL`)
|
|
162
|
+
}
|
|
163
|
+
return { id: entry.id, kind: entry.kind, entry: url.href }
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function uiSettingsPath(extraArgs = [], env = process.env, userHome = homedir()) {
|
|
94
168
|
for (let index = 0; index < extraArgs.length; index += 1) {
|
|
95
169
|
if (extraArgs[index] === '--session-root' && typeof extraArgs[index + 1] === 'string') {
|
|
96
|
-
return path.join(extraArgs[index + 1], '
|
|
170
|
+
return path.join(extraArgs[index + 1], 'settings.json')
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return path.join(marttyHome(env, userHome), 'settings.json')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function legacyUiSettingsPaths(extraArgs = [], userHome = homedir()) {
|
|
177
|
+
const paths = []
|
|
178
|
+
for (let index = 0; index < extraArgs.length; index += 1) {
|
|
179
|
+
if (extraArgs[index] === '--session-root' && typeof extraArgs[index + 1] === 'string') {
|
|
180
|
+
paths.push(path.join(extraArgs[index + 1], 'dsh-tui-settings.json'))
|
|
181
|
+
break
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
paths.push(path.join(userHome, '.dsh-tui', 'sessions', 'dsh-tui-settings.json'))
|
|
185
|
+
return [...new Set(paths)]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function migrateLegacyUiSettings(settingsPath, legacyPaths) {
|
|
189
|
+
if (existsSync(settingsPath)) return false
|
|
190
|
+
for (const legacyPath of legacyPaths) {
|
|
191
|
+
if (!existsSync(legacyPath)) continue
|
|
192
|
+
try {
|
|
193
|
+
const value = JSON.parse(readFileSync(legacyPath, 'utf8'))
|
|
194
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) continue
|
|
195
|
+
mkdirSync(path.dirname(settingsPath), { recursive: true })
|
|
196
|
+
copyFileSync(legacyPath, settingsPath, constants.COPYFILE_EXCL)
|
|
197
|
+
return true
|
|
198
|
+
} catch {
|
|
199
|
+
// A malformed or racing legacy file must not block TUI startup.
|
|
97
200
|
}
|
|
98
201
|
}
|
|
99
|
-
return
|
|
202
|
+
return false
|
|
100
203
|
}
|
|
101
204
|
|
|
102
205
|
/**
|
package/lib/client-process.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
/** Independent TUI Client Cordis process launched by the Host runner. */
|
|
4
4
|
|
|
5
|
-
import { bootClient } from './boot.js'
|
|
5
|
+
import { bootClient, parseClientPluginsEnv } from './boot.js'
|
|
6
6
|
|
|
7
7
|
await bootClient({
|
|
8
8
|
stream: { stdin: process.stdout, stdout: process.stdin },
|
|
9
9
|
extraArgs: process.argv.slice(2),
|
|
10
10
|
tty: { stdin: 3, stdout: 4 },
|
|
11
|
+
packagePlugins: parseClientPluginsEnv(process.env.DSH_TUI_CLIENT_PLUGINS_V0),
|
|
11
12
|
})
|
package/lib/client-run.js
CHANGED
|
@@ -76,6 +76,16 @@ export async function applyClientHalf(clientCode, env) {
|
|
|
76
76
|
}
|
|
77
77
|
throw error
|
|
78
78
|
}
|
|
79
|
+
return applyClientPlugin(plugin, env)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Apply an already imported Client plugin through the same restricted TUI facade as code.client.
|
|
84
|
+
* @param {unknown} plugin
|
|
85
|
+
* @param {{ pluginId?: string, tuiTheme?: object, tuiSlots?: object, tuiCommands?: object, tuiOverlay?: object, acpSessionConfig?: object, acpSessionPlan?: object, acpSessionStats?: object, acpSessionStatus?: object, timer?: object, invoke?: Function }} env
|
|
86
|
+
* @returns {Promise<{ waitingFor: string[], dispose: () => void }>}
|
|
87
|
+
*/
|
|
88
|
+
export async function applyClientPlugin(plugin, env) {
|
|
79
89
|
const evaluated = normalizePlugin(plugin)
|
|
80
90
|
const inject = evaluated.inject ?? []
|
|
81
91
|
for (const name of inject) {
|
package/lib/creator-overlay.js
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { createTuiPluginStore } from './tui-plugin-store.js'
|
|
7
8
|
|
|
8
9
|
export const name = 'tui-creator-overlay'
|
|
9
|
-
export const inject = [
|
|
10
|
+
export const inject = [
|
|
11
|
+
'agentPresets', 'skills', 'systemPrompt', 'loader', 'tools', 'dynamicCordisRunner',
|
|
12
|
+
]
|
|
10
13
|
|
|
11
14
|
const SKILL_NAME = 'tui-plugin-development'
|
|
12
15
|
const ROUTING_PROMPT = `# Dynamic TUI Plugin routing
|
|
@@ -16,6 +19,158 @@ Load cordis-plugin-development for every dynamic Plugin; it owns the common Plug
|
|
|
16
19
|
- When any requested behavior belongs to the TUI — terminal themes or backgrounds, native TUI slots, local TUI commands, native overlays, or current ACP Session options — also load tui-plugin-development. Its TUI Provider guidance overrides generic browser UI assumptions only for that half.
|
|
17
20
|
- Browser/Web-only work needs no TUI companion. A mixed Plugin uses the generic skill for common, Host, and Web behavior and the TUI skill for its terminal behavior. Do not translate a TUI request into Web Slots or add Host code unless the requested behavior owns Host data.`
|
|
18
21
|
|
|
22
|
+
const jsonOutput = {
|
|
23
|
+
schema: { type: 'json' },
|
|
24
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function requireAgent(exec) {
|
|
28
|
+
if (exec?.agent === undefined) {
|
|
29
|
+
throw new Error('TUI artifact tools require an Agent-backed Creator session')
|
|
30
|
+
}
|
|
31
|
+
return exec.agent
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function registerArtifactTools(ctx, scopedCtx, defineTool, store) {
|
|
35
|
+
const tools = scopedCtx.get('tools')
|
|
36
|
+
if (tools === undefined || typeof tools.register !== 'function') {
|
|
37
|
+
throw new Error('tui-creator-overlay: tools service is unavailable in the preset scope')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
tools.register(defineTool({
|
|
41
|
+
name: 'tui_plugin_save',
|
|
42
|
+
description:
|
|
43
|
+
'Persist one successfully activated, Client-only Cordis Package as a durable TUI artifact. '
|
|
44
|
+
+ 'Use this only after cordis_run succeeds. UI Presets and Theme Plugins survive restart only '
|
|
45
|
+
+ 'after this Tool returns saved. Replacing an existing artifact must be explicit.',
|
|
46
|
+
parameters: {
|
|
47
|
+
artifactId: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
required: true,
|
|
50
|
+
description: 'Stable lowercase artifact id used as its directory name.',
|
|
51
|
+
},
|
|
52
|
+
kind: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
required: true,
|
|
55
|
+
enum: ['ui-preset', 'theme'],
|
|
56
|
+
description: 'Durable TUI contribution kind.',
|
|
57
|
+
},
|
|
58
|
+
pluginId: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
required: true,
|
|
61
|
+
description: 'Dynamic Plugin id returned by cordis_define.',
|
|
62
|
+
},
|
|
63
|
+
packageId: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
required: true,
|
|
66
|
+
description: 'Exact immutable Package id that cordis_run activated successfully.',
|
|
67
|
+
},
|
|
68
|
+
replace: {
|
|
69
|
+
type: 'boolean',
|
|
70
|
+
description: 'Replace an existing artifact with this Package. Defaults to false.',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
output: jsonOutput,
|
|
74
|
+
async execute(args, exec) {
|
|
75
|
+
const source = ctx.dynamicCordisRunner.inspectPackage(
|
|
76
|
+
requireAgent(exec),
|
|
77
|
+
args.pluginId,
|
|
78
|
+
args.packageId,
|
|
79
|
+
)
|
|
80
|
+
if (source.currentPackageId !== args.packageId) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`tui_plugin_save: ${args.pluginId}/${args.packageId} is not the successful current Package; `
|
|
83
|
+
+ 'run it successfully before persisting it',
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
if (typeof source.code?.host === 'string') {
|
|
87
|
+
throw new Error(
|
|
88
|
+
'tui_plugin_save: durable TUI artifacts are client-only; a Package with a Host half '
|
|
89
|
+
+ 'must be shipped as a standard profile plugin instead',
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
if (typeof source.code?.client !== 'string') {
|
|
93
|
+
throw new Error('tui_plugin_save: Package has no Client half')
|
|
94
|
+
}
|
|
95
|
+
const saved = store.save({
|
|
96
|
+
id: args.artifactId,
|
|
97
|
+
kind: args.kind,
|
|
98
|
+
name: source.name,
|
|
99
|
+
purpose: source.purpose,
|
|
100
|
+
source: { pluginId: args.pluginId, packageId: args.packageId },
|
|
101
|
+
clientCode: source.code.client,
|
|
102
|
+
}, { replace: args.replace === true })
|
|
103
|
+
return {
|
|
104
|
+
status: 'saved',
|
|
105
|
+
artifact: { id: args.artifactId, kind: args.kind },
|
|
106
|
+
path: saved.path,
|
|
107
|
+
recovery: 'The TUI Client discovers this artifact from disk on startup.',
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
}))
|
|
111
|
+
|
|
112
|
+
tools.register(defineTool({
|
|
113
|
+
name: 'tui_plugin_list',
|
|
114
|
+
description:
|
|
115
|
+
'List durable user-authored TUI artifacts from disk, including broken rows that need repair or removal.',
|
|
116
|
+
parameters: {},
|
|
117
|
+
output: jsonOutput,
|
|
118
|
+
execute(_args, exec) {
|
|
119
|
+
requireAgent(exec)
|
|
120
|
+
return Promise.resolve({ root: store.root, artifacts: store.list() })
|
|
121
|
+
},
|
|
122
|
+
isConcurrencySafe() {
|
|
123
|
+
return true
|
|
124
|
+
},
|
|
125
|
+
}))
|
|
126
|
+
|
|
127
|
+
tools.register(defineTool({
|
|
128
|
+
name: 'tui_plugin_read',
|
|
129
|
+
description:
|
|
130
|
+
'Read one durable Creator-authored TUI artifact including its exact code.client source. '
|
|
131
|
+
+ 'Use this before continuing development after a restart; define a new temporary preview '
|
|
132
|
+
+ 'Plugin from the source, then save the successful result with replace:true.',
|
|
133
|
+
parameters: {
|
|
134
|
+
artifactId: {
|
|
135
|
+
type: 'string',
|
|
136
|
+
required: true,
|
|
137
|
+
description: 'Exact durable artifact id returned by tui_plugin_list.',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
output: jsonOutput,
|
|
141
|
+
execute(args, exec) {
|
|
142
|
+
requireAgent(exec)
|
|
143
|
+
return Promise.resolve({ artifact: store.resolve(args.artifactId) })
|
|
144
|
+
},
|
|
145
|
+
isConcurrencySafe() {
|
|
146
|
+
return true
|
|
147
|
+
},
|
|
148
|
+
}))
|
|
149
|
+
|
|
150
|
+
tools.register(defineTool({
|
|
151
|
+
name: 'tui_plugin_remove',
|
|
152
|
+
description:
|
|
153
|
+
'Remove one durable user-authored TUI artifact from disk. This does not undefine the current '
|
|
154
|
+
+ 'Session\'s temporary preview Plugin.',
|
|
155
|
+
parameters: {
|
|
156
|
+
artifactId: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
required: true,
|
|
159
|
+
description: 'Exact durable artifact id returned by tui_plugin_list.',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
output: jsonOutput,
|
|
163
|
+
execute(args, exec) {
|
|
164
|
+
requireAgent(exec)
|
|
165
|
+
const removed = store.remove(args.artifactId)
|
|
166
|
+
return Promise.resolve({
|
|
167
|
+
status: removed ? 'removed' : 'absent',
|
|
168
|
+
artifactId: args.artifactId,
|
|
169
|
+
})
|
|
170
|
+
},
|
|
171
|
+
}))
|
|
172
|
+
}
|
|
173
|
+
|
|
19
174
|
const skillDocument = readFileSync(
|
|
20
175
|
new URL('../skills/tui-plugin-development/SKILL.md', import.meta.url),
|
|
21
176
|
'utf8',
|
|
@@ -49,6 +204,10 @@ export async function apply(ctx, config = {}) {
|
|
|
49
204
|
const { createScope } = scopeModule
|
|
50
205
|
const overlay = createScope(ctx, key)
|
|
51
206
|
try {
|
|
207
|
+
const toolsModule = await ctx.loader.import('@deepseek-ai/dsh-tools')
|
|
208
|
+
if (typeof toolsModule?.defineTool !== 'function') {
|
|
209
|
+
throw new Error('tui-creator-overlay: profile dsh-tools module is unavailable')
|
|
210
|
+
}
|
|
52
211
|
const skills = overlay.ctx.get('skills')
|
|
53
212
|
if (skills === undefined || typeof skills.register !== 'function') {
|
|
54
213
|
throw new Error('tui-creator-overlay: skills service is unavailable in the preset scope')
|
|
@@ -69,6 +228,12 @@ export async function apply(ctx, config = {}) {
|
|
|
69
228
|
order: 116,
|
|
70
229
|
text: ROUTING_PROMPT,
|
|
71
230
|
})
|
|
231
|
+
registerArtifactTools(
|
|
232
|
+
ctx,
|
|
233
|
+
overlay.ctx,
|
|
234
|
+
toolsModule.defineTool,
|
|
235
|
+
createTuiPluginStore({ root: config.artifactRoot }),
|
|
236
|
+
)
|
|
72
237
|
ctx.effect(() => () => overlay.dispose(), `tui-creator-overlay(${JSON.stringify(preset)})`)
|
|
73
238
|
} catch (error) {
|
|
74
239
|
await overlay.dispose()
|
package/lib/index.js
CHANGED
|
@@ -30,16 +30,23 @@ const shellStateKey = Symbol.for('martty/shell-state')
|
|
|
30
30
|
* livePainter: null | ({ muxed: boolean } & Awaited<ReturnType<typeof spawnPluginTui>>),
|
|
31
31
|
* startingPainter: null | Promise<{ muxed: boolean } & Awaited<ReturnType<typeof spawnPluginTui>>>,
|
|
32
32
|
* refuseRespawn: boolean,
|
|
33
|
+
* processExitListener: null | (() => void),
|
|
33
34
|
* }}
|
|
34
35
|
*/
|
|
35
36
|
const shellState = globalThis[shellStateKey] ??= {
|
|
36
37
|
livePainter: null,
|
|
37
38
|
startingPainter: null,
|
|
38
39
|
refuseRespawn: false,
|
|
40
|
+
processExitListener: null,
|
|
39
41
|
}
|
|
42
|
+
if (shellState.processExitListener === undefined) shellState.processExitListener = null
|
|
40
43
|
|
|
41
44
|
/** Drop the sticky painter so tests can apply() more than once in-process. */
|
|
42
45
|
export function resetShellForTests() {
|
|
46
|
+
if (shellState.processExitListener !== null) {
|
|
47
|
+
process.off('exit', shellState.processExitListener)
|
|
48
|
+
shellState.processExitListener = null
|
|
49
|
+
}
|
|
43
50
|
shellState.livePainter = null
|
|
44
51
|
shellState.startingPainter = null
|
|
45
52
|
shellState.refuseRespawn = false
|
|
@@ -150,6 +157,10 @@ export async function applyShell(ctx, options = {}) {
|
|
|
150
157
|
shellState.livePainter = live
|
|
151
158
|
const { child } = live
|
|
152
159
|
child.on('exit', (code, signal) => {
|
|
160
|
+
if (shellState.processExitListener === processExitListener) {
|
|
161
|
+
process.off('exit', processExitListener)
|
|
162
|
+
shellState.processExitListener = null
|
|
163
|
+
}
|
|
153
164
|
try {
|
|
154
165
|
agent.child?.kill('SIGTERM')
|
|
155
166
|
} catch {
|
|
@@ -165,13 +176,15 @@ export async function applyShell(ctx, options = {}) {
|
|
|
165
176
|
console.error(`dsh-tui: ${err.message}`)
|
|
166
177
|
process.exit(1)
|
|
167
178
|
})
|
|
168
|
-
|
|
179
|
+
const processExitListener = () => {
|
|
169
180
|
try {
|
|
170
181
|
child.kill('SIGTERM')
|
|
171
182
|
} catch {
|
|
172
183
|
// already gone
|
|
173
184
|
}
|
|
174
|
-
}
|
|
185
|
+
}
|
|
186
|
+
shellState.processExitListener = processExitListener
|
|
187
|
+
process.once('exit', processExitListener)
|
|
175
188
|
return live
|
|
176
189
|
})()
|
|
177
190
|
}
|
package/lib/inspect.js
CHANGED
|
@@ -162,7 +162,7 @@ export function uiPresetInspectProvider(tuiPresets) {
|
|
|
162
162
|
'A synchronous callback that mounts multiple UI contributions and returns one disposer.',
|
|
163
163
|
},
|
|
164
164
|
selection: '/ui <id>',
|
|
165
|
-
persistence: 'The selected id survives restart in
|
|
165
|
+
persistence: 'The selected id survives restart in $MARTTY_HOME/settings.json.',
|
|
166
166
|
},
|
|
167
167
|
}
|
|
168
168
|
},
|
|
@@ -754,6 +754,7 @@ async function mountClientHalf(
|
|
|
754
754
|
* acpSessionPlan?: object,
|
|
755
755
|
* acpSessionStats?: object,
|
|
756
756
|
* acpSessionStatus?: object,
|
|
757
|
+
* localPlugins?: object,
|
|
757
758
|
* requestAgent: (method: string, params?: object) => Promise<unknown>,
|
|
758
759
|
* }} opts
|
|
759
760
|
* @returns {{ onHost: (message: object) => void }}
|
|
@@ -761,7 +762,7 @@ async function mountClientHalf(
|
|
|
761
762
|
export function attachTuiClient(opts) {
|
|
762
763
|
const {
|
|
763
764
|
ctx, tuiTheme, tuiPresets, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
764
|
-
acpSessionStats, acpSessionStatus,
|
|
765
|
+
acpSessionStats, acpSessionStatus, localPlugins,
|
|
765
766
|
requestAgent,
|
|
766
767
|
} = opts
|
|
767
768
|
const providers = [
|
|
@@ -780,6 +781,19 @@ export function attachTuiClient(opts) {
|
|
|
780
781
|
const loaded = new Map()
|
|
781
782
|
const pendingThemeSelections = new Map()
|
|
782
783
|
|
|
784
|
+
async function stopOwner(agentId, pluginId) {
|
|
785
|
+
if (localPlugins?.has?.(pluginId) === true) {
|
|
786
|
+
await localPlugins.stop(pluginId)
|
|
787
|
+
return
|
|
788
|
+
}
|
|
789
|
+
const stopped = await requestAgent(CORDIS_METHODS.pluginStop, { agentId, pluginId })
|
|
790
|
+
if (stopped?.ok === false) {
|
|
791
|
+
throw new Error(typeof stopped.message === 'string'
|
|
792
|
+
? stopped.message
|
|
793
|
+
: `failed to stop theme Plugin "${pluginId}"`)
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
783
797
|
function sync() {
|
|
784
798
|
void requestAgent(CORDIS_METHODS.inspectSync, { providers: providers.map((provider) => provider.manifest) }).catch(() => {
|
|
785
799
|
// Agent has no TUI Client extras (Zed / non-dsh ACP). Stay a standard client.
|
|
@@ -905,15 +919,7 @@ export function attachTuiClient(opts) {
|
|
|
905
919
|
&& previousThemeOwner !== undefined
|
|
906
920
|
&& previousThemeOwner !== selectedThemeOwner) {
|
|
907
921
|
try {
|
|
908
|
-
|
|
909
|
-
agentId: request.agentId,
|
|
910
|
-
pluginId: previousThemeOwner,
|
|
911
|
-
})
|
|
912
|
-
if (stopped?.ok === false) {
|
|
913
|
-
throw new Error(typeof stopped.message === 'string'
|
|
914
|
-
? stopped.message
|
|
915
|
-
: `failed to stop theme Plugin "${previousThemeOwner}"`)
|
|
916
|
-
}
|
|
922
|
+
await stopOwner(request.agentId, previousThemeOwner)
|
|
917
923
|
} catch (error) {
|
|
918
924
|
if (loaded.get(request.pluginId) === applied.dispose) {
|
|
919
925
|
loaded.delete(request.pluginId)
|
|
@@ -981,19 +987,31 @@ export function attachTuiClient(opts) {
|
|
|
981
987
|
const previousOwner = tuiTheme.owner?.(previousId)
|
|
982
988
|
const targetOwner = tuiTheme.owner?.(request.id)
|
|
983
989
|
|
|
990
|
+
if (targetOwner !== undefined && localPlugins?.has?.(targetOwner) === true) {
|
|
991
|
+
if (localPlugins.isLoaded?.(targetOwner) !== true) {
|
|
992
|
+
await localPlugins.start(targetOwner)
|
|
993
|
+
}
|
|
994
|
+
if (tuiTheme.isLoaded?.(request.id) !== true) {
|
|
995
|
+
throw new Error(`local theme Plugin "${targetOwner}" did not load palette "${request.id}"`)
|
|
996
|
+
}
|
|
997
|
+
tuiTheme.activate(request.id)
|
|
998
|
+
if (previousOwner !== undefined && previousOwner !== targetOwner) {
|
|
999
|
+
try {
|
|
1000
|
+
await stopOwner(request.agentId, previousOwner)
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
await localPlugins.stop(targetOwner)
|
|
1003
|
+
if (tuiTheme.isLoaded?.(previousId) === true) tuiTheme.activate(previousId)
|
|
1004
|
+
throw error
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return { ok: true, status: 'selected', id: request.id }
|
|
1008
|
+
}
|
|
1009
|
+
|
|
984
1010
|
if (targetOwner === undefined || loaded.has(targetOwner)) {
|
|
985
1011
|
tuiTheme.activate(request.id)
|
|
986
1012
|
if (previousOwner !== undefined && previousOwner !== targetOwner) {
|
|
987
1013
|
try {
|
|
988
|
-
|
|
989
|
-
agentId: request.agentId,
|
|
990
|
-
pluginId: previousOwner,
|
|
991
|
-
})
|
|
992
|
-
if (stopped?.ok === false) {
|
|
993
|
-
throw new Error(typeof stopped.message === 'string'
|
|
994
|
-
? stopped.message
|
|
995
|
-
: `failed to stop theme Plugin "${previousOwner}"`)
|
|
996
|
-
}
|
|
1014
|
+
await stopOwner(request.agentId, previousOwner)
|
|
997
1015
|
} catch (error) {
|
|
998
1016
|
if (tuiTheme.isLoaded?.(previousId) === true) tuiTheme.activate(previousId)
|
|
999
1017
|
throw error
|
|
@@ -1043,13 +1061,14 @@ export function apply(ctx) {
|
|
|
1043
1061
|
const acpSessionPlan = ctx.acpSessionPlan ?? ctx.get?.('acpSessionPlan')
|
|
1044
1062
|
const acpSessionStats = ctx.acpSessionStats ?? ctx.get?.('acpSessionStats')
|
|
1045
1063
|
const acpSessionStatus = ctx.acpSessionStatus ?? ctx.get?.('acpSessionStatus')
|
|
1064
|
+
const localPlugins = ctx.get?.('tuiLocalPlugins')
|
|
1046
1065
|
let client
|
|
1047
1066
|
const service = {
|
|
1048
1067
|
bindTransport(requestAgent) {
|
|
1049
1068
|
client?.dispose()
|
|
1050
1069
|
client = attachTuiClient({
|
|
1051
1070
|
ctx, tuiTheme, tuiPresets, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
1052
|
-
acpSessionStats, acpSessionStatus,
|
|
1071
|
+
acpSessionStats, acpSessionStatus, localPlugins,
|
|
1053
1072
|
requestAgent,
|
|
1054
1073
|
})
|
|
1055
1074
|
const attached = client
|
package/lib/runner.js
CHANGED
|
@@ -12,7 +12,7 @@ import { createRequire } from 'node:module'
|
|
|
12
12
|
import { fileURLToPath } from 'node:url'
|
|
13
13
|
|
|
14
14
|
export const name = 'dsh-tui-runner'
|
|
15
|
-
export const inject = ['loader', 'acpServer', 'cmdlineArgs', 'appExit']
|
|
15
|
+
export const inject = ['loader', 'acpServer', 'cmdlineArgs', 'appExit', 'tuiClientPlugins']
|
|
16
16
|
|
|
17
17
|
const clientEntry = fileURLToPath(new URL('./client-process.js', import.meta.url))
|
|
18
18
|
const requireFromTui = createRequire(import.meta.url)
|
|
@@ -44,7 +44,10 @@ export async function apply(ctx) {
|
|
|
44
44
|
// ACP owns the Client process's stdin/stdout. Its fd 3/4 retain the
|
|
45
45
|
// user's terminal for the Rust painter spawned inside that process.
|
|
46
46
|
stdio: ['pipe', 'pipe', 'inherit', process.stdin, process.stdout],
|
|
47
|
-
env:
|
|
47
|
+
env: {
|
|
48
|
+
...process.env,
|
|
49
|
+
DSH_TUI_CLIENT_PLUGINS_V0: JSON.stringify(ctx.tuiClientPlugins.list()),
|
|
50
|
+
},
|
|
48
51
|
},
|
|
49
52
|
)
|
|
50
53
|
const agentToClient = child.stdin
|