@oh-my-pi/pi-coding-agent 14.4.1 → 14.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 +56 -0
- package/package.json +7 -7
- package/src/cli.ts +0 -1
- package/src/config/prompt-templates.ts +0 -30
- package/src/config/settings-schema.ts +26 -36
- package/src/config/settings.ts +1 -1
- package/src/edit/index.ts +1 -53
- package/src/edit/line-hash.ts +0 -53
- package/src/edit/modes/atom.ts +82 -47
- package/src/edit/modes/hashline.ts +6 -8
- package/src/edit/renderer.ts +6 -8
- package/src/edit/streaming.ts +90 -114
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +10 -15
- package/src/internal-urls/docs-index.generated.ts +1 -2
- package/src/modes/components/settings-defs.ts +0 -5
- package/src/modes/components/tool-execution.ts +2 -5
- package/src/modes/controllers/btw-controller.ts +17 -105
- package/src/modes/controllers/todo-command-controller.ts +537 -0
- package/src/modes/interactive-mode.ts +35 -9
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +17 -0
- package/src/prompts/system/irc-incoming.md +8 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -0
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/atom.md +37 -26
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/grep.md +2 -5
- package/src/prompts/tools/irc.md +49 -0
- package/src/prompts/tools/job.md +11 -0
- package/src/prompts/tools/read.md +12 -13
- package/src/prompts/tools/task.md +1 -1
- package/src/prompts/tools/todo-write.md +14 -5
- package/src/registry/agent-registry.ts +139 -0
- package/src/sdk.ts +35 -0
- package/src/session/agent-session.ts +217 -5
- package/src/session/streaming-output.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +24 -0
- package/src/task/executor.ts +14 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/fetch.ts +18 -6
- package/src/tools/fs-cache-invalidation.ts +0 -5
- package/src/tools/grep.ts +4 -124
- package/src/tools/index.ts +12 -6
- package/src/tools/irc.ts +258 -0
- package/src/tools/job.ts +489 -0
- package/src/tools/match-line-format.ts +7 -6
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/read.ts +36 -126
- package/src/tools/renderers.ts +2 -0
- package/src/tools/todo-write.ts +243 -12
- package/src/utils/edit-mode.ts +1 -2
- package/src/utils/file-display-mode.ts +0 -3
- package/src/cli/read-cli.ts +0 -67
- package/src/commands/read.ts +0 -33
- package/src/edit/modes/chunk.ts +0 -832
- package/src/prompts/tools/cancel-job.md +0 -5
- package/src/prompts/tools/chunk-edit.md +0 -158
- package/src/prompts/tools/poll.md +0 -5
- package/src/prompts/tools/read-chunk.md +0 -73
- package/src/tools/cancel-job.ts +0 -95
- package/src/tools/poll-tool.ts +0 -173
package/src/tools/job.ts
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
4
|
+
import { prompt } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
6
|
+
import { isBackgroundJobSupportEnabled } from "../async";
|
|
7
|
+
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
8
|
+
import type { Theme } from "../modes/theme/theme";
|
|
9
|
+
import jobDescription from "../prompts/tools/job.md" with { type: "text" };
|
|
10
|
+
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
11
|
+
import type { ToolSession } from "./index";
|
|
12
|
+
import {
|
|
13
|
+
formatBadge,
|
|
14
|
+
formatDuration,
|
|
15
|
+
formatEmptyMessage,
|
|
16
|
+
formatStatusIcon,
|
|
17
|
+
getPreviewLines,
|
|
18
|
+
PREVIEW_LIMITS,
|
|
19
|
+
replaceTabs,
|
|
20
|
+
type ToolUIColor,
|
|
21
|
+
type ToolUIStatus,
|
|
22
|
+
} from "./render-utils";
|
|
23
|
+
|
|
24
|
+
const jobSchema = Type.Object({
|
|
25
|
+
poll: Type.Optional(
|
|
26
|
+
Type.Array(Type.String(), {
|
|
27
|
+
description: "background job ids to wait for; omit (with no `cancel`) to wait on all running jobs",
|
|
28
|
+
examples: [["job-1234"]],
|
|
29
|
+
}),
|
|
30
|
+
),
|
|
31
|
+
cancel: Type.Optional(
|
|
32
|
+
Type.Array(Type.String(), {
|
|
33
|
+
description: "background job ids to cancel",
|
|
34
|
+
examples: [["job-1234"]],
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
type JobParams = Static<typeof jobSchema>;
|
|
40
|
+
|
|
41
|
+
const WAIT_DURATION_MS: Record<string, number> = {
|
|
42
|
+
"5s": 5_000,
|
|
43
|
+
"10s": 10_000,
|
|
44
|
+
"30s": 30_000,
|
|
45
|
+
"1m": 60_000,
|
|
46
|
+
"5m": 5 * 60_000,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function parseWaitDurationMs(value: string | undefined): number {
|
|
50
|
+
return (value ? WAIT_DURATION_MS[value] : undefined) ?? WAIT_DURATION_MS["30s"];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface JobSnapshot {
|
|
54
|
+
id: string;
|
|
55
|
+
type: "bash" | "task";
|
|
56
|
+
status: "running" | "completed" | "failed" | "cancelled";
|
|
57
|
+
label: string;
|
|
58
|
+
durationMs: number;
|
|
59
|
+
resultText?: string;
|
|
60
|
+
errorText?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type CancelStatus = "cancelled" | "not_found" | "already_completed";
|
|
64
|
+
|
|
65
|
+
interface CancelOutcome {
|
|
66
|
+
id: string;
|
|
67
|
+
status: CancelStatus;
|
|
68
|
+
message: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface JobToolDetails {
|
|
72
|
+
jobs: JobSnapshot[];
|
|
73
|
+
cancelled?: { id: string; status: CancelStatus }[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
77
|
+
readonly name = "job";
|
|
78
|
+
readonly label = "Job";
|
|
79
|
+
readonly description: string;
|
|
80
|
+
readonly parameters = jobSchema;
|
|
81
|
+
readonly strict = true;
|
|
82
|
+
|
|
83
|
+
constructor(private readonly session: ToolSession) {
|
|
84
|
+
this.description = prompt.render(jobDescription);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static createIf(session: ToolSession): JobTool | null {
|
|
88
|
+
if (!isBackgroundJobSupportEnabled(session.settings)) return null;
|
|
89
|
+
return new JobTool(session);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async execute(
|
|
93
|
+
_toolCallId: string,
|
|
94
|
+
params: JobParams,
|
|
95
|
+
signal?: AbortSignal,
|
|
96
|
+
onUpdate?: AgentToolUpdateCallback<JobToolDetails>,
|
|
97
|
+
_context?: AgentToolContext,
|
|
98
|
+
): Promise<AgentToolResult<JobToolDetails>> {
|
|
99
|
+
const manager = this.session.asyncJobManager;
|
|
100
|
+
if (!manager) {
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: "text", text: "Async execution is disabled; no background jobs are available." }],
|
|
103
|
+
details: { jobs: [] },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const cancelIds = params.cancel ?? [];
|
|
108
|
+
const cancelOutcomes: CancelOutcome[] = [];
|
|
109
|
+
for (const id of cancelIds) {
|
|
110
|
+
const existing = manager.getJob(id);
|
|
111
|
+
if (!existing) {
|
|
112
|
+
cancelOutcomes.push({ id, status: "not_found", message: `Background job not found: ${id}` });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (existing.status !== "running") {
|
|
116
|
+
cancelOutcomes.push({
|
|
117
|
+
id,
|
|
118
|
+
status: "already_completed",
|
|
119
|
+
message: `Background job ${id} is already ${existing.status}.`,
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const cancelled = manager.cancel(id);
|
|
124
|
+
cancelOutcomes.push(
|
|
125
|
+
cancelled
|
|
126
|
+
? { id, status: "cancelled", message: `Cancelled background job ${id}.` }
|
|
127
|
+
: { id, status: "already_completed", message: `Background job ${id} is already completed.` },
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const requestedPollIds = params.poll;
|
|
132
|
+
// If only `cancel` was provided (no `poll`), don't wait — return immediately.
|
|
133
|
+
const shouldPoll = requestedPollIds !== undefined || cancelIds.length === 0;
|
|
134
|
+
|
|
135
|
+
if (!shouldPoll) {
|
|
136
|
+
const cancelledJobs = cancelIds.map(id => manager.getJob(id)).filter(j => j != null);
|
|
137
|
+
return this.#buildResult(manager, cancelledJobs, cancelOutcomes);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Resolve which jobs to watch.
|
|
141
|
+
// - If `poll` was passed explicitly, watch exactly those (filtered to existing).
|
|
142
|
+
// - If `poll` was omitted (and so was `cancel`), default to all running jobs.
|
|
143
|
+
const jobsToWatch = requestedPollIds
|
|
144
|
+
? requestedPollIds.map(id => manager.getJob(id)).filter(j => j != null)
|
|
145
|
+
: manager.getRunningJobs();
|
|
146
|
+
|
|
147
|
+
if (jobsToWatch.length === 0) {
|
|
148
|
+
if (cancelOutcomes.length > 0) {
|
|
149
|
+
const cancelledJobs = cancelIds.map(id => manager.getJob(id)).filter(j => j != null);
|
|
150
|
+
return this.#buildResult(manager, cancelledJobs, cancelOutcomes);
|
|
151
|
+
}
|
|
152
|
+
const message = requestedPollIds?.length
|
|
153
|
+
? `No matching jobs found for IDs: ${requestedPollIds.join(", ")}`
|
|
154
|
+
: "No running background jobs to wait for.";
|
|
155
|
+
return {
|
|
156
|
+
content: [{ type: "text", text: message }],
|
|
157
|
+
details: { jobs: [] },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// If all watched jobs are already done, build immediate result.
|
|
162
|
+
const runningJobs = jobsToWatch.filter(j => j.status === "running");
|
|
163
|
+
if (runningJobs.length === 0) {
|
|
164
|
+
const cancelledJobs = cancelIds.map(id => manager.getJob(id)).filter(j => j != null);
|
|
165
|
+
return this.#buildResult(manager, [...cancelledJobs, ...jobsToWatch], cancelOutcomes);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Wait until at least one running job finishes, the wait duration elapses, or the call is aborted.
|
|
169
|
+
const racePromises: Promise<unknown>[] = runningJobs.map(j => j.promise);
|
|
170
|
+
const waitMs = parseWaitDurationMs(this.session.settings.get("async.pollWaitDuration"));
|
|
171
|
+
const { promise: timeoutPromise, resolve: timeoutResolve } = Promise.withResolvers<void>();
|
|
172
|
+
const timeoutHandle = setTimeout(() => timeoutResolve(), waitMs);
|
|
173
|
+
racePromises.push(timeoutPromise);
|
|
174
|
+
|
|
175
|
+
const watchedJobIds = runningJobs.map(job => job.id);
|
|
176
|
+
manager.watchJobs(watchedJobIds);
|
|
177
|
+
|
|
178
|
+
const cancelledJobs = cancelIds.map(id => manager.getJob(id)).filter(j => j != null);
|
|
179
|
+
const allTrackedJobs = [...cancelledJobs, ...jobsToWatch];
|
|
180
|
+
|
|
181
|
+
const PROGRESS_INTERVAL_MS = 500;
|
|
182
|
+
const emitProgress = () => {
|
|
183
|
+
if (!onUpdate) return;
|
|
184
|
+
const snapshot = this.#snapshotJobs(allTrackedJobs);
|
|
185
|
+
onUpdate({
|
|
186
|
+
content: [{ type: "text", text: "" }],
|
|
187
|
+
details: {
|
|
188
|
+
jobs: snapshot,
|
|
189
|
+
...(cancelOutcomes.length
|
|
190
|
+
? { cancelled: cancelOutcomes.map(({ id, status }) => ({ id, status })) }
|
|
191
|
+
: {}),
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
const progressTimer = onUpdate ? setInterval(emitProgress, PROGRESS_INTERVAL_MS) : undefined;
|
|
196
|
+
emitProgress();
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
if (signal) {
|
|
200
|
+
const { promise: abortPromise, resolve: abortResolve } = Promise.withResolvers<void>();
|
|
201
|
+
const onAbort = () => abortResolve();
|
|
202
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
203
|
+
racePromises.push(abortPromise);
|
|
204
|
+
try {
|
|
205
|
+
await Promise.race(racePromises);
|
|
206
|
+
} finally {
|
|
207
|
+
signal.removeEventListener("abort", onAbort);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
await Promise.race(racePromises);
|
|
211
|
+
}
|
|
212
|
+
} finally {
|
|
213
|
+
manager.unwatchJobs(watchedJobIds);
|
|
214
|
+
clearTimeout(timeoutHandle);
|
|
215
|
+
if (progressTimer) clearInterval(progressTimer);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return this.#buildResult(manager, allTrackedJobs, cancelOutcomes);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#snapshotJobs(
|
|
222
|
+
jobs: {
|
|
223
|
+
id: string;
|
|
224
|
+
type: "bash" | "task";
|
|
225
|
+
status: string;
|
|
226
|
+
label: string;
|
|
227
|
+
startTime: number;
|
|
228
|
+
resultText?: string;
|
|
229
|
+
errorText?: string;
|
|
230
|
+
}[],
|
|
231
|
+
): JobSnapshot[] {
|
|
232
|
+
const now = Date.now();
|
|
233
|
+
return jobs.map(j => {
|
|
234
|
+
const current = this.session.asyncJobManager?.getJob(j.id);
|
|
235
|
+
const latest = current ?? j;
|
|
236
|
+
return {
|
|
237
|
+
id: latest.id,
|
|
238
|
+
type: latest.type,
|
|
239
|
+
status: latest.status as JobSnapshot["status"],
|
|
240
|
+
label: latest.label,
|
|
241
|
+
durationMs: Math.max(0, now - latest.startTime),
|
|
242
|
+
...(latest.resultText ? { resultText: latest.resultText } : {}),
|
|
243
|
+
...(latest.errorText ? { errorText: latest.errorText } : {}),
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#buildResult(
|
|
249
|
+
manager: NonNullable<ToolSession["asyncJobManager"]>,
|
|
250
|
+
jobs: {
|
|
251
|
+
id: string;
|
|
252
|
+
type: "bash" | "task";
|
|
253
|
+
status: string;
|
|
254
|
+
label: string;
|
|
255
|
+
startTime: number;
|
|
256
|
+
resultText?: string;
|
|
257
|
+
errorText?: string;
|
|
258
|
+
}[],
|
|
259
|
+
cancelOutcomes: CancelOutcome[],
|
|
260
|
+
): AgentToolResult<JobToolDetails> {
|
|
261
|
+
// Deduplicate by id (cancelled jobs may also appear in the watched set).
|
|
262
|
+
const seen = new Set<string>();
|
|
263
|
+
const uniqueJobs = jobs.filter(j => {
|
|
264
|
+
if (seen.has(j.id)) return false;
|
|
265
|
+
seen.add(j.id);
|
|
266
|
+
return true;
|
|
267
|
+
});
|
|
268
|
+
const jobResults = this.#snapshotJobs(uniqueJobs);
|
|
269
|
+
|
|
270
|
+
manager.acknowledgeDeliveries(jobResults.filter(j => j.status !== "running").map(j => j.id));
|
|
271
|
+
|
|
272
|
+
const completed = jobResults.filter(j => j.status !== "running");
|
|
273
|
+
const running = jobResults.filter(j => j.status === "running");
|
|
274
|
+
|
|
275
|
+
const lines: string[] = [];
|
|
276
|
+
|
|
277
|
+
if (cancelOutcomes.length > 0) {
|
|
278
|
+
lines.push(`## Cancelled (${cancelOutcomes.length})\n`);
|
|
279
|
+
for (const o of cancelOutcomes) lines.push(`- ${o.message}`);
|
|
280
|
+
lines.push("");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (completed.length > 0) {
|
|
284
|
+
lines.push(`## Completed (${completed.length})\n`);
|
|
285
|
+
for (const j of completed) {
|
|
286
|
+
lines.push(`### ${j.id} [${j.type}] — ${j.status}`);
|
|
287
|
+
lines.push(`Label: ${j.label}`);
|
|
288
|
+
if (j.resultText) {
|
|
289
|
+
lines.push("```", j.resultText, "```");
|
|
290
|
+
}
|
|
291
|
+
if (j.errorText) {
|
|
292
|
+
lines.push(`Error: ${j.errorText}`);
|
|
293
|
+
}
|
|
294
|
+
lines.push("");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (running.length > 0) {
|
|
299
|
+
lines.push(`## Still Running (${running.length})\n`);
|
|
300
|
+
for (const j of running) {
|
|
301
|
+
lines.push(`- \`${j.id}\` [${j.type}] — ${j.label}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: "text", text: lines.join("\n").trimEnd() }],
|
|
307
|
+
details: {
|
|
308
|
+
jobs: jobResults,
|
|
309
|
+
...(cancelOutcomes.length ? { cancelled: cancelOutcomes.map(({ id, status }) => ({ id, status })) } : {}),
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// =============================================================================
|
|
316
|
+
// TUI Renderer
|
|
317
|
+
// =============================================================================
|
|
318
|
+
|
|
319
|
+
interface JobRenderArgs {
|
|
320
|
+
poll?: string[];
|
|
321
|
+
cancel?: string[];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
325
|
+
const LABEL_MAX_WIDTH = 60;
|
|
326
|
+
const PREVIEW_LINES_COLLAPSED = 1;
|
|
327
|
+
const PREVIEW_LINES_EXPANDED = 4;
|
|
328
|
+
const PREVIEW_LINE_WIDTH = 80;
|
|
329
|
+
|
|
330
|
+
function statusToIcon(status: JobSnapshot["status"]): ToolUIStatus {
|
|
331
|
+
switch (status) {
|
|
332
|
+
case "completed":
|
|
333
|
+
return "success";
|
|
334
|
+
case "failed":
|
|
335
|
+
return "error";
|
|
336
|
+
case "cancelled":
|
|
337
|
+
return "aborted";
|
|
338
|
+
case "running":
|
|
339
|
+
return "running";
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function statusToColor(status: JobSnapshot["status"]): ToolUIColor {
|
|
344
|
+
switch (status) {
|
|
345
|
+
case "completed":
|
|
346
|
+
return "success";
|
|
347
|
+
case "failed":
|
|
348
|
+
return "error";
|
|
349
|
+
case "cancelled":
|
|
350
|
+
return "warning";
|
|
351
|
+
case "running":
|
|
352
|
+
return "accent";
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function describeTarget(args: JobRenderArgs | undefined): string {
|
|
357
|
+
const poll = args?.poll ?? [];
|
|
358
|
+
const cancel = args?.cancel ?? [];
|
|
359
|
+
const parts: string[] = [];
|
|
360
|
+
if (cancel.length > 0) {
|
|
361
|
+
parts.push(cancel.length === 1 ? `cancel ${cancel[0]}` : `cancel ${cancel.length} jobs`);
|
|
362
|
+
}
|
|
363
|
+
if (poll.length > 0) {
|
|
364
|
+
parts.push(poll.length === 1 ? `poll ${poll[0]}` : `poll ${poll.length} jobs`);
|
|
365
|
+
}
|
|
366
|
+
if (parts.length === 0) return "all running jobs";
|
|
367
|
+
return parts.join(", ");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export const jobToolRenderer = {
|
|
371
|
+
inline: true,
|
|
372
|
+
|
|
373
|
+
renderCall(args: JobRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
374
|
+
const text = renderStatusLine({ icon: "pending", title: "Job", description: describeTarget(args) }, uiTheme);
|
|
375
|
+
return new Text(text, 0, 0);
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
renderResult(
|
|
379
|
+
result: { content: Array<{ type: string; text?: string }>; details?: JobToolDetails; isError?: boolean },
|
|
380
|
+
options: RenderResultOptions,
|
|
381
|
+
uiTheme: Theme,
|
|
382
|
+
args?: JobRenderArgs,
|
|
383
|
+
): Component {
|
|
384
|
+
const jobs = result.details?.jobs ?? [];
|
|
385
|
+
|
|
386
|
+
if (jobs.length === 0) {
|
|
387
|
+
const fallback = result.content?.find(c => c.type === "text")?.text || "No jobs to process";
|
|
388
|
+
const header = renderStatusLine({ icon: "warning", title: "Job", description: describeTarget(args) }, uiTheme);
|
|
389
|
+
return new Text([header, formatEmptyMessage(fallback, uiTheme)].join("\n"), 0, 0);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const counts = { completed: 0, failed: 0, cancelled: 0, running: 0 };
|
|
393
|
+
for (const job of jobs) counts[job.status]++;
|
|
394
|
+
|
|
395
|
+
const meta: string[] = [];
|
|
396
|
+
if (counts.completed > 0) meta.push(uiTheme.fg("success", `${counts.completed} done`));
|
|
397
|
+
if (counts.failed > 0) meta.push(uiTheme.fg("error", `${counts.failed} failed`));
|
|
398
|
+
if (counts.cancelled > 0) meta.push(uiTheme.fg("warning", `${counts.cancelled} cancelled`));
|
|
399
|
+
if (counts.running > 0) meta.push(uiTheme.fg("accent", `${counts.running} running`));
|
|
400
|
+
|
|
401
|
+
const headerIcon: ToolUIStatus = counts.failed > 0 ? "warning" : counts.running > 0 ? "info" : "success";
|
|
402
|
+
const description =
|
|
403
|
+
counts.running > 0
|
|
404
|
+
? `waiting on ${counts.running} of ${jobs.length}`
|
|
405
|
+
: `${jobs.length} ${jobs.length === 1 ? "job" : "jobs"} settled`;
|
|
406
|
+
|
|
407
|
+
const header = renderStatusLine(
|
|
408
|
+
{
|
|
409
|
+
icon: headerIcon,
|
|
410
|
+
spinnerFrame: counts.running > 0 ? options.spinnerFrame : undefined,
|
|
411
|
+
title: "Job",
|
|
412
|
+
description,
|
|
413
|
+
meta,
|
|
414
|
+
},
|
|
415
|
+
uiTheme,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
// Sort: running first (so user sees what's still pending), then failed, then completed/cancelled.
|
|
419
|
+
const statusOrder: Record<JobSnapshot["status"], number> = {
|
|
420
|
+
running: 0,
|
|
421
|
+
failed: 1,
|
|
422
|
+
cancelled: 2,
|
|
423
|
+
completed: 3,
|
|
424
|
+
};
|
|
425
|
+
const sortedJobs = [...jobs].sort((a, b) => {
|
|
426
|
+
const diff = statusOrder[a.status] - statusOrder[b.status];
|
|
427
|
+
if (diff !== 0) return diff;
|
|
428
|
+
return b.durationMs - a.durationMs;
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
let cached: RenderCache | undefined;
|
|
432
|
+
return {
|
|
433
|
+
render(width: number): string[] {
|
|
434
|
+
const expanded = options.expanded;
|
|
435
|
+
const spinnerFrame = options.spinnerFrame ?? 0;
|
|
436
|
+
const key = new Hasher().bool(expanded).u32(width).u32(spinnerFrame).digest();
|
|
437
|
+
if (cached?.key === key) return cached.lines;
|
|
438
|
+
|
|
439
|
+
const itemLines = renderTreeList<JobSnapshot>(
|
|
440
|
+
{
|
|
441
|
+
items: sortedJobs,
|
|
442
|
+
expanded,
|
|
443
|
+
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
|
444
|
+
itemType: "job",
|
|
445
|
+
renderItem: job => {
|
|
446
|
+
const lines: string[] = [];
|
|
447
|
+
const icon = formatStatusIcon(
|
|
448
|
+
statusToIcon(job.status),
|
|
449
|
+
uiTheme,
|
|
450
|
+
job.status === "running" ? options.spinnerFrame : undefined,
|
|
451
|
+
);
|
|
452
|
+
const typeBadge = formatBadge(job.type, statusToColor(job.status), uiTheme);
|
|
453
|
+
const idText = uiTheme.fg("muted", job.id);
|
|
454
|
+
const label = truncateToWidth(
|
|
455
|
+
replaceTabs(job.label || "(no label)"),
|
|
456
|
+
LABEL_MAX_WIDTH,
|
|
457
|
+
Ellipsis.Unicode,
|
|
458
|
+
);
|
|
459
|
+
const labelText = uiTheme.fg("toolOutput", label);
|
|
460
|
+
const durationText = uiTheme.fg("dim", formatDuration(job.durationMs));
|
|
461
|
+
lines.push(`${icon} ${idText} ${typeBadge} ${labelText} ${durationText}`);
|
|
462
|
+
|
|
463
|
+
const preview = job.errorText?.trim() || job.resultText?.trim();
|
|
464
|
+
if (preview) {
|
|
465
|
+
const maxLines = expanded ? PREVIEW_LINES_EXPANDED : PREVIEW_LINES_COLLAPSED;
|
|
466
|
+
const previewLines = getPreviewLines(preview, maxLines, PREVIEW_LINE_WIDTH, Ellipsis.Unicode);
|
|
467
|
+
const tone = job.errorText ? "error" : "dim";
|
|
468
|
+
for (const pl of previewLines) {
|
|
469
|
+
lines.push(` ${uiTheme.fg(tone, pl)}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return lines;
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
uiTheme,
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
const all = [header, ...itemLines].map(l => truncateToWidth(l, width, Ellipsis.Unicode));
|
|
479
|
+
cached = { key, lines: all };
|
|
480
|
+
return all;
|
|
481
|
+
},
|
|
482
|
+
invalidate() {
|
|
483
|
+
cached = undefined;
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
mergeCallAndResult: true,
|
|
489
|
+
};
|
|
@@ -3,9 +3,10 @@ import { computeLineHash } from "../edit/line-hash";
|
|
|
3
3
|
/**
|
|
4
4
|
* Format a single line of match output for grep/ast-grep style results.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* The anchor/content separator is always `|`. Matched lines are prefixed
|
|
7
|
+
* with `*`; context lines are prefixed with a single space so anchors
|
|
8
|
+
* align in column. In hashline mode the anchor is `LINE+ID` (no `#`); in
|
|
9
|
+
* plain mode it is just the line number. Line numbers are never padded.
|
|
9
10
|
*/
|
|
10
11
|
export function formatMatchLine(
|
|
11
12
|
lineNumber: number,
|
|
@@ -13,9 +14,9 @@ export function formatMatchLine(
|
|
|
13
14
|
isMatch: boolean,
|
|
14
15
|
options: { useHashLines: boolean },
|
|
15
16
|
): string {
|
|
16
|
-
const
|
|
17
|
+
const marker = isMatch ? "*" : " ";
|
|
17
18
|
if (options.useHashLines) {
|
|
18
|
-
return `${lineNumber}${computeLineHash(lineNumber, line)}
|
|
19
|
+
return `${marker}${lineNumber}${computeLineHash(lineNumber, line)}|${line}`;
|
|
19
20
|
}
|
|
20
|
-
return `${
|
|
21
|
+
return `${marker}${lineNumber}|${line}`;
|
|
21
22
|
}
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -337,7 +337,7 @@ export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
if (truncation.nextOffset != null) {
|
|
340
|
-
notice += `. Use sel
|
|
340
|
+
notice += `. Use sel=${truncation.nextOffset} to continue`;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
if (truncation.artifactId != null) {
|