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.
- package/lib/agents-view.js +121 -0
- package/lib/boot.js +13 -0
- package/lib/client-process.js +10 -0
- package/lib/cordis-protocol.js +4 -0
- package/lib/index.js +17 -1
- package/lib/inspect.js +2 -1
- package/lib/mux.js +4 -0
- package/lib/plan-view.js +13 -11
- package/lib/queue-view.js +66 -0
- package/lib/tui-agents.js +133 -0
- package/lib/tui-queue.js +103 -0
- package/lib/tui-slots.js +12 -3
- package/package.json +8 -4
- 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
|
@@ -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)
|
package/lib/client-process.js
CHANGED
|
@@ -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),
|
package/lib/cordis-protocol.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
...(
|
|
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:
|
|
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
|
+
}
|
package/lib/tui-queue.js
ADDED
|
@@ -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: {
|
|
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.
|
|
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://
|
|
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.
|
|
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
|
package/vendor/darwin-x64/martty
CHANGED
|
Binary file
|
|
Binary file
|
package/vendor/linux-x64/martty
CHANGED
|
Binary file
|
|
Binary file
|