martty 0.2.21 → 0.2.24

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.
@@ -0,0 +1,121 @@
1
+ /** Built-in Client Plugin: Agent navigation above the composer's metadata row. */
2
+
3
+ export const name = 'agents-view'
4
+ export const inject = ['tuiAgents', 'tuiSlots', 'tuiCommands']
5
+
6
+ export function apply(ctx) {
7
+ let current = ctx.tuiAgents.current()
8
+ let panel
9
+
10
+ const stopSlot = ctx.tuiSlots.inject('conversation.navigation.dock', () => {
11
+ panel = ctx.tuiSlots.register(
12
+ { name: 'conversation.navigation.dock', id: 'agents-view', order: 0 },
13
+ dockNodes(current),
14
+ )
15
+ return () => panel.dispose()
16
+ })
17
+ const stopAgents = ctx.tuiAgents.subscribe((snapshot) => {
18
+ current = snapshot
19
+ panel?.update(dockNodes(current))
20
+ })
21
+ const stopCommand = ctx.tuiCommands.register({
22
+ name: 'agents',
23
+ description: 'Switch the visible Agent transcript',
24
+ }, async (args) => {
25
+ current = ctx.tuiAgents.current()
26
+ const target = args.trim()
27
+ if (target.length > 0) {
28
+ return ctx.tuiAgents.select(target)
29
+ }
30
+ return ctx.tuiAgents.navigate('begin')
31
+ })
32
+
33
+ return () => {
34
+ stopCommand?.()
35
+ stopAgents?.()
36
+ stopSlot?.()
37
+ }
38
+ }
39
+
40
+ function dockNodes(snapshot) {
41
+ if (!Array.isArray(snapshot?.items) || snapshot.items.length < 2) return []
42
+ const selecting = snapshot.selectedId !== null && snapshot.selectedId !== undefined
43
+ if (!selecting) {
44
+ const agents = snapshot.items.filter((item) => item.kind === 'subagent')
45
+ const marked = agents.filter((item) => item.current !== false)
46
+ const current = marked.length > 0 ? marked : agents
47
+ const completed = current.filter((item) => item.status === 'finished' || item.status === 'failed').length
48
+ const running = current.some((item) => item.status === 'running')
49
+ const failed = current.some((item) => item.status === 'failed')
50
+ return [
51
+ {
52
+ id: 'summary', kind: 'generic', title: '· Agents', body: `${completed}/${current.length}`,
53
+ status: failed ? 'err' : running ? 'running' : 'done', tone: 'caption',
54
+ },
55
+ {
56
+ id: 'switch', kind: 'generic', title: '↓ expand', body: '', tone: 'caption',
57
+ },
58
+ ]
59
+ }
60
+ const historyActive = snapshot.items.some((item) => (
61
+ item.kind === 'subagent'
62
+ && item.current === false
63
+ && sameId(item.id, snapshot.activeId)
64
+ ))
65
+ const visibleItems = snapshot.items
66
+ .filter((item) => item.kind !== 'subagent' || item.current !== false)
67
+ .sort((left, right) => Number(left.kind === 'history') - Number(right.kind === 'history'))
68
+ return [
69
+ {
70
+ id: 'label', kind: 'generic', title: '· Agents', body: '',
71
+ tone: 'caption',
72
+ },
73
+ ...visibleItems.map((item) => agentNode(item, {
74
+ active: sameId(item.id, snapshot.activeId) || (item.kind === 'history' && historyActive),
75
+ focused: selecting && sameId(item.id, snapshot.selectedId),
76
+ selecting,
77
+ })),
78
+ {
79
+ id: 'switch',
80
+ kind: 'generic',
81
+ title: '←/→ · enter · esc close',
82
+ body: '',
83
+ tone: 'caption',
84
+ },
85
+ ]
86
+ }
87
+
88
+ function agentNode(item, state) {
89
+ const prefix = state.focused
90
+ ? '▸ '
91
+ : state.selecting && state.active
92
+ ? '• '
93
+ : !state.selecting && state.active
94
+ ? '▸ '
95
+ : ''
96
+ return {
97
+ id: `agent-${item.id}`,
98
+ kind: 'generic',
99
+ title: `${prefix}${item.label}`,
100
+ body: '',
101
+ ...(item.kind === 'subagent' && item.status === 'running'
102
+ ? { status: 'running' }
103
+ : item.kind === 'subagent' && item.status === 'finished'
104
+ ? { status: 'done' }
105
+ : item.kind === 'subagent' && item.status === 'failed'
106
+ ? { status: 'err' }
107
+ : {}),
108
+ ...(state.focused ? { selected: true } : {}),
109
+ tone: state.focused
110
+ ? 'brand'
111
+ : state.active
112
+ ? state.selecting ? 'brand_soft' : 'brand'
113
+ : item.kind === 'history' ? 'caption' : 'fg',
114
+ }
115
+ }
116
+
117
+ function sameId(left, right) {
118
+ return right !== null && right !== undefined && String(left) === String(right)
119
+ }
120
+
121
+ export { dockNodes }
package/lib/boot.js CHANGED
@@ -23,9 +23,13 @@ import { apply as applyIceberg, inject as icebergInject } from './iceberg.js'
23
23
  import { apply as applySolarized, inject as solarizedInject } from './solarized.js'
24
24
  import { apply as applyCommands } from './tui-commands.js'
25
25
  import { apply as applyOverlay } from './tui-overlay.js'
26
+ import { apply as applyAgents } from './tui-agents.js'
27
+ import { apply as applyQueue } from './tui-queue.js'
26
28
  import { apply as applyPresets, inject as presetsInject } from './tui-presets.js'
27
29
  import { apply as applyMarttyPreset, inject as marttyPresetInject } from './martty-preset.js'
28
30
  import { apply as applyPlanView, inject as planViewInject } from './plan-view.js'
31
+ import { apply as applyAgentsView, inject as agentsViewInject } from './agents-view.js'
32
+ import { apply as applyQueueView, inject as queueViewInject } from './queue-view.js'
29
33
  import { apply as applyStatsView, inject as statsViewInject } from './stats-view.js'
30
34
  import { apply as applySessionStatus, inject as sessionStatusInject } from './acp-session-status.js'
31
35
  import { apply as applyStatusView, inject as statusViewInject } from './status-view.js'
@@ -64,10 +68,14 @@ export async function bootClient(options = {}) {
64
68
  await ctx.plugin({ name: 'tui-slots', inject: [], apply: applySlots })
65
69
  await ctx.plugin({ name: 'tui-commands', inject: [], apply: applyCommands })
66
70
  await ctx.plugin({ name: 'tui-overlay', inject: [], apply: applyOverlay })
71
+ await ctx.plugin({ name: 'tui-agents', inject: [], apply: applyAgents })
72
+ await ctx.plugin({ name: 'tui-queue', inject: [], apply: applyQueue })
67
73
  await ctx.plugin({ name: 'tui-presets', inject: presetsInject, apply: applyPresets }, presetConfig)
68
74
  await ctx.plugin({ name: 'martty-preset', inject: marttyPresetInject, apply: applyMarttyPreset })
69
75
  await ctx.plugin({ name: 'acp-client', inject: [], apply: applyAcpClient }, acpConfig)
70
76
  await ctx.plugin({ name: 'plan-view', inject: planViewInject, apply: applyPlanView })
77
+ await ctx.plugin({ name: 'agents-view', inject: agentsViewInject, apply: applyAgentsView })
78
+ await ctx.plugin({ name: 'queue-view', inject: queueViewInject, apply: applyQueueView })
71
79
  await ctx.plugin({ name: 'stats-view', inject: statsViewInject, apply: applyStatsView })
72
80
  await ctx.plugin({ name: 'acp-session-status', inject: sessionStatusInject, apply: applySessionStatus })
73
81
  await ctx.plugin({ name: 'status-view', inject: statusViewInject, apply: applyStatusView })
@@ -100,6 +108,7 @@ export async function bootClient(options = {}) {
100
108
  inject: [
101
109
  'acpClient', 'tuiTheme', 'tuiSlots', 'tuiCommands', 'tuiOverlay',
102
110
  'tuiPresets', 'acpClientEvents', 'acpSessionConfig', 'tuiCordisClientRunner',
111
+ 'tuiQueue', 'tuiAgents',
103
112
  ],
104
113
  apply: applyShell,
105
114
  },
@@ -116,10 +125,14 @@ export async function bootClient(options = {}) {
116
125
  applySlots(ctx)
117
126
  applyCommands(ctx)
118
127
  applyOverlay(ctx)
128
+ applyAgents(ctx)
129
+ applyQueue(ctx)
119
130
  applyPresets(ctx, presetConfig)
120
131
  applyMarttyPreset(ctx)
121
132
  applyAcpClient(ctx, acpConfig)
122
133
  applyPlanView(ctx)
134
+ applyAgentsView(ctx)
135
+ applyQueueView(ctx)
123
136
  applyStatsView(ctx)
124
137
  applySessionStatus(ctx)
125
138
  applyStatusView(ctx)
@@ -4,6 +4,16 @@
4
4
 
5
5
  import { bootClient, parseClientPluginsEnv } from './boot.js'
6
6
 
7
+ // The Host runner kills this process with SIGTERM on shutdown, and the user's
8
+ // Ctrl+C reaches it as SIGINT from the foreground terminal group. Node's
9
+ // default signal handling terminates without running 'exit' listeners, which
10
+ // would strand the Rust painter as an orphan holding the TTY (it is only
11
+ // killed from the Client's 'exit' hook). Convert signals into an orderly
12
+ // process.exit so painter teardown and TTY restore always run.
13
+ for (const [signal, code] of [['SIGTERM', 143], ['SIGINT', 130], ['SIGHUP', 129]]) {
14
+ process.on(signal, () => process.exit(code))
15
+ }
16
+
7
17
  await bootClient({
8
18
  stream: { stdin: process.stdout, stdout: process.stdin },
9
19
  extraArgs: process.argv.slice(2),
@@ -33,6 +33,10 @@ export const CORDIS_METHODS = Object.freeze({
33
33
  commandInvoke: '_dsh/cordis/tui/commands/invoke',
34
34
  overlayUpdate: '_dsh/cordis/tui/overlay/update',
35
35
  overlayEvent: '_dsh/cordis/tui/overlay/event',
36
+ queueUpdate: '_dsh/cordis/tui/queue/update',
37
+ agentsUpdate: '_dsh/cordis/tui/agents/update',
38
+ agentsSelect: '_dsh/cordis/tui/agents/select',
39
+ agentsNavigate: '_dsh/cordis/tui/agents/navigate',
36
40
  sessionConfigSet: '_dsh/cordis/tui/session-config/set',
37
41
  })
38
42
 
package/lib/index.js CHANGED
@@ -17,7 +17,7 @@ import { muxAcpAndCompositor } from './mux.js'
17
17
  export const name = 'dsh-tui-shell'
18
18
  export const inject = [
19
19
  'acpClient', 'tuiTheme', 'tuiPresets', 'tuiSlots', 'tuiCommands', 'tuiOverlay',
20
- 'acpClientEvents', 'acpSessionConfig', 'tuiCordisClientRunner',
20
+ 'acpClientEvents', 'acpSessionConfig', 'tuiQueue', 'tuiAgents', 'tuiCordisClientRunner',
21
21
  ]
22
22
 
23
23
  const shellStateKey = Symbol.for('martty/shell-state')
@@ -137,6 +137,15 @@ export async function applyShell(ctx, options = {}) {
137
137
  'dsh-tui-shell: ctx.acpClientEvents must expose ACP observers',
138
138
  )
139
139
  }
140
+ const queue = ctx.tuiQueue ?? ctx.get?.('tuiQueue')
141
+ if (queue === undefined || typeof queue.observe !== 'function') {
142
+ throw new Error('dsh-tui-shell: ctx.tuiQueue must expose observe')
143
+ }
144
+ const agents = ctx.tuiAgents ?? ctx.get?.('tuiAgents')
145
+ if (agents === undefined || typeof agents.observe !== 'function'
146
+ || typeof agents.bindNotify !== 'function') {
147
+ throw new Error('dsh-tui-shell: ctx.tuiAgents must expose observe and bindNotify')
148
+ }
140
149
 
141
150
  // Config-watch recomposes the tree at boot and disposes this fiber. Keep
142
151
  // one painter; do not kill it from ctx.effect or the screen never appears.
@@ -226,6 +235,12 @@ export async function applyShell(ctx, options = {}) {
226
235
  if (message.method === CORDIS_METHODS.overlayEvent) {
227
236
  return overlay.dispatch(message.params)
228
237
  }
238
+ if (message.method === CORDIS_METHODS.queueUpdate) {
239
+ return { ok: queue.observe(message.params) }
240
+ }
241
+ if (message.method === CORDIS_METHODS.agentsUpdate) {
242
+ return { ok: agents.observe(message.params) }
243
+ }
229
244
  if (message.method === CORDIS_METHODS.approvalRespond) {
230
245
  return clientRunner.respondApproval(message.params)
231
246
  }
@@ -242,6 +257,7 @@ export async function applyShell(ctx, options = {}) {
242
257
  slots.bindNotify(notifyTui)
243
258
  commands.bindNotify(notifyTui)
244
259
  overlay.bindNotify(notifyTui)
260
+ agents.bindNotify(notifyTui)
245
261
  }
246
262
  republishCompositorState()
247
263
  clientRunner.bindTransport(mux.requestAgent, notifyTui)
package/lib/inspect.js CHANGED
@@ -204,7 +204,8 @@ export function slotInspectProvider(tuiSlots) {
204
204
  note:
205
205
  'welcome.hero and welcome.info are independent single root regions; '
206
206
  + 'chrome.right is a root list slot; conversation.input.dock is the additive row above '
207
- + 'the composer; conversation.composer.dock is the additive compact row below it. '
207
+ + 'the composer; conversation.navigation.dock is compact session navigation inside '
208
+ + 'the composer above its metadata; conversation.composer.dock is the outer compact telemetry row. '
208
209
  + 'Node ids are stable inside one contribution; '
209
210
  + 'the compositor namespaces them by contribution id. Compose group/markdown/reasoning/'
210
211
  + 'user/generic/terminal/diff/image/notice/unknown nodes. Colors use theme tokens only.',
package/lib/mux.js CHANGED
@@ -20,6 +20,10 @@ const COMPOSITOR_METHODS = Object.freeze(new Set([
20
20
  CORDIS_METHODS.overlayUpdate,
21
21
  CORDIS_METHODS.commandInvoke,
22
22
  CORDIS_METHODS.overlayEvent,
23
+ CORDIS_METHODS.queueUpdate,
24
+ CORDIS_METHODS.agentsUpdate,
25
+ CORDIS_METHODS.agentsSelect,
26
+ CORDIS_METHODS.agentsNavigate,
23
27
  CORDIS_METHODS.sessionConfigSet,
24
28
  CORDIS_METHODS.approvalRespond,
25
29
  CORDIS_METHODS.uiSelected,
package/lib/plan-view.js CHANGED
@@ -46,22 +46,24 @@ function dockNodes(plan) {
46
46
  const focus = plan.entries.find((entry) => entry.status === 'in_progress')
47
47
  ?? plan.entries.find((entry) => entry.status !== 'completed')
48
48
  ?? plan.entries.at(-1)
49
- return [{
50
- id: 'summary',
51
- kind: 'generic',
52
- title: `Plan · ${completed}/${total}${focus ? ` · ${focus.content}` : ''}`,
53
- body: '',
54
- action: { kind: 'command', name: 'plan-view', args: '' },
55
- ...(completed === total ? { status: 'ok' } : { status: 'running' }),
56
- }]
49
+ const action = { kind: 'command', name: 'plan-view', args: '' }
50
+ return [
51
+ {
52
+ id: 'summary', kind: 'generic', title: Plan', body: `${completed}/${total}`,
53
+ tone: 'caption', status: completed === total ? 'done' : 'running', action,
54
+ },
55
+ ...(focus ? [{
56
+ id: 'focus', kind: 'generic', title: focus.content, body: '', tone: 'caption', action,
57
+ }] : []),
58
+ ]
57
59
  }
58
60
  return [{
59
61
  id: 'summary',
60
62
  kind: 'generic',
61
- title: plan.kind === 'file' ? `Plan · ${plan.uri}` : 'Plan · available',
62
- body: '',
63
+ title: '· Plan',
64
+ body: plan.kind === 'file' ? plan.uri : 'available',
65
+ tone: 'caption',
63
66
  action: { kind: 'command', name: 'plan-view', args: '' },
64
- status: 'running',
65
67
  }]
66
68
  }
67
69
 
@@ -0,0 +1,66 @@
1
+ /** Built-in Client Plugin: native Queue state in the composer input dock. */
2
+
3
+ export const name = 'queue-view'
4
+ export const inject = ['tuiQueue', 'tuiSlots']
5
+
6
+ export function apply(ctx) {
7
+ let current = ctx.tuiQueue.current()
8
+ let panel
9
+
10
+ const stopSlot = ctx.tuiSlots.inject('conversation.input.dock', () => {
11
+ panel = ctx.tuiSlots.register(
12
+ { name: 'conversation.input.dock', id: 'queue-view', order: -10 },
13
+ dockNodes(current),
14
+ )
15
+ return () => panel.dispose()
16
+ })
17
+ const stopQueue = ctx.tuiQueue.subscribe((snapshot) => {
18
+ current = snapshot
19
+ panel?.update(dockNodes(current))
20
+ })
21
+
22
+ return () => {
23
+ stopQueue?.()
24
+ stopSlot?.()
25
+ }
26
+ }
27
+
28
+ function dockNodes(queue) {
29
+ if (!Array.isArray(queue?.items) || queue.items.length === 0) return []
30
+ const expanded = queue.selectedId !== null || queue.editingId !== null
31
+ const count = Number.isInteger(queue.count) ? queue.count : queue.items.length
32
+ const title = expanded
33
+ ? queue.editingId !== null
34
+ ? `Queue · ${count} · enter save · ctrl+d delete · esc cancel`
35
+ : `Queue · ${count} · ↑/↓ choose · enter edit · esc close`
36
+ : `Queue · ${count} · enter send first · ⌥↑ edit`
37
+ const nodes = [{
38
+ id: 'summary',
39
+ kind: 'generic',
40
+ title,
41
+ body: '',
42
+ status: 'running',
43
+ }]
44
+ nodes.push({
45
+ id: 'items',
46
+ kind: 'group',
47
+ children: queue.items.map((item) => {
48
+ const editing = sameId(item.id, queue.editingId)
49
+ const selected = sameId(item.id, queue.selectedId)
50
+ const deleting = editing && queue.deleteConfirm
51
+ return {
52
+ id: `item-${item.id}`,
53
+ kind: 'text',
54
+ text: `${deleting ? '×' : editing ? '✎' : selected ? '▸' : '›'} ${item.ordinal} ${item.summary}`,
55
+ tone: deleting ? 'err' : editing ? 'warn' : selected ? 'brand' : 'fg_secondary',
56
+ }
57
+ }),
58
+ })
59
+ return nodes
60
+ }
61
+
62
+ function sameId(left, right) {
63
+ return right !== null && String(left) === String(right)
64
+ }
65
+
66
+ export { dockNodes }
@@ -0,0 +1,133 @@
1
+ /** Native-painter Agent navigation projected into the Cordis Client tree. */
2
+
3
+ import { Service } from '@deepseek-ai/cordis'
4
+ import { CORDIS_METHODS } from './cordis-protocol.js'
5
+
6
+ export const name = 'tui-agents'
7
+ export const inject = []
8
+
9
+ class TuiAgentsService extends Service {
10
+ constructor(ctx, core) {
11
+ super(ctx, 'tuiAgents')
12
+ this.core = core
13
+ }
14
+
15
+ current() { return this.core.current() }
16
+ subscribe(listener) { return this.core.subscribe(this.ctx, listener) }
17
+ observe(snapshot) { return this.core.observe(snapshot) }
18
+ select(id) { return this.core.select(id) }
19
+ navigate(action) { return this.core.navigate(action) }
20
+ bindNotify(notify) { return this.core.bindNotify(notify) }
21
+ }
22
+
23
+ const EMPTY = Object.freeze({ activeId: null, selectedId: null, items: [] })
24
+ const ITEM_KINDS = new Set(['main', 'subagent', 'history'])
25
+ const ITEM_STATUSES = new Set(['idle', 'running', 'finished', 'failed'])
26
+ const NAVIGATION_ACTIONS = new Set(['begin', 'previous', 'next', 'confirm', 'cancel'])
27
+
28
+ export function installTuiAgents(ctx, options = {}) {
29
+ let value = EMPTY
30
+ let send = typeof options.notify === 'function' ? options.notify : undefined
31
+ const listeners = new Set()
32
+
33
+ function current() {
34
+ return structuredClone(value)
35
+ }
36
+
37
+ function subscribe(effectCtx, listener) {
38
+ if (typeof listener !== 'function') {
39
+ throw new Error('tuiAgents.subscribe: listener must be a function')
40
+ }
41
+ const setup = () => {
42
+ listeners.add(listener)
43
+ return () => listeners.delete(listener)
44
+ }
45
+ const release = typeof effectCtx?.effect === 'function'
46
+ ? effectCtx.effect(setup, 'tuiAgents.subscribe')
47
+ : setup()
48
+ let disposed = false
49
+ return () => {
50
+ if (disposed) return
51
+ disposed = true
52
+ return release?.()
53
+ }
54
+ }
55
+
56
+ function observe(snapshot) {
57
+ if (!object(snapshot) || snapshot.protocol !== 0 || !Array.isArray(snapshot.items)) return false
58
+ const items = snapshot.items.map(normalizeItem).filter((item) => item !== null)
59
+ const activeId = idOrNull(snapshot.activeId)
60
+ const selectedId = idOrNull(snapshot.selectedId)
61
+ value = {
62
+ activeId: items.some((item) => sameId(item.id, activeId)) ? activeId : null,
63
+ selectedId: items.some((item) => sameId(item.id, selectedId)) ? selectedId : null,
64
+ items,
65
+ }
66
+ const next = current()
67
+ for (const listener of [...listeners]) listener(next)
68
+ return true
69
+ }
70
+
71
+ function bindNotify(notify) {
72
+ if (typeof notify !== 'function') throw new Error('tuiAgents.bindNotify: notify must be a function')
73
+ send = notify
74
+ }
75
+
76
+ function select(id) {
77
+ const selected = idOrNull(id)
78
+ if (selected === null || !value.items.some((item) => sameId(item.id, selected))) return false
79
+ if (typeof send !== 'function') return false
80
+ send(CORDIS_METHODS.agentsSelect, { protocol: 0, id: selected })
81
+ return true
82
+ }
83
+
84
+ function navigate(action) {
85
+ if (!NAVIGATION_ACTIONS.has(action) || typeof send !== 'function') return false
86
+ send(CORDIS_METHODS.agentsNavigate, { protocol: 0, action })
87
+ return true
88
+ }
89
+
90
+ const core = { current, subscribe, observe, select, navigate, bindNotify }
91
+ const service = typeof ctx.provide === 'function'
92
+ ? new TuiAgentsService(ctx, core)
93
+ : {
94
+ current,
95
+ subscribe(listener) { return subscribe(ctx, listener) },
96
+ observe,
97
+ select,
98
+ navigate,
99
+ bindNotify,
100
+ }
101
+ if (typeof ctx.provide !== 'function') ctx.tuiAgents = service
102
+ return service
103
+ }
104
+
105
+ function normalizeItem(item) {
106
+ if (!object(item)) return null
107
+ const id = idOrNull(item.id)
108
+ if (id === null || typeof item.label !== 'string' || item.label.length === 0
109
+ || !ITEM_KINDS.has(item.kind) || !ITEM_STATUSES.has(item.status)) return null
110
+ return {
111
+ id, label: item.label, kind: item.kind, status: item.status,
112
+ ...(typeof item.current === 'boolean' ? { current: item.current } : {}),
113
+ }
114
+ }
115
+
116
+ function idOrNull(value) {
117
+ return (typeof value === 'string' && value.length > 0)
118
+ || (Number.isSafeInteger(value) && value >= 0)
119
+ ? value
120
+ : null
121
+ }
122
+
123
+ function sameId(left, right) {
124
+ return right !== null && String(left) === String(right)
125
+ }
126
+
127
+ function object(value) {
128
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
129
+ }
130
+
131
+ export function apply(ctx) {
132
+ installTuiAgents(ctx)
133
+ }
@@ -0,0 +1,103 @@
1
+ /** Native-painter Queue state projected into the Cordis Client tree. */
2
+
3
+ import { Service } from '@deepseek-ai/cordis'
4
+
5
+ export const name = 'tui-queue'
6
+ export const inject = []
7
+
8
+ class TuiQueueService extends Service {
9
+ constructor(ctx, core) {
10
+ super(ctx, 'tuiQueue')
11
+ this.core = core
12
+ }
13
+
14
+ current() { return this.core.current() }
15
+ subscribe(listener) { return this.core.subscribe(this.ctx, listener) }
16
+ observe(snapshot) { return this.core.observe(snapshot) }
17
+ }
18
+
19
+ const EMPTY = Object.freeze({
20
+ count: 0,
21
+ items: [],
22
+ selectedId: null,
23
+ editingId: null,
24
+ deleteConfirm: false,
25
+ })
26
+
27
+ export function installTuiQueue(ctx) {
28
+ let value = EMPTY
29
+ const listeners = new Set()
30
+
31
+ function current() {
32
+ return structuredClone(value)
33
+ }
34
+
35
+ function subscribe(effectCtx, listener) {
36
+ if (typeof listener !== 'function') {
37
+ throw new Error('tuiQueue.subscribe: listener must be a function')
38
+ }
39
+ const setup = () => {
40
+ listeners.add(listener)
41
+ return () => listeners.delete(listener)
42
+ }
43
+ const release = typeof effectCtx?.effect === 'function'
44
+ ? effectCtx.effect(setup, 'tuiQueue.subscribe')
45
+ : setup()
46
+ let disposed = false
47
+ return () => {
48
+ if (disposed) return
49
+ disposed = true
50
+ return release?.()
51
+ }
52
+ }
53
+
54
+ function observe(snapshot) {
55
+ if (!object(snapshot) || snapshot.protocol !== 0 || !Array.isArray(snapshot.items)) return false
56
+ const items = snapshot.items.map(normalizeItem).filter((item) => item !== null)
57
+ value = {
58
+ count: Number.isInteger(snapshot.count) && snapshot.count >= items.length
59
+ ? snapshot.count
60
+ : items.length,
61
+ items,
62
+ selectedId: idOrNull(snapshot.selectedId),
63
+ editingId: idOrNull(snapshot.editingId),
64
+ deleteConfirm: snapshot.deleteConfirm === true,
65
+ }
66
+ const next = current()
67
+ for (const listener of [...listeners]) listener(next)
68
+ return true
69
+ }
70
+
71
+ const core = { current, subscribe, observe }
72
+ const service = typeof ctx.provide === 'function'
73
+ ? new TuiQueueService(ctx, core)
74
+ : {
75
+ current,
76
+ subscribe(listener) { return subscribe(ctx, listener) },
77
+ observe,
78
+ }
79
+ if (typeof ctx.provide !== 'function') ctx.tuiQueue = service
80
+ return service
81
+ }
82
+
83
+ function normalizeItem(item) {
84
+ if (!object(item) || idOrNull(item.id) === null
85
+ || !Number.isInteger(item.ordinal) || item.ordinal < 1
86
+ || typeof item.summary !== 'string') return null
87
+ return { id: item.id, ordinal: item.ordinal, summary: item.summary }
88
+ }
89
+
90
+ function idOrNull(value) {
91
+ return (typeof value === 'string' && value.length > 0)
92
+ || (Number.isSafeInteger(value) && value >= 0)
93
+ ? value
94
+ : null
95
+ }
96
+
97
+ function object(value) {
98
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
99
+ }
100
+
101
+ export function apply(ctx) {
102
+ installTuiQueue(ctx)
103
+ }
package/lib/tui-slots.js CHANGED
@@ -14,6 +14,7 @@ export const SLOT_NAMES = Object.freeze([
14
14
  'welcome.info',
15
15
  'chrome.right',
16
16
  'conversation.input.dock',
17
+ 'conversation.navigation.dock',
17
18
  'conversation.composer.dock',
18
19
  ])
19
20
 
@@ -22,6 +23,7 @@ const SLOT_DEFINITIONS = Object.freeze({
22
23
  'welcome.info': Object.freeze({ kind: 'single', scope: 'root' }),
23
24
  'chrome.right': Object.freeze({ kind: 'list', scope: 'root' }),
24
25
  'conversation.input.dock': Object.freeze({ kind: 'list', scope: 'session' }),
26
+ 'conversation.navigation.dock': Object.freeze({ kind: 'list', scope: 'session' }),
25
27
  'conversation.composer.dock': Object.freeze({ kind: 'list', scope: 'session' }),
26
28
  })
27
29
 
@@ -64,7 +66,10 @@ const NODE_FIELDS = Object.freeze({
64
66
  markdown: { required: ['id', 'kind', 'text'], optional: ['streaming'] },
65
67
  reasoning: { required: ['id', 'kind', 'text', 'done'], optional: ['seconds'] },
66
68
  user: { required: ['id', 'kind', 'text'], optional: ['queued'] },
67
- generic: { required: ['id', 'kind', 'title', 'body'], optional: ['status', 'action'] },
69
+ generic: {
70
+ required: ['id', 'kind', 'title', 'body'],
71
+ optional: ['status', 'tone', 'selected', 'action'],
72
+ },
68
73
  terminal: { required: ['id', 'kind', 'title', 'body'], optional: ['exit'] },
69
74
  diff: { required: ['id', 'kind', 'title', 'unified'], optional: ['path'] },
70
75
  image: { required: ['id', 'kind', 'name', 'mime'], optional: ['dataBase64'] },
@@ -168,9 +173,13 @@ function validateNode(node, path, ids) {
168
173
  case 'generic':
169
174
  string(node.title, `${path}.title`)
170
175
  string(node.body, `${path}.body`)
171
- if (node.status !== undefined && !['running', 'ok', 'err'].includes(node.status)) {
172
- throw new Error(`tuiSlots: ${path}.status must be running, ok, or err`)
176
+ if (node.status !== undefined && !['running', 'done', 'ok', 'err'].includes(node.status)) {
177
+ throw new Error(`tuiSlots: ${path}.status must be running, done, ok, or err`)
173
178
  }
179
+ if (node.tone !== undefined && !THEME_TOKENS.has(node.tone)) {
180
+ throw new Error(`tuiSlots: ${path}.tone must be a theme token`)
181
+ }
182
+ optionalBoolean(node.selected, `${path}.selected`)
174
183
  optionalAction(node.action, `${path}.action`)
175
184
  break
176
185
  case 'terminal':
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "martty",
3
- "version": "0.2.21",
3
+ "version": "0.2.24",
4
4
  "description": "Terminal-native ACP client UI; Cordis client tree, any ACP agent",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/openma-ai/Martty.git"
9
9
  },
10
- "homepage": "https://dshsuite.dev",
10
+ "homepage": "https://martty.sh",
11
11
  "bugs": {
12
12
  "url": "https://github.com/openma-ai/Martty/issues"
13
13
  },
@@ -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/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/mux.test.mjs ../scripts/acp-client.test.mjs ../scripts/acp-client-events.test.mjs ../scripts/acp-session-config.test.mjs ../scripts/acp-session-plan.test.mjs ../scripts/acp-session-stats.test.mjs ../scripts/acp-session-status.test.mjs ../scripts/plan-view.test.mjs ../scripts/stats-view.test.mjs ../scripts/status-view.test.mjs ../scripts/deepseek-logo.test.mjs ../scripts/runner.test.mjs ../scripts/inspect.test.mjs ../scripts/creator-overlay.test.mjs ../scripts/real-agent-e2e.test.mjs",
18
+ "test": "node --test ../scripts/package-native.test.mjs ../scripts/check-release-tag.test.mjs ../scripts/check-static-elf.test.mjs ../scripts/smoke-old-linux.test.mjs ../scripts/cargo-guard.test.mjs ../scripts/build-npm.test.mjs ../scripts/client-profile.test.mjs ../scripts/plugin-runner.test.mjs ../scripts/profile-link-resolution.test.mjs ../scripts/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",
19
19
  "test:profile-install-matrix": "node --test ../scripts/profile-install-matrix.test.mjs"
20
20
  },
21
21
  "publishConfig": {
@@ -30,10 +30,14 @@
30
30
  "./slots": "./lib/tui-slots.js",
31
31
  "./commands": "./lib/tui-commands.js",
32
32
  "./overlay": "./lib/tui-overlay.js",
33
+ "./agents": "./lib/tui-agents.js",
34
+ "./agents-view": "./lib/agents-view.js",
35
+ "./queue": "./lib/tui-queue.js",
33
36
  "./session-plan": "./lib/acp-session-plan.js",
34
37
  "./session-stats": "./lib/acp-session-stats.js",
35
38
  "./session-status": "./lib/acp-session-status.js",
36
39
  "./plan-view": "./lib/plan-view.js",
40
+ "./queue-view": "./lib/queue-view.js",
37
41
  "./stats-view": "./lib/stats-view.js",
38
42
  "./status-view": "./lib/status-view.js",
39
43
  "./deepseek-logo": "./lib/deepseek-logo.js",
@@ -71,7 +75,7 @@
71
75
  },
72
76
  "dependencies": {
73
77
  "@deepseek-ai/cordis": "^4.0.1",
74
- "@openma/deepseek-harness-acp": "0.4.22"
78
+ "@openma/deepseek-harness-acp": "0.4.25"
75
79
  },
76
80
  "devDependencies": {
77
81
  "@deepseek-ai/dsh": "0.1.1-rc.2"
Binary file
Binary file
Binary file
Binary file
Binary file