@narumitw/pi-subagents 0.42.0 → 0.43.1
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 +142 -32
- package/package.json +1 -1
- package/src/agents.ts +49 -8
- package/src/config-ui.ts +223 -28
- package/src/consult-policy.ts +15 -0
- package/src/consult-render.ts +194 -0
- package/src/consult.ts +815 -0
- package/src/cwd-policy.ts +183 -0
- package/src/execution.ts +135 -66
- package/src/in-process-transport.ts +3 -3
- package/src/inspect-render.ts +234 -0
- package/src/inspect.ts +453 -0
- package/src/limits.ts +1 -0
- package/src/params.ts +2 -0
- package/src/persistence.ts +29 -0
- package/src/registry.ts +87 -0
- package/src/render-common.ts +252 -0
- package/src/render.ts +134 -99
- package/src/runner.ts +162 -22
- package/src/safe-text.ts +67 -0
- package/src/settings.ts +199 -12
- package/src/stateful-guidance.ts +35 -0
- package/src/stateful-lifecycle.ts +31 -0
- package/src/stateful-render.ts +249 -0
- package/src/stateful-safety.ts +91 -0
- package/src/stateful.ts +254 -225
- package/src/subagents.ts +100 -19
- package/src/subprocess-transport.ts +19 -2
package/src/params.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
2
|
import { type Static, Type } from "typebox";
|
|
3
3
|
import { THINKING_LEVELS } from "./agents.js";
|
|
4
|
+
import { MAX_SUBAGENT_TIMEOUT_MS } from "./limits.js";
|
|
4
5
|
|
|
5
6
|
const TimeoutMs = Type.Number({
|
|
6
7
|
description:
|
|
7
8
|
"Hard timeout in milliseconds for each subagent subprocess. Defaults to PI_SUBAGENT_TIMEOUT_MS or 600000.",
|
|
8
9
|
minimum: 1,
|
|
10
|
+
maximum: MAX_SUBAGENT_TIMEOUT_MS,
|
|
9
11
|
});
|
|
10
12
|
|
|
11
13
|
const ThinkingLevelSchema = StringEnum(THINKING_LEVELS, {
|
package/src/persistence.ts
CHANGED
|
@@ -167,6 +167,8 @@ function isStoredState(value: unknown): value is StoredState {
|
|
|
167
167
|
Number.isFinite(record.updatedAt) &&
|
|
168
168
|
(record.parentId === undefined || typeof record.parentId === "string") &&
|
|
169
169
|
(record.thinkingLevel === undefined || isThinkingLevel(record.thinkingLevel)) &&
|
|
170
|
+
(record.workspaceMode === undefined || record.workspaceMode === "worktree") &&
|
|
171
|
+
(record.target === undefined || isTargetPolicyAudit(record.target)) &&
|
|
170
172
|
(record.children === undefined ||
|
|
171
173
|
(Array.isArray(record.children) &&
|
|
172
174
|
record.children.every((id) => typeof id === "string"))) &&
|
|
@@ -178,6 +180,33 @@ function isStoredState(value: unknown): value is StoredState {
|
|
|
178
180
|
});
|
|
179
181
|
}
|
|
180
182
|
|
|
183
|
+
function isTargetPolicyAudit(value: unknown): boolean {
|
|
184
|
+
if (!value || typeof value !== "object") return false;
|
|
185
|
+
const target = value as Record<string, unknown>;
|
|
186
|
+
if (
|
|
187
|
+
typeof target.cwd !== "string" ||
|
|
188
|
+
(target.boundary !== "current-workspace" && target.boundary !== "external") ||
|
|
189
|
+
!target.trust ||
|
|
190
|
+
typeof target.trust !== "object"
|
|
191
|
+
) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
const trust = target.trust as Record<string, unknown>;
|
|
195
|
+
return (
|
|
196
|
+
[
|
|
197
|
+
"session-trusted",
|
|
198
|
+
"session-untrusted",
|
|
199
|
+
"saved-trusted",
|
|
200
|
+
"saved-denied",
|
|
201
|
+
"unsaved",
|
|
202
|
+
"trust-error",
|
|
203
|
+
].includes(String(trust.kind)) &&
|
|
204
|
+
typeof trust.projectTrusted === "boolean" &&
|
|
205
|
+
(trust.sourcePath === undefined || typeof trust.sourcePath === "string") &&
|
|
206
|
+
(trust.warning === undefined || typeof trust.warning === "string")
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
181
210
|
function isAgentTurn(value: unknown): boolean {
|
|
182
211
|
if (!value || typeof value !== "object") return false;
|
|
183
212
|
const turn = value as Record<string, unknown>;
|
package/src/registry.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { SubagentThinkingLevel } from "./agents.js";
|
|
3
|
+
import type { TargetPolicyAudit } from "./cwd-policy.js";
|
|
3
4
|
import { DEFAULT_MAX_CONTEXT_BYTES, DEFAULT_MAX_OUTPUT_BYTES, truncateUtf8 } from "./limits.js";
|
|
4
5
|
import { type AgentTurnRunner, normalizeTransport, type SubagentTransport } from "./transport.js";
|
|
5
6
|
|
|
@@ -50,11 +51,38 @@ export interface ManagedAgent {
|
|
|
50
51
|
context?: string;
|
|
51
52
|
contextSourceIds?: string[];
|
|
52
53
|
contextTruncated?: boolean;
|
|
54
|
+
workspaceMode?: "worktree";
|
|
55
|
+
target?: TargetPolicyAudit;
|
|
53
56
|
policy?: { inherited: string[]; overridden: string[]; unsupported: string[] };
|
|
54
57
|
mailbox: AgentMailboxMessage[];
|
|
55
58
|
currentMailboxMessageIds?: string[];
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
export interface AgentRunInspectionSummary {
|
|
62
|
+
id: string;
|
|
63
|
+
agent: string;
|
|
64
|
+
state: AgentLifecycleState;
|
|
65
|
+
createdAt: number;
|
|
66
|
+
updatedAt: number;
|
|
67
|
+
historyCount: number;
|
|
68
|
+
unreadMessages: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface AgentRunInspectionDetail extends AgentRunInspectionSummary {
|
|
72
|
+
cwd: string;
|
|
73
|
+
thinkingLevel?: SubagentThinkingLevel;
|
|
74
|
+
currentTask?: string;
|
|
75
|
+
error?: string;
|
|
76
|
+
workspaceMode?: "worktree";
|
|
77
|
+
target?: TargetPolicyAudit;
|
|
78
|
+
policy?: { inherited: string[]; overridden: string[]; unsupported: string[] };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface AgentInspectionCounts {
|
|
82
|
+
activeAgents: number;
|
|
83
|
+
retainedAgents: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
58
86
|
export interface TurnOutcome {
|
|
59
87
|
output: string;
|
|
60
88
|
exitCode: number;
|
|
@@ -220,6 +248,8 @@ export class AgentRegistry {
|
|
|
220
248
|
context?: string;
|
|
221
249
|
contextSourceIds?: string[];
|
|
222
250
|
contextTruncated?: boolean;
|
|
251
|
+
workspaceMode?: "worktree";
|
|
252
|
+
target?: TargetPolicyAudit;
|
|
223
253
|
}): Promise<ManagedAgent> {
|
|
224
254
|
if (!input.task.trim()) throw new Error("Subagent tasks cannot be empty");
|
|
225
255
|
const task = truncateUtf8(input.task, this.maxTaskBytes).text;
|
|
@@ -263,6 +293,8 @@ export class AgentRegistry {
|
|
|
263
293
|
context: input.context,
|
|
264
294
|
contextSourceIds: input.contextSourceIds,
|
|
265
295
|
contextTruncated: input.contextTruncated,
|
|
296
|
+
workspaceMode: input.workspaceMode,
|
|
297
|
+
target: input.target,
|
|
266
298
|
};
|
|
267
299
|
this.agents.set(record.id, record);
|
|
268
300
|
if (parent) {
|
|
@@ -500,6 +532,44 @@ export class AgentRegistry {
|
|
|
500
532
|
if (shutdownError) throw shutdownError;
|
|
501
533
|
}
|
|
502
534
|
|
|
535
|
+
inspectionCounts(): AgentInspectionCounts {
|
|
536
|
+
let activeAgents = 0;
|
|
537
|
+
let retainedAgents = 0;
|
|
538
|
+
for (const agent of this.agents.values()) {
|
|
539
|
+
if (agent.state === "starting" || agent.state === "running") activeAgents++;
|
|
540
|
+
if (agent.state !== "closed") retainedAgents++;
|
|
541
|
+
}
|
|
542
|
+
return { activeAgents, retainedAgents };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
listInspection(includeClosed = false): AgentRunInspectionSummary[] {
|
|
546
|
+
return [...this.agents.values()]
|
|
547
|
+
.filter((agent) => includeClosed || agent.state !== "closed")
|
|
548
|
+
.sort((left, right) => left.createdAt - right.createdAt)
|
|
549
|
+
.map((agent) => this.inspectSummary(agent));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
getInspection(id: string): AgentRunInspectionDetail | undefined {
|
|
553
|
+
const agent = this.agents.get(id);
|
|
554
|
+
if (!agent) return undefined;
|
|
555
|
+
return {
|
|
556
|
+
...this.inspectSummary(agent),
|
|
557
|
+
cwd: agent.cwd,
|
|
558
|
+
thinkingLevel: agent.thinkingLevel,
|
|
559
|
+
currentTask: agent.currentTask,
|
|
560
|
+
error: agent.error,
|
|
561
|
+
workspaceMode: agent.workspaceMode,
|
|
562
|
+
target: agent.target ? { ...agent.target, trust: { ...agent.target.trust } } : undefined,
|
|
563
|
+
policy: agent.policy
|
|
564
|
+
? {
|
|
565
|
+
inherited: [...agent.policy.inherited],
|
|
566
|
+
overridden: [...agent.policy.overridden],
|
|
567
|
+
unsupported: [...agent.policy.unsupported],
|
|
568
|
+
}
|
|
569
|
+
: undefined,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
503
573
|
list(includeClosed = false, rootId?: string): ManagedAgent[] {
|
|
504
574
|
return [...this.agents.values()]
|
|
505
575
|
.filter((agent) => !rootId || agent.rootId === rootId)
|
|
@@ -755,6 +825,22 @@ export class AgentRegistry {
|
|
|
755
825
|
return next;
|
|
756
826
|
}
|
|
757
827
|
|
|
828
|
+
private inspectSummary(agent: ManagedAgent): AgentRunInspectionSummary {
|
|
829
|
+
let unreadMessages = 0;
|
|
830
|
+
for (const message of agent.mailbox) {
|
|
831
|
+
if (message.readAt === undefined) unreadMessages++;
|
|
832
|
+
}
|
|
833
|
+
return {
|
|
834
|
+
id: agent.id,
|
|
835
|
+
agent: agent.agent,
|
|
836
|
+
state: agent.state,
|
|
837
|
+
createdAt: agent.createdAt,
|
|
838
|
+
updatedAt: agent.updatedAt,
|
|
839
|
+
historyCount: agent.history.length,
|
|
840
|
+
unreadMessages,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
758
844
|
private copy(agent: ManagedAgent): ManagedAgent {
|
|
759
845
|
return {
|
|
760
846
|
...agent,
|
|
@@ -765,6 +851,7 @@ export class AgentRegistry {
|
|
|
765
851
|
: undefined,
|
|
766
852
|
history: agent.history.map((turn) => ({ ...turn })),
|
|
767
853
|
mailbox: agent.mailbox.map((message) => ({ ...message })),
|
|
854
|
+
target: agent.target ? { ...agent.target, trust: { ...agent.target.trust } } : undefined,
|
|
768
855
|
policy: agent.policy
|
|
769
856
|
? {
|
|
770
857
|
inherited: [...agent.policy.inherited],
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import {
|
|
4
|
+
keyHint,
|
|
5
|
+
type Theme,
|
|
6
|
+
type ThemeColor,
|
|
7
|
+
type ToolRenderResultOptions,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
10
|
+
import { boundedPrivateText, safeTerminalLine } from "./safe-text.js";
|
|
11
|
+
|
|
12
|
+
export const COLLAPSED_LIST_LIMIT = 5;
|
|
13
|
+
export const COLLAPSED_ANSWER_LINES = 3;
|
|
14
|
+
|
|
15
|
+
export interface ToolRendererContext<TArgs> {
|
|
16
|
+
args: TArgs;
|
|
17
|
+
isError: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type RenderStatus =
|
|
21
|
+
| "starting"
|
|
22
|
+
| "running"
|
|
23
|
+
| "completed"
|
|
24
|
+
| "failed"
|
|
25
|
+
| "cancelled"
|
|
26
|
+
| "interrupted"
|
|
27
|
+
| "idle"
|
|
28
|
+
| "closed"
|
|
29
|
+
| "warning";
|
|
30
|
+
|
|
31
|
+
const STATUS_PRESENTATION: Record<
|
|
32
|
+
RenderStatus,
|
|
33
|
+
{ icon: string; label: string; color: ThemeColor }
|
|
34
|
+
> = {
|
|
35
|
+
starting: { icon: "⏳", label: "Starting", color: "warning" },
|
|
36
|
+
running: { icon: "⏳", label: "Running", color: "warning" },
|
|
37
|
+
completed: { icon: "✓", label: "Completed", color: "success" },
|
|
38
|
+
failed: { icon: "✗", label: "Failed", color: "error" },
|
|
39
|
+
cancelled: { icon: "■", label: "Cancelled", color: "warning" },
|
|
40
|
+
interrupted: { icon: "■", label: "Interrupted", color: "warning" },
|
|
41
|
+
idle: { icon: "○", label: "Idle", color: "muted" },
|
|
42
|
+
closed: { icon: "✓", label: "Closed", color: "muted" },
|
|
43
|
+
warning: { icon: "◐", label: "Warning", color: "warning" },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function recordValue(value: unknown): Record<string, unknown> | undefined {
|
|
47
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
48
|
+
? (value as Record<string, unknown>)
|
|
49
|
+
: undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function recordList(value: unknown): Record<string, unknown>[] {
|
|
53
|
+
return Array.isArray(value)
|
|
54
|
+
? value.flatMap((item) => {
|
|
55
|
+
const record = recordValue(item);
|
|
56
|
+
return record ? [record] : [];
|
|
57
|
+
})
|
|
58
|
+
: [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function stringValue(value: unknown, fallback = ""): string {
|
|
62
|
+
return typeof value === "string" ? value : fallback;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function numberValue(value: unknown, fallback = 0): number {
|
|
66
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function booleanValue(value: unknown): boolean {
|
|
70
|
+
return value === true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function safeLine(value: unknown, fallback = "...", maxBytes = 2 * 1024): string {
|
|
74
|
+
if (typeof value !== "string" || !value.trim()) return fallback;
|
|
75
|
+
return safeTerminalLine(value, maxBytes) || fallback;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function safeBlock(value: unknown, fallback = "", maxBytes = 50 * 1024): string {
|
|
79
|
+
if (typeof value !== "string" || !value) return fallback;
|
|
80
|
+
return boundedPrivateText(value, maxBytes);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function previewLines(value: unknown, maxLines = COLLAPSED_ANSWER_LINES): string {
|
|
84
|
+
const text = safeBlock(value, "", 8 * 1024).trim();
|
|
85
|
+
return text.split("\n").slice(0, maxLines).join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function textResult(result: AgentToolResult<unknown>): string {
|
|
89
|
+
return result.content
|
|
90
|
+
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
91
|
+
.join("\n")
|
|
92
|
+
.trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function toolHeader(
|
|
96
|
+
theme: Theme,
|
|
97
|
+
toolName: string,
|
|
98
|
+
primary?: unknown,
|
|
99
|
+
metadata: readonly string[] = [],
|
|
100
|
+
): string {
|
|
101
|
+
let text = theme.fg("toolTitle", theme.bold(`${toolName} `));
|
|
102
|
+
if (primary !== undefined) text += theme.fg("accent", safeLine(primary));
|
|
103
|
+
const safeMetadata = metadata.filter(Boolean).map((item) => safeLine(item, "", 512));
|
|
104
|
+
if (safeMetadata.length > 0) text += theme.fg("muted", ` · ${safeMetadata.join(" · ")}`);
|
|
105
|
+
return text.trimEnd();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function statusBadge(theme: Theme, status: RenderStatus, suffix?: string): string {
|
|
109
|
+
const presentation = STATUS_PRESENTATION[status];
|
|
110
|
+
const label = suffix
|
|
111
|
+
? `${presentation.label} · ${safeLine(suffix, "", 2 * 1024)}`
|
|
112
|
+
: presentation.label;
|
|
113
|
+
return `${theme.fg(presentation.color, presentation.icon)} ${theme.fg(presentation.color, label)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function renderFallbackResult(
|
|
117
|
+
result: AgentToolResult<unknown>,
|
|
118
|
+
options: ToolRenderResultOptions,
|
|
119
|
+
theme: Theme,
|
|
120
|
+
isError = false,
|
|
121
|
+
) {
|
|
122
|
+
const status: RenderStatus = isError ? "failed" : options.isPartial ? "running" : "completed";
|
|
123
|
+
const content = safeBlock(textResult(result), "(no output)", 8 * 1024);
|
|
124
|
+
return new Text(`${statusBadge(theme, status)}\n${theme.fg("toolOutput", content)}`, 0, 0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function expansionHint(): string {
|
|
128
|
+
return keyHint("app.tools.expand", "to expand");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface RenderActivityItem {
|
|
132
|
+
type: "text" | "toolCall";
|
|
133
|
+
text?: string;
|
|
134
|
+
name?: string;
|
|
135
|
+
args?: Record<string, unknown>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function projectRenderActivity(value: unknown): RenderActivityItem[] {
|
|
139
|
+
if (!Array.isArray(value)) return [];
|
|
140
|
+
const items: RenderActivityItem[] = [];
|
|
141
|
+
for (const item of value) {
|
|
142
|
+
const record = recordValue(item);
|
|
143
|
+
if (!record) continue;
|
|
144
|
+
if (record.type === "text" && typeof record.text === "string") {
|
|
145
|
+
items.push({ type: "text", text: safeBlock(record.text, "", 1024) });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (record.type === "toolCall" && typeof record.name === "string") {
|
|
149
|
+
items.push({
|
|
150
|
+
type: "toolCall",
|
|
151
|
+
name: safeLine(record.name, "tool", 256),
|
|
152
|
+
args: recordValue(record.args) ?? {},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return items;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function renderActivityLines(
|
|
160
|
+
items: readonly RenderActivityItem[],
|
|
161
|
+
theme: Theme,
|
|
162
|
+
limit?: number,
|
|
163
|
+
total = items.length,
|
|
164
|
+
): string {
|
|
165
|
+
const selected = limit === undefined ? items : items.slice(-limit);
|
|
166
|
+
const lines: string[] = [];
|
|
167
|
+
const skipped = Math.max(0, total - selected.length);
|
|
168
|
+
if (skipped > 0) lines.push(theme.fg("muted", `… ${skipped} earlier activities`));
|
|
169
|
+
for (const item of selected) {
|
|
170
|
+
if (item.type === "text") {
|
|
171
|
+
const text = safeBlock(item.text, "", 1024).trim();
|
|
172
|
+
if (text) lines.push(theme.fg("toolOutput", text));
|
|
173
|
+
} else {
|
|
174
|
+
lines.push(
|
|
175
|
+
theme.fg("muted", "→ ") +
|
|
176
|
+
formatToolActivity(item.name ?? "tool", item.args ?? {}, theme.fg.bind(theme)),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return lines.join("\n");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function formatToolActivity(
|
|
184
|
+
toolNameValue: unknown,
|
|
185
|
+
argsValue: unknown,
|
|
186
|
+
themeFg: (color: ThemeColor, text: string) => string,
|
|
187
|
+
): string {
|
|
188
|
+
const toolName = safeLine(toolNameValue, "tool", 256);
|
|
189
|
+
const args = recordValue(argsValue) ?? {};
|
|
190
|
+
const shortenPath = (value: unknown, fallback = ".") => {
|
|
191
|
+
const filePath = safeLine(value, fallback, 2 * 1024);
|
|
192
|
+
const home = os.homedir();
|
|
193
|
+
return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
switch (toolName) {
|
|
197
|
+
case "bash": {
|
|
198
|
+
const command = safeLine(args.command, "...", 512);
|
|
199
|
+
return themeFg("muted", "$ ") + themeFg("toolOutput", command);
|
|
200
|
+
}
|
|
201
|
+
case "read": {
|
|
202
|
+
const filePath = shortenPath(args.file_path ?? args.path, "...");
|
|
203
|
+
const offset = typeof args.offset === "number" ? args.offset : undefined;
|
|
204
|
+
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
205
|
+
let text = themeFg("accent", filePath);
|
|
206
|
+
if (offset !== undefined || limit !== undefined) {
|
|
207
|
+
const startLine = offset ?? 1;
|
|
208
|
+
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
|
209
|
+
text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
|
210
|
+
}
|
|
211
|
+
return themeFg("muted", "read ") + text;
|
|
212
|
+
}
|
|
213
|
+
case "write": {
|
|
214
|
+
const filePath = shortenPath(args.file_path ?? args.path, "...");
|
|
215
|
+
const content = safeBlock(args.content, "", 2 * 1024);
|
|
216
|
+
const lines = content ? content.split("\n").length : 0;
|
|
217
|
+
return (
|
|
218
|
+
themeFg("muted", "write ") +
|
|
219
|
+
themeFg("accent", filePath) +
|
|
220
|
+
(lines > 1 ? themeFg("dim", ` (${lines} lines)`) : "")
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
case "edit":
|
|
224
|
+
return (
|
|
225
|
+
themeFg("muted", "edit ") +
|
|
226
|
+
themeFg("accent", shortenPath(args.file_path ?? args.path, "..."))
|
|
227
|
+
);
|
|
228
|
+
case "ls":
|
|
229
|
+
return themeFg("muted", "ls ") + themeFg("accent", shortenPath(args.path));
|
|
230
|
+
case "find":
|
|
231
|
+
return (
|
|
232
|
+
themeFg("muted", "find ") +
|
|
233
|
+
themeFg("accent", safeLine(args.pattern, "*", 512)) +
|
|
234
|
+
themeFg("dim", ` in ${shortenPath(args.path)}`)
|
|
235
|
+
);
|
|
236
|
+
case "grep":
|
|
237
|
+
return (
|
|
238
|
+
themeFg("muted", "grep ") +
|
|
239
|
+
themeFg("accent", `/${safeLine(args.pattern, "", 512)}/`) +
|
|
240
|
+
themeFg("dim", ` in ${shortenPath(args.path)}`)
|
|
241
|
+
);
|
|
242
|
+
default: {
|
|
243
|
+
let serialized = "{}";
|
|
244
|
+
try {
|
|
245
|
+
serialized = JSON.stringify(args);
|
|
246
|
+
} catch {
|
|
247
|
+
serialized = "{…}";
|
|
248
|
+
}
|
|
249
|
+
return themeFg("accent", toolName) + themeFg("dim", ` ${safeLine(serialized, "{}", 512)}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|