@tintinweb/pi-subagents 0.2.7 → 0.3.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.
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Model resolution: exact match ("provider/modelId") with fuzzy fallback.
3
+ */
4
+
5
+ export interface ModelEntry {
6
+ id: string;
7
+ name: string;
8
+ provider: string;
9
+ }
10
+
11
+ export interface ModelRegistry {
12
+ find(provider: string, modelId: string): any;
13
+ getAll(): any[];
14
+ getAvailable?(): any[];
15
+ }
16
+
17
+ /**
18
+ * Resolve a model string to a Model instance.
19
+ * Tries exact match first ("provider/modelId"), then fuzzy match against all available models.
20
+ * Returns the Model on success, or an error message string on failure.
21
+ */
22
+ export function resolveModel(
23
+ input: string,
24
+ registry: ModelRegistry,
25
+ ): any | string {
26
+ // Available models (those with auth configured)
27
+ const all = (registry.getAvailable?.() ?? registry.getAll()) as ModelEntry[];
28
+ const availableSet = new Set(all.map(m => `${m.provider}/${m.id}`.toLowerCase()));
29
+
30
+ // 1. Exact match: "provider/modelId" — only if available (has auth)
31
+ const slashIdx = input.indexOf("/");
32
+ if (slashIdx !== -1) {
33
+ const provider = input.slice(0, slashIdx);
34
+ const modelId = input.slice(slashIdx + 1);
35
+ if (availableSet.has(input.toLowerCase())) {
36
+ const found = registry.find(provider, modelId);
37
+ if (found) return found;
38
+ }
39
+ }
40
+
41
+ // 2. Fuzzy match against available models
42
+ const query = input.toLowerCase();
43
+
44
+ // Score each model: prefer exact id match > id contains > name contains > provider+id contains
45
+ let bestMatch: ModelEntry | undefined;
46
+ let bestScore = 0;
47
+
48
+ for (const m of all) {
49
+ const id = m.id.toLowerCase();
50
+ const name = m.name.toLowerCase();
51
+ const full = `${m.provider}/${m.id}`.toLowerCase();
52
+
53
+ let score = 0;
54
+ if (id === query || full === query) {
55
+ score = 100; // exact
56
+ } else if (id.includes(query) || full.includes(query)) {
57
+ score = 60 + (query.length / id.length) * 30; // substring, prefer tighter matches
58
+ } else if (name.includes(query)) {
59
+ score = 40 + (query.length / name.length) * 20;
60
+ } else if (query.split(/[\s\-/]+/).every(part => id.includes(part) || name.includes(part) || m.provider.toLowerCase().includes(part))) {
61
+ score = 20; // all parts present somewhere
62
+ }
63
+
64
+ if (score > bestScore) {
65
+ bestScore = score;
66
+ bestMatch = m;
67
+ }
68
+ }
69
+
70
+ if (bestMatch && bestScore >= 20) {
71
+ const found = registry.find(bestMatch.provider, bestMatch.id);
72
+ if (found) return found;
73
+ }
74
+
75
+ // 3. No match — list available models
76
+ const modelList = all
77
+ .map(m => ` ${m.provider}/${m.id}`)
78
+ .sort()
79
+ .join("\n");
80
+ return `Model not found: "${input}".\n\nAvailable models:\n${modelList}`;
81
+ }
package/src/prompts.ts CHANGED
@@ -1,53 +1,16 @@
1
1
  /**
2
- * prompts.ts — System prompts per agent type.
2
+ * prompts.ts — System prompt builder for agents.
3
3
  */
4
4
 
5
- import type { EnvInfo } from "./types.js";
5
+ import type { AgentConfig, EnvInfo } from "./types.js";
6
6
 
7
- // ---- Reusable prompt blocks ----
8
-
9
- const READ_ONLY_PROHIBITION = `You are STRICTLY PROHIBITED from:
10
- - Creating new files
11
- - Modifying existing files
12
- - Deleting files
13
- - Moving or copying files
14
- - Creating temporary files anywhere, including /tmp
15
- - Using redirect operators (>, >>, |) or heredocs to write to files
16
- - Running ANY commands that change system state`;
17
-
18
- const READ_ONLY_TOOLS = `# Tool Usage
19
- - Use the find tool for file pattern matching (NOT the bash find command)
20
- - Use the grep tool for content search (NOT bash grep/rg command)
21
- - Use the read tool for reading files (NOT bash cat/head/tail)
22
- - Use Bash ONLY for read-only operations`;
23
-
24
- const FULL_TOOL_USAGE = `# Tool Usage
25
- - Use the read tool instead of cat/head/tail
26
- - Use the edit tool instead of sed/awk
27
- - Use the write tool instead of echo/heredoc
28
- - Use the find tool instead of bash find/ls for file search
29
- - Use the grep tool instead of bash grep/rg for content search
30
- - Make independent tool calls in parallel`;
31
-
32
- const GIT_SAFETY = `# Git Safety
33
- - NEVER update git config
34
- - NEVER run destructive git commands (push --force, reset --hard, checkout ., restore ., clean -f, branch -D) without explicit request
35
- - NEVER skip hooks (--no-verify, --no-gpg-sign) unless explicitly asked
36
- - NEVER force push to main/master — warn the user if they request it
37
- - Always create NEW commits, never amend existing ones. When a pre-commit hook fails, the commit did NOT happen — so --amend would modify the PREVIOUS commit. Fix the issue, re-stage, and create a NEW commit
38
- - Stage specific files by name, not git add -A or git add .
39
- - NEVER commit changes unless the user explicitly asks
40
- - NEVER push unless the user explicitly asks
41
- - NEVER use git commands with the -i flag (like git rebase -i or git add -i) — they require interactive input
42
- - Do not use --no-edit with git rebase commands
43
- - Do not commit files that likely contain secrets (.env, credentials.json, etc); warn the user if they request it`;
44
-
45
- const OUTPUT_RULES = `# Output
46
- - Use absolute file paths
47
- - Do not use emojis
48
- - Be concise but complete`;
49
-
50
- export function buildSystemPrompt(type: string, cwd: string, env: EnvInfo): string {
7
+ /**
8
+ * Build the system prompt for an agent from its config.
9
+ *
10
+ * - "replace" mode: common header + config.systemPrompt
11
+ * - "append" mode: common header + generic base + config.systemPrompt
12
+ */
13
+ export function buildAgentPrompt(config: AgentConfig, cwd: string, env: EnvInfo): string {
51
14
  const commonHeader = `You are a pi coding agent sub-agent.
52
15
  You have been invoked to handle a specific task autonomously.
53
16
 
@@ -56,108 +19,30 @@ Working directory: ${cwd}
56
19
  ${env.isGitRepo ? `Git repository: yes\nBranch: ${env.branch}` : "Not a git repository"}
57
20
  Platform: ${env.platform}`;
58
21
 
59
- switch (type) {
60
- case "Explore":
61
- return `${commonHeader}
62
-
63
- # CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
64
- You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
65
- Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools.
66
-
67
- ${READ_ONLY_PROHIBITION}
68
-
69
- Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find, cat, head, tail.
70
-
71
- ${READ_ONLY_TOOLS}
72
- - Make independent tool calls in parallel for efficiency
73
- - Adapt search approach based on thoroughness level specified
74
-
75
- # Output
76
- - Use absolute file paths in all references
77
- - Report findings as regular messages
78
- - Do not use emojis
79
- - Be thorough and precise`;
80
-
81
- case "Plan":
82
- return `${commonHeader}
83
-
84
- # CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
85
- You are a software architect and planning specialist.
86
- Your role is EXCLUSIVELY to explore the codebase and design implementation plans.
87
- You do NOT have access to file editing tools — attempting to edit files will fail.
88
-
89
- ${READ_ONLY_PROHIBITION}
90
-
91
- # Planning Process
92
- 1. Understand requirements
93
- 2. Explore thoroughly (read files, find patterns, understand architecture)
94
- 3. Design solution based on your assigned perspective
95
- 4. Detail the plan with step-by-step implementation strategy
96
-
97
- # Requirements
98
- - Consider trade-offs and architectural decisions
99
- - Identify dependencies and sequencing
100
- - Anticipate potential challenges
101
- - Follow existing patterns where appropriate
102
-
103
- ${READ_ONLY_TOOLS}
104
-
105
- # Output Format
106
- - Use absolute file paths
107
- - Do not use emojis
108
- - End your response with:
109
-
110
- ### Critical Files for Implementation
111
- List 3-5 files most critical for implementing this plan:
112
- - /absolute/path/to/file.ts - [Brief reason]`;
113
-
114
- case "general-purpose":
115
- return `${commonHeader}
22
+ if (config.promptMode === "append") {
23
+ const genericBase = `
116
24
 
117
25
  # Role
118
26
  You are a general-purpose coding agent for complex, multi-step tasks.
119
27
  You have full access to read, write, edit files, and execute commands.
120
28
  Do what has been asked; nothing more, nothing less.
121
29
 
122
- ${FULL_TOOL_USAGE}
123
-
124
- # File Operations
125
- - NEVER create files unless absolutely necessary
126
- - Prefer editing existing files over creating new ones
127
- - NEVER create documentation files unless explicitly requested
128
-
129
- ${GIT_SAFETY}
130
-
131
- ${OUTPUT_RULES}`;
132
-
133
- case "statusline-setup":
134
- return `${commonHeader}
135
-
136
- # Role
137
- You configure settings. You can read and edit files only.
138
- Focus on the specific configuration task requested.
139
- Use absolute file paths.`;
140
-
141
- case "claude-code-guide":
142
- return `${commonHeader}
143
-
144
- # Role
145
- You help answer questions about the tool, its features, and capabilities.
146
- Search documentation, read config files, and provide accurate answers.
147
- You have read-only access to the codebase for reference.
148
- Use absolute file paths.`;
149
-
150
- default:
151
- // Custom agents or unknown: general-purpose base without git safety / file ops
152
- return `${commonHeader}
153
-
154
- # Role
155
- You are a general-purpose coding agent for complex, multi-step tasks.
156
- You have full access to read, write, edit files, and execute commands.
157
- Do what has been asked; nothing more, nothing less.
30
+ # Tool Usage
31
+ - Use the read tool instead of cat/head/tail
32
+ - Use the edit tool instead of sed/awk
33
+ - Use the write tool instead of echo/heredoc
34
+ - Use the find tool instead of bash find/ls for file search
35
+ - Use the grep tool instead of bash grep/rg for content search
36
+ - Make independent tool calls in parallel
158
37
 
159
- ${FULL_TOOL_USAGE}
38
+ # Output
39
+ - Use absolute file paths
40
+ - Do not use emojis
41
+ - Be concise but complete`;
160
42
 
161
- ${OUTPUT_RULES}`;
43
+ return commonHeader + genericBase + "\n\n" + config.systemPrompt;
162
44
  }
45
+
46
+ // "replace" mode — header + the config's full system prompt
47
+ return commonHeader + "\n\n" + config.systemPrompt;
163
48
  }
package/src/types.ts CHANGED
@@ -7,44 +7,18 @@ import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
7
7
 
8
8
  export type { ThinkingLevel };
9
9
 
10
- /** Built-in agent types. Custom agents use arbitrary string names. */
11
- export type BuiltinSubagentType = "general-purpose" | "Explore" | "Plan" | "statusline-setup" | "claude-code-guide";
10
+ /** Agent type: any string name (built-in defaults or user-defined). */
11
+ export type SubagentType = string;
12
12
 
13
- /** Agent type: built-in or custom (any string). */
14
- export type SubagentType = BuiltinSubagentType | (string & {});
13
+ /** Names of the three embedded default agents. */
14
+ export const DEFAULT_AGENT_NAMES = ["general-purpose", "Explore", "Plan"] as const;
15
15
 
16
- /** Display name mapping for built-in types. */
17
- export const DISPLAY_NAMES: Record<BuiltinSubagentType, string> = {
18
- "general-purpose": "Agent",
19
- "Explore": "Explore",
20
- "Plan": "Plan",
21
- "statusline-setup": "Config",
22
- "claude-code-guide": "Guide",
23
- };
24
-
25
- export const SUBAGENT_TYPES: BuiltinSubagentType[] = [
26
- "general-purpose",
27
- "Explore",
28
- "Plan",
29
- "statusline-setup",
30
- "claude-code-guide",
31
- ];
32
-
33
- export interface SubagentTypeConfig {
34
- displayName: string;
35
- description: string;
36
- builtinToolNames: string[];
37
- /** true = inherit all, string[] = only listed, false = none */
38
- extensions: true | string[] | false;
39
- /** true = inherit all, string[] = only listed, false = none */
40
- skills: true | string[] | false;
41
- }
42
-
43
- /** Configuration for a custom agent loaded from .pi/agents/<name>.md */
44
- export interface CustomAgentConfig {
16
+ /** Unified agent configuration — used for both default and user-defined agents. */
17
+ export interface AgentConfig {
45
18
  name: string;
19
+ displayName?: string;
46
20
  description: string;
47
- builtinToolNames: string[];
21
+ builtinToolNames?: string[];
48
22
  /** true = inherit all, string[] = only listed, false = none */
49
23
  extensions: true | string[] | false;
50
24
  /** true = inherit all, string[] = only listed, false = none */
@@ -60,6 +34,12 @@ export interface CustomAgentConfig {
60
34
  runInBackground: boolean;
61
35
  /** Default for spawn: no extension tools */
62
36
  isolated: boolean;
37
+ /** true = this is an embedded default agent (informational) */
38
+ isDefault?: boolean;
39
+ /** false = agent is hidden from the registry */
40
+ enabled?: boolean;
41
+ /** Where this agent was loaded from */
42
+ source?: "default" | "project" | "global";
63
43
  }
64
44
 
65
45
  export type JoinMode = 'async' | 'group' | 'smart';
@@ -0,0 +1,241 @@
1
+ /**
2
+ * conversation-viewer.ts — Live conversation overlay for viewing agent sessions.
3
+ *
4
+ * Displays a scrollable, live-updating view of an agent's conversation.
5
+ * Subscribes to session events for real-time streaming updates.
6
+ */
7
+
8
+ import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type TUI } from "@mariozechner/pi-tui";
9
+ import type { AgentSession } from "@mariozechner/pi-coding-agent";
10
+ import type { Theme } from "./agent-widget.js";
11
+ import { formatTokens, formatDuration, getDisplayName, describeActivity, type AgentActivity } from "./agent-widget.js";
12
+ import type { AgentRecord } from "../types.js";
13
+ import { extractText } from "../context.js";
14
+
15
+ /** Lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
16
+ const CHROME_LINES = 6;
17
+ const MIN_VIEWPORT = 3;
18
+
19
+ export class ConversationViewer implements Component {
20
+ private scrollOffset = 0;
21
+ private autoScroll = true;
22
+ private unsubscribe: (() => void) | undefined;
23
+ private lastInnerW = 0;
24
+ private closed = false;
25
+
26
+ constructor(
27
+ private tui: TUI,
28
+ private session: AgentSession,
29
+ private record: AgentRecord,
30
+ private activity: AgentActivity | undefined,
31
+ private theme: Theme,
32
+ private done: (result: undefined) => void,
33
+ ) {
34
+ this.unsubscribe = session.subscribe(() => {
35
+ if (this.closed) return;
36
+ this.tui.requestRender();
37
+ });
38
+ }
39
+
40
+ handleInput(data: string): void {
41
+ if (matchesKey(data, "escape") || matchesKey(data, "q")) {
42
+ this.closed = true;
43
+ this.done(undefined);
44
+ return;
45
+ }
46
+
47
+ const totalLines = this.buildContentLines(this.lastInnerW).length;
48
+ const viewportHeight = this.viewportHeight();
49
+ const maxScroll = Math.max(0, totalLines - viewportHeight);
50
+
51
+ if (matchesKey(data, "up") || matchesKey(data, "k")) {
52
+ this.scrollOffset = Math.max(0, this.scrollOffset - 1);
53
+ this.autoScroll = this.scrollOffset >= maxScroll;
54
+ } else if (matchesKey(data, "down") || matchesKey(data, "j")) {
55
+ this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
56
+ this.autoScroll = this.scrollOffset >= maxScroll;
57
+ } else if (matchesKey(data, "pageUp")) {
58
+ this.scrollOffset = Math.max(0, this.scrollOffset - viewportHeight);
59
+ this.autoScroll = false;
60
+ } else if (matchesKey(data, "pageDown")) {
61
+ this.scrollOffset = Math.min(maxScroll, this.scrollOffset + viewportHeight);
62
+ this.autoScroll = this.scrollOffset >= maxScroll;
63
+ } else if (matchesKey(data, "home")) {
64
+ this.scrollOffset = 0;
65
+ this.autoScroll = false;
66
+ } else if (matchesKey(data, "end")) {
67
+ this.scrollOffset = maxScroll;
68
+ this.autoScroll = true;
69
+ }
70
+ }
71
+
72
+ render(width: number): string[] {
73
+ if (width < 6) return []; // too narrow for any meaningful rendering
74
+ const th = this.theme;
75
+ const innerW = width - 4; // border + padding
76
+ this.lastInnerW = innerW;
77
+ const lines: string[] = [];
78
+
79
+ const pad = (s: string, len: number) => {
80
+ const vis = visibleWidth(s);
81
+ return s + " ".repeat(Math.max(0, len - vis));
82
+ };
83
+ const row = (content: string) =>
84
+ th.fg("border", "│") + " " + truncateToWidth(pad(content, innerW), innerW) + " " + th.fg("border", "│");
85
+ const hrTop = th.fg("border", `╭${"─".repeat(width - 2)}╮`);
86
+ const hrBot = th.fg("border", `╰${"─".repeat(width - 2)}╯`);
87
+ const hrMid = row(th.fg("dim", "─".repeat(innerW)));
88
+
89
+ // Header
90
+ lines.push(hrTop);
91
+ const name = getDisplayName(this.record.type);
92
+ const statusIcon = this.record.status === "running"
93
+ ? th.fg("accent", "●")
94
+ : this.record.status === "completed"
95
+ ? th.fg("success", "✓")
96
+ : this.record.status === "error"
97
+ ? th.fg("error", "✗")
98
+ : th.fg("dim", "○");
99
+ const duration = formatDuration(this.record.startedAt, this.record.completedAt);
100
+
101
+ const headerParts: string[] = [duration];
102
+ const toolUses = this.activity?.toolUses ?? this.record.toolUses;
103
+ if (toolUses > 0) headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
104
+ if (this.activity?.session) {
105
+ try {
106
+ const tokens = this.activity.session.getSessionStats().tokens.total;
107
+ if (tokens > 0) headerParts.push(formatTokens(tokens));
108
+ } catch { /* */ }
109
+ }
110
+
111
+ lines.push(row(
112
+ `${statusIcon} ${th.bold(name)} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${th.fg("dim", headerParts.join(" · "))}`,
113
+ ));
114
+ lines.push(hrMid);
115
+
116
+ // Content area — rebuild every render (live data, no cache needed)
117
+ const contentLines = this.buildContentLines(innerW);
118
+ const viewportHeight = this.viewportHeight();
119
+ const maxScroll = Math.max(0, contentLines.length - viewportHeight);
120
+
121
+ if (this.autoScroll) {
122
+ this.scrollOffset = maxScroll;
123
+ }
124
+
125
+ const visibleStart = Math.min(this.scrollOffset, maxScroll);
126
+ const visible = contentLines.slice(visibleStart, visibleStart + viewportHeight);
127
+
128
+ for (let i = 0; i < viewportHeight; i++) {
129
+ lines.push(row(visible[i] ?? ""));
130
+ }
131
+
132
+ // Footer
133
+ lines.push(hrMid);
134
+ const scrollPct = contentLines.length <= viewportHeight
135
+ ? "100%"
136
+ : `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
137
+ const footerLeft = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
138
+ const footerRight = th.fg("dim", "↑↓ scroll · PgUp/PgDn · Esc close");
139
+ const footerGap = Math.max(1, innerW - visibleWidth(footerLeft) - visibleWidth(footerRight));
140
+ lines.push(row(footerLeft + " ".repeat(footerGap) + footerRight));
141
+ lines.push(hrBot);
142
+
143
+ return lines;
144
+ }
145
+
146
+ invalidate(): void { /* no cached state to clear */ }
147
+
148
+ dispose(): void {
149
+ this.closed = true;
150
+ if (this.unsubscribe) {
151
+ this.unsubscribe();
152
+ this.unsubscribe = undefined;
153
+ }
154
+ }
155
+
156
+ // ---- Private ----
157
+
158
+ private viewportHeight(): number {
159
+ return Math.max(MIN_VIEWPORT, this.tui.terminal.rows - CHROME_LINES);
160
+ }
161
+
162
+ private buildContentLines(width: number): string[] {
163
+ if (width <= 0) return [];
164
+
165
+ const th = this.theme;
166
+ const messages = this.session.messages;
167
+ const lines: string[] = [];
168
+
169
+ if (messages.length === 0) {
170
+ lines.push(th.fg("dim", "(waiting for first message...)"));
171
+ return lines;
172
+ }
173
+
174
+ let needsSeparator = false;
175
+ for (const msg of messages) {
176
+ if (msg.role === "user") {
177
+ const text = typeof msg.content === "string"
178
+ ? msg.content
179
+ : extractText(msg.content);
180
+ if (!text.trim()) continue;
181
+ if (needsSeparator) lines.push(th.fg("dim", "───"));
182
+ lines.push(th.fg("accent", "[User]"));
183
+ for (const line of wrapTextWithAnsi(text.trim(), width)) {
184
+ lines.push(line);
185
+ }
186
+ } else if (msg.role === "assistant") {
187
+ const textParts: string[] = [];
188
+ const toolCalls: string[] = [];
189
+ for (const c of msg.content) {
190
+ if (c.type === "text" && c.text) textParts.push(c.text);
191
+ else if (c.type === "toolCall") {
192
+ toolCalls.push((c as any).toolName ?? "unknown");
193
+ }
194
+ }
195
+ if (needsSeparator) lines.push(th.fg("dim", "───"));
196
+ lines.push(th.bold("[Assistant]"));
197
+ if (textParts.length > 0) {
198
+ for (const line of wrapTextWithAnsi(textParts.join("\n").trim(), width)) {
199
+ lines.push(line);
200
+ }
201
+ }
202
+ for (const name of toolCalls) {
203
+ lines.push(truncateToWidth(th.fg("muted", ` [Tool: ${name}]`), width));
204
+ }
205
+ } else if (msg.role === "toolResult") {
206
+ const text = extractText(msg.content);
207
+ const truncated = text.length > 500 ? text.slice(0, 500) + "... (truncated)" : text;
208
+ if (!truncated.trim()) continue;
209
+ if (needsSeparator) lines.push(th.fg("dim", "───"));
210
+ lines.push(th.fg("dim", "[Result]"));
211
+ for (const line of wrapTextWithAnsi(truncated.trim(), width)) {
212
+ lines.push(th.fg("dim", line));
213
+ }
214
+ } else if ((msg as any).role === "bashExecution") {
215
+ const bash = msg as any;
216
+ if (needsSeparator) lines.push(th.fg("dim", "───"));
217
+ lines.push(truncateToWidth(th.fg("muted", ` $ ${bash.command}`), width));
218
+ if (bash.output?.trim()) {
219
+ const out = bash.output.length > 500
220
+ ? bash.output.slice(0, 500) + "... (truncated)"
221
+ : bash.output;
222
+ for (const line of wrapTextWithAnsi(out.trim(), width)) {
223
+ lines.push(th.fg("dim", line));
224
+ }
225
+ }
226
+ } else {
227
+ continue;
228
+ }
229
+ needsSeparator = true;
230
+ }
231
+
232
+ // Streaming indicator for running agents
233
+ if (this.record.status === "running" && this.activity) {
234
+ const act = describeActivity(this.activity.activeTools, this.activity.responseText);
235
+ lines.push("");
236
+ lines.push(truncateToWidth(th.fg("accent", "▍ ") + th.fg("dim", act), width));
237
+ }
238
+
239
+ return lines;
240
+ }
241
+ }