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 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. The Node Client process owns a Cordis tree and starts the Rust painter. A sibling
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 { bootClient, parseClientArgv, painterArgs } from '../lib/boot.js'
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 {
@@ -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' && value.state === 'idle') {
112
- value.state = 'starting'
113
- publish()
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 === 'starting' && RUNNING_UPDATE_TYPES.has(type)) {
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 {
@@ -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?.update(dockNodes(current))
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: 'Switch the visible Agent transcript',
33
+ description: 'Toggle the Agent panel (on/off)',
34
+ input: { hint: '[on|off|agent-id]' },
24
35
  }, async (args) => {
25
- current = ctx.tuiAgents.current()
26
- const target = args.trim()
27
- if (target.length > 0) {
28
- return ctx.tuiAgents.select(target)
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
- return ctx.tuiAgents.navigate('begin')
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
- return { agent: resolveStackedAgent(), rustArgs }
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
+ }
@@ -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 and session/update messages',
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: 'Plan',
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 (headings, bold, strikethrough).
82
- const total = plan.entries.length
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', 'acpSessionStatus', 'tuiSlots']
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, status),
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, status))
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, status) {
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 used = (usage.input ?? 0) + (usage.output ?? 0)
40
- + (usage.cached ?? 0) + (usage.reasoning ?? 0)
41
- const size = contextWindowOf(model)
42
- if (used > 0 && size > 0) {
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 window of the current model (tokens). The ACP surface does not
87
- * carry the window size, so the known DeepSeek family sizes stand in;
88
- * unknown models fall back to a common 128K.
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 contextWindowOf(model) {
91
- switch (model) {
92
- case 'deepseek-v4-flash':
93
- case 'deepseek-v4-pro':
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
@@ -27,7 +27,8 @@ export function apply(ctx) {
27
27
  }
28
28
 
29
29
  /**
30
- * One `## status` markdown node. Run-state facts come from
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 = ['## status', '']
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) {
@@ -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.29",
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.26"
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
Binary file
Binary file
Binary file
Binary file