@abelxiaoxing/cadence 2.0.1 → 2.0.3
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 +104 -44
- package/docs/reuse-and-distribution.md +3 -2
- package/docs/service-management.md +62 -3
- package/package.json +10 -7
- package/src/agent-panel-cadence.ts +651 -0
- package/src/agent-panel-model.ts +521 -0
- package/src/agent-panel.ts +474 -0
- package/src/index.ts +12 -4
- package/src/install-config.mjs +25 -0
- package/src/native-client.ts +142 -3
- package/src/operator-cli.mjs +52 -16
- package/src/service/coordination.ts +35 -16
- package/src/service/private-files.ts +141 -28
- package/src/service-cli.mjs +793 -149
- package/src/service-host.mjs +847 -319
- package/src/task-shell.ts +27 -14
- package/src/task-store.ts +104 -25
- package/src/worker-tools.mjs +42 -17
- package/web/dist/assets/cadence.css +1 -1
- package/web/dist/assets/cadence.js +7 -7
- package/web/dist/manifest.json +6 -6
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/** Background-agent panel for Pi's TUI. Observation only: it subscribes to
|
|
2
|
+
* the `subagent:*` events on Pi's shared extension bus and renders them; it
|
|
3
|
+
* never starts, cancels or keeps an agent or the Pi process alive. */
|
|
4
|
+
import {
|
|
5
|
+
AssistantMessageComponent,
|
|
6
|
+
type ExtensionAPI,
|
|
7
|
+
type ExtensionContext,
|
|
8
|
+
getMarkdownTheme,
|
|
9
|
+
type Theme,
|
|
10
|
+
ToolExecutionComponent,
|
|
11
|
+
UserMessageComponent,
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import {
|
|
14
|
+
type Component,
|
|
15
|
+
Container,
|
|
16
|
+
Editor,
|
|
17
|
+
isKeyRelease,
|
|
18
|
+
Key,
|
|
19
|
+
type KeyId,
|
|
20
|
+
matchesKey,
|
|
21
|
+
Spacer,
|
|
22
|
+
Text,
|
|
23
|
+
type TUI,
|
|
24
|
+
truncateToWidth,
|
|
25
|
+
} from "@earendil-works/pi-tui";
|
|
26
|
+
import {
|
|
27
|
+
factsText,
|
|
28
|
+
type Paint,
|
|
29
|
+
type PanelAction,
|
|
30
|
+
PanelFocus,
|
|
31
|
+
type PanelKey,
|
|
32
|
+
panelLines,
|
|
33
|
+
SUBAGENT_EVENTS,
|
|
34
|
+
SUBAGENT_VIEW,
|
|
35
|
+
type SubagentState,
|
|
36
|
+
SubagentStore,
|
|
37
|
+
statusSymbol,
|
|
38
|
+
summaryLines,
|
|
39
|
+
} from "./agent-panel-model.ts";
|
|
40
|
+
|
|
41
|
+
export interface AgentPanelOptions {
|
|
42
|
+
/** Agent rows shown at once; more collapse into `+K more`. */
|
|
43
|
+
maxRows?: number;
|
|
44
|
+
/** Keep `● main` visible with no agents (CADENCE_AGENT_PANEL=always). */
|
|
45
|
+
showEmpty?: boolean;
|
|
46
|
+
now?: () => number;
|
|
47
|
+
}
|
|
48
|
+
const PANEL = "cadence-agents",
|
|
49
|
+
SUMMARY = "cadence-agents-summary",
|
|
50
|
+
SPIN_MS = 100;
|
|
51
|
+
|
|
52
|
+
/** Rows kept for Pi's footer/status lines while an agent view is shown. */
|
|
53
|
+
const RESERVED = 5;
|
|
54
|
+
const KEYS: [KeyId, PanelKey][] = [
|
|
55
|
+
[Key.down, "down"],
|
|
56
|
+
[Key.up, "up"],
|
|
57
|
+
[Key.ctrl("n"), "next"],
|
|
58
|
+
[Key.ctrl("p"), "previous"],
|
|
59
|
+
[Key.enter, "enter"],
|
|
60
|
+
[Key.escape, "escape"],
|
|
61
|
+
[Key.ctrl("c"), "interrupt"],
|
|
62
|
+
[Key.pageUp, "pageUp"],
|
|
63
|
+
[Key.pageDown, "pageDown"],
|
|
64
|
+
[Key.ctrl("u"), "halfUp"],
|
|
65
|
+
[Key.ctrl("d"), "halfDown"],
|
|
66
|
+
[Key.home, "home"],
|
|
67
|
+
[Key.end, "end"],
|
|
68
|
+
];
|
|
69
|
+
/** Key releases (Kitty protocol) stay Pi's. */
|
|
70
|
+
export function panelKey(data: string): PanelKey | undefined {
|
|
71
|
+
if (isKeyRelease(data)) return undefined;
|
|
72
|
+
return KEYS.find(([id]) => matchesKey(data, id))?.[1] ?? "other";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const themePaint =
|
|
76
|
+
(theme: Theme): Paint =>
|
|
77
|
+
(role, text) =>
|
|
78
|
+
role === "selected" ? theme.inverse(text) : theme.fg(role, text);
|
|
79
|
+
|
|
80
|
+
export function registerAgentPanel(
|
|
81
|
+
pi: ExtensionAPI,
|
|
82
|
+
options: AgentPanelOptions = {},
|
|
83
|
+
): void {
|
|
84
|
+
const store = new SubagentStore(options.now);
|
|
85
|
+
for (const channel of Object.values(SUBAGENT_EVENTS))
|
|
86
|
+
pi.events.on(channel, (data) => {
|
|
87
|
+
store.apply(channel, data);
|
|
88
|
+
});
|
|
89
|
+
let detach: (() => void) | undefined;
|
|
90
|
+
pi.on("session_start", (_event, ctx) => {
|
|
91
|
+
detach?.();
|
|
92
|
+
detach =
|
|
93
|
+
ctx.mode === "tui" && ctx.hasUI
|
|
94
|
+
? attachPanel(ctx, store, options, (id) =>
|
|
95
|
+
pi.events.emit(SUBAGENT_VIEW, { id }),
|
|
96
|
+
)
|
|
97
|
+
: undefined;
|
|
98
|
+
});
|
|
99
|
+
pi.on("session_shutdown", () => {
|
|
100
|
+
detach?.();
|
|
101
|
+
detach = undefined;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function attachPanel(
|
|
106
|
+
ctx: ExtensionContext,
|
|
107
|
+
store: SubagentStore,
|
|
108
|
+
options: AgentPanelOptions,
|
|
109
|
+
announce: (id: string | null) => void,
|
|
110
|
+
): () => void {
|
|
111
|
+
const now = options.now ?? Date.now;
|
|
112
|
+
const maxRows = Math.max(1, options.maxRows ?? 6);
|
|
113
|
+
const showEmpty =
|
|
114
|
+
options.showEmpty ?? process.env.CADENCE_AGENT_PANEL === "always";
|
|
115
|
+
const focus = new PanelFocus(store);
|
|
116
|
+
let tui: TUI | undefined,
|
|
117
|
+
paint: Paint = (_role, text) => text,
|
|
118
|
+
frame = 0,
|
|
119
|
+
spinner: ReturnType<typeof setInterval> | undefined,
|
|
120
|
+
view: { component: Component; done: () => void } | undefined,
|
|
121
|
+
// Widgets mount with the first agent, so ordinary sessions show nothing.
|
|
122
|
+
mounted = false,
|
|
123
|
+
// The agent last announced on `subagent:view`, so producers load only it.
|
|
124
|
+
announced: string | undefined;
|
|
125
|
+
const announceView = () => {
|
|
126
|
+
if (focus.view === announced) return;
|
|
127
|
+
announced = focus.view;
|
|
128
|
+
announce(announced ?? null);
|
|
129
|
+
};
|
|
130
|
+
const transcripts = new Map<string, Transcript>();
|
|
131
|
+
const stopSpinner = () => {
|
|
132
|
+
clearInterval(spinner);
|
|
133
|
+
spinner = undefined;
|
|
134
|
+
};
|
|
135
|
+
// One redraw per spinner frame while anything runs; Pi's differential
|
|
136
|
+
// renderer rewrites only the changed rows.
|
|
137
|
+
const changed = () => {
|
|
138
|
+
if (!mounted && (store.size > 0 || showEmpty)) mount();
|
|
139
|
+
if (store.running() === 0) stopSpinner();
|
|
140
|
+
else if (!spinner) {
|
|
141
|
+
spinner = setInterval(() => {
|
|
142
|
+
frame++;
|
|
143
|
+
changed();
|
|
144
|
+
}, SPIN_MS);
|
|
145
|
+
spinner.unref?.();
|
|
146
|
+
}
|
|
147
|
+
tui?.requestRender();
|
|
148
|
+
};
|
|
149
|
+
const unsubscribe = store.subscribe(changed);
|
|
150
|
+
const focused = () =>
|
|
151
|
+
(
|
|
152
|
+
tui as { getFocusedComponent?(): Component | null } | undefined
|
|
153
|
+
)?.getFocusedComponent?.();
|
|
154
|
+
/** Pi's own input box has focus: no dialog, selector or overlay is up. */
|
|
155
|
+
const editorFocused = () => focused() instanceof Editor && !tui?.hasOverlay();
|
|
156
|
+
const act = (action: PanelAction) => {
|
|
157
|
+
if (action === "open") openView();
|
|
158
|
+
else if (action === "close") view?.done();
|
|
159
|
+
announceView();
|
|
160
|
+
tui?.requestRender();
|
|
161
|
+
return action === "pass" ? undefined : { consume: true };
|
|
162
|
+
};
|
|
163
|
+
const unlisten = ctx.ui.onTerminalInput((data) => {
|
|
164
|
+
if (focus.view !== undefined) {
|
|
165
|
+
// The view handles its own keys. Pi may have put its editor back in the
|
|
166
|
+
// view's slot (another dialog came and went); then the view is gone.
|
|
167
|
+
if (!(focused() instanceof Editor)) return undefined;
|
|
168
|
+
focus.abandon();
|
|
169
|
+
view = undefined;
|
|
170
|
+
announceView();
|
|
171
|
+
}
|
|
172
|
+
if (focus.focus === "panel" && !editorFocused()) {
|
|
173
|
+
// A dialog took focus: its keys are not the panel's.
|
|
174
|
+
focus.focus = "input";
|
|
175
|
+
tui?.requestRender();
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
const key = panelKey(data);
|
|
179
|
+
if (!key) return undefined;
|
|
180
|
+
const idle = ctx.ui.getEditorText() === "" && editorFocused();
|
|
181
|
+
return act(focus.key(key, idle));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const openView = () => {
|
|
185
|
+
void ctx.ui
|
|
186
|
+
.custom<void>((_tui, _theme, _keybindings, done) => {
|
|
187
|
+
const component: Component = {
|
|
188
|
+
render: (width) => viewLines(width),
|
|
189
|
+
handleInput(data) {
|
|
190
|
+
const key = panelKey(data);
|
|
191
|
+
if (key) act(focus.key(key, false));
|
|
192
|
+
},
|
|
193
|
+
invalidate() {},
|
|
194
|
+
};
|
|
195
|
+
view = { component, done: () => done() };
|
|
196
|
+
return component;
|
|
197
|
+
})
|
|
198
|
+
.finally(() => {
|
|
199
|
+
view = undefined;
|
|
200
|
+
tui?.requestRender();
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
const panelFrame = () => ({
|
|
204
|
+
agents: store.ordered(),
|
|
205
|
+
focus: focus.focus,
|
|
206
|
+
selected: focus.selected,
|
|
207
|
+
view: focus.view,
|
|
208
|
+
now: now(),
|
|
209
|
+
frame,
|
|
210
|
+
maxRows,
|
|
211
|
+
showEmpty,
|
|
212
|
+
});
|
|
213
|
+
const viewLines = (width: number): string[] => {
|
|
214
|
+
const agent = focus.view === undefined ? undefined : store.get(focus.view);
|
|
215
|
+
if (!agent) return [];
|
|
216
|
+
const panelHeight = panelLines(panelFrame(), width, paint).length;
|
|
217
|
+
const height = Math.max(
|
|
218
|
+
3,
|
|
219
|
+
(tui?.terminal.rows ?? 24) - panelHeight - RESERVED,
|
|
220
|
+
);
|
|
221
|
+
let transcript = transcripts.get(agent.id);
|
|
222
|
+
if (!transcript && tui) {
|
|
223
|
+
transcript = new Transcript(tui, ctx.cwd, ctx.ui.getToolsExpanded());
|
|
224
|
+
transcripts.set(agent.id, transcript);
|
|
225
|
+
}
|
|
226
|
+
const body = transcript?.render(agent, width, paint) ?? [];
|
|
227
|
+
const top = focus.settle(agent.id, body.length, height - 2);
|
|
228
|
+
const rows = body.slice(top, top + height - 2);
|
|
229
|
+
while (rows.length < height - 2) rows.push("");
|
|
230
|
+
return [
|
|
231
|
+
truncateToWidth(banner(agent), width, "…"),
|
|
232
|
+
...rows,
|
|
233
|
+
truncateToWidth(
|
|
234
|
+
paint(
|
|
235
|
+
"dim",
|
|
236
|
+
"↑↓ select · Enter open · PgUp/PgDn scroll · Esc back to main",
|
|
237
|
+
),
|
|
238
|
+
width,
|
|
239
|
+
"…",
|
|
240
|
+
),
|
|
241
|
+
];
|
|
242
|
+
};
|
|
243
|
+
const banner = (agent: SubagentState): string => {
|
|
244
|
+
if (agent.status === "running")
|
|
245
|
+
return paint(
|
|
246
|
+
"dim",
|
|
247
|
+
`→ viewing: ${agent.label} (Esc to return to main)`,
|
|
248
|
+
);
|
|
249
|
+
const [symbol, role] = statusSymbol(agent, frame);
|
|
250
|
+
const word = { done: "finished", error: "failed", cancelled: "cancelled" }[
|
|
251
|
+
agent.status
|
|
252
|
+
];
|
|
253
|
+
const facts = factsText(agent, now());
|
|
254
|
+
return `${paint(role, `${symbol} ${word}`)}${paint(
|
|
255
|
+
"dim",
|
|
256
|
+
` (${facts}) · ${agent.label} (Esc to return to main)`,
|
|
257
|
+
)}`;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const mount = () => {
|
|
261
|
+
mounted = true;
|
|
262
|
+
ctx.ui.setWidget(
|
|
263
|
+
SUMMARY,
|
|
264
|
+
(t, theme) => {
|
|
265
|
+
tui = t;
|
|
266
|
+
paint = themePaint(theme);
|
|
267
|
+
return {
|
|
268
|
+
// The agent view takes Z1's place; its summary would sit above it.
|
|
269
|
+
render: (width) =>
|
|
270
|
+
view ? [] : summaryLines(store.ordered(), width, paint, maxRows),
|
|
271
|
+
invalidate() {},
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
{ placement: "aboveEditor" },
|
|
275
|
+
);
|
|
276
|
+
ctx.ui.setWidget(
|
|
277
|
+
PANEL,
|
|
278
|
+
(t, theme) => {
|
|
279
|
+
tui = t;
|
|
280
|
+
paint = themePaint(theme);
|
|
281
|
+
return {
|
|
282
|
+
render: (width) => panelLines(panelFrame(), width, paint),
|
|
283
|
+
invalidate() {},
|
|
284
|
+
};
|
|
285
|
+
},
|
|
286
|
+
{ placement: "belowEditor" },
|
|
287
|
+
);
|
|
288
|
+
};
|
|
289
|
+
changed();
|
|
290
|
+
return () => {
|
|
291
|
+
unsubscribe();
|
|
292
|
+
unlisten();
|
|
293
|
+
stopSpinner();
|
|
294
|
+
if (announced !== undefined) announce(null);
|
|
295
|
+
try {
|
|
296
|
+
view?.done();
|
|
297
|
+
if (mounted) {
|
|
298
|
+
ctx.ui.setWidget(SUMMARY, undefined);
|
|
299
|
+
ctx.ui.setWidget(PANEL, undefined);
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
// A replaced runtime already cleared its UI.
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: match OSC 133 terminal marks
|
|
308
|
+
const OSC_133 = /\x1b\]133;[^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
309
|
+
type Message = Record<string, unknown> & { content?: unknown };
|
|
310
|
+
const textOf = (content: unknown): string =>
|
|
311
|
+
typeof content === "string"
|
|
312
|
+
? content
|
|
313
|
+
: Array.isArray(content)
|
|
314
|
+
? content
|
|
315
|
+
.map((block) =>
|
|
316
|
+
block?.type === "text"
|
|
317
|
+
? String(block.text ?? "")
|
|
318
|
+
: block?.type === "image"
|
|
319
|
+
? "[image]"
|
|
320
|
+
: "",
|
|
321
|
+
)
|
|
322
|
+
.filter(Boolean)
|
|
323
|
+
.join("\n")
|
|
324
|
+
: "";
|
|
325
|
+
|
|
326
|
+
/** One agent's messages as Pi's own transcript components, the way Pi renders
|
|
327
|
+
* a resumed session. Only the tail is rebuilt: new messages, a replaced
|
|
328
|
+
* streaming snapshot, or everything once older messages were dropped. */
|
|
329
|
+
class Transcript {
|
|
330
|
+
private readonly root = new Container();
|
|
331
|
+
private parts: Component[][] = [];
|
|
332
|
+
private tail: object | undefined;
|
|
333
|
+
private tailStreaming = false;
|
|
334
|
+
private omitted = 0;
|
|
335
|
+
private revision = -1;
|
|
336
|
+
private readonly tools = new Map<string, ToolExecutionComponent>();
|
|
337
|
+
constructor(
|
|
338
|
+
private readonly tui: TUI,
|
|
339
|
+
private readonly cwd: string,
|
|
340
|
+
private readonly expanded: boolean,
|
|
341
|
+
) {}
|
|
342
|
+
|
|
343
|
+
render(agent: SubagentState, width: number, paint: Paint): string[] {
|
|
344
|
+
this.sync(agent);
|
|
345
|
+
// Pi marks its own prompts for terminal shell integration; a sliced view
|
|
346
|
+
// of another agent's prompts must not open unmatched marks.
|
|
347
|
+
let lines: string[];
|
|
348
|
+
try {
|
|
349
|
+
lines = this.root.render(width).map((line) => line.replace(OSC_133, ""));
|
|
350
|
+
} catch {
|
|
351
|
+
// Producer data must never break Pi's own render loop.
|
|
352
|
+
lines = [paint("error", " (this transcript could not be rendered)")];
|
|
353
|
+
}
|
|
354
|
+
if (agent.omitted)
|
|
355
|
+
lines.unshift(
|
|
356
|
+
paint("dim", `… ${agent.omitted} earlier messages not retained`),
|
|
357
|
+
);
|
|
358
|
+
if (!agent.messages.length) lines.push(paint("dim", " (no messages yet)"));
|
|
359
|
+
return lines;
|
|
360
|
+
}
|
|
361
|
+
private sync(agent: SubagentState): void {
|
|
362
|
+
if (agent.revision === this.revision) return;
|
|
363
|
+
this.revision = agent.revision;
|
|
364
|
+
const { messages } = agent;
|
|
365
|
+
if (agent.omitted !== this.omitted) {
|
|
366
|
+
this.root.clear();
|
|
367
|
+
this.parts = [];
|
|
368
|
+
this.tools.clear();
|
|
369
|
+
this.omitted = agent.omitted;
|
|
370
|
+
}
|
|
371
|
+
let from = this.parts.length;
|
|
372
|
+
// A streaming snapshot may be replaced or mutated in place.
|
|
373
|
+
if (from > 0 && (this.tailStreaming || messages[from - 1] !== this.tail))
|
|
374
|
+
from--;
|
|
375
|
+
for (const part of this.parts.splice(from))
|
|
376
|
+
for (const component of part) this.root.removeChild(component);
|
|
377
|
+
for (let i = from; i < messages.length; i++) {
|
|
378
|
+
const last = i === messages.length - 1;
|
|
379
|
+
const part = this.build(messages[i] as Message, last && agent.streaming);
|
|
380
|
+
for (const component of part) this.root.addChild(component);
|
|
381
|
+
this.parts.push(part);
|
|
382
|
+
}
|
|
383
|
+
this.tail = messages.at(-1);
|
|
384
|
+
this.tailStreaming = agent.streaming;
|
|
385
|
+
}
|
|
386
|
+
private build(message: Message, streaming: boolean): Component[] {
|
|
387
|
+
switch (message.role) {
|
|
388
|
+
case "user": {
|
|
389
|
+
const text = textOf(message.content);
|
|
390
|
+
return text
|
|
391
|
+
? [new Spacer(1), new UserMessageComponent(text, getMarkdownTheme())]
|
|
392
|
+
: [];
|
|
393
|
+
}
|
|
394
|
+
case "assistant": {
|
|
395
|
+
// Pi's component assumes well-formed blocks; keep only those.
|
|
396
|
+
const content = (
|
|
397
|
+
Array.isArray(message.content) ? message.content : []
|
|
398
|
+
).filter(
|
|
399
|
+
(b) =>
|
|
400
|
+
(b?.type === "text" && typeof b.text === "string") ||
|
|
401
|
+
(b?.type === "thinking" && typeof b.thinking === "string") ||
|
|
402
|
+
b?.type === "toolCall",
|
|
403
|
+
);
|
|
404
|
+
const parts: Component[] = [
|
|
405
|
+
new AssistantMessageComponent(
|
|
406
|
+
{ ...message, content } as never,
|
|
407
|
+
false,
|
|
408
|
+
getMarkdownTheme(),
|
|
409
|
+
),
|
|
410
|
+
];
|
|
411
|
+
const failed =
|
|
412
|
+
message.stopReason === "aborted" || message.stopReason === "error";
|
|
413
|
+
for (const block of content) {
|
|
414
|
+
if (block.type !== "toolCall") continue;
|
|
415
|
+
const tool = this.tool(
|
|
416
|
+
String(block.name ?? "tool"),
|
|
417
|
+
String(block.id ?? ""),
|
|
418
|
+
block.arguments ?? {},
|
|
419
|
+
);
|
|
420
|
+
// Pi's edit renderer previews complete arguments against the file
|
|
421
|
+
// as it is now, where an applied edit no longer matches and would
|
|
422
|
+
// read as a failure; a transcript edit shows its call and result only.
|
|
423
|
+
if (!streaming && block.name !== "edit") tool.setArgsComplete();
|
|
424
|
+
if (failed)
|
|
425
|
+
tool.updateResult({
|
|
426
|
+
content: [
|
|
427
|
+
{
|
|
428
|
+
type: "text",
|
|
429
|
+
text: String(message.errorMessage ?? "Operation aborted"),
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
isError: true,
|
|
433
|
+
});
|
|
434
|
+
parts.push(tool);
|
|
435
|
+
}
|
|
436
|
+
return parts;
|
|
437
|
+
}
|
|
438
|
+
case "toolResult": {
|
|
439
|
+
const result = {
|
|
440
|
+
content: Array.isArray(message.content) ? message.content : [],
|
|
441
|
+
details: message.details,
|
|
442
|
+
isError: message.isError === true,
|
|
443
|
+
};
|
|
444
|
+
const id = String(message.toolCallId ?? "");
|
|
445
|
+
const pending = this.tools.get(id);
|
|
446
|
+
if (pending) {
|
|
447
|
+
pending.updateResult(result, streaming);
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
const orphan = this.tool(String(message.toolName ?? "tool"), id, {});
|
|
451
|
+
orphan.updateResult(result, streaming);
|
|
452
|
+
return [orphan];
|
|
453
|
+
}
|
|
454
|
+
default: {
|
|
455
|
+
const text = textOf(message.content);
|
|
456
|
+
return text ? [new Spacer(1), new Text(text, 1, 0)] : [];
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
private tool(name: string, id: string, args: unknown) {
|
|
461
|
+
const tool = new ToolExecutionComponent(
|
|
462
|
+
name,
|
|
463
|
+
id,
|
|
464
|
+
args,
|
|
465
|
+
{},
|
|
466
|
+
undefined,
|
|
467
|
+
this.tui,
|
|
468
|
+
this.cwd,
|
|
469
|
+
);
|
|
470
|
+
tool.setExpanded(this.expanded);
|
|
471
|
+
this.tools.set(id, tool);
|
|
472
|
+
return tool;
|
|
473
|
+
}
|
|
474
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { registerAgentPanel } from "./agent-panel.ts";
|
|
4
|
+
import { registerCadenceAgentFeed } from "./agent-panel-cadence.ts";
|
|
5
|
+
import { autostartNotice } from "./native-client.ts";
|
|
3
6
|
import { startupCardLines } from "./startup-card.ts";
|
|
4
7
|
import { registerTaskShell } from "./task-shell.ts";
|
|
5
8
|
|
|
@@ -8,6 +11,10 @@ import { registerTaskShell } from "./task-shell.ts";
|
|
|
8
11
|
export default function register(pi: ExtensionAPI): void {
|
|
9
12
|
if (process.env.CADENCE_SUBAGENT_CHILD === "1") return;
|
|
10
13
|
registerTaskShell(pi);
|
|
14
|
+
// Observes `subagent:*` bus events only; renders nothing until one arrives.
|
|
15
|
+
registerAgentPanel(pi);
|
|
16
|
+
// Publishes a running owner's child runs there; readonly, never starts one.
|
|
17
|
+
registerCadenceAgentFeed(pi);
|
|
11
18
|
pi.on("session_start", (_event, ctx) => {
|
|
12
19
|
if (!ctx.hasUI || (ctx.mode !== "tui" && ctx.mode !== "rpc")) return;
|
|
13
20
|
// Preserve independent, nonactivating configuration presentation. This
|
|
@@ -18,10 +25,11 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
18
25
|
home: os.homedir(),
|
|
19
26
|
projectTrusted: ctx.isProjectTrusted(),
|
|
20
27
|
});
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
);
|
|
28
|
+
const shown = lines.length > 1 ? lines.slice(1) : lines;
|
|
29
|
+
// npm hides the install hook's guidance; boot-start gaps surface here.
|
|
30
|
+
const autostart = autostartNotice();
|
|
31
|
+
if (autostart) shown.push(autostart);
|
|
32
|
+
ctx.ui.notify(shown.join("\n"), "info");
|
|
25
33
|
} catch {
|
|
26
34
|
// Presentation only; shell lifecycle still runs.
|
|
27
35
|
}
|
package/src/install-config.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { closeSync, constants, mkdirSync, openSync, writeSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
@@ -40,6 +41,22 @@ export function ensureUserConfig(home = homedir()) {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
/** Pi install/update hook. The management client decides whether this is Pi's
|
|
45
|
+
* Linux user-scope install and refreshes the systemd user unit; only its human
|
|
46
|
+
* guidance (stderr) reaches the installer. */
|
|
47
|
+
export function refreshAutostart(run = spawnSync) {
|
|
48
|
+
if (process.platform !== "linux" || process.versions.bun) return;
|
|
49
|
+
run(
|
|
50
|
+
process.execPath,
|
|
51
|
+
[
|
|
52
|
+
fileURLToPath(new URL("./service-cli.mjs", import.meta.url)),
|
|
53
|
+
"autostart",
|
|
54
|
+
"refresh",
|
|
55
|
+
],
|
|
56
|
+
{ stdio: ["ignore", "ignore", "inherit"] },
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
if (
|
|
44
61
|
process.argv[1] &&
|
|
45
62
|
path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))
|
|
@@ -54,4 +71,12 @@ if (
|
|
|
54
71
|
);
|
|
55
72
|
process.exitCode = 0;
|
|
56
73
|
}
|
|
74
|
+
try {
|
|
75
|
+
refreshAutostart();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
process.stderr.write(
|
|
78
|
+
`Cadence could not refresh its systemd user unit: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
79
|
+
);
|
|
80
|
+
process.exitCode = 0;
|
|
81
|
+
}
|
|
57
82
|
}
|