martty 0.2.33 → 0.2.35

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/lib/index.js CHANGED
@@ -223,8 +223,10 @@ export async function applyShell(ctx, options = {}) {
223
223
  },
224
224
  onAcp(direction, message) {
225
225
  if (direction === 'client') {
226
+ agent.observeClient?.(message)
226
227
  clientEvents.observeClient(message)
227
228
  } else {
229
+ agent.observeAgent?.(message)
228
230
  clientEvents.observeAgent(message)
229
231
  }
230
232
  },
@@ -260,6 +262,8 @@ export async function applyShell(ctx, options = {}) {
260
262
  throw new Error(`unsupported Cordis TUI method: ${String(message.method)}`)
261
263
  },
262
264
  })
265
+ agent.onSwitch?.(() => mux.resetAgent())
266
+ agent.onFailure?.((error) => mux.failAgent(error))
263
267
  const notifyTui = (method, params) => mux.notifyTui(method, params)
264
268
  republishCompositorState = () => {
265
269
  handle.bindNotify(notifyTui)
package/lib/mux.js CHANGED
@@ -126,6 +126,8 @@ export function onJsonLines(source, onLine) {
126
126
  * notifyTui: (method: string, params?: object) => void,
127
127
  * requestAgent: (method: string, params?: object) => Promise<unknown>,
128
128
  * requestTui: (method: string, params?: object) => Promise<unknown>,
129
+ * resetAgent: () => void,
130
+ * failAgent: (error: Error) => void,
129
131
  * }}
130
132
  */
131
133
  export function muxAcpAndCompositor(opts) {
@@ -137,6 +139,7 @@ export function muxAcpAndCompositor(opts) {
137
139
  /** @type {Map<string, { resolve: (value: unknown) => void, reject: (error: Error) => void }>} */
138
140
  const pendingAgent = new Map()
139
141
  const pendingTui = new Map()
142
+ const pendingClient = new Set()
140
143
  let nextHostId = 0
141
144
  let nextTuiId = 0
142
145
  /** @type {unknown} */
@@ -167,6 +170,7 @@ export function muxAcpAndCompositor(opts) {
167
170
  if (agentCordis !== null) onHost?.(message)
168
171
  return
169
172
  }
173
+ if (message?.id !== undefined && typeof message.method !== 'string') pendingClient.delete(message.id)
170
174
  onAcp?.('agent', message)
171
175
  tui.output.write(`${line}\n`)
172
176
  if (initializeId !== undefined && message && typeof message === 'object' && message.id === initializeId) {
@@ -244,10 +248,29 @@ export function muxAcpAndCompositor(opts) {
244
248
  initializeId = outgoing.id
245
249
  }
246
250
  onAcp?.('client', outgoing)
251
+ if (outgoing.id !== undefined && typeof outgoing.method === 'string') pendingClient.add(outgoing.id)
247
252
  writeJsonLine(agent.stdin, outgoing)
248
253
  })
249
254
 
255
+ const failAgent = (error) => {
256
+ for (const waiter of pendingAgent.values()) waiter.reject(error)
257
+ pendingAgent.clear()
258
+ const requests = [...pendingClient]
259
+ pendingClient.clear()
260
+ for (const id of requests) {
261
+ const response = { jsonrpc: '2.0', id, error: { code: -32603, message: error.message } }
262
+ onAcp?.('agent', response)
263
+ writeJsonLine(tui.output, response)
264
+ }
265
+ }
266
+
250
267
  return {
268
+ failAgent,
269
+ resetAgent() {
270
+ initializeId = undefined
271
+ agentCordis = null
272
+ failAgent(new Error('ACP Agent was replaced'))
273
+ },
251
274
  notifyTui(method, params) {
252
275
  writeJsonLine(
253
276
  tui.output,
package/lib/plan-view.js CHANGED
@@ -43,7 +43,7 @@ function dockNodes(plan) {
43
43
  const total = plan.entries.length
44
44
  if (total === 0) return []
45
45
  const completed = plan.entries.filter((entry) => entry.status === 'completed').length
46
- const focus = plan.entries.find((entry) => entry.status === 'in_progress')
46
+ const focus = plan.entries.find((entry) => isInProgress(entry.status))
47
47
  ?? plan.entries.find((entry) => entry.status !== 'completed')
48
48
  ?? plan.entries.at(-1)
49
49
  const action = { kind: 'command', name: 'plan-view', args: '' }
@@ -53,7 +53,10 @@ function dockNodes(plan) {
53
53
  tone: 'caption', status: completed === total ? 'done' : 'running', action,
54
54
  },
55
55
  ...(focus ? [{
56
- id: 'focus', kind: 'generic', title: focus.content, body: '', tone: 'caption', action,
56
+ id: 'focus', kind: 'generic', title: focus.content,
57
+ body: statusLabel(focus.status),
58
+ ...(isInProgress(focus.status) ? { status: 'running' } : {}),
59
+ tone: 'caption', action,
57
60
  }] : []),
58
61
  ]
59
62
  }
@@ -100,10 +103,22 @@ function viewNodes(plan) {
100
103
  } else if (entry.status === 'cancelled' || entry.status === 'failed') {
101
104
  item = `- [ ] ~~${content}~~`
102
105
  } else {
103
- item = entry.status === 'in_progress' ? `- [ ] **${content}**` : `- [ ] ${content}`
106
+ item = isInProgress(entry.status) ? `- **${content}** · in progress` : `- [ ] ${content}`
104
107
  }
105
108
  if (entry.priority) item += ` · priority · ${entry.priority}`
106
109
  lines.push(item)
107
110
  }
108
111
  return [{ id: 'content', kind: 'markdown', text: lines.join('\n') }]
109
112
  }
113
+
114
+ function isInProgress(status) {
115
+ return status === 'in_progress' || status === 'in-progress'
116
+ }
117
+
118
+ function statusLabel(status) {
119
+ if (isInProgress(status)) return 'in progress'
120
+ if (status === 'completed') return 'completed'
121
+ if (status === 'cancelled') return 'cancelled'
122
+ if (status === 'failed') return 'failed'
123
+ return 'pending'
124
+ }
@@ -41,8 +41,10 @@ function statusMarkdown(status, stats) {
41
41
  const lines = []
42
42
  lines.push(`- state · ${status.state ?? 'idle'}`)
43
43
  lines.push(`- acp · ${status.connection ?? 'not attached'}`)
44
+ if (status.error) lines.push(`- connection error · ${status.error}`)
44
45
  if (status.auth?.status !== undefined) {
45
46
  lines.push(`- auth · ${status.auth.status}${status.auth.method ? ` · ${status.auth.method}` : ''}`)
47
+ if (status.auth.message) lines.push(`- auth error · ${status.auth.message}`)
46
48
  }
47
49
  lines.push(`- session · ${status.session?.bound ? status.session.sessionId : 'unbound'}`)
48
50
  if (status.server !== undefined) lines.push(`- server · ${status.server}`)
@@ -75,7 +75,7 @@ function validateInput(input) {
75
75
  throw new Error('tuiCommands.register: each input option must be an object')
76
76
  }
77
77
  const unknown = Object.keys(option)
78
- .filter((key) => !['value', 'label', 'description'].includes(key))
78
+ .filter((key) => !['value', 'label', 'description', 'disabled'].includes(key))
79
79
  if (unknown.length > 0) {
80
80
  throw new Error(`tuiCommands.register: unknown input option field(s) ${unknown.join(', ')}`)
81
81
  }
@@ -90,6 +90,7 @@ function validateInput(input) {
90
90
  }
91
91
  return {
92
92
  value: option.value,
93
+ ...(option.disabled === undefined ? {} : { disabled: booleanOption(option.disabled) }),
93
94
  ...(option.label === undefined ? {} : { label: option.label }),
94
95
  ...(option.description === undefined ? {} : { description: option.description }),
95
96
  }
@@ -98,6 +99,11 @@ function validateInput(input) {
98
99
  return normalized
99
100
  }
100
101
 
102
+ function booleanOption(value) {
103
+ if (typeof value !== 'boolean') throw new Error('tuiCommands.register: input option disabled must be a boolean')
104
+ return value
105
+ }
106
+
101
107
  /**
102
108
  * @param {object} ctx
103
109
  * @param {{ notify?: (method: string, params: object) => void }} [options]
@@ -151,17 +151,31 @@ function validateSelect(input) {
151
151
  if (option.description !== undefined && typeof option.description !== 'string') {
152
152
  throw new Error(`tuiOverlay.openSelect: options[${index}].description must be a string`)
153
153
  }
154
+ if (option.disabled !== undefined && typeof option.disabled !== 'boolean') {
155
+ throw new Error(`tuiOverlay.openSelect: options[${index}].disabled must be a boolean`)
156
+ }
157
+ if (option.deletable !== undefined && typeof option.deletable !== 'boolean') {
158
+ throw new Error(`tuiOverlay.openSelect: options[${index}].deletable must be a boolean`)
159
+ }
160
+ if (option.group !== undefined && (typeof option.group !== 'string'
161
+ || option.group.trim().length === 0 || /[\x00-\x1f\x7f-\x9f\u2028\u2029]/.test(option.group))) {
162
+ throw new Error(`tuiOverlay.openSelect: options[${index}].group must be a non-empty single-line string`)
163
+ }
154
164
  return {
155
165
  value: option.value,
156
166
  label: option.label,
167
+ ...(option.disabled === undefined ? {} : { disabled: option.disabled }),
168
+ ...(option.deletable === undefined ? {} : { deletable: option.deletable }),
157
169
  ...(option.description === undefined ? {} : { description: option.description }),
170
+ ...(option.group === undefined ? {} : { group: option.group }),
158
171
  }
159
172
  })
160
173
  const value = input.value === undefined ? options[0].value : input.value
161
174
  if (typeof value !== 'string' || !values.has(value)) {
162
175
  throw new Error('tuiOverlay.openSelect: value must match one option')
163
176
  }
164
- return { kind: 'select', id: input.id, title: input.title, value, options }
177
+ return { kind: 'select', id: input.id, title: input.title, value, options,
178
+ ...(input.searchable === true ? { searchable: true } : {}) }
165
179
  }
166
180
 
167
181
  /**
@@ -213,10 +227,15 @@ export function installTuiOverlay(ctx, options = {}) {
213
227
  }
214
228
 
215
229
  function openSelect(options, handlers = {}) {
230
+ const select = validateSelect(options)
216
231
  if (current !== null) {
217
- throw new Error(`tuiOverlay.openSelect: overlay "${current.overlay.id}" is already open`)
232
+ if (current.overlay.kind !== 'select' || current.overlay.id !== select.id) {
233
+ throw new Error(`tuiOverlay.openSelect: overlay "${current.overlay.id}" is already open`)
234
+ }
235
+ // A background catalog refresh replaces only this modal's snapshot.
236
+ // Retire its old handles so an earlier async callback cannot close it.
237
+ current.closed = true
218
238
  }
219
- const select = validateSelect(options)
220
239
  const entry = { overlay: select, handlers, closed: false }
221
240
  current = entry
222
241
  publish(structuredClone(select))
@@ -265,7 +284,7 @@ export function installTuiOverlay(ctx, options = {}) {
265
284
  }
266
285
 
267
286
  if (entry.overlay.kind === 'select') {
268
- if (!['change', 'submit', 'cancel'].includes(params.event)) {
287
+ if (!['change', 'submit', 'cancel', 'delete'].includes(params.event)) {
269
288
  throw new Error(`tuiOverlay.dispatch: unknown select event "${String(params.event)}"`)
270
289
  }
271
290
  if (params.event !== 'cancel') {
@@ -273,6 +292,9 @@ export function installTuiOverlay(ctx, options = {}) {
273
292
  || !entry.overlay.options.some((option) => option.value === params.value)) {
274
293
  throw new Error('tuiOverlay.dispatch: select value is not an option')
275
294
  }
295
+ if (entry.overlay.options.find((option) => option.value === params.value)?.disabled) return
296
+ if (params.event === 'delete' && (!entry.overlay.options.find(option => option.value === params.value)?.deletable
297
+ || typeof entry.handlers.onDelete !== 'function')) return
276
298
  entry.overlay.value = params.value
277
299
  }
278
300
  if (params.event === 'change') {
@@ -280,7 +302,7 @@ export function installTuiOverlay(ctx, options = {}) {
280
302
  }
281
303
  const handler = params.event === 'submit'
282
304
  ? entry.handlers.onSubmit
283
- : entry.handlers.onCancel
305
+ : params.event === 'delete' ? entry.handlers.onDelete : entry.handlers.onCancel
284
306
  entry.closed = true
285
307
  if (current === entry) {
286
308
  current = null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "martty",
3
- "version": "0.2.33",
3
+ "version": "0.2.35",
4
4
  "description": "Terminal-native ACP client UI; Cordis client tree, any ACP agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,8 +14,9 @@
14
14
  "type": "module",
15
15
  "main": "lib/index.js",
16
16
  "scripts": {
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/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",
17
+ "test:harness-ui": "node --test ../scripts/harness-management-tui.test.mjs",
18
+ "pretest": "node --test ../scripts/workflow-release.test.mjs ../scripts/package-alias.test.mjs ../scripts/harness-removal.test.mjs",
19
+ "test": "node --test ../scripts/download.test.mjs ../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-discovery.test.mjs ../scripts/harness-discovery-scenario.test.mjs ../scripts/harness-view.test.mjs ../scripts/harness-onboarding.test.mjs ../scripts/harness-registry.test.mjs ../scripts/harness-package.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
20
  "test:profile-install-matrix": "node --test ../scripts/profile-install-matrix.test.mjs"
20
21
  },
21
22
  "publishConfig": {
@@ -75,7 +76,9 @@
75
76
  },
76
77
  "dependencies": {
77
78
  "@deepseek-ai/cordis": "^4.0.1",
78
- "@openma/deepseek-harness-acp": "0.4.27"
79
+ "@openma/deepseek-harness-acp": "0.4.29",
80
+ "cross-spawn": "^7.0.6",
81
+ "node-downloader-helper": "2.1.11"
79
82
  },
80
83
  "devDependencies": {
81
84
  "@deepseek-ai/dsh": "0.1.1-rc.2"
Binary file
Binary file
Binary file
Binary file
Binary file