martty 0.2.29 → 0.2.31
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 +20 -1
- package/bin/martty.js +30 -1
- package/lib/acp-session-stats.js +14 -0
- package/lib/acp-session-status.js +48 -5
- package/lib/agent.js +9 -1
- package/lib/agents-view.js +34 -9
- package/lib/boot.js +38 -3
- package/lib/harness-view.js +61 -0
- package/lib/harnesses.js +199 -0
- package/lib/inspect.js +3 -1
- package/lib/one.js +21 -0
- package/lib/palettes/one.json +44 -0
- package/lib/palettes/tomorrow.json +44 -0
- package/lib/plan-view.js +15 -5
- package/lib/stats-view.js +20 -28
- package/lib/status-view.js +3 -2
- package/lib/tomorrow.js +22 -0
- package/package.json +3 -3
- package/vendor/darwin-arm64/martty +0 -0
- package/vendor/darwin-x64/martty +0 -0
- package/vendor/linux-arm64/martty +0 -0
- package/vendor/linux-x64/martty +0 -0
- package/vendor/win32-x64/martty.exe +0 -0
package/README.md
CHANGED
|
@@ -64,7 +64,26 @@ profiles remain compatible.
|
|
|
64
64
|
The profile Host mounts the ACP plugin on Base, then starts a separate TUI
|
|
65
65
|
Client process over standard ACP stdin/stdout. For standalone use, run
|
|
66
66
|
`martty` and use `--agent <cmd>` plus repeated `--agent-arg <arg>` for another
|
|
67
|
-
ACP server.
|
|
67
|
+
ACP server. Named standalone harnesses can also be discovered, saved, and selected:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
martty harness list
|
|
71
|
+
martty harness add local --command local-acp --arg --stdio
|
|
72
|
+
martty harness use local
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The same registry is available through three entry points: edit
|
|
76
|
+
`$MARTTY_HOME/settings.json`, use `martty harness`, or run `/harness` (or
|
|
77
|
+
`/harness <id>`) inside the TUI. A saved choice takes effect on the next
|
|
78
|
+
standalone launch, which starts a fresh ACP session. It does not replace a
|
|
79
|
+
running or profile-owned Host, and it never carries a session across Harnesses.
|
|
80
|
+
|
|
81
|
+
The selected entry is stored in `$MARTTY_HOME/settings.json` and takes effect
|
|
82
|
+
on the next standalone launch through a new `session/new`. `--agent` and
|
|
83
|
+
`DSH_TUI_AGENT` remain higher priority. This does not replace the Host-owned
|
|
84
|
+
runtime or session of `dsh --profile martty`.
|
|
85
|
+
|
|
86
|
+
The Node Client process owns a Cordis tree and starts the Rust painter. A sibling
|
|
68
87
|
`tui-cordis-client-runner` publishes TUI Client capabilities and evaluates
|
|
69
88
|
approved `code.client` packages from `dsh-tool-cordis` against that client tree.
|
|
70
89
|
|
package/bin/martty.js
CHANGED
|
@@ -4,7 +4,9 @@ import fs from 'node:fs'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { spawnSync } from 'node:child_process'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
|
-
import {
|
|
7
|
+
import { resolveDependencyStack } from '../lib/agent.js'
|
|
8
|
+
import { bootClient, parseClientArgv, painterArgs, uiSettingsPath } from '../lib/boot.js'
|
|
9
|
+
import { runHarnessCommand } from '../lib/harnesses.js'
|
|
8
10
|
|
|
9
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
12
|
const platformKey = process.platform + '-' + process.arch
|
|
@@ -24,6 +26,33 @@ const wantDemo = argv.includes('--demo')
|
|
|
24
26
|
const wantHelp = argv.includes('-h') || argv.includes('--help')
|
|
25
27
|
const wantVersion = argv.includes('-V') || argv.includes('--version')
|
|
26
28
|
|
|
29
|
+
if (argv[0] === 'harness') {
|
|
30
|
+
let defaults = []
|
|
31
|
+
try {
|
|
32
|
+
const agent = resolveDependencyStack()
|
|
33
|
+
defaults = [{
|
|
34
|
+
id: 'builtin-dsh',
|
|
35
|
+
label: 'Bundled DeepSeek Harness',
|
|
36
|
+
...agent,
|
|
37
|
+
source: 'builtin',
|
|
38
|
+
}]
|
|
39
|
+
} catch {
|
|
40
|
+
// Source checkouts without installed dependencies still list PATH entries.
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const result = runHarnessCommand(argv.slice(1), {
|
|
44
|
+
settingsPath: uiSettingsPath(),
|
|
45
|
+
defaults,
|
|
46
|
+
})
|
|
47
|
+
if (result.stdout) process.stdout.write(result.stdout)
|
|
48
|
+
if (result.stderr) process.stderr.write(result.stderr)
|
|
49
|
+
process.exit(result.code)
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`martty harness: ${error instanceof Error ? error.message : String(error)}`)
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
if (!fs.existsSync(binaryPath)) {
|
|
28
57
|
let available = []
|
|
29
58
|
try {
|
package/lib/acp-session-stats.js
CHANGED
|
@@ -25,6 +25,7 @@ function zero(sessionId) {
|
|
|
25
25
|
usage: {
|
|
26
26
|
input: 0, output: 0, cached: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0,
|
|
27
27
|
},
|
|
28
|
+
context: { used: 0, size: 0 },
|
|
28
29
|
stats: {
|
|
29
30
|
turns: 0,
|
|
30
31
|
steps: 0,
|
|
@@ -137,6 +138,19 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
const type = readString(update, 'sessionUpdate', 'session_update')
|
|
141
|
+
if (type === 'usage_update') {
|
|
142
|
+
// Harness-authoritative context gauge: `used` is the final prompt
|
|
143
|
+
// size (input + cache + output) of the last step and `size` the
|
|
144
|
+
// real model window from `request/context`. Client-side
|
|
145
|
+
// accumulation cannot derive either: per-prompt input sums every
|
|
146
|
+
// step's full context, and standard ACP does not carry the window.
|
|
147
|
+
const size = number(update.size)
|
|
148
|
+
if (size > 0) {
|
|
149
|
+
value.context = { used: number(update.used), size }
|
|
150
|
+
publish()
|
|
151
|
+
}
|
|
152
|
+
return
|
|
153
|
+
}
|
|
140
154
|
const prompt = sessionId === undefined ? undefined : activePrompts.get(sessionId)
|
|
141
155
|
if (prompt !== undefined && prompt.firstToken === undefined
|
|
142
156
|
&& (type === 'agent_message_chunk' || type === 'agent_thought_chunk')
|
|
@@ -16,9 +16,15 @@ export const inject = ['acpClientEvents', 'acpSessionConfig']
|
|
|
16
16
|
const AUTH_REQUIRED_CODE = -32000
|
|
17
17
|
const SETUP_METHODS = new Set(['session/new', 'session/load'])
|
|
18
18
|
const RUNNING_UPDATE_TYPES = new Set([
|
|
19
|
+
'user_message_chunk',
|
|
19
20
|
'agent_message_chunk',
|
|
20
21
|
'agent_thought_chunk',
|
|
21
22
|
'tool_call',
|
|
23
|
+
'tool_call_update',
|
|
24
|
+
'plan',
|
|
25
|
+
'plan_update',
|
|
26
|
+
'plan_removed',
|
|
27
|
+
'usage_update',
|
|
22
28
|
])
|
|
23
29
|
|
|
24
30
|
class AcpSessionStatusService extends Service {
|
|
@@ -58,6 +64,7 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
58
64
|
let initializeId
|
|
59
65
|
let pendingAuthenticate = new Set()
|
|
60
66
|
const pendingSetup = new Map()
|
|
67
|
+
const pendingPrompts = new Map()
|
|
61
68
|
let permissionPreset
|
|
62
69
|
let sandboxMode
|
|
63
70
|
|
|
@@ -108,9 +115,15 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
108
115
|
)
|
|
109
116
|
return
|
|
110
117
|
}
|
|
111
|
-
if (message.method === 'session/prompt' &&
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
if (message.method === 'session/prompt' && message.id !== undefined) {
|
|
119
|
+
pendingPrompts.set(
|
|
120
|
+
message.id,
|
|
121
|
+
readString(message.params, 'sessionId', 'session_id'),
|
|
122
|
+
)
|
|
123
|
+
if (value.state === 'idle') {
|
|
124
|
+
value.state = 'starting'
|
|
125
|
+
publish()
|
|
126
|
+
}
|
|
114
127
|
}
|
|
115
128
|
}
|
|
116
129
|
|
|
@@ -155,6 +168,21 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
155
168
|
publish()
|
|
156
169
|
return
|
|
157
170
|
}
|
|
171
|
+
if (response(message) && pendingPrompts.has(message.id)) {
|
|
172
|
+
pendingPrompts.delete(message.id)
|
|
173
|
+
let changed = false
|
|
174
|
+
if (message.error !== undefined && isAuthRequired(message.error)
|
|
175
|
+
&& value.auth.status !== 'needs sign-in') {
|
|
176
|
+
value.auth.status = 'needs sign-in'
|
|
177
|
+
changed = true
|
|
178
|
+
}
|
|
179
|
+
if (pendingPrompts.size === 0 && value.state !== 'idle') {
|
|
180
|
+
value.state = 'idle'
|
|
181
|
+
changed = true
|
|
182
|
+
}
|
|
183
|
+
if (changed) publish()
|
|
184
|
+
return
|
|
185
|
+
}
|
|
158
186
|
if (message.error !== undefined && isAuthRequired(message.error)) {
|
|
159
187
|
value.auth.status = 'needs sign-in'
|
|
160
188
|
publish()
|
|
@@ -163,7 +191,7 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
163
191
|
|
|
164
192
|
if (message.method === 'session.status' && object(message.params)) {
|
|
165
193
|
const status = readString(message.params, 'status')
|
|
166
|
-
if (status === 'running' || status === 'idle') {
|
|
194
|
+
if (status === 'running' || (status === 'idle' && pendingPrompts.size === 0)) {
|
|
167
195
|
value.state = status
|
|
168
196
|
publish()
|
|
169
197
|
}
|
|
@@ -200,14 +228,24 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
200
228
|
return
|
|
201
229
|
}
|
|
202
230
|
if (message.method !== 'session/update' || !object(message.params)) return
|
|
231
|
+
const sessionId = readString(message.params, 'sessionId', 'session_id')
|
|
203
232
|
const update = object(message.params.update) ? message.params.update : undefined
|
|
204
233
|
const type = readString(update, 'sessionUpdate', 'session_update')
|
|
205
|
-
if (value.state
|
|
234
|
+
if (value.state !== 'running' && hasPendingPrompt(sessionId)
|
|
235
|
+
&& RUNNING_UPDATE_TYPES.has(type)) {
|
|
206
236
|
value.state = 'running'
|
|
207
237
|
publish()
|
|
208
238
|
}
|
|
209
239
|
}
|
|
210
240
|
|
|
241
|
+
function hasPendingPrompt(sessionId) {
|
|
242
|
+
if (sessionId === undefined) return false
|
|
243
|
+
for (const pendingSessionId of pendingPrompts.values()) {
|
|
244
|
+
if (pendingSessionId === sessionId) return true
|
|
245
|
+
}
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
|
|
211
249
|
function onConfigSnapshot(snapshot) {
|
|
212
250
|
if (snapshot === null || typeof snapshot !== 'object') return
|
|
213
251
|
if (typeof snapshot.sessionId === 'string' && snapshot.sessionId.length > 0
|
|
@@ -271,6 +309,11 @@ function object(value) {
|
|
|
271
309
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
272
310
|
}
|
|
273
311
|
|
|
312
|
+
function response(message) {
|
|
313
|
+
return !Object.hasOwn(message, 'method')
|
|
314
|
+
&& (Object.hasOwn(message, 'result') || Object.hasOwn(message, 'error'))
|
|
315
|
+
}
|
|
316
|
+
|
|
274
317
|
export function apply(ctx, options = {}) {
|
|
275
318
|
installAcpSessionStatus(ctx, options)
|
|
276
319
|
}
|
package/lib/agent.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createRequire } from 'node:module'
|
|
|
5
5
|
import { homedir } from 'node:os'
|
|
6
6
|
import { dirname, join, resolve } from 'node:path'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { selectedHarness } from './harnesses.js'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Resolve the ACP package and TUI's internal Creator overlay bundle.
|
|
@@ -31,14 +32,21 @@ export function resolveDependencyStack(anchor = import.meta.url) {
|
|
|
31
32
|
/**
|
|
32
33
|
* Resolve the standalone ACP command without affecting the profile path.
|
|
33
34
|
* @param {string | URL} [anchor]
|
|
35
|
+
* @param {{ settingsPath?: string }} [options]
|
|
34
36
|
* @returns {{ command: string, args: string[] }}
|
|
35
37
|
*/
|
|
36
|
-
export function resolveStackedAgent(anchor = import.meta.url) {
|
|
38
|
+
export function resolveStackedAgent(anchor = import.meta.url, options = {}) {
|
|
37
39
|
const envCmd = process.env.DSH_TUI_AGENT
|
|
38
40
|
if (typeof envCmd === 'string' && envCmd.trim().length > 0) {
|
|
39
41
|
const tokens = envCmd.trim().split(/\s+/)
|
|
40
42
|
return { command: tokens[0], args: tokens.slice(1) }
|
|
41
43
|
}
|
|
44
|
+
if (typeof options.settingsPath === 'string') {
|
|
45
|
+
const selected = selectedHarness(options.settingsPath)
|
|
46
|
+
if (selected !== undefined) {
|
|
47
|
+
return { command: selected.command, args: selected.args }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
42
50
|
try {
|
|
43
51
|
return resolveDependencyStack(anchor)
|
|
44
52
|
} catch {
|
package/lib/agents-view.js
CHANGED
|
@@ -5,29 +5,49 @@ export const inject = ['tuiAgents', 'tuiSlots', 'tuiCommands']
|
|
|
5
5
|
|
|
6
6
|
export function apply(ctx) {
|
|
7
7
|
let current = ctx.tuiAgents.current()
|
|
8
|
+
// Issue #80: `/agents` is an on/off switch for the panel. `visible` is
|
|
9
|
+
// the user preference; `forced` remembers `/agents on` so the summary
|
|
10
|
+
// stays open even after every task ended, until the next batch starts.
|
|
11
|
+
let visible = true
|
|
12
|
+
let forced = false
|
|
8
13
|
let panel
|
|
9
14
|
|
|
10
15
|
const stopSlot = ctx.tuiSlots.inject('conversation.navigation.dock', () => {
|
|
11
16
|
panel = ctx.tuiSlots.register(
|
|
12
17
|
{ name: 'conversation.navigation.dock', id: 'agents-view', order: 0 },
|
|
13
|
-
dockNodes(current),
|
|
18
|
+
dockNodes(current, visible, forced),
|
|
14
19
|
)
|
|
15
20
|
return () => panel.dispose()
|
|
16
21
|
})
|
|
17
22
|
const stopAgents = ctx.tuiAgents.subscribe((snapshot) => {
|
|
18
23
|
current = snapshot
|
|
19
|
-
panel
|
|
24
|
+
// A new running batch clears the forced-open state so the panel
|
|
25
|
+
// auto-closes again once this batch ends.
|
|
26
|
+
if (snapshot.items.some((item) => item.kind === 'subagent' && item.status === 'running')) {
|
|
27
|
+
forced = false
|
|
28
|
+
}
|
|
29
|
+
panel?.update(dockNodes(current, visible, forced))
|
|
20
30
|
})
|
|
21
31
|
const stopCommand = ctx.tuiCommands.register({
|
|
22
32
|
name: 'agents',
|
|
23
|
-
description: '
|
|
33
|
+
description: 'Toggle the Agent panel (on/off)',
|
|
34
|
+
input: { hint: '[on|off|agent-id]' },
|
|
24
35
|
}, async (args) => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
const arg = args.trim().toLowerCase()
|
|
37
|
+
if (arg === 'on') {
|
|
38
|
+
visible = true
|
|
39
|
+
forced = true
|
|
40
|
+
} else if (arg === 'off') {
|
|
41
|
+
visible = false
|
|
42
|
+
forced = false
|
|
43
|
+
} else if (arg === '') {
|
|
44
|
+
visible = !visible
|
|
45
|
+
forced = visible
|
|
46
|
+
} else {
|
|
47
|
+
return ctx.tuiAgents.select(args.trim())
|
|
29
48
|
}
|
|
30
|
-
|
|
49
|
+
panel?.update(dockNodes(current, visible, forced))
|
|
50
|
+
return true
|
|
31
51
|
})
|
|
32
52
|
|
|
33
53
|
return () => {
|
|
@@ -37,9 +57,10 @@ export function apply(ctx) {
|
|
|
37
57
|
}
|
|
38
58
|
}
|
|
39
59
|
|
|
40
|
-
function dockNodes(snapshot) {
|
|
60
|
+
function dockNodes(snapshot, visible = true, forced = false) {
|
|
41
61
|
if (!Array.isArray(snapshot?.items) || snapshot.items.length < 2) return []
|
|
42
62
|
const selecting = snapshot.selectedId !== null && snapshot.selectedId !== undefined
|
|
63
|
+
if (!visible && !selecting) return []
|
|
43
64
|
if (!selecting) {
|
|
44
65
|
const agents = snapshot.items.filter((item) => item.kind === 'subagent')
|
|
45
66
|
const marked = agents.filter((item) => item.current !== false)
|
|
@@ -47,6 +68,10 @@ function dockNodes(snapshot) {
|
|
|
47
68
|
const completed = current.filter((item) => item.status === 'finished' || item.status === 'failed').length
|
|
48
69
|
const running = current.some((item) => item.status === 'running')
|
|
49
70
|
const failed = current.some((item) => item.status === 'failed')
|
|
71
|
+
// Issue #80: auto-close once every Agent task has ended. A failed task
|
|
72
|
+
// keeps the summary so the failure stays visible; `/agents on` (forced)
|
|
73
|
+
// holds it open until the next batch starts running.
|
|
74
|
+
if (!running && !failed && !forced) return []
|
|
50
75
|
return [
|
|
51
76
|
{
|
|
52
77
|
id: 'summary', kind: 'generic', title: '· Agents', body: `${completed}/${current.length}`,
|
package/lib/boot.js
CHANGED
|
@@ -33,7 +33,7 @@ import { homedir } from 'node:os'
|
|
|
33
33
|
import path from 'node:path'
|
|
34
34
|
import { apply as applyCordisClientRunner } from './inspect.js'
|
|
35
35
|
import { apply as applyShell } from './index.js'
|
|
36
|
-
import { resolveStackedAgent } from './agent.js'
|
|
36
|
+
import { resolveDependencyStack, resolveStackedAgent } from './agent.js'
|
|
37
37
|
import { apply as applySlots } from './tui-slots.js'
|
|
38
38
|
import { apply as applyTheme } from './tui-theme.js'
|
|
39
39
|
import { apply as applyAyu, inject as ayuInject } from './ayu.js'
|
|
@@ -42,6 +42,8 @@ import { apply as applyKanagawa, inject as kanagawaInject } from './kanagawa.js'
|
|
|
42
42
|
import { apply as applyEverforest, inject as everforestInject } from './everforest.js'
|
|
43
43
|
import { apply as applyIceberg, inject as icebergInject } from './iceberg.js'
|
|
44
44
|
import { apply as applySolarized, inject as solarizedInject } from './solarized.js'
|
|
45
|
+
import { apply as applyOne, inject as oneInject } from './one.js'
|
|
46
|
+
import { apply as applyTomorrow, inject as tomorrowInject } from './tomorrow.js'
|
|
45
47
|
import { apply as applyCommands } from './tui-commands.js'
|
|
46
48
|
import { apply as applyOverlay } from './tui-overlay.js'
|
|
47
49
|
import { apply as applyAgents } from './tui-agents.js'
|
|
@@ -54,6 +56,7 @@ import { apply as applyQueueView, inject as queueViewInject } from './queue-view
|
|
|
54
56
|
import { apply as applyStatsView, inject as statsViewInject } from './stats-view.js'
|
|
55
57
|
import { apply as applySessionStatus, inject as sessionStatusInject } from './acp-session-status.js'
|
|
56
58
|
import { apply as applyStatusView, inject as statusViewInject } from './status-view.js'
|
|
59
|
+
import { apply as applyHarnessView, inject as harnessViewInject } from './harness-view.js'
|
|
57
60
|
import { apply as applyDeepseekLogo, inject as deepseekLogoInject } from './deepseek-logo.js'
|
|
58
61
|
import { createTuiPluginStore, marttyHome } from './tui-plugin-store.js'
|
|
59
62
|
import { installTuiLocalPlugins } from './tui-local-plugins.js'
|
|
@@ -65,6 +68,8 @@ import { installTuiLocalPlugins } from './tui-local-plugins.js'
|
|
|
65
68
|
* @param {{ stdin: number | 'inherit', stdout: number | 'inherit' }} [options.tty]
|
|
66
69
|
* @param {string} [options.settingsPath]
|
|
67
70
|
* @param {string} [options.artifactRoot]
|
|
71
|
+
* @param {Array<{ id: string, label: string, command: string, args?: string[], source?: string }>} [options.harnessDefaults]
|
|
72
|
+
* @param {string} [options.harnessPathValue]
|
|
68
73
|
* @param {Array<{ id: string, kind: 'theme' | 'ui', entry: string }>} [options.packagePlugins]
|
|
69
74
|
*/
|
|
70
75
|
export async function bootClient(options = {}) {
|
|
@@ -78,6 +83,25 @@ export async function bootClient(options = {}) {
|
|
|
78
83
|
migrateLegacyUiSettings(settingsPath, legacyUiSettingsPaths(options.extraArgs ?? []))
|
|
79
84
|
}
|
|
80
85
|
const presetConfig = { settingsPath }
|
|
86
|
+
let harnessDefaults = options.harnessDefaults
|
|
87
|
+
if (harnessDefaults === undefined) {
|
|
88
|
+
try {
|
|
89
|
+
harnessDefaults = [{
|
|
90
|
+
id: 'builtin-dsh',
|
|
91
|
+
label: 'Bundled DeepSeek Harness',
|
|
92
|
+
...resolveDependencyStack(),
|
|
93
|
+
source: 'builtin',
|
|
94
|
+
}]
|
|
95
|
+
} catch {
|
|
96
|
+
harnessDefaults = []
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const harnessConfig = {
|
|
100
|
+
settingsPath,
|
|
101
|
+
defaults: harnessDefaults,
|
|
102
|
+
pathValue: options.harnessPathValue,
|
|
103
|
+
hostOwned: options.stream !== undefined,
|
|
104
|
+
}
|
|
81
105
|
if (typeof ctx.plugin === 'function') {
|
|
82
106
|
await ctx.plugin({ name: 'tui-theme', inject: [], apply: applyTheme }, presetConfig)
|
|
83
107
|
await ctx.plugin({ name: 'tui-theme-ayu', inject: ayuInject, apply: applyAyu })
|
|
@@ -86,6 +110,8 @@ export async function bootClient(options = {}) {
|
|
|
86
110
|
await ctx.plugin({ name: 'tui-theme-everforest', inject: everforestInject, apply: applyEverforest })
|
|
87
111
|
await ctx.plugin({ name: 'tui-theme-iceberg', inject: icebergInject, apply: applyIceberg })
|
|
88
112
|
await ctx.plugin({ name: 'tui-theme-solarized', inject: solarizedInject, apply: applySolarized })
|
|
113
|
+
await ctx.plugin({ name: 'tui-theme-one', inject: oneInject, apply: applyOne })
|
|
114
|
+
await ctx.plugin({ name: 'tui-theme-tomorrow', inject: tomorrowInject, apply: applyTomorrow })
|
|
89
115
|
restorePreferredTheme(ctx.get('tuiTheme'))
|
|
90
116
|
await ctx.plugin({ name: 'tui-slots', inject: [], apply: applySlots })
|
|
91
117
|
await ctx.plugin({ name: 'tui-commands', inject: [], apply: applyCommands })
|
|
@@ -101,6 +127,7 @@ export async function bootClient(options = {}) {
|
|
|
101
127
|
await ctx.plugin({ name: 'stats-view', inject: statsViewInject, apply: applyStatsView })
|
|
102
128
|
await ctx.plugin({ name: 'acp-session-status', inject: sessionStatusInject, apply: applySessionStatus })
|
|
103
129
|
await ctx.plugin({ name: 'status-view', inject: statusViewInject, apply: applyStatusView })
|
|
130
|
+
await ctx.plugin({ name: 'harness-view', inject: harnessViewInject, apply: applyHarnessView }, harnessConfig)
|
|
104
131
|
await ctx.plugin({ name: 'deepseek-logo', inject: deepseekLogoInject, apply: applyDeepseekLogo })
|
|
105
132
|
const localPlugins = installTuiLocalPlugins(ctx, {
|
|
106
133
|
store: createTuiPluginStore({ root: options.artifactRoot }),
|
|
@@ -144,6 +171,8 @@ export async function bootClient(options = {}) {
|
|
|
144
171
|
applyEverforest(ctx)
|
|
145
172
|
applyIceberg(ctx)
|
|
146
173
|
applySolarized(ctx)
|
|
174
|
+
applyOne(ctx)
|
|
175
|
+
applyTomorrow(ctx)
|
|
147
176
|
restorePreferredTheme(ctx.tuiTheme)
|
|
148
177
|
applySlots(ctx)
|
|
149
178
|
applyCommands(ctx)
|
|
@@ -159,6 +188,7 @@ export async function bootClient(options = {}) {
|
|
|
159
188
|
applyStatsView(ctx)
|
|
160
189
|
applySessionStatus(ctx)
|
|
161
190
|
applyStatusView(ctx)
|
|
191
|
+
applyHarnessView(ctx, harnessConfig)
|
|
162
192
|
applyDeepseekLogo(ctx)
|
|
163
193
|
const localPlugins = installTuiLocalPlugins(ctx, {
|
|
164
194
|
store: createTuiPluginStore({ root: options.artifactRoot }),
|
|
@@ -265,9 +295,10 @@ export function migrateLegacyUiSettings(settingsPath, legacyPaths) {
|
|
|
265
295
|
* `--agent` is also forwarded via {@link painterArgs} so Terminal Auth can
|
|
266
296
|
* re-exec the same command.
|
|
267
297
|
* @param {string[]} argv
|
|
298
|
+
* @param {{ settingsPath?: string }} [options]
|
|
268
299
|
* @returns {{ agent: { command: string, args: string[] }, rustArgs: string[] }}
|
|
269
300
|
*/
|
|
270
|
-
export function parseClientArgv(argv) {
|
|
301
|
+
export function parseClientArgv(argv, options = {}) {
|
|
271
302
|
const rustArgs = []
|
|
272
303
|
let command
|
|
273
304
|
const args = []
|
|
@@ -286,7 +317,11 @@ export function parseClientArgv(argv) {
|
|
|
286
317
|
rustArgs.push(token)
|
|
287
318
|
}
|
|
288
319
|
if (command === undefined) {
|
|
289
|
-
|
|
320
|
+
const settingsPath = options.settingsPath ?? uiSettingsPath(rustArgs)
|
|
321
|
+
return {
|
|
322
|
+
agent: resolveStackedAgent(import.meta.url, { settingsPath }),
|
|
323
|
+
rustArgs,
|
|
324
|
+
}
|
|
290
325
|
}
|
|
291
326
|
return { agent: { command, args }, rustArgs }
|
|
292
327
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Built-in Client Plugin: configure the next standalone ACP harness. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
activateHarness,
|
|
5
|
+
discoverHarnesses,
|
|
6
|
+
selectedHarness,
|
|
7
|
+
upsertHarness,
|
|
8
|
+
} from './harnesses.js'
|
|
9
|
+
|
|
10
|
+
export const name = 'harness-view'
|
|
11
|
+
export const inject = ['tuiCommands', 'tuiOverlay']
|
|
12
|
+
|
|
13
|
+
function entryOptions(settingsPath, options) {
|
|
14
|
+
return discoverHarnesses(settingsPath, options).map((entry) => ({
|
|
15
|
+
value: entry.id,
|
|
16
|
+
label: entry.label,
|
|
17
|
+
description: `${entry.source} · ${[entry.command, ...entry.args]
|
|
18
|
+
.map((part) => /\s/.test(part) ? JSON.stringify(part) : part)
|
|
19
|
+
.join(' ')}`,
|
|
20
|
+
}))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function apply(ctx, options = {}) {
|
|
24
|
+
const settingsPath = options.settingsPath
|
|
25
|
+
const choices = () => entryOptions(settingsPath, options)
|
|
26
|
+
const save = (id) => {
|
|
27
|
+
const entry = discoverHarnesses(settingsPath, options).find(({ id: candidate }) => candidate === id)
|
|
28
|
+
if (entry === undefined) throw new Error(`unknown harness ${JSON.stringify(id)}`)
|
|
29
|
+
upsertHarness(settingsPath, entry)
|
|
30
|
+
activateHarness(settingsPath, id)
|
|
31
|
+
ctx.tuiOverlay.openView({
|
|
32
|
+
id: 'harness-saved',
|
|
33
|
+
title: 'Harness saved',
|
|
34
|
+
nodes: [{
|
|
35
|
+
id: 'notice',
|
|
36
|
+
kind: 'notice',
|
|
37
|
+
level: 'info',
|
|
38
|
+
text: options.hostOwned
|
|
39
|
+
? `${entry.label} is saved for a new standalone session. The current dsh profile and session remain Host-owned.`
|
|
40
|
+
: `${entry.label} will start in a new session after restarting standalone Martty.`,
|
|
41
|
+
}],
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
const command = ctx.tuiCommands.register({
|
|
45
|
+
name: 'harness',
|
|
46
|
+
description: 'Switch harness in a new session on the next standalone launch',
|
|
47
|
+
input: { hint: '[id]', options: choices() },
|
|
48
|
+
}, async (args) => {
|
|
49
|
+
const requested = args.trim()
|
|
50
|
+
if (requested.length > 0) return save(requested)
|
|
51
|
+
const entries = choices()
|
|
52
|
+
const selected = selectedHarness(settingsPath)?.id
|
|
53
|
+
ctx.tuiOverlay.openSelect({
|
|
54
|
+
id: 'harness',
|
|
55
|
+
title: 'Harness · next standalone session',
|
|
56
|
+
value: entries.some(({ value }) => value === selected) ? selected : entries[0].value,
|
|
57
|
+
options: entries,
|
|
58
|
+
}, { onSubmit: save })
|
|
59
|
+
})
|
|
60
|
+
return () => command?.()
|
|
61
|
+
}
|
package/lib/harnesses.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
statSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
|
|
12
|
+
const HARNESS_ID = /^[a-z0-9][a-z0-9-]*$/
|
|
13
|
+
const HARNESS_HELP = `martty harness — configure standalone ACP harnesses
|
|
14
|
+
|
|
15
|
+
USAGE:
|
|
16
|
+
martty harness list
|
|
17
|
+
martty harness add <id> --command <cmd> [--label <label>] [--arg <arg>]...
|
|
18
|
+
martty harness use <id>
|
|
19
|
+
|
|
20
|
+
list discovers executable *-acp and *_acp commands on PATH. The active harness
|
|
21
|
+
is saved in $MARTTY_HOME/settings.json. The next standalone launch starts that
|
|
22
|
+
harness with a new ACP session; sessions are never carried across harnesses.
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
function readSettings(settingsPath) {
|
|
26
|
+
if (!existsSync(settingsPath)) return {}
|
|
27
|
+
let value
|
|
28
|
+
try {
|
|
29
|
+
value = JSON.parse(readFileSync(settingsPath, 'utf8'))
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new Error(`invalid Martty settings: ${error.message}`)
|
|
32
|
+
}
|
|
33
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
34
|
+
throw new Error('invalid Martty settings: root must be an object')
|
|
35
|
+
}
|
|
36
|
+
return value
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeSettings(settingsPath, value) {
|
|
40
|
+
mkdirSync(path.dirname(settingsPath), { recursive: true })
|
|
41
|
+
const temporary = `${settingsPath}.${process.pid}.${Date.now()}.tmp`
|
|
42
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`)
|
|
43
|
+
renameSync(temporary, settingsPath)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateHarness(value) {
|
|
47
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
48
|
+
throw new Error('harness must be an object')
|
|
49
|
+
}
|
|
50
|
+
if (typeof value.id !== 'string' || !HARNESS_ID.test(value.id)) {
|
|
51
|
+
throw new Error('harness id must use lowercase letters, numbers, and hyphens')
|
|
52
|
+
}
|
|
53
|
+
if (typeof value.command !== 'string' || value.command.trim().length === 0) {
|
|
54
|
+
throw new Error('harness command must be a non-empty string')
|
|
55
|
+
}
|
|
56
|
+
const args = value.args === undefined ? [] : value.args
|
|
57
|
+
if (!Array.isArray(args) || args.some((arg) => typeof arg !== 'string')) {
|
|
58
|
+
throw new Error('harness args must be an array of strings')
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
id: value.id,
|
|
62
|
+
label: typeof value.label === 'string' && value.label.trim().length > 0
|
|
63
|
+
? value.label
|
|
64
|
+
: value.id,
|
|
65
|
+
command: value.command,
|
|
66
|
+
args: [...args],
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function configuredHarnesses(settings) {
|
|
71
|
+
if (!Array.isArray(settings.harnesses)) return []
|
|
72
|
+
return settings.harnesses.map(validateHarness)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function upsertHarness(settingsPath, harness) {
|
|
76
|
+
const next = validateHarness(harness)
|
|
77
|
+
const settings = readSettings(settingsPath)
|
|
78
|
+
const harnesses = configuredHarnesses(settings)
|
|
79
|
+
const index = harnesses.findIndex(({ id }) => id === next.id)
|
|
80
|
+
if (index === -1) harnesses.push(next)
|
|
81
|
+
else harnesses[index] = next
|
|
82
|
+
writeSettings(settingsPath, { ...settings, harnesses })
|
|
83
|
+
return next
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function activateHarness(settingsPath, id) {
|
|
87
|
+
const settings = readSettings(settingsPath)
|
|
88
|
+
const harnesses = configuredHarnesses(settings)
|
|
89
|
+
if (!harnesses.some((harness) => harness.id === id)) {
|
|
90
|
+
throw new Error(`unknown harness ${JSON.stringify(id)}`)
|
|
91
|
+
}
|
|
92
|
+
writeSettings(settingsPath, { ...settings, harnesses, activeHarness: id })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function selectedHarness(settingsPath) {
|
|
96
|
+
const settings = readSettings(settingsPath)
|
|
97
|
+
if (typeof settings.activeHarness !== 'string') return undefined
|
|
98
|
+
return configuredHarnesses(settings).find(({ id }) => id === settings.activeHarness)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function discoverPathHarnesses(pathValue = process.env.PATH ?? '') {
|
|
102
|
+
const found = []
|
|
103
|
+
const names = new Set()
|
|
104
|
+
for (const directory of pathValue.split(path.delimiter).filter(Boolean)) {
|
|
105
|
+
let entries
|
|
106
|
+
try {
|
|
107
|
+
entries = readdirSync(directory, { withFileTypes: true })
|
|
108
|
+
} catch {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue
|
|
113
|
+
const name = entry.name.replace(/\.(?:cmd|exe|bat|com)$/i, '')
|
|
114
|
+
if (!/(?:^|[-_])acp$/i.test(name) || names.has(name)) continue
|
|
115
|
+
const command = path.resolve(directory, entry.name)
|
|
116
|
+
try {
|
|
117
|
+
const stat = statSync(command)
|
|
118
|
+
if (!stat.isFile()) continue
|
|
119
|
+
if (process.platform !== 'win32' && (stat.mode & 0o111) === 0) continue
|
|
120
|
+
} catch {
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
names.add(name)
|
|
124
|
+
found.push({
|
|
125
|
+
id: `path-${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`,
|
|
126
|
+
label: name,
|
|
127
|
+
command,
|
|
128
|
+
args: [],
|
|
129
|
+
source: 'path',
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return found
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function discoverHarnesses(settingsPath, options = {}) {
|
|
137
|
+
const configured = configuredHarnesses(readSettings(settingsPath))
|
|
138
|
+
.map((harness) => ({ ...harness, source: 'configured' }))
|
|
139
|
+
const defaults = (options.defaults ?? []).map((entry) => ({
|
|
140
|
+
...validateHarness(entry),
|
|
141
|
+
source: typeof entry.source === 'string' ? entry.source : 'builtin',
|
|
142
|
+
}))
|
|
143
|
+
const pathEntries = discoverPathHarnesses(options.pathValue)
|
|
144
|
+
const seen = new Set()
|
|
145
|
+
return [...configured, ...defaults, ...pathEntries].filter((entry) => {
|
|
146
|
+
if (seen.has(entry.id)) return false
|
|
147
|
+
seen.add(entry.id)
|
|
148
|
+
return true
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function runHarnessCommand(argv, options) {
|
|
153
|
+
const settingsPath = options?.settingsPath
|
|
154
|
+
if (typeof settingsPath !== 'string' || settingsPath.length === 0) {
|
|
155
|
+
throw new Error('harness command needs a settings path')
|
|
156
|
+
}
|
|
157
|
+
const [action, id, ...tokens] = argv
|
|
158
|
+
if (action === 'help' || action === undefined) {
|
|
159
|
+
return { code: 0, stdout: HARNESS_HELP, stderr: '' }
|
|
160
|
+
}
|
|
161
|
+
if (action === 'list') {
|
|
162
|
+
const settings = readSettings(settingsPath)
|
|
163
|
+
const active = typeof settings.activeHarness === 'string' ? settings.activeHarness : undefined
|
|
164
|
+
const stdout = discoverHarnesses(settingsPath, options).map((entry) => {
|
|
165
|
+
const marker = entry.id === active ? '*' : ' '
|
|
166
|
+
const command = [entry.command, ...entry.args]
|
|
167
|
+
.map((part) => /\s/.test(part) ? JSON.stringify(part) : part)
|
|
168
|
+
.join(' ')
|
|
169
|
+
return `${marker} ${entry.id}\t${entry.source}\t${entry.label}\t${command}`
|
|
170
|
+
}).join('\n')
|
|
171
|
+
return { code: 0, stdout: stdout.length > 0 ? `${stdout}\n` : '', stderr: '' }
|
|
172
|
+
}
|
|
173
|
+
if (action === 'use') {
|
|
174
|
+
const harness = discoverHarnesses(settingsPath, options).find((entry) => entry.id === id)
|
|
175
|
+
if (harness === undefined) throw new Error(`unknown harness ${JSON.stringify(id ?? '')}`)
|
|
176
|
+
upsertHarness(settingsPath, harness)
|
|
177
|
+
activateHarness(settingsPath, id)
|
|
178
|
+
return {
|
|
179
|
+
code: 0,
|
|
180
|
+
stdout: `active harness ${id}; next standalone launch starts a new session\n`,
|
|
181
|
+
stderr: '',
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (action !== 'add') throw new Error(`unknown harness command ${JSON.stringify(action ?? '')}`)
|
|
185
|
+
let command
|
|
186
|
+
let label
|
|
187
|
+
const args = []
|
|
188
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
189
|
+
const token = tokens[index]
|
|
190
|
+
const value = tokens[index + 1]
|
|
191
|
+
if (token === '--command') command = value
|
|
192
|
+
else if (token === '--label') label = value
|
|
193
|
+
else if (token === '--arg') args.push(value ?? '')
|
|
194
|
+
else throw new Error(`unknown harness add option ${JSON.stringify(token)}`)
|
|
195
|
+
index += 1
|
|
196
|
+
}
|
|
197
|
+
upsertHarness(settingsPath, { id, label, command, args })
|
|
198
|
+
return { code: 0, stdout: `saved harness ${id}\n`, stderr: '' }
|
|
199
|
+
}
|
package/lib/inspect.js
CHANGED
|
@@ -683,7 +683,9 @@ export function statusInspectProvider(acpSessionStatus) {
|
|
|
683
683
|
call: 'ctx.acpSessionStatus.subscribe((snapshot) => { ... })',
|
|
684
684
|
returns: { type: 'function', role: 'dispose', idempotent: true },
|
|
685
685
|
},
|
|
686
|
-
transport: 'standard ACP initialize, authenticate, session
|
|
686
|
+
transport: 'standard ACP initialize, authenticate, session, session/prompt response, '
|
|
687
|
+
+ 'and session/update messages, plus optional Martty session.status/session.event '
|
|
688
|
+
+ 'extensions',
|
|
687
689
|
},
|
|
688
690
|
referencedTypes: ['SessionStatusSnapshot'],
|
|
689
691
|
}
|
package/lib/one.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gallery palette pack `one`. Registers complete dark/light token maps:
|
|
3
|
+
* dark from One Dark, light from One Light (terminalcolors.com/themes/one). Does not activate:
|
|
4
|
+
* `/theme` covers it. `inject = ['tuiTheme']`: sibling profile row, not
|
|
5
|
+
* `ctx.plugin` inside the runner.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
|
|
10
|
+
const onePalette = JSON.parse(
|
|
11
|
+
readFileSync(new URL('./palettes/one.json', import.meta.url), 'utf8'),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export const name = 'tui-theme-one'
|
|
15
|
+
export const inject = ['tuiTheme']
|
|
16
|
+
|
|
17
|
+
export function apply(ctx) {
|
|
18
|
+
ctx.effect(() => ctx.tuiTheme.register(onePalette, { activate: false }))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { onePalette }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "one",
|
|
3
|
+
"label": "One",
|
|
4
|
+
"dark": {
|
|
5
|
+
"bg": "#282c34",
|
|
6
|
+
"surface": "#282c34",
|
|
7
|
+
"panel": "#1e2127",
|
|
8
|
+
"fg": "#abb2bf",
|
|
9
|
+
"fg_secondary": "#5c6370",
|
|
10
|
+
"fg_tertiary": "#5c6370",
|
|
11
|
+
"caption": "#5c6370",
|
|
12
|
+
"brand": "#61afef",
|
|
13
|
+
"brand_soft": "#c678dd",
|
|
14
|
+
"bubble_bg": "#abb2bf",
|
|
15
|
+
"bubble_fg": "#282c34",
|
|
16
|
+
"border": "#5c6370",
|
|
17
|
+
"code_bg": "#282c34",
|
|
18
|
+
"ok": "#98c379",
|
|
19
|
+
"warn": "#d19a66",
|
|
20
|
+
"err": "#e06c75",
|
|
21
|
+
"hint": "#56b6c2",
|
|
22
|
+
"chip_bg": "#1e2127"
|
|
23
|
+
},
|
|
24
|
+
"light": {
|
|
25
|
+
"bg": "#f8f8f8",
|
|
26
|
+
"surface": "#f8f8f8",
|
|
27
|
+
"panel": "#bbbbbb",
|
|
28
|
+
"fg": "#2a2b33",
|
|
29
|
+
"fg_secondary": "#5c6370",
|
|
30
|
+
"fg_tertiary": "#5c6370",
|
|
31
|
+
"caption": "#5c6370",
|
|
32
|
+
"brand": "#2f5af3",
|
|
33
|
+
"brand_soft": "#a00095",
|
|
34
|
+
"bubble_bg": "#2a2b33",
|
|
35
|
+
"bubble_fg": "#f8f8f8",
|
|
36
|
+
"border": "#5c6370",
|
|
37
|
+
"code_bg": "#f8f8f8",
|
|
38
|
+
"ok": "#3e953a",
|
|
39
|
+
"warn": "#c18401",
|
|
40
|
+
"err": "#de3d35",
|
|
41
|
+
"hint": "#0184bc",
|
|
42
|
+
"chip_bg": "#bbbbbb"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "tomorrow",
|
|
3
|
+
"label": "Tomorrow",
|
|
4
|
+
"dark": {
|
|
5
|
+
"bg": "#000000",
|
|
6
|
+
"surface": "#000000",
|
|
7
|
+
"panel": "#424242",
|
|
8
|
+
"fg": "#eaeaea",
|
|
9
|
+
"fg_secondary": "#969896",
|
|
10
|
+
"fg_tertiary": "#969896",
|
|
11
|
+
"caption": "#969896",
|
|
12
|
+
"brand": "#7aa6da",
|
|
13
|
+
"brand_soft": "#c397d8",
|
|
14
|
+
"bubble_bg": "#424242",
|
|
15
|
+
"bubble_fg": "#eaeaea",
|
|
16
|
+
"border": "#969896",
|
|
17
|
+
"code_bg": "#000000",
|
|
18
|
+
"ok": "#b9ca4a",
|
|
19
|
+
"warn": "#e7c547",
|
|
20
|
+
"err": "#d54e53",
|
|
21
|
+
"hint": "#70c0b1",
|
|
22
|
+
"chip_bg": "#424242"
|
|
23
|
+
},
|
|
24
|
+
"light": {
|
|
25
|
+
"bg": "#ffffff",
|
|
26
|
+
"surface": "#ffffff",
|
|
27
|
+
"panel": "#d6d6d6",
|
|
28
|
+
"fg": "#4d4d4c",
|
|
29
|
+
"fg_secondary": "#8e908c",
|
|
30
|
+
"fg_tertiary": "#8e908c",
|
|
31
|
+
"caption": "#8e908c",
|
|
32
|
+
"brand": "#4271ae",
|
|
33
|
+
"brand_soft": "#8959a8",
|
|
34
|
+
"bubble_bg": "#d6d6d6",
|
|
35
|
+
"bubble_fg": "#4d4d4c",
|
|
36
|
+
"border": "#8e908c",
|
|
37
|
+
"code_bg": "#ffffff",
|
|
38
|
+
"ok": "#718c00",
|
|
39
|
+
"warn": "#eab700",
|
|
40
|
+
"err": "#c82829",
|
|
41
|
+
"hint": "#3e999f",
|
|
42
|
+
"chip_bg": "#d6d6d6"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/lib/plan-view.js
CHANGED
|
@@ -25,7 +25,7 @@ export function apply(ctx) {
|
|
|
25
25
|
current = ctx.acpSessionPlan.current()
|
|
26
26
|
ctx.tuiOverlay.openView({
|
|
27
27
|
id: 'plan-view',
|
|
28
|
-
title:
|
|
28
|
+
title: viewTitle(current),
|
|
29
29
|
nodes: viewNodes(current),
|
|
30
30
|
})
|
|
31
31
|
})
|
|
@@ -67,6 +67,18 @@ function dockNodes(plan) {
|
|
|
67
67
|
}]
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// The overlay window title: the `n/m` counter lives in the border, not in
|
|
71
|
+
// the body, so the review never repeats its heading (issue #85).
|
|
72
|
+
function viewTitle(plan) {
|
|
73
|
+
if (plan !== null && plan.kind === 'items') {
|
|
74
|
+
const total = plan.entries.length
|
|
75
|
+
if (total === 0) return 'Plan'
|
|
76
|
+
const completed = plan.entries.filter((entry) => entry.status === 'completed').length
|
|
77
|
+
return `Plan ${completed}/${total}`
|
|
78
|
+
}
|
|
79
|
+
return 'Plan'
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
function viewNodes(plan) {
|
|
71
83
|
if (plan === null) {
|
|
72
84
|
return [{ id: 'empty', kind: 'notice', level: 'info', text: 'No active plan' }]
|
|
@@ -78,10 +90,8 @@ function viewNodes(plan) {
|
|
|
78
90
|
return [{ id: 'file', kind: 'notice', level: 'info', text: plan.uri }]
|
|
79
91
|
}
|
|
80
92
|
// Items render as one markdown node: a task-list review the transcript
|
|
81
|
-
// pipeline can fully render (
|
|
82
|
-
const
|
|
83
|
-
const completed = plan.entries.filter((entry) => entry.status === 'completed').length
|
|
84
|
-
const lines = [`## Plan · ${completed}/${total}`, '']
|
|
93
|
+
// pipeline can fully render (bold, strikethrough).
|
|
94
|
+
const lines = []
|
|
85
95
|
for (const entry of plan.entries) {
|
|
86
96
|
const content = entry.content.replace(/\s*\n\s*/g, ' ')
|
|
87
97
|
let item
|
package/lib/stats-view.js
CHANGED
|
@@ -1,47 +1,38 @@
|
|
|
1
1
|
/** Built-in Client Plugin: standard ACP usage and timing in the composer dock. */
|
|
2
2
|
|
|
3
3
|
export const name = 'stats-view'
|
|
4
|
-
export const inject = ['acpSessionStats', '
|
|
4
|
+
export const inject = ['acpSessionStats', 'tuiSlots']
|
|
5
5
|
|
|
6
6
|
export function apply(ctx) {
|
|
7
7
|
let current = ctx.acpSessionStats.current()
|
|
8
|
-
let status = ctx.acpSessionStatus.current()
|
|
9
8
|
let panel
|
|
10
9
|
const stopSlot = ctx.tuiSlots.inject('conversation.composer.dock', () => {
|
|
11
10
|
panel = ctx.tuiSlots.register(
|
|
12
11
|
{ name: 'conversation.composer.dock', id: 'stats' },
|
|
13
|
-
nodesOf(current
|
|
12
|
+
nodesOf(current),
|
|
14
13
|
)
|
|
15
14
|
return () => panel.dispose()
|
|
16
15
|
})
|
|
17
16
|
const stopStats = ctx.acpSessionStats.subscribe((snapshot) => {
|
|
18
17
|
current = snapshot
|
|
19
|
-
panel?.update(nodesOf(current
|
|
20
|
-
})
|
|
21
|
-
const stopStatus = ctx.acpSessionStatus.subscribe((snapshot) => {
|
|
22
|
-
status = snapshot
|
|
23
|
-
panel?.update(nodesOf(current, status))
|
|
18
|
+
panel?.update(nodesOf(current))
|
|
24
19
|
})
|
|
25
20
|
return () => {
|
|
26
21
|
stopStats?.()
|
|
27
|
-
stopStatus?.()
|
|
28
22
|
stopSlot?.()
|
|
29
23
|
}
|
|
30
24
|
}
|
|
31
25
|
|
|
32
|
-
function nodesOf(snapshot
|
|
26
|
+
function nodesOf(snapshot) {
|
|
33
27
|
const usage = snapshot?.usage ?? {}
|
|
34
28
|
const stats = snapshot?.stats ?? {}
|
|
35
|
-
const model = status?.model
|
|
36
29
|
const nodes = []
|
|
37
30
|
if ((usage.input ?? 0) > 0 || (usage.output ?? 0) > 0) {
|
|
38
31
|
nodes.push(node('tokens', `↑${formatTokens(usage.input)} · ↓${formatTokens(usage.output)}`))
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const pct = Math.min(100, used / size * 100).toFixed(1)
|
|
44
|
-
nodes.push(node('context', `${pct}%/${contextSizeLabel(size)}`))
|
|
32
|
+
const gauge = contextGauge(snapshot)
|
|
33
|
+
if (gauge.used > 0 && gauge.size > 0) {
|
|
34
|
+
const pct = Math.min(100, gauge.used / gauge.size * 100).toFixed(1)
|
|
35
|
+
nodes.push(node('context', `${pct}%/${contextSizeLabel(gauge.size)}`))
|
|
45
36
|
}
|
|
46
37
|
}
|
|
47
38
|
if ((stats.turns ?? 0) > 0 || (stats.steps ?? 0) > 0) {
|
|
@@ -83,18 +74,19 @@ function node(id, title) {
|
|
|
83
74
|
function plural(value, singular) { return value === 1 ? singular : `${singular}s` }
|
|
84
75
|
|
|
85
76
|
/**
|
|
86
|
-
* Context
|
|
87
|
-
*
|
|
88
|
-
*
|
|
77
|
+
* Context-window gauge. Only the harness `usage_update` readout is shown:
|
|
78
|
+
* `used` is the final prompt size and `size` the real model window from
|
|
79
|
+
* `request/context`. Nothing client-side can reproduce either — per-turn
|
|
80
|
+
* usage sums every step's full context (multi-step turns over-report),
|
|
81
|
+
* and a model-name guess does not know the window (the old 128K guess is
|
|
82
|
+
* what pegged the dock at 100% early in issue #77). Without an
|
|
83
|
+
* authoritative readout the gauge is hidden instead of showing a wrong
|
|
84
|
+
* percentage.
|
|
89
85
|
*/
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return 1_000_000
|
|
95
|
-
default:
|
|
96
|
-
return 128_000
|
|
97
|
-
}
|
|
86
|
+
function contextGauge(snapshot) {
|
|
87
|
+
const context = snapshot?.context
|
|
88
|
+
if (context?.size > 0) return { used: context.used ?? 0, size: context.size }
|
|
89
|
+
return { used: 0, size: 0 }
|
|
98
90
|
}
|
|
99
91
|
|
|
100
92
|
/** `1000000` → `1.0M`, `128000` → `128K` — keeps the one-decimal look of
|
package/lib/status-view.js
CHANGED
|
@@ -27,7 +27,8 @@ export function apply(ctx) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* One
|
|
30
|
+
* One markdown node of run-state facts (the window border already names
|
|
31
|
+
* the popup, so no in-body heading). Facts come from
|
|
31
32
|
* `acpSessionStatus`; every token/turn/step/timing fact comes from
|
|
32
33
|
* `acpSessionStats.current()` — the same snapshot `stats-view` renders in
|
|
33
34
|
* the composer dock, so the two readouts can never drift apart.
|
|
@@ -37,7 +38,7 @@ function statusMarkdown(status, stats) {
|
|
|
37
38
|
const folded = stats?.stats ?? {}
|
|
38
39
|
const total = (usage.input ?? 0) + (usage.output ?? 0)
|
|
39
40
|
+ (usage.cached ?? 0) + (usage.reasoning ?? 0)
|
|
40
|
-
const lines = [
|
|
41
|
+
const lines = []
|
|
41
42
|
lines.push(`- state · ${status.state ?? 'idle'}`)
|
|
42
43
|
lines.push(`- acp · ${status.connection ?? 'not attached'}`)
|
|
43
44
|
if (status.auth?.status !== undefined) {
|
package/lib/tomorrow.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gallery palette pack `tomorrow`. Registers complete dark/light token maps:
|
|
3
|
+
* dark from Tomorrow Night Bright, light from Tomorrow
|
|
4
|
+
* (terminalcolors.com/themes/tomorrow). Does not activate:
|
|
5
|
+
* `/theme` covers it. `inject = ['tuiTheme']`: sibling profile row, not
|
|
6
|
+
* `ctx.plugin` inside the runner.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from 'node:fs'
|
|
10
|
+
|
|
11
|
+
const tomorrowPalette = JSON.parse(
|
|
12
|
+
readFileSync(new URL('./palettes/tomorrow.json', import.meta.url), 'utf8'),
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
export const name = 'tui-theme-tomorrow'
|
|
16
|
+
export const inject = ['tuiTheme']
|
|
17
|
+
|
|
18
|
+
export function apply(ctx) {
|
|
19
|
+
ctx.effect(() => ctx.tuiTheme.register(tomorrowPalette, { activate: false }))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { tomorrowPalette }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "martty",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.31",
|
|
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/packaged-acp-permission.test.mjs ../scripts/profile-smoke-tui.test.mjs ../scripts/release.test.mjs ../scripts/jsonrpc-line-transport.test.mjs ../scripts/tui-theme.test.mjs ../scripts/tui-presets.test.mjs ../scripts/tui-plugin-store.test.mjs ../scripts/tui-local-plugins.test.mjs ../scripts/tui-client-plugin-registry.test.mjs ../scripts/tui-slots.test.mjs ../scripts/tui-commands.test.mjs ../scripts/tui-overlay.test.mjs ../scripts/tui-agents.test.mjs ../scripts/tui-queue.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/agents-view.test.mjs ../scripts/plan-view.test.mjs ../scripts/queue-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/packaged-acp-permission.test.mjs ../scripts/profile-smoke-tui.test.mjs ../scripts/release.test.mjs ../scripts/jsonrpc-line-transport.test.mjs ../scripts/tui-theme.test.mjs ../scripts/tui-presets.test.mjs ../scripts/tui-plugin-store.test.mjs ../scripts/tui-local-plugins.test.mjs ../scripts/tui-client-plugin-registry.test.mjs ../scripts/tui-slots.test.mjs ../scripts/tui-commands.test.mjs ../scripts/tui-overlay.test.mjs ../scripts/tui-agents.test.mjs ../scripts/tui-queue.test.mjs ../scripts/mux.test.mjs ../scripts/acp-client.test.mjs ../scripts/harnesses.test.mjs ../scripts/harness-view.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/agents-view.test.mjs ../scripts/plan-view.test.mjs ../scripts/queue-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
|
"test:profile-install-matrix": "node --test ../scripts/profile-install-matrix.test.mjs"
|
|
20
20
|
},
|
|
21
21
|
"publishConfig": {
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
77
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
78
|
-
"@openma/deepseek-harness-acp": "0.4.
|
|
78
|
+
"@openma/deepseek-harness-acp": "0.4.27"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@deepseek-ai/dsh": "0.1.1-rc.2"
|
|
Binary file
|
package/vendor/darwin-x64/martty
CHANGED
|
Binary file
|
|
Binary file
|
package/vendor/linux-x64/martty
CHANGED
|
Binary file
|
|
Binary file
|