martty 0.2.13 → 0.2.15-beta.0
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 +2 -0
- package/lib/acp-session-status.js +276 -0
- package/lib/boot.js +10 -1
- package/lib/client-run.js +18 -3
- package/lib/deepseek-logo.js +62 -0
- package/lib/inspect.js +49 -5
- package/lib/plan-view.js +19 -14
- package/lib/stats-view.js +1 -1
- package/lib/status-view.js +70 -0
- package/package.json +5 -2
- package/skills/tui-plugin-development/SKILL.md +3 -1
- package/vendor/darwin-arm64/dsh-tui +0 -0
- package/vendor/darwin-x64/dsh-tui +0 -0
- package/vendor/linux-arm64/dsh-tui +0 -0
- package/vendor/linux-x64/dsh-tui +0 -0
- package/vendor/win32-x64/dsh-tui.exe +0 -0
package/README.md
CHANGED
|
@@ -85,6 +85,8 @@ dsh-tui --demo-skin
|
|
|
85
85
|
platform-native modifier bindings, mouse selection, and inline-expanded tools.
|
|
86
86
|
- Dark/light themes, clipboard routing for local, tmux, and SSH sessions, plus
|
|
87
87
|
the optional `/liang` pixel companion.
|
|
88
|
+
- A built-in `/deepseeklogo` Client Plugin that opens the classic DeepSeek
|
|
89
|
+
Harness whale as a semantic overlay without writing to the transcript.
|
|
88
90
|
- A root `chrome.right` plugin rail for validated TuiNode trees, with live
|
|
89
91
|
update/unload and Client inspect support for Creator-authored plugins.
|
|
90
92
|
- Lifecycle-owned local commands and native slider overlays, plus transactions
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard-ACP-backed run-state projection for the Client tree.
|
|
3
|
+
*
|
|
4
|
+
* Folds the non-statistics facts `/status` needs — connection, server,
|
|
5
|
+
* authenticate state, session binding, model, effort, permission, plan,
|
|
6
|
+
* agent preset, and the running/idle state — from messages the mux already
|
|
7
|
+
* observes. Statistics stay in `acpSessionStats`; this service never
|
|
8
|
+
* counts tokens or timings, so there is exactly one stats source.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Service } from '@deepseek-ai/cordis'
|
|
12
|
+
|
|
13
|
+
export const name = 'acp-session-status'
|
|
14
|
+
export const inject = ['acpClientEvents', 'acpSessionConfig']
|
|
15
|
+
|
|
16
|
+
const AUTH_REQUIRED_CODE = -32000
|
|
17
|
+
const SETUP_METHODS = new Set(['session/new', 'session/load'])
|
|
18
|
+
const RUNNING_UPDATE_TYPES = new Set([
|
|
19
|
+
'agent_message_chunk',
|
|
20
|
+
'agent_thought_chunk',
|
|
21
|
+
'tool_call',
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
class AcpSessionStatusService extends Service {
|
|
25
|
+
constructor(ctx, core) {
|
|
26
|
+
super(ctx, 'acpSessionStatus')
|
|
27
|
+
this.core = core
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
current() { return this.core.current() }
|
|
31
|
+
subscribe(listener) { return this.core.subscribe(this.ctx, listener) }
|
|
32
|
+
observeClient(message) { return this.core.observeClient(message) }
|
|
33
|
+
observeAgent(message) { return this.core.observeAgent(message) }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function zero() {
|
|
37
|
+
return {
|
|
38
|
+
state: 'idle',
|
|
39
|
+
connection: 'connecting',
|
|
40
|
+
server: undefined,
|
|
41
|
+
auth: { status: undefined, method: undefined },
|
|
42
|
+
session: { sessionId: undefined, bound: false },
|
|
43
|
+
model: undefined,
|
|
44
|
+
effort: undefined,
|
|
45
|
+
permission: undefined,
|
|
46
|
+
plan: undefined,
|
|
47
|
+
agent: undefined,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function installAcpSessionStatus(ctx, options = {}) {
|
|
52
|
+
const events = options.events ?? ctx.acpClientEvents ?? ctx.get?.('acpClientEvents')
|
|
53
|
+
const sessionConfig = options.sessionConfig
|
|
54
|
+
?? ctx.acpSessionConfig ?? ctx.get?.('acpSessionConfig')
|
|
55
|
+
|
|
56
|
+
const listeners = new Set()
|
|
57
|
+
let value = zero()
|
|
58
|
+
let initializeId
|
|
59
|
+
let pendingAuthenticate = new Set()
|
|
60
|
+
const pendingSetup = new Map()
|
|
61
|
+
let permissionPreset
|
|
62
|
+
let sandboxMode
|
|
63
|
+
|
|
64
|
+
function current() { return structuredClone(value) }
|
|
65
|
+
|
|
66
|
+
function publish() {
|
|
67
|
+
const snapshot = current()
|
|
68
|
+
for (const listener of [...listeners]) listener(snapshot)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function subscribe(effectCtx, listener) {
|
|
72
|
+
if (typeof listener !== 'function') {
|
|
73
|
+
throw new Error('acpSessionStatus.subscribe: listener must be a function')
|
|
74
|
+
}
|
|
75
|
+
const setup = () => {
|
|
76
|
+
listeners.add(listener)
|
|
77
|
+
return () => listeners.delete(listener)
|
|
78
|
+
}
|
|
79
|
+
const release = typeof effectCtx?.effect === 'function'
|
|
80
|
+
? effectCtx.effect(setup, 'acpSessionStatus.subscribe')
|
|
81
|
+
: setup()
|
|
82
|
+
let disposed = false
|
|
83
|
+
return () => {
|
|
84
|
+
if (disposed) return
|
|
85
|
+
disposed = true
|
|
86
|
+
return release?.()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function observeClient(message) {
|
|
91
|
+
if (!object(message) || typeof message.method !== 'string') return
|
|
92
|
+
if (message.method === 'initialize' && message.id !== undefined) {
|
|
93
|
+
initializeId = message.id
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (message.method === 'authenticate' && message.id !== undefined) {
|
|
97
|
+
pendingAuthenticate.add(message.id)
|
|
98
|
+
value.auth.status = 'signing in'
|
|
99
|
+
publish()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
if (SETUP_METHODS.has(message.method) && message.id !== undefined) {
|
|
103
|
+
pendingSetup.set(
|
|
104
|
+
message.id,
|
|
105
|
+
message.method === 'session/load'
|
|
106
|
+
? readString(message.params, 'sessionId', 'session_id')
|
|
107
|
+
: undefined,
|
|
108
|
+
)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
if (message.method === 'session/prompt' && value.state === 'idle') {
|
|
112
|
+
value.state = 'starting'
|
|
113
|
+
publish()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function observeAgent(message) {
|
|
118
|
+
if (!object(message)) return
|
|
119
|
+
if (message.id !== undefined && message.id === initializeId) {
|
|
120
|
+
initializeId = undefined
|
|
121
|
+
value.connection = 'attached'
|
|
122
|
+
const result = object(message.result) ? message.result : undefined
|
|
123
|
+
value.server = readString(result?.agentInfo, 'name')
|
|
124
|
+
const methods = Array.isArray(result?.authMethods) ? result.authMethods : []
|
|
125
|
+
const method = methods.find((candidate) => object(candidate)
|
|
126
|
+
&& typeof candidate.id === 'string' && candidate.id.length > 0)
|
|
127
|
+
if (method !== undefined && value.auth.status === undefined) {
|
|
128
|
+
value.auth.method = readString(method, 'name', 'label') ?? method.id
|
|
129
|
+
}
|
|
130
|
+
publish()
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
if (message.id !== undefined && pendingAuthenticate.has(message.id)) {
|
|
134
|
+
pendingAuthenticate.delete(message.id)
|
|
135
|
+
if (message.error === undefined) {
|
|
136
|
+
value.auth.status = 'configured'
|
|
137
|
+
} else if (isAuthRequired(message.error)) {
|
|
138
|
+
value.auth.status = 'needs sign-in'
|
|
139
|
+
} else {
|
|
140
|
+
value.auth.status = undefined
|
|
141
|
+
}
|
|
142
|
+
publish()
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
if (message.id !== undefined && pendingSetup.has(message.id)) {
|
|
146
|
+
const requested = pendingSetup.get(message.id)
|
|
147
|
+
pendingSetup.delete(message.id)
|
|
148
|
+
if (message.error !== undefined) {
|
|
149
|
+
if (isAuthRequired(message.error)) value.auth.status = 'needs sign-in'
|
|
150
|
+
value.session = { sessionId: requested, bound: false }
|
|
151
|
+
} else {
|
|
152
|
+
const sessionId = readString(message.result, 'sessionId', 'session_id') ?? requested
|
|
153
|
+
value.session = { sessionId, bound: sessionId !== undefined }
|
|
154
|
+
}
|
|
155
|
+
publish()
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
if (message.error !== undefined && isAuthRequired(message.error)) {
|
|
159
|
+
value.auth.status = 'needs sign-in'
|
|
160
|
+
publish()
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (message.method === 'session.status' && object(message.params)) {
|
|
165
|
+
const status = readString(message.params, 'status')
|
|
166
|
+
if (status === 'running' || status === 'idle') {
|
|
167
|
+
value.state = status
|
|
168
|
+
publish()
|
|
169
|
+
}
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
if (message.method === 'session.event' && object(message.params)) {
|
|
173
|
+
const event = object(message.params.event) ? message.params.event : undefined
|
|
174
|
+
const type = readString(event, 'type')
|
|
175
|
+
const data = object(event?.data) ? event.data : undefined
|
|
176
|
+
if (type === 'permission/preset') {
|
|
177
|
+
const preset = readString(data, 'preset')
|
|
178
|
+
if (preset !== undefined) {
|
|
179
|
+
permissionPreset = preset
|
|
180
|
+
value.permission = permissionPreset ?? sandboxMode
|
|
181
|
+
publish()
|
|
182
|
+
}
|
|
183
|
+
} else if (type === 'sandbox/mode') {
|
|
184
|
+
const mode = readString(data, 'mode')
|
|
185
|
+
if (mode !== undefined) {
|
|
186
|
+
sandboxMode = mode
|
|
187
|
+
value.permission = permissionPreset ?? sandboxMode
|
|
188
|
+
publish()
|
|
189
|
+
}
|
|
190
|
+
} else if (type === 'plan/mode' && typeof data?.active === 'boolean') {
|
|
191
|
+
value.plan = data.active
|
|
192
|
+
publish()
|
|
193
|
+
} else if (type === 'agent-preset/selected') {
|
|
194
|
+
const preset = readString(data, 'agentPreset')
|
|
195
|
+
if (preset !== undefined) {
|
|
196
|
+
value.agent = preset
|
|
197
|
+
publish()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
if (message.method !== 'session/update' || !object(message.params)) return
|
|
203
|
+
const update = object(message.params.update) ? message.params.update : undefined
|
|
204
|
+
const type = readString(update, 'sessionUpdate', 'session_update')
|
|
205
|
+
if (value.state === 'starting' && RUNNING_UPDATE_TYPES.has(type)) {
|
|
206
|
+
value.state = 'running'
|
|
207
|
+
publish()
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function onConfigSnapshot(snapshot) {
|
|
212
|
+
if (snapshot === null || typeof snapshot !== 'object') return
|
|
213
|
+
if (typeof snapshot.sessionId === 'string' && snapshot.sessionId.length > 0
|
|
214
|
+
&& value.session.sessionId === undefined) {
|
|
215
|
+
value.session = { sessionId: snapshot.sessionId, bound: true }
|
|
216
|
+
}
|
|
217
|
+
value.model = optionValue(snapshot.options, 'model') ?? value.model
|
|
218
|
+
value.effort = optionValue(snapshot.options, 'effort') ?? value.effort
|
|
219
|
+
publish()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function optionValue(options, id) {
|
|
223
|
+
if (!Array.isArray(options)) return undefined
|
|
224
|
+
const option = options.find((candidate) => candidate?.id === id)
|
|
225
|
+
if (option === undefined) return undefined
|
|
226
|
+
const raw = option.currentValue ?? option.current_value
|
|
227
|
+
return typeof raw === 'string' ? raw : undefined
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const core = { current, subscribe, observeClient, observeAgent }
|
|
231
|
+
const service = typeof ctx.provide === 'function'
|
|
232
|
+
? new AcpSessionStatusService(ctx, core)
|
|
233
|
+
: {
|
|
234
|
+
current,
|
|
235
|
+
subscribe(listener) { return subscribe(ctx, listener) },
|
|
236
|
+
observeClient,
|
|
237
|
+
observeAgent,
|
|
238
|
+
}
|
|
239
|
+
if (typeof ctx.provide !== 'function') ctx.acpSessionStatus = service
|
|
240
|
+
|
|
241
|
+
if (typeof sessionConfig?.subscribe === 'function') {
|
|
242
|
+
sessionConfig.subscribe(onConfigSnapshot)
|
|
243
|
+
}
|
|
244
|
+
// Seed model/effort from anything already folded (session/load replay).
|
|
245
|
+
if (typeof sessionConfig?.list === 'function') {
|
|
246
|
+
onConfigSnapshot({ sessionId: undefined, options: sessionConfig.list() })
|
|
247
|
+
}
|
|
248
|
+
if (typeof events?.register === 'function') {
|
|
249
|
+
// register(observer) only: the service scopes the subscription to its
|
|
250
|
+
// own Context, so pass the folding object alone.
|
|
251
|
+
events.register({ observeClient, observeAgent })
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return service
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function isAuthRequired(error) {
|
|
258
|
+
return object(error) && (error.code === AUTH_REQUIRED_CODE
|
|
259
|
+
|| error.code === 'auth_required' || error.code === 'AuthRequired')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function readString(value, ...keys) {
|
|
263
|
+
if (!object(value)) return undefined
|
|
264
|
+
for (const key of keys) {
|
|
265
|
+
if (typeof value[key] === 'string' && value[key].length > 0) return value[key]
|
|
266
|
+
}
|
|
267
|
+
return undefined
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function object(value) {
|
|
271
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function apply(ctx, options = {}) {
|
|
275
|
+
installAcpSessionStatus(ctx, options)
|
|
276
|
+
}
|
package/lib/boot.js
CHANGED
|
@@ -16,6 +16,9 @@ import { apply as applyCommands } from './tui-commands.js'
|
|
|
16
16
|
import { apply as applyOverlay } from './tui-overlay.js'
|
|
17
17
|
import { apply as applyPlanView, inject as planViewInject } from './plan-view.js'
|
|
18
18
|
import { apply as applyStatsView, inject as statsViewInject } from './stats-view.js'
|
|
19
|
+
import { apply as applySessionStatus, inject as sessionStatusInject } from './acp-session-status.js'
|
|
20
|
+
import { apply as applyStatusView, inject as statusViewInject } from './status-view.js'
|
|
21
|
+
import { apply as applyDeepseekLogo, inject as deepseekLogoInject } from './deepseek-logo.js'
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* @param {object} [options]
|
|
@@ -37,11 +40,14 @@ export async function bootClient(options = {}) {
|
|
|
37
40
|
await ctx.plugin({ name: 'acp-client', inject: [], apply: applyAcpClient }, acpConfig)
|
|
38
41
|
await ctx.plugin({ name: 'plan-view', inject: planViewInject, apply: applyPlanView })
|
|
39
42
|
await ctx.plugin({ name: 'stats-view', inject: statsViewInject, apply: applyStatsView })
|
|
43
|
+
await ctx.plugin({ name: 'acp-session-status', inject: sessionStatusInject, apply: applySessionStatus })
|
|
44
|
+
await ctx.plugin({ name: 'status-view', inject: statusViewInject, apply: applyStatusView })
|
|
45
|
+
await ctx.plugin({ name: 'deepseek-logo', inject: deepseekLogoInject, apply: applyDeepseekLogo })
|
|
40
46
|
await ctx.plugin({
|
|
41
47
|
name: 'tui-cordis-client-runner',
|
|
42
48
|
inject: [
|
|
43
49
|
'tuiTheme', 'tuiSlots', 'tuiCommands', 'tuiOverlay', 'acpSessionConfig',
|
|
44
|
-
'acpSessionPlan', 'acpSessionStats',
|
|
50
|
+
'acpSessionPlan', 'acpSessionStats', 'acpSessionStatus',
|
|
45
51
|
],
|
|
46
52
|
apply: applyCordisClientRunner,
|
|
47
53
|
})
|
|
@@ -64,6 +70,9 @@ export async function bootClient(options = {}) {
|
|
|
64
70
|
applyAcpClient(ctx, acpConfig)
|
|
65
71
|
applyPlanView(ctx)
|
|
66
72
|
applyStatsView(ctx)
|
|
73
|
+
applySessionStatus(ctx)
|
|
74
|
+
applyStatusView(ctx)
|
|
75
|
+
applyDeepseekLogo(ctx)
|
|
67
76
|
applyCordisClientRunner(ctx)
|
|
68
77
|
await applyShell(ctx, { extraArgs: options.extraArgs ?? [], tty: options.tty })
|
|
69
78
|
}
|
package/lib/client-run.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Same closure convention as the web runner: the source is an async function
|
|
5
5
|
* body that returns a plugin. Open services include `tuiTheme`, `tuiSlots`,
|
|
6
|
-
* `acpSessionConfig`, `acpSessionPlan`, `acpSessionStats`,
|
|
6
|
+
* `acpSessionConfig`, `acpSessionPlan`, `acpSessionStats`, `acpSessionStatus`,
|
|
7
|
+
* and lifecycle-owned `timer`; `host.call` reaches this
|
|
7
8
|
* Package's Host half.
|
|
8
9
|
* No React, browser slots, TTY, or raw Host service names.
|
|
9
10
|
*/
|
|
@@ -16,6 +17,7 @@ const ALLOWED_INJECT = new Set([
|
|
|
16
17
|
'acpSessionConfig',
|
|
17
18
|
'acpSessionPlan',
|
|
18
19
|
'acpSessionStats',
|
|
20
|
+
'acpSessionStatus',
|
|
19
21
|
'timer',
|
|
20
22
|
])
|
|
21
23
|
|
|
@@ -32,7 +34,7 @@ const SLOT_TEACHING =
|
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* @param {string} clientCode
|
|
35
|
-
* @param {{ pluginId?: string, tuiTheme?: object, tuiSlots?: object, tuiCommands?: object, tuiOverlay?: object, acpSessionConfig?: object, acpSessionPlan?: object, acpSessionStats?: object, timer?: object, invoke?: Function }} env
|
|
37
|
+
* @param {{ pluginId?: string, tuiTheme?: object, tuiSlots?: object, tuiCommands?: object, tuiOverlay?: object, acpSessionConfig?: object, acpSessionPlan?: object, acpSessionStats?: object, acpSessionStatus?: object, timer?: object, invoke?: Function }} env
|
|
36
38
|
* @returns {Promise<{ waitingFor: string[], dispose: () => void }>}
|
|
37
39
|
*/
|
|
38
40
|
export async function applyClientHalf(clientCode, env) {
|
|
@@ -84,7 +86,7 @@ export async function applyClientHalf(clientCode, env) {
|
|
|
84
86
|
}
|
|
85
87
|
if (!ALLOWED_INJECT.has(name)) {
|
|
86
88
|
throw new Error(
|
|
87
|
-
`TUI Client inject "${name}" is not open. Open services: tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan, acpSessionStats, timer.`,
|
|
89
|
+
`TUI Client inject "${name}" is not open. Open services: tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan, acpSessionStats, acpSessionStatus, timer.`,
|
|
88
90
|
)
|
|
89
91
|
}
|
|
90
92
|
}
|
|
@@ -137,6 +139,7 @@ function restrictedCtx(env, own, inject) {
|
|
|
137
139
|
const sourceSessionConfig = env.acpSessionConfig
|
|
138
140
|
const sourceSessionPlan = env.acpSessionPlan
|
|
139
141
|
const sourceSessionStats = env.acpSessionStats
|
|
142
|
+
const sourceSessionStatus = env.acpSessionStatus
|
|
140
143
|
const sourceTimer = env.timer
|
|
141
144
|
const tuiTheme = sourceTheme === undefined
|
|
142
145
|
? undefined
|
|
@@ -254,6 +257,16 @@ function restrictedCtx(env, own, inject) {
|
|
|
254
257
|
return own(sourceSessionStats.subscribe(listener))
|
|
255
258
|
},
|
|
256
259
|
}
|
|
260
|
+
const acpSessionStatus = sourceSessionStatus === undefined
|
|
261
|
+
? undefined
|
|
262
|
+
: {
|
|
263
|
+
current() {
|
|
264
|
+
return sourceSessionStatus.current()
|
|
265
|
+
},
|
|
266
|
+
subscribe(listener) {
|
|
267
|
+
return own(sourceSessionStatus.subscribe(listener))
|
|
268
|
+
},
|
|
269
|
+
}
|
|
257
270
|
const timer = sourceTimer === undefined || !inject.has('timer')
|
|
258
271
|
? undefined
|
|
259
272
|
: {
|
|
@@ -272,6 +285,7 @@ function restrictedCtx(env, own, inject) {
|
|
|
272
285
|
acpSessionConfig,
|
|
273
286
|
acpSessionPlan,
|
|
274
287
|
acpSessionStats,
|
|
288
|
+
acpSessionStatus,
|
|
275
289
|
timer,
|
|
276
290
|
interval: timer?.interval,
|
|
277
291
|
timeout: timer?.timeout,
|
|
@@ -283,6 +297,7 @@ function restrictedCtx(env, own, inject) {
|
|
|
283
297
|
if (name === 'acpSessionConfig') return acpSessionConfig
|
|
284
298
|
if (name === 'acpSessionPlan') return acpSessionPlan
|
|
285
299
|
if (name === 'acpSessionStats') return acpSessionStats
|
|
300
|
+
if (name === 'acpSessionStatus') return acpSessionStatus
|
|
286
301
|
if (name === 'timer') return timer
|
|
287
302
|
return undefined
|
|
288
303
|
},
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Built-in Client Plugin: the classic DeepSeek Harness whale lockup. */
|
|
2
|
+
|
|
3
|
+
export const name = 'deepseek-logo'
|
|
4
|
+
export const inject = ['tuiCommands', 'tuiOverlay']
|
|
5
|
+
|
|
6
|
+
const WHALE_LG = [
|
|
7
|
+
' ▄▄▄▄ ▄▄▄███ █▄',
|
|
8
|
+
' ▄▄█████████████ ███▄▄ ▄█',
|
|
9
|
+
' ▄█████████████████▄▄ █████▄██████',
|
|
10
|
+
' ▄█████████████████████▄ ▀██████████',
|
|
11
|
+
' ▄████████████████████████▄ ▀████▀▀▀',
|
|
12
|
+
' ██▀ ▀▀▀██████████▀▀█████▄████',
|
|
13
|
+
' ███ ▀███████▀▄ ▀████████',
|
|
14
|
+
' ███ ▀███████ ██████▀',
|
|
15
|
+
' ████ ▀██████████████',
|
|
16
|
+
' ████ ▀████████████',
|
|
17
|
+
' ████▄ ▄▄▄ █████████▀',
|
|
18
|
+
' ▀████▄ ███▄▄ ▀██████▄',
|
|
19
|
+
' ▀█████████████▄▄▄████████',
|
|
20
|
+
' ▀▀████████████▀▀',
|
|
21
|
+
' ▀▀▀▀▀▀',
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const WORDMARK_SMALL = [
|
|
25
|
+
' ___ ___ ___ ___ ___ ___ ___ _ __',
|
|
26
|
+
'| \\| __| __| _ \\/ __| __| __| |/ /',
|
|
27
|
+
'| |) | _|| _|| _/\\__ \\ _|| _|| \' < ',
|
|
28
|
+
'|___/|___|___|_| |___/___|___|_|\\_\\',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
export function deepseekLogoMarkdown() {
|
|
32
|
+
return [
|
|
33
|
+
'## DeepSeek Harness',
|
|
34
|
+
'',
|
|
35
|
+
'```text',
|
|
36
|
+
...WHALE_LG,
|
|
37
|
+
'',
|
|
38
|
+
...WORDMARK_SMALL,
|
|
39
|
+
'H A R N E S S',
|
|
40
|
+
'```',
|
|
41
|
+
'',
|
|
42
|
+
'_Into the Unknown_',
|
|
43
|
+
].join('\n')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function apply(ctx) {
|
|
47
|
+
const stopCommand = ctx.tuiCommands.register({
|
|
48
|
+
name: 'deepseeklogo',
|
|
49
|
+
description: 'Open the classic DeepSeek Harness whale',
|
|
50
|
+
}, async () => {
|
|
51
|
+
ctx.tuiOverlay.openView({
|
|
52
|
+
id: 'deepseek-logo',
|
|
53
|
+
title: 'DeepSeek Harness',
|
|
54
|
+
nodes: [{
|
|
55
|
+
id: 'lockup',
|
|
56
|
+
kind: 'markdown',
|
|
57
|
+
text: deepseekLogoMarkdown(),
|
|
58
|
+
}],
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
return () => stopCommand?.()
|
|
62
|
+
}
|
package/lib/inspect.js
CHANGED
|
@@ -10,7 +10,7 @@ import { CORDIS_METHODS } from './cordis-protocol.js'
|
|
|
10
10
|
export const name = 'tui-cordis-client-runner'
|
|
11
11
|
export const inject = [
|
|
12
12
|
'tuiTheme', 'tuiSlots', 'tuiCommands', 'tuiOverlay', 'acpSessionConfig',
|
|
13
|
-
'acpSessionPlan', 'acpSessionStats',
|
|
13
|
+
'acpSessionPlan', 'acpSessionStats', 'acpSessionStatus',
|
|
14
14
|
]
|
|
15
15
|
|
|
16
16
|
const EMPTY_INPUT = Object.freeze({ type: 'object', properties: {}, additionalProperties: false })
|
|
@@ -589,6 +589,45 @@ export function statsInspectProvider(acpSessionStats) {
|
|
|
589
589
|
}
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
+
/** Describe non-statistics run-state facts folded from standard ACP. */
|
|
593
|
+
export function statusInspectProvider(acpSessionStatus) {
|
|
594
|
+
return {
|
|
595
|
+
manifest: {
|
|
596
|
+
id: 'Status',
|
|
597
|
+
description:
|
|
598
|
+
'Current ACP run-state facts (connection, server, auth, session, model, '
|
|
599
|
+
+ 'effort, permission, plan, agent) folded from standard ACP traffic. '
|
|
600
|
+
+ 'Statistics stay in the Stats provider.',
|
|
601
|
+
methods: [{
|
|
602
|
+
name: 'current',
|
|
603
|
+
description: 'Return the current status snapshot and the read/subscribe contract.',
|
|
604
|
+
inputSchema: EMPTY_INPUT,
|
|
605
|
+
outputSchema: ANY_OUTPUT,
|
|
606
|
+
}],
|
|
607
|
+
},
|
|
608
|
+
query(method) {
|
|
609
|
+
if (method !== 'current') throw new Error(`unknown Status inspect method "${method}"`)
|
|
610
|
+
return {
|
|
611
|
+
current: acpSessionStatus.current(),
|
|
612
|
+
api: {
|
|
613
|
+
service: 'acpSessionStatus',
|
|
614
|
+
inject: ['acpSessionStatus'],
|
|
615
|
+
current: {
|
|
616
|
+
call: 'ctx.acpSessionStatus.current()',
|
|
617
|
+
returns: 'SessionStatusSnapshot',
|
|
618
|
+
},
|
|
619
|
+
subscribe: {
|
|
620
|
+
call: 'ctx.acpSessionStatus.subscribe((snapshot) => { ... })',
|
|
621
|
+
returns: { type: 'function', role: 'dispose', idempotent: true },
|
|
622
|
+
},
|
|
623
|
+
transport: 'standard ACP initialize, authenticate, session and session/update messages',
|
|
624
|
+
},
|
|
625
|
+
referencedTypes: ['SessionStatusSnapshot'],
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
592
631
|
async function mountClientHalf(
|
|
593
632
|
ctx,
|
|
594
633
|
pluginId,
|
|
@@ -600,6 +639,7 @@ async function mountClientHalf(
|
|
|
600
639
|
acpSessionConfig,
|
|
601
640
|
acpSessionPlan,
|
|
602
641
|
acpSessionStats,
|
|
642
|
+
acpSessionStatus,
|
|
603
643
|
timer,
|
|
604
644
|
invoke,
|
|
605
645
|
) {
|
|
@@ -607,7 +647,7 @@ async function mountClientHalf(
|
|
|
607
647
|
return applyClientHalf(clientCode, {
|
|
608
648
|
pluginId,
|
|
609
649
|
tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
610
|
-
acpSessionStats, timer, invoke,
|
|
650
|
+
acpSessionStats, acpSessionStatus, timer, invoke,
|
|
611
651
|
})
|
|
612
652
|
}
|
|
613
653
|
|
|
@@ -619,7 +659,7 @@ async function mountClientHalf(
|
|
|
619
659
|
applied = await applyClientHalf(clientCode, {
|
|
620
660
|
pluginId,
|
|
621
661
|
tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
622
|
-
acpSessionStats, timer, invoke,
|
|
662
|
+
acpSessionStats, acpSessionStatus, timer, invoke,
|
|
623
663
|
})
|
|
624
664
|
return applied.dispose
|
|
625
665
|
},
|
|
@@ -650,6 +690,7 @@ async function mountClientHalf(
|
|
|
650
690
|
* acpSessionConfig?: object,
|
|
651
691
|
* acpSessionPlan?: object,
|
|
652
692
|
* acpSessionStats?: object,
|
|
693
|
+
* acpSessionStatus?: object,
|
|
653
694
|
* requestAgent: (method: string, params?: object) => Promise<unknown>,
|
|
654
695
|
* }} opts
|
|
655
696
|
* @returns {{ onHost: (message: object) => void }}
|
|
@@ -657,7 +698,7 @@ async function mountClientHalf(
|
|
|
657
698
|
export function attachTuiClient(opts) {
|
|
658
699
|
const {
|
|
659
700
|
ctx, tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
660
|
-
acpSessionStats,
|
|
701
|
+
acpSessionStats, acpSessionStatus,
|
|
661
702
|
requestAgent,
|
|
662
703
|
} = opts
|
|
663
704
|
const providers = [
|
|
@@ -668,6 +709,7 @@ export function attachTuiClient(opts) {
|
|
|
668
709
|
...(acpSessionConfig === undefined ? [] : [configOptionsInspectProvider(acpSessionConfig)]),
|
|
669
710
|
...(acpSessionPlan === undefined ? [] : [planInspectProvider(acpSessionPlan)]),
|
|
670
711
|
...(acpSessionStats === undefined ? [] : [statsInspectProvider(acpSessionStats)]),
|
|
712
|
+
...(acpSessionStatus === undefined ? [] : [statusInspectProvider(acpSessionStatus)]),
|
|
671
713
|
]
|
|
672
714
|
const timer = createClientTimer()
|
|
673
715
|
/** @type {Map<string, () => void>} */
|
|
@@ -765,6 +807,7 @@ export function attachTuiClient(opts) {
|
|
|
765
807
|
acpSessionConfig,
|
|
766
808
|
acpSessionPlan,
|
|
767
809
|
acpSessionStats,
|
|
810
|
+
acpSessionStatus,
|
|
768
811
|
timer,
|
|
769
812
|
async (method, args) => {
|
|
770
813
|
const answered = await requestAgent(CORDIS_METHODS.pluginInvoke, {
|
|
@@ -933,13 +976,14 @@ export function apply(ctx) {
|
|
|
933
976
|
const acpSessionConfig = ctx.acpSessionConfig ?? ctx.get?.('acpSessionConfig')
|
|
934
977
|
const acpSessionPlan = ctx.acpSessionPlan ?? ctx.get?.('acpSessionPlan')
|
|
935
978
|
const acpSessionStats = ctx.acpSessionStats ?? ctx.get?.('acpSessionStats')
|
|
979
|
+
const acpSessionStatus = ctx.acpSessionStatus ?? ctx.get?.('acpSessionStatus')
|
|
936
980
|
let client
|
|
937
981
|
const service = {
|
|
938
982
|
bindTransport(requestAgent) {
|
|
939
983
|
client?.dispose()
|
|
940
984
|
client = attachTuiClient({
|
|
941
985
|
ctx, tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan,
|
|
942
|
-
acpSessionStats,
|
|
986
|
+
acpSessionStats, acpSessionStatus,
|
|
943
987
|
requestAgent,
|
|
944
988
|
})
|
|
945
989
|
const attached = client
|
package/lib/plan-view.js
CHANGED
|
@@ -75,18 +75,23 @@ function viewNodes(plan) {
|
|
|
75
75
|
if (plan.kind === 'file') {
|
|
76
76
|
return [{ id: 'file', kind: 'notice', level: 'info', text: plan.uri }]
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
78
|
+
// Items render as one markdown node: a task-list review the transcript
|
|
79
|
+
// pipeline can fully render (headings, bold, strikethrough).
|
|
80
|
+
const total = plan.entries.length
|
|
81
|
+
const completed = plan.entries.filter((entry) => entry.status === 'completed').length
|
|
82
|
+
const lines = [`## Plan · ${completed}/${total}`, '']
|
|
83
|
+
for (const entry of plan.entries) {
|
|
84
|
+
const content = entry.content.replace(/\s*\n\s*/g, ' ')
|
|
85
|
+
let item
|
|
86
|
+
if (entry.status === 'completed') {
|
|
87
|
+
item = `- [x] ${content}`
|
|
88
|
+
} else if (entry.status === 'cancelled' || entry.status === 'failed') {
|
|
89
|
+
item = `- [ ] ~~${content}~~`
|
|
90
|
+
} else {
|
|
91
|
+
item = entry.status === 'in_progress' ? `- [ ] **${content}**` : `- [ ] ${content}`
|
|
92
|
+
}
|
|
93
|
+
if (entry.priority) item += ` · priority · ${entry.priority}`
|
|
94
|
+
lines.push(item)
|
|
95
|
+
}
|
|
96
|
+
return [{ id: 'content', kind: 'markdown', text: lines.join('\n') }]
|
|
92
97
|
}
|
package/lib/stats-view.js
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Built-in Client Plugin: the `/status` run-state overlay. */
|
|
2
|
+
|
|
3
|
+
import { formatDuration, formatRate, formatTokens } from './stats-view.js'
|
|
4
|
+
|
|
5
|
+
export const name = 'status-view'
|
|
6
|
+
export const inject = ['acpSessionStatus', 'acpSessionStats', 'tuiCommands', 'tuiOverlay']
|
|
7
|
+
|
|
8
|
+
export function apply(ctx) {
|
|
9
|
+
const stopCommand = ctx.tuiCommands.register({
|
|
10
|
+
name: 'status',
|
|
11
|
+
description: 'Session run state and key stats',
|
|
12
|
+
}, async () => {
|
|
13
|
+
ctx.tuiOverlay.openView({
|
|
14
|
+
id: 'status',
|
|
15
|
+
title: 'Status',
|
|
16
|
+
nodes: [{
|
|
17
|
+
id: 'content',
|
|
18
|
+
kind: 'markdown',
|
|
19
|
+
text: statusMarkdown(
|
|
20
|
+
ctx.acpSessionStatus.current(),
|
|
21
|
+
ctx.acpSessionStats.current(),
|
|
22
|
+
),
|
|
23
|
+
}],
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
return () => stopCommand?.()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One `## status` markdown node. Run-state facts come from
|
|
31
|
+
* `acpSessionStatus`; every token/turn/step/timing fact comes from
|
|
32
|
+
* `acpSessionStats.current()` — the same snapshot `stats-view` renders in
|
|
33
|
+
* the composer dock, so the two readouts can never drift apart.
|
|
34
|
+
*/
|
|
35
|
+
function statusMarkdown(status, stats) {
|
|
36
|
+
const usage = stats?.usage ?? {}
|
|
37
|
+
const folded = stats?.stats ?? {}
|
|
38
|
+
const total = (usage.input ?? 0) + (usage.output ?? 0)
|
|
39
|
+
+ (usage.cached ?? 0) + (usage.reasoning ?? 0)
|
|
40
|
+
const lines = ['## status', '']
|
|
41
|
+
lines.push(`- state · ${status.state ?? 'idle'}`)
|
|
42
|
+
lines.push(`- acp · ${status.connection ?? 'not attached'}`)
|
|
43
|
+
if (status.auth?.status !== undefined) {
|
|
44
|
+
lines.push(`- auth · ${status.auth.status}${status.auth.method ? ` · ${status.auth.method}` : ''}`)
|
|
45
|
+
}
|
|
46
|
+
lines.push(`- session · ${status.session?.bound ? status.session.sessionId : 'unbound'}`)
|
|
47
|
+
if (status.server !== undefined) lines.push(`- server · ${status.server}`)
|
|
48
|
+
if (status.model !== undefined) lines.push(`- model · ${status.model}`)
|
|
49
|
+
if (status.agent !== undefined) lines.push(`- agent · ${status.agent}`)
|
|
50
|
+
if (status.permission !== undefined) lines.push(`- permission · ${status.permission}`)
|
|
51
|
+
if (status.plan !== undefined) lines.push(`- plan · ${status.plan ? 'on' : 'off'}`)
|
|
52
|
+
if (status.effort !== undefined) lines.push(`- effort · ${status.effort}`)
|
|
53
|
+
lines.push(
|
|
54
|
+
`- tokens · ↑${formatTokens(usage.input ?? 0)} ↓${formatTokens(usage.output ?? 0)}`
|
|
55
|
+
+ ` (cached ${formatTokens(usage.cached ?? 0)}) · Σ ${formatTokens(total)}`,
|
|
56
|
+
)
|
|
57
|
+
lines.push(`- turns · ${folded.turns ?? 0} · steps · ${folded.steps ?? 0}`)
|
|
58
|
+
lines.push(`- LLM · ${formatDuration(folded.llmMillis ?? 0)} · tool · ${formatDuration(folded.toolMillis ?? 0)}`)
|
|
59
|
+
if ((folded.ttftCount ?? 0) > 0) {
|
|
60
|
+
lines.push(`- TTFT avg · ${formatDuration(folded.ttftTotalMillis / folded.ttftCount)}`)
|
|
61
|
+
}
|
|
62
|
+
const llmMillis = folded.llmMillis ?? 0
|
|
63
|
+
const output = usage.output ?? 0
|
|
64
|
+
if (llmMillis > 0 && output > 0) {
|
|
65
|
+
lines.push(`- rate · ${formatRate(output / (llmMillis / 1000))} tok/s`)
|
|
66
|
+
}
|
|
67
|
+
return lines.join('\n')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { statusMarkdown }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "martty",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15-beta.0",
|
|
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/release.test.mjs ../scripts/jsonrpc-line-transport.test.mjs ../scripts/tui-theme.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/plan-view.test.mjs ../scripts/stats-view.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/release.test.mjs ../scripts/jsonrpc-line-transport.test.mjs ../scripts/tui-theme.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"
|
|
19
19
|
},
|
|
20
20
|
"publishConfig": {
|
|
21
21
|
"access": "public",
|
|
@@ -30,8 +30,11 @@
|
|
|
30
30
|
"./overlay": "./lib/tui-overlay.js",
|
|
31
31
|
"./session-plan": "./lib/acp-session-plan.js",
|
|
32
32
|
"./session-stats": "./lib/acp-session-stats.js",
|
|
33
|
+
"./session-status": "./lib/acp-session-status.js",
|
|
33
34
|
"./plan-view": "./lib/plan-view.js",
|
|
34
35
|
"./stats-view": "./lib/stats-view.js",
|
|
36
|
+
"./status-view": "./lib/status-view.js",
|
|
37
|
+
"./deepseek-logo": "./lib/deepseek-logo.js",
|
|
35
38
|
"./right-demo": "./lib/right-demo.js",
|
|
36
39
|
"./acp-client": "./lib/acp-client.js",
|
|
37
40
|
"./acp-client-events": "./lib/acp-client-events.js",
|
|
@@ -62,6 +62,7 @@ Use the narrowest capability:
|
|
|
62
62
|
| Current ACP Session option | advertised config-option Provider | `acpSessionConfig` |
|
|
63
63
|
| Current structured ACP Plan | `Plans` | `acpSessionPlan` |
|
|
64
64
|
| Current ACP Session statistics | `Stats` | `acpSessionStats` |
|
|
65
|
+
| Current ACP run-state facts | `Status` | `acpSessionStatus` |
|
|
65
66
|
|
|
66
67
|
A transient control is not a side panel. When the user did not request
|
|
67
68
|
persistent content, do not query Slots or mount `chrome.right`.
|
|
@@ -92,7 +93,8 @@ compose only in the Plugin code. The services do not imply one another:
|
|
|
92
93
|
asked for persistent shell UI. Use stable node ids and update the existing
|
|
93
94
|
contribution instead of creating parallel panels. Select the live seat from
|
|
94
95
|
`Slots.list`: all current seats aggregate contributors. Use
|
|
95
|
-
`conversation.input.dock` for content needing its own line
|
|
96
|
+
`conversation.input.dock` for content needing its own line (it owns the
|
|
97
|
+
single cap row and displaces the tip line while present) and keep
|
|
96
98
|
`conversation.composer.dock` contributions compact.
|
|
97
99
|
- **Overlays:** open a transient native control only in response to the
|
|
98
100
|
interaction that needs it. A command is one possible trigger, not part of
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/vendor/linux-x64/dsh-tui
CHANGED
|
Binary file
|
|
Binary file
|