@gotgenes/pi-subagents 6.10.0 → 6.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/docs/architecture/architecture.md +21 -31
- package/docs/plans/0133-inject-sdk-boundary-into-agent-runner.md +373 -0
- package/docs/plans/0134-reduce-as-any-casts.md +366 -0
- package/docs/retro/0132-inject-io-into-session-config.md +33 -0
- package/docs/retro/0133-inject-sdk-boundary-into-agent-runner.md +45 -0
- package/package.json +1 -1
- package/src/agent-runner.ts +107 -35
- package/src/index.ts +32 -3
- package/src/runtime.ts +14 -2
- package/src/tools/agent-tool.ts +1 -1
- package/src/tools/helpers.ts +1 -1
- package/src/ui/conversation-viewer.ts +38 -8
package/src/agent-runner.ts
CHANGED
|
@@ -6,26 +6,35 @@ import type { Model } from "@earendil-works/pi-ai";
|
|
|
6
6
|
import {
|
|
7
7
|
type AgentSession,
|
|
8
8
|
type AgentSessionEvent,
|
|
9
|
-
|
|
10
|
-
DefaultResourceLoader,
|
|
11
|
-
getAgentDir,
|
|
12
|
-
SessionManager,
|
|
13
|
-
SettingsManager,
|
|
9
|
+
type SettingsManager,
|
|
14
10
|
} from "@earendil-works/pi-coding-agent";
|
|
15
11
|
import type { AgentConfigLookup } from "./agent-types.js";
|
|
16
12
|
import { extractText } from "./context.js";
|
|
17
|
-
import {
|
|
18
|
-
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
13
|
+
import type { EnvInfo } from "./env.js";
|
|
19
14
|
import type { ParentSnapshot } from "./parent-snapshot.js";
|
|
20
|
-
import { buildAgentPrompt } from "./prompts.js";
|
|
21
15
|
import { type AssemblerIO, assembleSessionConfig } from "./session-config.js";
|
|
22
|
-
import { deriveSubagentSessionDir } from "./session-dir.js";
|
|
23
|
-
import { preloadSkills } from "./skill-loader.js";
|
|
24
16
|
import type { ShellExec, SubagentType, ThinkingLevel } from "./types.js";
|
|
25
17
|
|
|
26
18
|
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
27
19
|
const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
|
|
28
20
|
|
|
21
|
+
// ── Local message-shape types ───────────────────────────────────────────────
|
|
22
|
+
// The Pi SDK does not export a narrow type for tool-call content variants.
|
|
23
|
+
|
|
24
|
+
/** Tool-call content item — SDK exposes this variant at runtime but doesn’t export the narrow type. */
|
|
25
|
+
interface ToolCallContent {
|
|
26
|
+
type: "toolCall";
|
|
27
|
+
name?: string;
|
|
28
|
+
toolName?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Extracts the display name from a tool-call content item. */
|
|
32
|
+
function getToolCallName(c: { type: string }): string {
|
|
33
|
+
if (c.type !== "toolCall") return "unknown";
|
|
34
|
+
const tc = c as ToolCallContent;
|
|
35
|
+
return tc.name ?? tc.toolName ?? "unknown";
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
/**
|
|
30
39
|
* Filter the session's active tool names according to extension/denylist rules.
|
|
31
40
|
*
|
|
@@ -68,6 +77,64 @@ export function normalizeMaxTurns(n: number | undefined): number | undefined {
|
|
|
68
77
|
return Math.max(1, n);
|
|
69
78
|
}
|
|
70
79
|
|
|
80
|
+
// ── IO boundary ───────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/** Minimal resource-loader contract used by the runner. */
|
|
83
|
+
export interface ResourceLoaderLike {
|
|
84
|
+
reload(): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Minimal session-manager contract used by the runner. */
|
|
88
|
+
export interface SessionManagerLike {
|
|
89
|
+
newSession(opts: { parentSession?: string }): void;
|
|
90
|
+
getSessionFile(): string | undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Options passed to RunnerIO.createResourceLoader. */
|
|
94
|
+
export interface ResourceLoaderOptions {
|
|
95
|
+
cwd: string;
|
|
96
|
+
agentDir: string;
|
|
97
|
+
noExtensions?: boolean;
|
|
98
|
+
noSkills?: boolean;
|
|
99
|
+
noPromptTemplates?: boolean;
|
|
100
|
+
noThemes?: boolean;
|
|
101
|
+
noContextFiles?: boolean;
|
|
102
|
+
systemPromptOverride?: () => string;
|
|
103
|
+
/** Override the append system prompt. Receives the current base value; return the replacement. */
|
|
104
|
+
appendSystemPromptOverride?: (base: string[]) => string[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Options passed to RunnerIO.createSession. */
|
|
108
|
+
export interface CreateSessionOptions {
|
|
109
|
+
cwd: string;
|
|
110
|
+
agentDir: string;
|
|
111
|
+
sessionManager: SessionManagerLike;
|
|
112
|
+
settingsManager: SettingsManager;
|
|
113
|
+
modelRegistry: unknown;
|
|
114
|
+
model?: unknown;
|
|
115
|
+
tools: string[];
|
|
116
|
+
resourceLoader: ResourceLoaderLike;
|
|
117
|
+
thinkingLevel?: ThinkingLevel;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* IO boundary injected into runAgent().
|
|
122
|
+
*
|
|
123
|
+
* Decouples the runner from direct Pi SDK imports and sibling-module IO,
|
|
124
|
+
* making it testable via plain stub objects without vi.mock().
|
|
125
|
+
*/
|
|
126
|
+
export interface RunnerIO {
|
|
127
|
+
detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
|
|
128
|
+
getAgentDir: () => string;
|
|
129
|
+
createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoaderLike;
|
|
130
|
+
deriveSessionDir: (parentSessionFile: string | undefined, effectiveCwd: string) => string;
|
|
131
|
+
createSessionManager: (cwd: string, sessionDir: string) => SessionManagerLike;
|
|
132
|
+
createSettingsManager: (cwd: string, agentDir: string) => SettingsManager;
|
|
133
|
+
createSession: (opts: CreateSessionOptions) => Promise<{ session: AgentSession }>;
|
|
134
|
+
assemblerIO: AssemblerIO;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Public interfaces ─────────────────────────────────────────────────────────
|
|
71
138
|
|
|
72
139
|
export interface RunOptions {
|
|
73
140
|
/** Shell-exec callback for detectEnv — injected from pi.exec(). */
|
|
@@ -125,6 +192,20 @@ export interface AgentRunner {
|
|
|
125
192
|
resume(session: AgentSession, prompt: string, options?: ResumeOptions): Promise<string>;
|
|
126
193
|
}
|
|
127
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Create an AgentRunner backed by the given IO boundary.
|
|
197
|
+
*
|
|
198
|
+
* Captures io at construction time so AgentManager remains IO-unaware.
|
|
199
|
+
*/
|
|
200
|
+
export function createAgentRunner(io: RunnerIO): AgentRunner {
|
|
201
|
+
return {
|
|
202
|
+
run: (snapshot, type, prompt, options) => runAgent(snapshot, type, prompt, options, io),
|
|
203
|
+
resume: resumeAgent,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── Private helpers ───────────────────────────────────────────────────────────
|
|
208
|
+
|
|
128
209
|
/**
|
|
129
210
|
* Subscribe to a session and collect the last assistant message text.
|
|
130
211
|
* Returns an object with a `getText()` getter and an `unsubscribe` function.
|
|
@@ -170,23 +251,20 @@ function forwardAbortSignal(
|
|
|
170
251
|
return () => signal.removeEventListener("abort", onAbort);
|
|
171
252
|
}
|
|
172
253
|
|
|
254
|
+
// ── Public functions ──────────────────────────────────────────────────────────
|
|
255
|
+
|
|
173
256
|
export async function runAgent(
|
|
174
257
|
snapshot: ParentSnapshot,
|
|
175
258
|
type: SubagentType,
|
|
176
259
|
prompt: string,
|
|
177
260
|
options: RunOptions,
|
|
261
|
+
io: RunnerIO,
|
|
178
262
|
): Promise<RunResult> {
|
|
179
263
|
// Resolve working directory upfront — needed for detectEnv before assembly.
|
|
180
264
|
const effectiveCwd = options.cwd ?? snapshot.cwd;
|
|
181
|
-
const env = await detectEnv(options.exec, effectiveCwd);
|
|
265
|
+
const env = await io.detectEnv(options.exec, effectiveCwd);
|
|
182
266
|
|
|
183
267
|
// Assemble session configuration (synchronous, no SDK objects).
|
|
184
|
-
const io: AssemblerIO = {
|
|
185
|
-
preloadSkills,
|
|
186
|
-
buildMemoryBlock,
|
|
187
|
-
buildReadOnlyMemoryBlock,
|
|
188
|
-
buildAgentPrompt,
|
|
189
|
-
};
|
|
190
268
|
const cfg = assembleSessionConfig(
|
|
191
269
|
type,
|
|
192
270
|
{
|
|
@@ -203,10 +281,10 @@ export async function runAgent(
|
|
|
203
281
|
},
|
|
204
282
|
env,
|
|
205
283
|
options.registry,
|
|
206
|
-
io,
|
|
284
|
+
io.assemblerIO,
|
|
207
285
|
);
|
|
208
286
|
|
|
209
|
-
const agentDir = getAgentDir();
|
|
287
|
+
const agentDir = io.getAgentDir();
|
|
210
288
|
|
|
211
289
|
// Load extensions/skills: true or string[] → load; false → don't.
|
|
212
290
|
// Suppress AGENTS.md/CLAUDE.md and APPEND_SYSTEM.md — upstream's
|
|
@@ -214,7 +292,7 @@ export async function runAgent(
|
|
|
214
292
|
// would defeat prompt_mode: replace and isolated: true. Parent context, if
|
|
215
293
|
// wanted, reaches the subagent via prompt_mode: append (parentSystemPrompt
|
|
216
294
|
// is embedded in systemPromptOverride) or inherit_context (conversation).
|
|
217
|
-
const loader =
|
|
295
|
+
const loader = io.createResourceLoader({
|
|
218
296
|
cwd: cfg.effectiveCwd,
|
|
219
297
|
agentDir,
|
|
220
298
|
noExtensions: cfg.extensions === false,
|
|
@@ -230,25 +308,21 @@ export async function runAgent(
|
|
|
230
308
|
// Create a persisted SessionManager so transcripts are written in Pi's
|
|
231
309
|
// official JSONL format. Falls back to a temp directory when the parent
|
|
232
310
|
// session is not persisted (e.g. headless/API mode).
|
|
233
|
-
const sessionDir =
|
|
234
|
-
const sessionManager =
|
|
311
|
+
const sessionDir = io.deriveSessionDir(options.parentSessionFile, cfg.effectiveCwd);
|
|
312
|
+
const sessionManager = io.createSessionManager(cfg.effectiveCwd, sessionDir);
|
|
235
313
|
sessionManager.newSession({ parentSession: options.parentSessionId });
|
|
236
314
|
|
|
237
|
-
const
|
|
315
|
+
const { session } = await io.createSession({
|
|
238
316
|
cwd: cfg.effectiveCwd,
|
|
239
317
|
agentDir,
|
|
240
318
|
sessionManager,
|
|
241
|
-
settingsManager:
|
|
242
|
-
modelRegistry: snapshot.modelRegistry
|
|
243
|
-
model: cfg.model
|
|
319
|
+
settingsManager: io.createSettingsManager(cfg.effectiveCwd, agentDir),
|
|
320
|
+
modelRegistry: snapshot.modelRegistry,
|
|
321
|
+
model: cfg.model,
|
|
244
322
|
tools: cfg.toolNames,
|
|
245
323
|
resourceLoader: loader,
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
sessionOpts.thinkingLevel = cfg.thinkingLevel;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const { session } = await createAgentSession(sessionOpts);
|
|
324
|
+
thinkingLevel: cfg.thinkingLevel,
|
|
325
|
+
});
|
|
252
326
|
|
|
253
327
|
// Filter active tools: remove our own tools to prevent nesting,
|
|
254
328
|
// apply extension allowlist if specified, and apply disallowedTools denylist.
|
|
@@ -391,9 +465,7 @@ export function getAgentConversation(session: AgentSession): string {
|
|
|
391
465
|
for (const c of msg.content) {
|
|
392
466
|
if (c.type === "text" && c.text) textParts.push(c.text);
|
|
393
467
|
else if (c.type === "toolCall")
|
|
394
|
-
toolCalls.push(
|
|
395
|
-
` Tool: ${(c as any).name ?? (c as any).toolName ?? "unknown"}`,
|
|
396
|
-
);
|
|
468
|
+
toolCalls.push(` Tool: ${getToolCallName(c)}`);
|
|
397
469
|
}
|
|
398
470
|
if (textParts.length > 0)
|
|
399
471
|
parts.push(`[Assistant]: ${textParts.join("\n")}`);
|
package/src/index.ts
CHANGED
|
@@ -11,19 +11,32 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
createAgentSession,
|
|
16
|
+
DefaultResourceLoader,
|
|
17
|
+
defineTool,
|
|
18
|
+
type ExtensionAPI,
|
|
19
|
+
getAgentDir,
|
|
20
|
+
SettingsManager as SdkSettingsManager,
|
|
21
|
+
SessionManager,
|
|
22
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
23
|
import { AgentManager, type AgentManagerObserver } from "./agent-manager.js";
|
|
16
|
-
import {
|
|
24
|
+
import { createAgentRunner, getAgentConversation, type RunnerIO, steerAgent } from "./agent-runner.js";
|
|
17
25
|
import { AgentTypeRegistry } from "./agent-types.js";
|
|
18
26
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
27
|
+
import { detectEnv } from "./env.js";
|
|
19
28
|
import { SessionLifecycleHandler, ToolStartHandler } from "./handlers/index.js";
|
|
29
|
+
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
20
30
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
21
31
|
import { buildEventData, type NotificationDetails, NotificationManager } from "./notification.js";
|
|
32
|
+
import { buildAgentPrompt } from "./prompts.js";
|
|
22
33
|
import { createNotificationRenderer } from "./renderer.js";
|
|
23
34
|
import { createSubagentRuntime } from "./runtime.js";
|
|
24
35
|
import { publishSubagentsService, unpublishSubagentsService } from "./service.js";
|
|
25
36
|
import { createSubagentsService } from "./service-adapter.js";
|
|
37
|
+
import { deriveSubagentSessionDir } from "./session-dir.js";
|
|
26
38
|
import { SettingsManager } from "./settings.js";
|
|
39
|
+
import { preloadSkills } from "./skill-loader.js";
|
|
27
40
|
import { createAgentTool } from "./tools/agent-tool.js";
|
|
28
41
|
import { createGetResultTool } from "./tools/get-result-tool.js";
|
|
29
42
|
import { getModelLabelFromConfig } from "./tools/helpers.js";
|
|
@@ -120,8 +133,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
120
133
|
},
|
|
121
134
|
};
|
|
122
135
|
|
|
136
|
+
const runnerIO: RunnerIO = {
|
|
137
|
+
detectEnv,
|
|
138
|
+
getAgentDir,
|
|
139
|
+
createResourceLoader: (opts) => new DefaultResourceLoader(opts),
|
|
140
|
+
deriveSessionDir: deriveSubagentSessionDir,
|
|
141
|
+
createSessionManager: (cwd, dir) => SessionManager.create(cwd, dir),
|
|
142
|
+
createSettingsManager: (cwd, dir) => SdkSettingsManager.create(cwd, dir),
|
|
143
|
+
createSession: (opts) => createAgentSession(opts as any),
|
|
144
|
+
assemblerIO: {
|
|
145
|
+
preloadSkills,
|
|
146
|
+
buildMemoryBlock,
|
|
147
|
+
buildReadOnlyMemoryBlock,
|
|
148
|
+
buildAgentPrompt,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
123
152
|
const manager = new AgentManager({
|
|
124
|
-
runner:
|
|
153
|
+
runner: createAgentRunner(runnerIO),
|
|
125
154
|
worktrees: new GitWorktreeManager(process.cwd()),
|
|
126
155
|
exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
|
|
127
156
|
registry,
|
package/src/runtime.ts
CHANGED
|
@@ -7,7 +7,19 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { AgentActivityTracker } from "./ui/agent-activity-tracker.js";
|
|
10
|
-
import type {
|
|
10
|
+
import type { UICtx } from "./ui/agent-widget.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Narrow widget interface consumed by SubagentRuntime delegation methods.
|
|
14
|
+
* AgentWidget satisfies this structurally; tests use plain stubs.
|
|
15
|
+
*/
|
|
16
|
+
export interface WidgetLike {
|
|
17
|
+
setUICtx(ctx: UICtx): void;
|
|
18
|
+
onTurnStart(): void;
|
|
19
|
+
markFinished(id: string): void;
|
|
20
|
+
update(): void;
|
|
21
|
+
ensureTimer(): void;
|
|
22
|
+
}
|
|
11
23
|
|
|
12
24
|
/**
|
|
13
25
|
* Narrow config subset read by AgentManager when constructing RunOptions.
|
|
@@ -37,7 +49,7 @@ export class SubagentRuntime {
|
|
|
37
49
|
* Persistent widget reference. Null until constructed after AgentManager.
|
|
38
50
|
* Delegation methods use optional chaining so callers never need `widget!`.
|
|
39
51
|
*/
|
|
40
|
-
widget:
|
|
52
|
+
widget: WidgetLike | null = null;
|
|
41
53
|
|
|
42
54
|
// ── Session-context methods ──────────────────────────────────────────────
|
|
43
55
|
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from "../ui/agent-widget.js";
|
|
22
22
|
import { spawnBackground } from "./background-spawner.js";
|
|
23
23
|
import { runForeground } from "./foreground-runner.js";
|
|
24
|
-
import { buildDetails, buildTypeListText,
|
|
24
|
+
import { buildDetails, buildTypeListText, textResult } from "./helpers.js";
|
|
25
25
|
|
|
26
26
|
// ---- Deps interface ----
|
|
27
27
|
|
package/src/tools/helpers.ts
CHANGED
|
@@ -48,7 +48,7 @@ export function buildDetails(
|
|
|
48
48
|
|
|
49
49
|
/** Tool execute return value for a text response. */
|
|
50
50
|
export function textResult(msg: string, details?: unknown) {
|
|
51
|
-
return { content: [{ type: "text" as const, text: msg }], details
|
|
51
|
+
return { content: [{ type: "text" as const, text: msg }], details };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/** Format an agent's lifetime token total, or "" when zero. */
|
|
@@ -14,6 +14,37 @@ import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
|
|
|
14
14
|
import type { AgentActivityTracker } from "./agent-activity-tracker.js";
|
|
15
15
|
import { buildInvocationTags, describeActivity, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel, type Theme } from "./agent-widget.js";
|
|
16
16
|
|
|
17
|
+
// ── Local message-shape types ───────────────────────────────────────────────
|
|
18
|
+
// The Pi SDK does not export narrow types for all message content variants.
|
|
19
|
+
// These file-local types document the runtime shapes this module handles.
|
|
20
|
+
|
|
21
|
+
/** Tool-call content item — SDK exposes this variant at runtime but doesn't export the narrow type. */
|
|
22
|
+
interface ToolCallContent {
|
|
23
|
+
type: "toolCall";
|
|
24
|
+
name?: string;
|
|
25
|
+
toolName?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Extracts the tool name from a content item, falling back to 'unknown'. */
|
|
29
|
+
function getToolCallName(c: { type: string }): string {
|
|
30
|
+
if (c.type !== "toolCall") return "unknown";
|
|
31
|
+
const tc = c as ToolCallContent;
|
|
32
|
+
return tc.name ?? tc.toolName ?? "unknown";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Bash execution message — 'bashExecution' role is not in the SDK's AgentSession message role union. */
|
|
36
|
+
interface BashExecutionMessage {
|
|
37
|
+
role: "bashExecution";
|
|
38
|
+
command: string;
|
|
39
|
+
output?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isBashExecution(msg: { role: string }): msg is BashExecutionMessage {
|
|
43
|
+
return msg.role === "bashExecution";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
17
48
|
/** Base lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
|
|
18
49
|
const CHROME_LINES_BASE = 6;
|
|
19
50
|
const MIN_VIEWPORT = 3;
|
|
@@ -228,7 +259,7 @@ export class ConversationViewer implements Component {
|
|
|
228
259
|
for (const c of msg.content) {
|
|
229
260
|
if (c.type === "text" && c.text) textParts.push(c.text);
|
|
230
261
|
else if (c.type === "toolCall") {
|
|
231
|
-
toolCalls.push((c
|
|
262
|
+
toolCalls.push(getToolCallName(c));
|
|
232
263
|
}
|
|
233
264
|
}
|
|
234
265
|
if (needsSeparator) lines.push(th.fg("dim", "───"));
|
|
@@ -250,14 +281,13 @@ export class ConversationViewer implements Component {
|
|
|
250
281
|
for (const line of wrapTextWithAnsi(truncated.trim(), width)) {
|
|
251
282
|
lines.push(th.fg("dim", line));
|
|
252
283
|
}
|
|
253
|
-
} else if ((msg
|
|
254
|
-
const bash = msg as any;
|
|
284
|
+
} else if (isBashExecution(msg)) {
|
|
255
285
|
if (needsSeparator) lines.push(th.fg("dim", "───"));
|
|
256
|
-
lines.push(truncateToWidth(th.fg("muted", ` $ ${
|
|
257
|
-
if (
|
|
258
|
-
const out =
|
|
259
|
-
?
|
|
260
|
-
:
|
|
286
|
+
lines.push(truncateToWidth(th.fg("muted", ` $ ${msg.command}`), width));
|
|
287
|
+
if (msg.output?.trim()) {
|
|
288
|
+
const out = msg.output.length > 500
|
|
289
|
+
? msg.output.slice(0, 500) + "... (truncated)"
|
|
290
|
+
: msg.output;
|
|
261
291
|
for (const line of wrapTextWithAnsi(out.trim(), width)) {
|
|
262
292
|
lines.push(th.fg("dim", line));
|
|
263
293
|
}
|