@oh-my-pi/pi-coding-agent 16.4.2 → 16.4.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/CHANGELOG.md +42 -0
- package/dist/cli.js +17514 -17799
- package/dist/types/advisor/config.d.ts +4 -3
- package/dist/types/commit/agentic/agent.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +28 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +10 -22
- package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
- package/dist/types/modes/components/move-overlay.d.ts +1 -1
- package/dist/types/modes/components/status-line/component.d.ts +3 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +8 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +16 -0
- package/dist/types/task/executor.d.ts +31 -0
- package/dist/types/tools/__tests__/vibe-render.test.d.ts +1 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
- package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
- package/dist/types/tools/browser/run-output.d.ts +25 -0
- package/dist/types/tools/browser/tab-worker.d.ts +16 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/vibe.d.ts +161 -0
- package/dist/types/utils/changelog.d.ts +35 -1
- package/dist/types/vibe/runtime.d.ts +124 -0
- package/dist/types/vibe/state.d.ts +4 -0
- package/dist/types/web/search/provider.d.ts +2 -0
- package/dist/types/web/search/providers/bing.d.ts +14 -0
- package/dist/types/web/search/providers/browser-headers.d.ts +9 -0
- package/dist/types/web/search/providers/browser-page.d.ts +32 -0
- package/dist/types/web/search/providers/ecosia.d.ts +14 -0
- package/dist/types/web/search/providers/google.d.ts +13 -0
- package/dist/types/web/search/providers/mojeek.d.ts +14 -0
- package/dist/types/web/search/providers/public.d.ts +37 -0
- package/dist/types/web/search/providers/startpage.d.ts +14 -0
- package/dist/types/web/search/providers/yahoo.d.ts +14 -0
- package/dist/types/web/search/types.d.ts +28 -0
- package/package.json +15 -18
- package/scripts/build-binary.ts +56 -73
- package/scripts/bundle-dist.ts +36 -40
- package/scripts/compile-binary.ts +68 -0
- package/scripts/generate-docs-index.ts +3 -93
- package/scripts/legacy-pi-virtual-module.ts +192 -0
- package/src/advisor/__tests__/advisor.test.ts +13 -0
- package/src/advisor/__tests__/config.test.ts +36 -0
- package/src/advisor/config.ts +11 -8
- package/src/commit/agentic/agent.ts +4 -0
- package/src/commit/agentic/index.ts +46 -21
- package/src/edit/index.ts +10 -76
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +328 -162
- package/src/extensibility/plugins/legacy-pi-virtual-modules.d.ts +4 -0
- package/src/internal-urls/docs-index.ts +2 -1
- package/src/internal-urls/skill-protocol.ts +1 -1
- package/src/lsp/deferred-diagnostics.ts +66 -0
- package/src/main.ts +13 -13
- package/src/mcp/transports/stdio.test.ts +45 -1
- package/src/mcp/transports/stdio.ts +6 -3
- package/src/modes/acp/acp-agent.ts +65 -1
- package/src/modes/acp/acp-event-mapper.ts +5 -0
- package/src/modes/acp/acp-mode.ts +11 -0
- package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
- package/src/modes/components/advisor-config.ts +15 -7
- package/src/modes/components/move-overlay.ts +2 -3
- package/src/modes/components/status-line/component.ts +6 -0
- package/src/modes/components/status-line/segments.ts +6 -0
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/controllers/command-controller.ts +8 -10
- package/src/modes/interactive-mode.ts +117 -1
- package/src/modes/types.ts +2 -0
- package/src/prompts/system/eager-task.md +2 -2
- package/src/prompts/system/system-prompt.md +9 -2
- package/src/prompts/system/vibe-mode-active.md +23 -0
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/vibe-kill.md +3 -0
- package/src/prompts/tools/vibe-list.md +3 -0
- package/src/prompts/tools/vibe-send.md +9 -0
- package/src/prompts/tools/vibe-spawn.md +10 -0
- package/src/prompts/tools/vibe-turn-result.md +19 -0
- package/src/prompts/tools/vibe-wait.md +8 -0
- package/src/sdk.ts +5 -0
- package/src/session/agent-session.ts +103 -13
- package/src/session/snapcompact-inline.ts +3 -19
- package/src/slash-commands/builtin-registry.ts +24 -8
- package/src/task/agents.ts +0 -2
- package/src/task/executor.ts +105 -0
- package/src/tools/__tests__/vibe-render.test.ts +210 -0
- package/src/tools/bash-skill-urls.ts +1 -1
- package/src/tools/browser/cmux/cmux-tab.ts +46 -48
- package/src/tools/browser/cmux/rpc.ts +50 -0
- package/src/tools/browser/run-output.ts +76 -0
- package/src/tools/browser/tab-worker.ts +264 -129
- package/src/tools/glob.ts +20 -6
- package/src/tools/index.ts +1 -0
- package/src/tools/read.ts +17 -9
- package/src/tools/renderers.ts +6 -0
- package/src/tools/vibe.ts +608 -0
- package/src/tools/write.ts +15 -2
- package/src/utils/changelog.ts +106 -9
- package/src/utils/git.ts +0 -1
- package/src/utils/title-generator.ts +70 -7
- package/src/vibe/runtime.ts +710 -0
- package/src/vibe/state.ts +4 -0
- package/src/web/search/index.ts +14 -6
- package/src/web/search/provider.ts +37 -1
- package/src/web/search/providers/bing.ts +197 -0
- package/src/web/search/providers/browser-headers.ts +92 -0
- package/src/web/search/providers/browser-page.ts +123 -0
- package/src/web/search/providers/duckduckgo.ts +13 -34
- package/src/web/search/providers/ecosia.ts +178 -0
- package/src/web/search/providers/google.ts +193 -0
- package/src/web/search/providers/mojeek.ts +206 -0
- package/src/web/search/providers/public.ts +201 -0
- package/src/web/search/providers/startpage.ts +213 -0
- package/src/web/search/providers/yahoo.ts +179 -0
- package/src/web/search/types.ts +35 -0
- package/dist/types/extensibility/plugins/legacy-pi-bundled-keys.d.ts +0 -10
- package/dist/types/extensibility/plugins/legacy-pi-bundled-registry.d.ts +0 -10
- package/scripts/generate-legacy-pi-bundled-registry.ts +0 -420
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +0 -1011
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +0 -3430
- package/src/internal-urls/docs-index.generated.txt +0 -2
- package/src/prompts/agents/plan.md +0 -47
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vibe mode tools — the director's entire non-read surface.
|
|
3
|
+
*
|
|
4
|
+
* Five thin tools over {@link VibeSessionRegistry}: spawn/send/wait/kill/list
|
|
5
|
+
* persistent worker sessions ("fast"/"good" CLIs). Spawns and sends return
|
|
6
|
+
* immediately; turn results self-deliver through the async job manager.
|
|
7
|
+
*
|
|
8
|
+
* The TUI renderers lean into the "you are driving little CLIs" fiction:
|
|
9
|
+
* spawn/send draw a mini composer (a message typed into a tiny Claude-Code-like
|
|
10
|
+
* terminal), and wait/list draw the "TV wall" — one live screen per worker,
|
|
11
|
+
* stacked, each showing its tool calls and streamed text as it works.
|
|
12
|
+
*/
|
|
13
|
+
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
14
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
15
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
16
|
+
import { prompt } from "@oh-my-pi/pi-utils";
|
|
17
|
+
import { type } from "arktype";
|
|
18
|
+
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
19
|
+
import { shimmerEnabled, shimmerText } from "../modes/theme/shimmer";
|
|
20
|
+
import type { Theme } from "../modes/theme/theme";
|
|
21
|
+
import vibeKillDescription from "../prompts/tools/vibe-kill.md" with { type: "text" };
|
|
22
|
+
import vibeListDescription from "../prompts/tools/vibe-list.md" with { type: "text" };
|
|
23
|
+
import vibeSendDescription from "../prompts/tools/vibe-send.md" with { type: "text" };
|
|
24
|
+
import vibeSpawnDescription from "../prompts/tools/vibe-spawn.md" with { type: "text" };
|
|
25
|
+
import vibeWaitDescription from "../prompts/tools/vibe-wait.md" with { type: "text" };
|
|
26
|
+
import { MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
27
|
+
import { oneLineLabel } from "../task/types";
|
|
28
|
+
import { renderStatusLine } from "../tui";
|
|
29
|
+
import {
|
|
30
|
+
type VibeCli,
|
|
31
|
+
type VibeKillOutcome,
|
|
32
|
+
type VibeScreenSnapshot,
|
|
33
|
+
type VibeSendOutcome,
|
|
34
|
+
VibeSessionRegistry,
|
|
35
|
+
type VibeSessionState,
|
|
36
|
+
type VibeWaitOutcome,
|
|
37
|
+
} from "../vibe/runtime";
|
|
38
|
+
import type { Tool, ToolSession } from "./index";
|
|
39
|
+
import {
|
|
40
|
+
Ellipsis,
|
|
41
|
+
formatBadge,
|
|
42
|
+
formatDuration,
|
|
43
|
+
formatStatusIcon,
|
|
44
|
+
replaceTabs,
|
|
45
|
+
type ToolUIColor,
|
|
46
|
+
type ToolUIStatus,
|
|
47
|
+
truncateToWidth,
|
|
48
|
+
} from "./render-utils";
|
|
49
|
+
|
|
50
|
+
export const VIBE_TOOL_NAMES = ["vibe_spawn", "vibe_send", "vibe_wait", "vibe_kill", "vibe_list"] as const;
|
|
51
|
+
|
|
52
|
+
const vibeSpawnSchema = type({
|
|
53
|
+
cli: type("'fast' | 'good'").describe(
|
|
54
|
+
"worker flavor: fast = low-latency model for mechanical work; good = strong model for hard work",
|
|
55
|
+
),
|
|
56
|
+
"name?": type("string <= 48").describe("optional session name; generated when omitted"),
|
|
57
|
+
prompt: type("string > 0").describe("first instruction; the worker starts with no other context"),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const vibeSendSchema = type({
|
|
61
|
+
session: type("string > 0").describe("session id from vibe_spawn / vibe_list"),
|
|
62
|
+
message: type("string > 0").describe("message for the session; steers mid-turn, else runs as its next turn"),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const vibeWaitSchema = type({
|
|
66
|
+
"sessions?": type("string[]").describe("session ids to watch; omit to watch every session with a turn in flight"),
|
|
67
|
+
"timeout?": type("number > 0").describe("max seconds to wait (default 30)"),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const vibeKillSchema = type({
|
|
71
|
+
session: type("string > 0").describe("session id to terminate"),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const vibeListSchema = type({});
|
|
75
|
+
|
|
76
|
+
type VibeOp = "spawn" | "send" | "wait" | "kill" | "list";
|
|
77
|
+
|
|
78
|
+
/** Details payload shared by every vibe tool for TUI rendering. */
|
|
79
|
+
export interface VibeToolDetails {
|
|
80
|
+
op: VibeOp;
|
|
81
|
+
/** Live TV-wall snapshot of the owner's worker sessions at (or during) the call. */
|
|
82
|
+
screens: VibeScreenSnapshot[];
|
|
83
|
+
spawned?: { id: string; cli: VibeCli; jobId: string };
|
|
84
|
+
send?: VibeSendOutcome;
|
|
85
|
+
wait?: {
|
|
86
|
+
settled: Array<{ id: string; jobId: string; status: "completed" | "failed" | "cancelled" }>;
|
|
87
|
+
stillRunning: string[];
|
|
88
|
+
timedOut: boolean;
|
|
89
|
+
/** True on interim progress emissions while the wait is still blocking. */
|
|
90
|
+
waiting?: boolean;
|
|
91
|
+
};
|
|
92
|
+
killed?: VibeKillOutcome;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function screensOf(session: ToolSession, ids?: string[]): VibeScreenSnapshot[] {
|
|
96
|
+
return VibeSessionRegistry.global().screens(session.getAgentId?.() ?? MAIN_AGENT_ID, ids);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function textResult(text: string, details: VibeToolDetails): AgentToolResult<VibeToolDetails> {
|
|
100
|
+
return { content: [{ type: "text", text }], details };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class VibeSpawnTool implements AgentTool<typeof vibeSpawnSchema, VibeToolDetails> {
|
|
104
|
+
readonly name = "vibe_spawn";
|
|
105
|
+
readonly approval = "exec" as const;
|
|
106
|
+
readonly label = "Vibe Spawn";
|
|
107
|
+
readonly summary = "Start a persistent fast/good worker session";
|
|
108
|
+
readonly description: string;
|
|
109
|
+
readonly parameters = vibeSpawnSchema;
|
|
110
|
+
readonly strict = true;
|
|
111
|
+
constructor(private readonly session: ToolSession) {
|
|
112
|
+
this.description = prompt.render(vibeSpawnDescription);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async execute(_toolCallId: string, params: typeof vibeSpawnSchema.infer): Promise<AgentToolResult<VibeToolDetails>> {
|
|
116
|
+
const { id, jobId } = await VibeSessionRegistry.global().spawn(this.session, params);
|
|
117
|
+
return textResult(
|
|
118
|
+
`Spawned ${params.cli} session \`${id}\` (turn job \`${jobId}\`). The turn result will be delivered when it finishes — keep directing other sessions meanwhile. Continue this one with vibe_send \`${id}\`.`,
|
|
119
|
+
{ op: "spawn", screens: screensOf(this.session), spawned: { id, cli: params.cli, jobId } },
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export class VibeSendTool implements AgentTool<typeof vibeSendSchema, VibeToolDetails> {
|
|
125
|
+
readonly name = "vibe_send";
|
|
126
|
+
readonly approval = "exec" as const;
|
|
127
|
+
readonly label = "Vibe Send";
|
|
128
|
+
readonly summary = "Message a worker session (steer or next turn)";
|
|
129
|
+
readonly description: string;
|
|
130
|
+
readonly parameters = vibeSendSchema;
|
|
131
|
+
readonly strict = true;
|
|
132
|
+
constructor(private readonly session: ToolSession) {
|
|
133
|
+
this.description = prompt.render(vibeSendDescription);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async execute(_toolCallId: string, params: typeof vibeSendSchema.infer): Promise<AgentToolResult<VibeToolDetails>> {
|
|
137
|
+
const outcome = await VibeSessionRegistry.global().send(this.session, params);
|
|
138
|
+
const ack =
|
|
139
|
+
outcome.mode === "turn"
|
|
140
|
+
? `Started a new turn on \`${outcome.id}\` (job \`${outcome.jobId}\`). Its result will be delivered when the turn finishes.`
|
|
141
|
+
: outcome.mode === "steered"
|
|
142
|
+
? `Steered \`${outcome.id}\` mid-turn — the running turn sees your message at its next step.`
|
|
143
|
+
: `\`${outcome.id}\` is mid-turn; your message is queued and runs automatically as the next turn.`;
|
|
144
|
+
return textResult(ack, { op: "send", screens: screensOf(this.session), send: outcome });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const WAIT_PROGRESS_INTERVAL_MS = 500;
|
|
149
|
+
|
|
150
|
+
export class VibeWaitTool implements AgentTool<typeof vibeWaitSchema, VibeToolDetails> {
|
|
151
|
+
readonly name = "vibe_wait";
|
|
152
|
+
readonly approval = "read" as const;
|
|
153
|
+
readonly label = "Vibe Wait";
|
|
154
|
+
readonly summary = "Block until a worker session finishes its turn";
|
|
155
|
+
readonly description: string;
|
|
156
|
+
readonly parameters = vibeWaitSchema;
|
|
157
|
+
readonly strict = true;
|
|
158
|
+
readonly interruptible = true;
|
|
159
|
+
constructor(private readonly session: ToolSession) {
|
|
160
|
+
this.description = prompt.render(vibeWaitDescription);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async execute(
|
|
164
|
+
_toolCallId: string,
|
|
165
|
+
params: typeof vibeWaitSchema.infer,
|
|
166
|
+
signal?: AbortSignal,
|
|
167
|
+
onUpdate?: AgentToolUpdateCallback<VibeToolDetails>,
|
|
168
|
+
): Promise<AgentToolResult<VibeToolDetails>> {
|
|
169
|
+
const registry = VibeSessionRegistry.global();
|
|
170
|
+
// Live TV-wall frames while the wait blocks: each tick re-snapshots the
|
|
171
|
+
// watched workers so their tool calls and streamed text play in place.
|
|
172
|
+
const emitProgress = (): void => {
|
|
173
|
+
onUpdate?.({
|
|
174
|
+
content: [{ type: "text", text: "" }],
|
|
175
|
+
details: {
|
|
176
|
+
op: "wait",
|
|
177
|
+
screens: screensOf(this.session, params.sessions),
|
|
178
|
+
wait: { settled: [], stillRunning: [], timedOut: false, waiting: true },
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
const progressTimer = onUpdate ? setInterval(emitProgress, WAIT_PROGRESS_INTERVAL_MS) : undefined;
|
|
183
|
+
emitProgress();
|
|
184
|
+
let outcome: VibeWaitOutcome;
|
|
185
|
+
try {
|
|
186
|
+
outcome = await registry.wait(this.session, {
|
|
187
|
+
sessions: params.sessions,
|
|
188
|
+
timeoutMs: params.timeout !== undefined ? params.timeout * 1000 : undefined,
|
|
189
|
+
signal,
|
|
190
|
+
});
|
|
191
|
+
} finally {
|
|
192
|
+
clearInterval(progressTimer);
|
|
193
|
+
}
|
|
194
|
+
const details: VibeToolDetails = {
|
|
195
|
+
op: "wait",
|
|
196
|
+
screens: screensOf(this.session, params.sessions),
|
|
197
|
+
wait: {
|
|
198
|
+
settled: outcome.settled.map(({ id, jobId, status }) => ({ id, jobId, status })),
|
|
199
|
+
stillRunning: outcome.stillRunning,
|
|
200
|
+
timedOut: outcome.timedOut,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
if (outcome.settled.length === 0 && outcome.stillRunning.length === 0) {
|
|
204
|
+
return { ...textResult("No turns in flight to wait for.", details), useless: true };
|
|
205
|
+
}
|
|
206
|
+
const lines: string[] = [];
|
|
207
|
+
for (const entry of outcome.settled) {
|
|
208
|
+
lines.push(`## \`${entry.id}\` — ${entry.status}`, entry.resultText, "");
|
|
209
|
+
}
|
|
210
|
+
if (outcome.stillRunning.length > 0) {
|
|
211
|
+
lines.push(`Still running: ${outcome.stillRunning.map(id => `\`${id}\``).join(", ")}.`);
|
|
212
|
+
}
|
|
213
|
+
if (outcome.timedOut) {
|
|
214
|
+
lines.push("Wait window elapsed before any turn settled — re-issue vibe_wait to keep waiting.");
|
|
215
|
+
}
|
|
216
|
+
const result = textResult(lines.join("\n").trimEnd(), details);
|
|
217
|
+
// A pure "still waiting" frame is noise once a newer wait exists.
|
|
218
|
+
return outcome.settled.length === 0 ? { ...result, useless: true } : result;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export class VibeKillTool implements AgentTool<typeof vibeKillSchema, VibeToolDetails> {
|
|
223
|
+
readonly name = "vibe_kill";
|
|
224
|
+
readonly approval = "read" as const;
|
|
225
|
+
readonly label = "Vibe Kill";
|
|
226
|
+
readonly summary = "Terminate a worker session";
|
|
227
|
+
readonly description: string;
|
|
228
|
+
readonly parameters = vibeKillSchema;
|
|
229
|
+
readonly strict = true;
|
|
230
|
+
constructor(private readonly session: ToolSession) {
|
|
231
|
+
this.description = prompt.render(vibeKillDescription);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async execute(_toolCallId: string, params: typeof vibeKillSchema.infer): Promise<AgentToolResult<VibeToolDetails>> {
|
|
235
|
+
const outcome = await VibeSessionRegistry.global().kill(this.session, params.session);
|
|
236
|
+
const cancelNote = outcome.cancelledTurn ? " Its in-flight turn was cancelled." : "";
|
|
237
|
+
return textResult(
|
|
238
|
+
`Killed session \`${outcome.id}\`.${cancelNote} Transcript remains at history://${outcome.id}.`,
|
|
239
|
+
{
|
|
240
|
+
op: "kill",
|
|
241
|
+
screens: screensOf(this.session),
|
|
242
|
+
killed: outcome,
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export class VibeListTool implements AgentTool<typeof vibeListSchema, VibeToolDetails> {
|
|
249
|
+
readonly name = "vibe_list";
|
|
250
|
+
readonly approval = "read" as const;
|
|
251
|
+
readonly label = "Vibe List";
|
|
252
|
+
readonly summary = "List worker sessions and their states";
|
|
253
|
+
readonly description: string;
|
|
254
|
+
readonly parameters = vibeListSchema;
|
|
255
|
+
readonly strict = true;
|
|
256
|
+
constructor(private readonly session: ToolSession) {
|
|
257
|
+
this.description = prompt.render(vibeListDescription);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async execute(): Promise<AgentToolResult<VibeToolDetails>> {
|
|
261
|
+
const screens = screensOf(this.session);
|
|
262
|
+
const details: VibeToolDetails = { op: "list", screens };
|
|
263
|
+
if (screens.length === 0) {
|
|
264
|
+
return textResult("No vibe sessions. Spawn one with vibe_spawn.", details);
|
|
265
|
+
}
|
|
266
|
+
const lines = screens.map(screen => {
|
|
267
|
+
const parts = [
|
|
268
|
+
`- \`${screen.id}\` [${screen.cli}] ${screen.state}`,
|
|
269
|
+
`${screen.turns} turn${screen.turns === 1 ? "" : "s"}`,
|
|
270
|
+
];
|
|
271
|
+
if (screen.queued > 0) parts.push(`${screen.queued} queued`);
|
|
272
|
+
if (screen.model) parts.push(screen.model);
|
|
273
|
+
if (screen.lastActivity) parts.push(`last: ${screen.lastActivity}`);
|
|
274
|
+
return parts.join(" · ");
|
|
275
|
+
});
|
|
276
|
+
return textResult(lines.join("\n"), details);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Creates the ephemeral tools installed while `/vibe` mode is active. */
|
|
281
|
+
export function createVibeTools(session: ToolSession): Tool[] {
|
|
282
|
+
return [
|
|
283
|
+
new VibeSpawnTool(session),
|
|
284
|
+
new VibeSendTool(session),
|
|
285
|
+
new VibeWaitTool(session),
|
|
286
|
+
new VibeKillTool(session),
|
|
287
|
+
new VibeListTool(session),
|
|
288
|
+
];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// =============================================================================
|
|
292
|
+
// TUI Renderer — mini composer (spawn/send) + TV wall (wait/list)
|
|
293
|
+
// =============================================================================
|
|
294
|
+
|
|
295
|
+
const COMPOSER_LINE_MAX = 96;
|
|
296
|
+
const TV_LINE_MAX = 110;
|
|
297
|
+
const TV_TRACE_COLLAPSED = 2;
|
|
298
|
+
const TV_TRACE_EXPANDED = 6;
|
|
299
|
+
const TV_OUTPUT_COLLAPSED = 1;
|
|
300
|
+
const TV_OUTPUT_EXPANDED = 3;
|
|
301
|
+
const CURSOR_GLYPH = "▌";
|
|
302
|
+
|
|
303
|
+
function stateToIcon(state: VibeSessionState): ToolUIStatus {
|
|
304
|
+
switch (state) {
|
|
305
|
+
case "running":
|
|
306
|
+
return "running";
|
|
307
|
+
case "starting":
|
|
308
|
+
return "pending";
|
|
309
|
+
case "idle":
|
|
310
|
+
return "done";
|
|
311
|
+
case "dead":
|
|
312
|
+
return "aborted";
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function stateToColor(state: VibeSessionState): ToolUIColor {
|
|
317
|
+
switch (state) {
|
|
318
|
+
case "running":
|
|
319
|
+
return "accent";
|
|
320
|
+
case "starting":
|
|
321
|
+
return "accent";
|
|
322
|
+
case "idle":
|
|
323
|
+
return "success";
|
|
324
|
+
case "dead":
|
|
325
|
+
return "muted";
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
interface VibeRenderArgs {
|
|
330
|
+
cli?: VibeCli;
|
|
331
|
+
prompt?: string;
|
|
332
|
+
name?: string;
|
|
333
|
+
session?: string;
|
|
334
|
+
message?: string;
|
|
335
|
+
sessions?: string[];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** One-line, escape-stripped fragment for embedding in a frame row. */
|
|
339
|
+
function frameText(text: string, max: number): string {
|
|
340
|
+
return oneLineLabel(replaceTabs(text), max);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Draw a left-railed mini terminal:
|
|
345
|
+
* ```
|
|
346
|
+
* ╭─ <header>
|
|
347
|
+
* │ <body…>
|
|
348
|
+
* ╰─ <footer>
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
function miniFrame(uiTheme: Theme, header: string, body: string[], footer?: string): string[] {
|
|
352
|
+
const box = uiTheme.boxRound;
|
|
353
|
+
const rail = (glyph: string) => uiTheme.fg("dim", glyph);
|
|
354
|
+
const lines = [`${rail(`${box.topLeft}${box.horizontal}`)} ${header}`];
|
|
355
|
+
for (const row of body) {
|
|
356
|
+
lines.push(`${rail(box.vertical)} ${row}`);
|
|
357
|
+
}
|
|
358
|
+
lines.push(
|
|
359
|
+
footer ? `${rail(`${box.bottomLeft}${box.horizontal}`)} ${footer}` : rail(`${box.bottomLeft}${box.horizontal}`),
|
|
360
|
+
);
|
|
361
|
+
return lines;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** The `>` composer rows of the mini CLI: the director's message being typed in. */
|
|
365
|
+
function composerRows(uiTheme: Theme, message: string, options: { cursor: boolean; expanded: boolean }): string[] {
|
|
366
|
+
const promptGlyph = uiTheme.fg("accent", ">");
|
|
367
|
+
const rawLines = message.split(/\r?\n/).filter(line => line.trim().length > 0);
|
|
368
|
+
const maxRows = options.expanded ? 6 : 2;
|
|
369
|
+
const visible = rawLines.slice(0, maxRows).map(line => frameText(line, COMPOSER_LINE_MAX));
|
|
370
|
+
if (visible.length === 0) visible.push("");
|
|
371
|
+
if (rawLines.length > maxRows) {
|
|
372
|
+
visible[visible.length - 1] = `${visible[visible.length - 1]} …`;
|
|
373
|
+
} else if (options.cursor) {
|
|
374
|
+
visible[visible.length - 1] = `${visible[visible.length - 1]}${uiTheme.fg("accent", CURSOR_GLYPH)}`;
|
|
375
|
+
}
|
|
376
|
+
return visible.map((line, index) =>
|
|
377
|
+
index === 0 ? `${promptGlyph} ${uiTheme.fg("toolOutput", line)}` : ` ${uiTheme.fg("toolOutput", line)}`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Render one worker "TV": header + live tool calls + streamed text tail. */
|
|
382
|
+
function tvScreen(
|
|
383
|
+
uiTheme: Theme,
|
|
384
|
+
screen: VibeScreenSnapshot,
|
|
385
|
+
options: RenderResultOptions,
|
|
386
|
+
settledStatus?: "completed" | "failed" | "cancelled",
|
|
387
|
+
): string[] {
|
|
388
|
+
const live = screen.state === "running" || screen.state === "starting";
|
|
389
|
+
const spinnerFrame = live ? options.spinnerFrame : undefined;
|
|
390
|
+
const icon = formatStatusIcon(
|
|
391
|
+
settledStatus === "failed" ? "error" : settledStatus === "cancelled" ? "aborted" : stateToIcon(screen.state),
|
|
392
|
+
uiTheme,
|
|
393
|
+
spinnerFrame,
|
|
394
|
+
);
|
|
395
|
+
const badge = formatBadge(screen.cli, stateToColor(screen.state), uiTheme);
|
|
396
|
+
const idText =
|
|
397
|
+
live && options.spinnerFrame !== undefined && shimmerEnabled()
|
|
398
|
+
? shimmerText(screen.id, uiTheme)
|
|
399
|
+
: uiTheme.fg(live ? "accent" : "toolOutput", screen.id);
|
|
400
|
+
const headParts = [icon, badge, idText, uiTheme.fg("dim", settledStatus ?? screen.state)];
|
|
401
|
+
const turnsLabel = `${screen.turns}t${screen.queued > 0 ? `+${screen.queued}q` : ""}`;
|
|
402
|
+
headParts.push(uiTheme.fg("muted", turnsLabel));
|
|
403
|
+
if (screen.turnStartedAt !== undefined) {
|
|
404
|
+
headParts.push(uiTheme.fg("dim", formatDuration(Date.now() - screen.turnStartedAt)));
|
|
405
|
+
}
|
|
406
|
+
if (screen.model) headParts.push(uiTheme.fg("muted", frameText(screen.model, 40)));
|
|
407
|
+
|
|
408
|
+
const body: string[] = [];
|
|
409
|
+
const hook = uiTheme.tree.hook;
|
|
410
|
+
if (live) {
|
|
411
|
+
if (screen.turnMessage) {
|
|
412
|
+
body.push(`${uiTheme.fg("accent", ">")} ${uiTheme.fg("dim", frameText(screen.turnMessage, TV_LINE_MAX))}`);
|
|
413
|
+
}
|
|
414
|
+
const traceCap = options.expanded ? TV_TRACE_EXPANDED : TV_TRACE_COLLAPSED;
|
|
415
|
+
for (const line of screen.trace.slice(-traceCap)) {
|
|
416
|
+
body.push(`${uiTheme.fg("dim", hook)} ${uiTheme.fg("dim", frameText(line, TV_LINE_MAX))}`);
|
|
417
|
+
}
|
|
418
|
+
if (screen.currentTool) {
|
|
419
|
+
const detail = screen.lastIntent ?? screen.currentToolArgs;
|
|
420
|
+
const label = `${screen.currentTool}${detail ? `: ${detail}` : ""}`;
|
|
421
|
+
const painted =
|
|
422
|
+
options.spinnerFrame !== undefined && shimmerEnabled()
|
|
423
|
+
? shimmerText(frameText(label, TV_LINE_MAX), uiTheme)
|
|
424
|
+
: uiTheme.fg("muted", frameText(label, TV_LINE_MAX));
|
|
425
|
+
body.push(`${uiTheme.fg("accent", hook)} ${painted}`);
|
|
426
|
+
} else if (screen.lastIntent) {
|
|
427
|
+
body.push(`${uiTheme.fg("accent", hook)} ${uiTheme.fg("muted", frameText(screen.lastIntent, TV_LINE_MAX))}`);
|
|
428
|
+
}
|
|
429
|
+
const outputCap = options.expanded ? TV_OUTPUT_EXPANDED : TV_OUTPUT_COLLAPSED;
|
|
430
|
+
for (const line of screen.outputTail.slice(-outputCap)) {
|
|
431
|
+
if (line.trim().length === 0) continue;
|
|
432
|
+
body.push(` ${uiTheme.fg("muted", frameText(line, TV_LINE_MAX))}`);
|
|
433
|
+
}
|
|
434
|
+
} else if (screen.lastActivity) {
|
|
435
|
+
body.push(`${uiTheme.fg("dim", hook)} ${uiTheme.fg("muted", frameText(screen.lastActivity, TV_LINE_MAX))}`);
|
|
436
|
+
}
|
|
437
|
+
const footer = settledStatus
|
|
438
|
+
? uiTheme.fg(
|
|
439
|
+
settledStatus === "completed" ? "success" : settledStatus === "failed" ? "error" : "warning",
|
|
440
|
+
`turn ${settledStatus} — result delivered`,
|
|
441
|
+
)
|
|
442
|
+
: undefined;
|
|
443
|
+
return miniFrame(uiTheme, headParts.join(" "), body, footer);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Width-aware component over prebuilt lines, or — given a builder — lines
|
|
448
|
+
* recomputed on every paint. Spinner ticks repaint the tool block WITHOUT
|
|
449
|
+
* re-invoking renderCall/renderResult, so time-based content (shimmer sweep,
|
|
450
|
+
* spinner glyph, cursor blink, elapsed turn duration) must be produced inside
|
|
451
|
+
* a builder that reads the shared mutable `options` at paint time; prebuilt
|
|
452
|
+
* arrays are for static frames only.
|
|
453
|
+
*/
|
|
454
|
+
function linesComponent(lines: string[] | (() => string[])): Component {
|
|
455
|
+
return {
|
|
456
|
+
render(width: number): readonly string[] {
|
|
457
|
+
const rows = typeof lines === "function" ? lines() : lines;
|
|
458
|
+
return rows.map(line => truncateToWidth(line, width, Ellipsis.Unicode));
|
|
459
|
+
},
|
|
460
|
+
invalidate() {},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function describeCall(op: VibeOp, args: VibeRenderArgs | undefined): string {
|
|
465
|
+
switch (op) {
|
|
466
|
+
case "spawn":
|
|
467
|
+
return `spawn ${args?.cli ?? "?"}${args?.name ? ` · ${frameText(args.name, 40)}` : ""}`;
|
|
468
|
+
case "send":
|
|
469
|
+
return `send → ${args?.session ? frameText(args.session, 40) : "?"}`;
|
|
470
|
+
case "wait":
|
|
471
|
+
return args?.sessions?.length
|
|
472
|
+
? `wait on ${frameText(args.sessions.join(", "), 60)}`
|
|
473
|
+
: "wait on running sessions";
|
|
474
|
+
case "kill":
|
|
475
|
+
return `kill ${args?.session ? frameText(args.session, 40) : "?"}`;
|
|
476
|
+
case "list":
|
|
477
|
+
return "sessions";
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Build the shared vibe renderer for one tool name. */
|
|
482
|
+
export function createVibeToolRenderer(op: VibeOp) {
|
|
483
|
+
const composerOp = op === "spawn" || op === "send";
|
|
484
|
+
return {
|
|
485
|
+
inline: true,
|
|
486
|
+
mergeCallAndResult: true,
|
|
487
|
+
animatedPendingPreview: composerOp,
|
|
488
|
+
animatedPartialResult: op === "wait",
|
|
489
|
+
|
|
490
|
+
renderCall(args: VibeRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
491
|
+
const title = uiTheme.fg("muted", `vibe ${describeCall(op, args)}`);
|
|
492
|
+
if (composerOp) {
|
|
493
|
+
const message = op === "spawn" ? (args?.prompt ?? "") : (args?.message ?? "");
|
|
494
|
+
return linesComponent(() => {
|
|
495
|
+
const cursorOn = ((options.spinnerFrame ?? 0) & 1) === 0;
|
|
496
|
+
return miniFrame(
|
|
497
|
+
uiTheme,
|
|
498
|
+
title,
|
|
499
|
+
composerRows(uiTheme, message, { cursor: cursorOn, expanded: options.expanded }),
|
|
500
|
+
uiTheme.fg("dim", op === "spawn" ? "booting CLI…" : "delivering…"),
|
|
501
|
+
);
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
return new Text(renderStatusLine({ icon: "pending", title: `vibe ${describeCall(op, args)}` }, uiTheme), 0, 0);
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
renderResult(
|
|
508
|
+
result: { content: Array<{ type: string; text?: string }>; details?: VibeToolDetails; isError?: boolean },
|
|
509
|
+
options: RenderResultOptions,
|
|
510
|
+
uiTheme: Theme,
|
|
511
|
+
args?: VibeRenderArgs,
|
|
512
|
+
): Component {
|
|
513
|
+
const details = result.details;
|
|
514
|
+
if (!details || result.isError) {
|
|
515
|
+
const fallback = result.content.find(part => part.type === "text")?.text ?? "";
|
|
516
|
+
const header = renderStatusLine(
|
|
517
|
+
{ icon: result.isError ? "error" : "done", title: `vibe ${describeCall(op, args)}` },
|
|
518
|
+
uiTheme,
|
|
519
|
+
);
|
|
520
|
+
const body = fallback
|
|
521
|
+
? `\n ${uiTheme.fg(result.isError ? "error" : "dim", frameText(fallback, TV_LINE_MAX))}`
|
|
522
|
+
: "";
|
|
523
|
+
return new Text(`${header}${body}`, 0, 0);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (composerOp) {
|
|
527
|
+
const message = op === "spawn" ? (args?.prompt ?? "") : (args?.message ?? "");
|
|
528
|
+
const target =
|
|
529
|
+
op === "spawn"
|
|
530
|
+
? `${uiTheme.fg("muted", "vibe spawn")} ${formatBadge(details.spawned?.cli ?? args?.cli ?? "?", "accent", uiTheme)} ${uiTheme.fg("accent", frameText(details.spawned?.id ?? args?.name ?? "", 40))}`
|
|
531
|
+
: `${uiTheme.fg("muted", "vibe send →")} ${uiTheme.fg("accent", frameText(args?.session ?? "?", 40))}`;
|
|
532
|
+
const ack =
|
|
533
|
+
op === "spawn"
|
|
534
|
+
? uiTheme.fg("success", `turn started${details.spawned ? ` (job ${details.spawned.jobId})` : ""}`)
|
|
535
|
+
: details.send?.mode === "steered"
|
|
536
|
+
? uiTheme.fg("success", "steered into the running turn")
|
|
537
|
+
: details.send?.mode === "queued"
|
|
538
|
+
? uiTheme.fg("warning", "mid-turn — queued as the next turn")
|
|
539
|
+
: uiTheme.fg(
|
|
540
|
+
"success",
|
|
541
|
+
`turn started${details.send?.jobId ? ` (job ${details.send.jobId})` : ""}`,
|
|
542
|
+
);
|
|
543
|
+
const lines = miniFrame(
|
|
544
|
+
uiTheme,
|
|
545
|
+
target,
|
|
546
|
+
composerRows(uiTheme, message, { cursor: false, expanded: options.expanded }),
|
|
547
|
+
ack,
|
|
548
|
+
);
|
|
549
|
+
return linesComponent(lines);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (op === "kill") {
|
|
553
|
+
const killedNote = details.killed?.cancelledTurn ? " (in-flight turn cancelled)" : "";
|
|
554
|
+
const header = renderStatusLine(
|
|
555
|
+
{
|
|
556
|
+
icon: "done",
|
|
557
|
+
title: `vibe kill ${frameText(details.killed?.id ?? args?.session ?? "?", 40)}${killedNote}`,
|
|
558
|
+
},
|
|
559
|
+
uiTheme,
|
|
560
|
+
);
|
|
561
|
+
return new Text(header, 0, 0);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// wait/list: the TV wall.
|
|
565
|
+
const screens = details.screens;
|
|
566
|
+
if (screens.length === 0) {
|
|
567
|
+
const fallback = result.content.find(part => part.type === "text")?.text ?? "no sessions";
|
|
568
|
+
return new Text(
|
|
569
|
+
renderStatusLine(
|
|
570
|
+
{ icon: "warning", title: `vibe ${op}`, meta: [uiTheme.fg("dim", frameText(fallback, 60))] },
|
|
571
|
+
uiTheme,
|
|
572
|
+
),
|
|
573
|
+
0,
|
|
574
|
+
0,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
const waiting = details.wait?.waiting === true;
|
|
578
|
+
const settledById = new Map(details.wait?.settled.map(entry => [entry.id, entry.status] as const) ?? []);
|
|
579
|
+
return linesComponent(() => {
|
|
580
|
+
const running = screens.filter(screen => screen.state === "running" || screen.state === "starting").length;
|
|
581
|
+
const meta: string[] = [];
|
|
582
|
+
if (running > 0) meta.push(uiTheme.fg("accent", `${running} on air`));
|
|
583
|
+
if (settledById.size > 0) meta.push(uiTheme.fg("success", `${settledById.size} settled`));
|
|
584
|
+
if (details.wait?.timedOut) meta.push(uiTheme.fg("warning", "timed out"));
|
|
585
|
+
const title =
|
|
586
|
+
op === "wait"
|
|
587
|
+
? waiting
|
|
588
|
+
? "vibe wait — watching the wall"
|
|
589
|
+
: "vibe wait"
|
|
590
|
+
: `vibe sessions (${screens.length})`;
|
|
591
|
+
const header = renderStatusLine(
|
|
592
|
+
{
|
|
593
|
+
icon: details.wait?.timedOut ? "warning" : running > 0 ? "info" : "done",
|
|
594
|
+
spinnerFrame: running > 0 ? options.spinnerFrame : undefined,
|
|
595
|
+
title,
|
|
596
|
+
meta,
|
|
597
|
+
},
|
|
598
|
+
uiTheme,
|
|
599
|
+
);
|
|
600
|
+
const lines = [header];
|
|
601
|
+
for (const screen of screens) {
|
|
602
|
+
lines.push(...tvScreen(uiTheme, screen, options, settledById.get(screen.id)));
|
|
603
|
+
}
|
|
604
|
+
return lines;
|
|
605
|
+
});
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
}
|
package/src/tools/write.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
|
20
20
|
import { InternalUrlRouter } from "../internal-urls";
|
|
21
21
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
22
22
|
import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "../lsp";
|
|
23
|
+
import { DeferredDiagnostics } from "../lsp/deferred-diagnostics";
|
|
23
24
|
import { getDiagnosticsLedger } from "../lsp/diagnostics-ledger";
|
|
24
25
|
import { getLanguageFromPath, highlightCode, type Theme } from "../modes/theme/theme";
|
|
25
26
|
import writeDescription from "../prompts/tools/write.md" with { type: "text" };
|
|
@@ -323,12 +324,15 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
323
324
|
}
|
|
324
325
|
|
|
325
326
|
readonly #writethrough: WritethroughCallback;
|
|
327
|
+
readonly #deferredDiagnostics: DeferredDiagnostics | undefined;
|
|
326
328
|
|
|
327
329
|
constructor(private readonly session: ToolSession) {
|
|
328
330
|
const enableLsp = session.enableLsp ?? true;
|
|
329
331
|
const enableFormat = enableLsp && session.settings.get("lsp.formatOnWrite");
|
|
330
332
|
const enableDiagnostics = enableLsp && session.settings.get("lsp.diagnosticsOnWrite");
|
|
331
333
|
const dedup = enableDiagnostics && session.settings.get("lsp.diagnosticsDeduplicate");
|
|
334
|
+
this.#deferredDiagnostics =
|
|
335
|
+
enableDiagnostics && session.queueDeferredDiagnostics ? new DeferredDiagnostics(session, dedup) : undefined;
|
|
332
336
|
this.#writethrough = enableLsp
|
|
333
337
|
? createLspWritethrough(session.cwd, {
|
|
334
338
|
enableFormat,
|
|
@@ -931,9 +935,18 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
931
935
|
};
|
|
932
936
|
}
|
|
933
937
|
|
|
934
|
-
const diagnostics = await this.#writethrough(
|
|
938
|
+
const diagnostics = await this.#writethrough(
|
|
939
|
+
absolutePath,
|
|
940
|
+
cleanContent,
|
|
941
|
+
signal,
|
|
942
|
+
undefined,
|
|
943
|
+
batchRequest,
|
|
944
|
+
dst => this.#deferredDiagnostics?.begin(dst),
|
|
945
|
+
);
|
|
935
946
|
invalidateFsScanAfterWrite(absolutePath);
|
|
936
|
-
this
|
|
947
|
+
if (!this.#deferredDiagnostics || batchRequest?.flush === false) {
|
|
948
|
+
this.session.bumpFileMutationVersion?.(absolutePath);
|
|
949
|
+
}
|
|
937
950
|
const madeExecutable = await maybeMarkExecutableForShebang(absolutePath, cleanContent);
|
|
938
951
|
|
|
939
952
|
const header = maybeWriteSnapshotHeader(this.session, absolutePath, cleanContent);
|