@gotgenes/pi-subagents 17.5.0 → 18.0.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 +13 -0
- package/docs/architecture/architecture.md +24 -19
- package/docs/decisions/0004-reconsider-ui-direction.md +17 -0
- package/docs/plans/0441-remove-agent-definition-management-subtree.md +180 -0
- package/docs/plans/0442-dissolve-agents-conversation-viewer.md +195 -0
- package/docs/retro/0441-remove-agent-definition-management-subtree.md +45 -0
- package/docs/retro/0442-dissolve-agents-conversation-viewer.md +108 -0
- package/docs/retro/0463-file-snapshot-source-evicted-agents.md +54 -0
- package/package.json +1 -1
- package/src/index.ts +4 -31
- package/src/ui/session-navigation.ts +1 -1
- package/src/ui/session-navigator.ts +3 -3
- package/src/ui/subagents-settings.ts +2 -2
- package/src/ui/agent-config-editor.ts +0 -199
- package/src/ui/agent-creation-wizard.ts +0 -233
- package/src/ui/agent-file-ops.ts +0 -59
- package/src/ui/agent-file-writer.ts +0 -55
- package/src/ui/agent-menu.ts +0 -331
- package/src/ui/conversation-viewer.ts +0 -241
- package/src/ui/message-formatters.ts +0 -195
|
@@ -1,241 +0,0 @@
|
|
|
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 { type Component, matchesKey, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
|
-
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
10
|
-
import { getLifetimeTotal } from "#src/lifecycle/usage";
|
|
11
|
-
import type { Subagent } from "#src/types";
|
|
12
|
-
import { buildInvocationTags, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel, type Theme } from "#src/ui/display";
|
|
13
|
-
import { formatMessage, formatStreamingIndicator } from "#src/ui/message-formatters";
|
|
14
|
-
|
|
15
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
/** Base lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
|
|
18
|
-
const CHROME_LINES_BASE = 6;
|
|
19
|
-
const MIN_VIEWPORT = 3;
|
|
20
|
-
/** Height ceiling shared by the overlay's `maxHeight` and the viewer's internal viewport cap. */
|
|
21
|
-
export const VIEWPORT_HEIGHT_PCT = 70;
|
|
22
|
-
|
|
23
|
-
export interface ConversationViewerOptions {
|
|
24
|
-
tui: TUI;
|
|
25
|
-
record: Subagent;
|
|
26
|
-
theme: Theme;
|
|
27
|
-
done: (result: undefined) => void;
|
|
28
|
-
registry: AgentConfigLookup;
|
|
29
|
-
wrapText: (text: string, width: number) => string[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class ConversationViewer implements Component {
|
|
33
|
-
private scrollOffset = 0;
|
|
34
|
-
private autoScroll = true;
|
|
35
|
-
private unsubscribe: (() => void) | undefined;
|
|
36
|
-
private lastInnerW = 0;
|
|
37
|
-
private closed = false;
|
|
38
|
-
|
|
39
|
-
private tui: TUI;
|
|
40
|
-
private record: Subagent;
|
|
41
|
-
private theme: Theme;
|
|
42
|
-
private done: (result: undefined) => void;
|
|
43
|
-
private registry: AgentConfigLookup;
|
|
44
|
-
private wrapText: (text: string, width: number) => string[];
|
|
45
|
-
|
|
46
|
-
constructor({
|
|
47
|
-
tui,
|
|
48
|
-
record,
|
|
49
|
-
theme,
|
|
50
|
-
done,
|
|
51
|
-
registry,
|
|
52
|
-
wrapText,
|
|
53
|
-
}: ConversationViewerOptions) {
|
|
54
|
-
this.tui = tui;
|
|
55
|
-
this.record = record;
|
|
56
|
-
this.theme = theme;
|
|
57
|
-
this.done = done;
|
|
58
|
-
this.registry = registry;
|
|
59
|
-
this.wrapText = wrapText;
|
|
60
|
-
this.unsubscribe = record.subscribeToUpdates(() => {
|
|
61
|
-
if (this.closed) return;
|
|
62
|
-
this.tui.requestRender();
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// fallow-ignore-next-line unused-class-member
|
|
67
|
-
handleInput(data: string): void {
|
|
68
|
-
if (matchesKey(data, "escape") || matchesKey(data, "q")) {
|
|
69
|
-
this.closed = true;
|
|
70
|
-
this.done(undefined);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const totalLines = this.buildContentLines(this.lastInnerW).length;
|
|
75
|
-
const viewportHeight = this.viewportHeight();
|
|
76
|
-
const maxScroll = Math.max(0, totalLines - viewportHeight);
|
|
77
|
-
|
|
78
|
-
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
79
|
-
this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
|
80
|
-
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
81
|
-
} else if (matchesKey(data, "down") || matchesKey(data, "j")) {
|
|
82
|
-
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
|
|
83
|
-
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
84
|
-
} else if (matchesKey(data, "pageUp") || matchesKey(data, "shift+up")) {
|
|
85
|
-
this.scrollOffset = Math.max(0, this.scrollOffset - viewportHeight);
|
|
86
|
-
this.autoScroll = false;
|
|
87
|
-
} else if (matchesKey(data, "pageDown") || matchesKey(data, "shift+down")) {
|
|
88
|
-
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + viewportHeight);
|
|
89
|
-
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
90
|
-
} else if (matchesKey(data, "home")) {
|
|
91
|
-
this.scrollOffset = 0;
|
|
92
|
-
this.autoScroll = false;
|
|
93
|
-
} else if (matchesKey(data, "end")) {
|
|
94
|
-
this.scrollOffset = maxScroll;
|
|
95
|
-
this.autoScroll = true;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
render(width: number): string[] {
|
|
100
|
-
if (width < 6) return []; // too narrow for any meaningful rendering
|
|
101
|
-
const th = this.theme;
|
|
102
|
-
const innerW = width - 4; // border + padding
|
|
103
|
-
this.lastInnerW = innerW;
|
|
104
|
-
const lines: string[] = [];
|
|
105
|
-
|
|
106
|
-
const pad = (s: string, len: number) => {
|
|
107
|
-
const vis = visibleWidth(s);
|
|
108
|
-
return s + " ".repeat(Math.max(0, len - vis));
|
|
109
|
-
};
|
|
110
|
-
const row = (content: string) =>
|
|
111
|
-
th.fg("border", "│") + " " + truncateToWidth(pad(content, innerW), innerW) + " " + th.fg("border", "│");
|
|
112
|
-
const hrTop = th.fg("border", `╭${"─".repeat(width - 2)}╮`);
|
|
113
|
-
const hrBot = th.fg("border", `╰${"─".repeat(width - 2)}╯`);
|
|
114
|
-
const hrMid = row(th.fg("dim", "─".repeat(innerW)));
|
|
115
|
-
|
|
116
|
-
// Header
|
|
117
|
-
lines.push(hrTop);
|
|
118
|
-
const name = getDisplayName(this.record.type, this.registry);
|
|
119
|
-
const modeLabel = getPromptModeLabel(this.record.type, this.registry);
|
|
120
|
-
const modeTag = modeLabel ? ` ${th.fg("dim", `(${modeLabel})`)}` : "";
|
|
121
|
-
const statusIcon = this.record.status === "running"
|
|
122
|
-
? th.fg("accent", "●")
|
|
123
|
-
: this.record.status === "completed"
|
|
124
|
-
? th.fg("success", "✓")
|
|
125
|
-
: this.record.status === "error"
|
|
126
|
-
? th.fg("error", "✗")
|
|
127
|
-
: th.fg("dim", "○");
|
|
128
|
-
const duration = formatDuration(this.record.startedAt, this.record.completedAt);
|
|
129
|
-
|
|
130
|
-
const headerParts: string[] = [duration];
|
|
131
|
-
const toolUses = this.record.toolUses;
|
|
132
|
-
if (toolUses > 0) headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
|
|
133
|
-
const tokens = getLifetimeTotal(this.record.lifetimeUsage);
|
|
134
|
-
if (tokens > 0) {
|
|
135
|
-
const percent = this.record.getContextPercent();
|
|
136
|
-
headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
lines.push(row(
|
|
140
|
-
`${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${th.fg("dim", headerParts.join(" · "))}`,
|
|
141
|
-
));
|
|
142
|
-
const invocationLine = this.invocationLine();
|
|
143
|
-
if (invocationLine) lines.push(row(invocationLine));
|
|
144
|
-
lines.push(hrMid);
|
|
145
|
-
|
|
146
|
-
// Content area — rebuild every render (live data, no cache needed)
|
|
147
|
-
const contentLines = this.buildContentLines(innerW);
|
|
148
|
-
const viewportHeight = this.viewportHeight();
|
|
149
|
-
const maxScroll = Math.max(0, contentLines.length - viewportHeight);
|
|
150
|
-
|
|
151
|
-
if (this.autoScroll) {
|
|
152
|
-
this.scrollOffset = maxScroll;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const visibleStart = Math.min(this.scrollOffset, maxScroll);
|
|
156
|
-
const visible = contentLines.slice(visibleStart, visibleStart + viewportHeight);
|
|
157
|
-
|
|
158
|
-
for (let i = 0; i < viewportHeight; i++) {
|
|
159
|
-
lines.push(row(visible[i] ?? ""));
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Footer
|
|
163
|
-
lines.push(hrMid);
|
|
164
|
-
const scrollPct = contentLines.length <= viewportHeight
|
|
165
|
-
? "100%"
|
|
166
|
-
: `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
|
|
167
|
-
const footerLeft = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
|
|
168
|
-
const footerRight = th.fg("dim", "↑↓ scroll · PgUp/PgDn or Shift+↑↓ · Esc close");
|
|
169
|
-
const footerGap = Math.max(1, innerW - visibleWidth(footerLeft) - visibleWidth(footerRight));
|
|
170
|
-
lines.push(row(footerLeft + " ".repeat(footerGap) + footerRight));
|
|
171
|
-
lines.push(hrBot);
|
|
172
|
-
|
|
173
|
-
return lines;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// fallow-ignore-next-line unused-class-member
|
|
177
|
-
invalidate(): void { /* no cached state to clear */ }
|
|
178
|
-
|
|
179
|
-
// fallow-ignore-next-line unused-class-member
|
|
180
|
-
dispose(): void {
|
|
181
|
-
this.closed = true;
|
|
182
|
-
if (this.unsubscribe) {
|
|
183
|
-
this.unsubscribe();
|
|
184
|
-
this.unsubscribe = undefined;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ---- Private ----
|
|
189
|
-
|
|
190
|
-
private viewportHeight(): number {
|
|
191
|
-
// Cap mirrors the overlay's maxHeight — otherwise the viewer would render
|
|
192
|
-
// more lines than the overlay shows and clip the footer.
|
|
193
|
-
const maxRows = Math.floor((this.tui.terminal.rows * VIEWPORT_HEIGHT_PCT) / 100);
|
|
194
|
-
return Math.max(MIN_VIEWPORT, maxRows - this.chromeLines());
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
private chromeLines(): number {
|
|
198
|
-
return CHROME_LINES_BASE + (this.invocationLine() ? 1 : 0);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private invocationLine(): string | undefined {
|
|
202
|
-
const { modelName, tags } = buildInvocationTags(this.record.invocation);
|
|
203
|
-
const parts = modelName ? [modelName, ...tags] : tags;
|
|
204
|
-
if (parts.length === 0) return undefined;
|
|
205
|
-
return this.theme.fg("dim", ` ↳ ${parts.join(" · ")}`);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
private buildContentLines(width: number): string[] {
|
|
209
|
-
if (width <= 0) return [];
|
|
210
|
-
|
|
211
|
-
const th = this.theme;
|
|
212
|
-
const ctx = { theme: th, wrapText: this.wrapText };
|
|
213
|
-
const messages = this.record.messages;
|
|
214
|
-
|
|
215
|
-
if (messages.length === 0) {
|
|
216
|
-
return [th.fg("dim", "(waiting for first message...)")];
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const lines: string[] = [];
|
|
220
|
-
let needsSeparator = false;
|
|
221
|
-
for (const msg of messages) {
|
|
222
|
-
const formatted = formatMessage(msg as { role: string; [key: string]: unknown }, width, ctx);
|
|
223
|
-
if (!formatted) continue;
|
|
224
|
-
if (needsSeparator) lines.push(th.fg("dim", "───"));
|
|
225
|
-
lines.push(...formatted);
|
|
226
|
-
needsSeparator = true;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Streaming indicator for running agents — read activity off the record
|
|
230
|
-
if (this.record.status === "running") {
|
|
231
|
-
lines.push(...formatStreamingIndicator(
|
|
232
|
-
this.record.activeTools,
|
|
233
|
-
this.record.responseText,
|
|
234
|
-
width,
|
|
235
|
-
th,
|
|
236
|
-
));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return lines.map(l => truncateToWidth(l, width));
|
|
240
|
-
}
|
|
241
|
-
}
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* message-formatters.ts — Pure formatting functions for each session message type.
|
|
3
|
-
*
|
|
4
|
-
* Each function converts a single message or content block into display lines.
|
|
5
|
-
* Returns null for empty/skippable content (caller skips the separator).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
|
-
import { extractAssistantContent } from "#src/session/content-items";
|
|
10
|
-
import { extractText } from "#src/session/context";
|
|
11
|
-
import type { Theme } from "#src/ui/display";
|
|
12
|
-
import { describeActivity } from "#src/ui/display";
|
|
13
|
-
|
|
14
|
-
// ── Types ────────────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
/** Narrow context shared by all message formatters. */
|
|
17
|
-
export interface FormatterContext {
|
|
18
|
-
theme: Theme;
|
|
19
|
-
wrapText: (text: string, width: number) => string[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// ── File-local types and guards ─────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
/** Bash execution message — 'bashExecution' role is not in the SDK's AgentSession message role union. */
|
|
25
|
-
export interface BashExecutionMessage {
|
|
26
|
-
role: "bashExecution";
|
|
27
|
-
command: string;
|
|
28
|
-
output?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Type guard for bash execution messages. */
|
|
32
|
-
export function isBashExecution(msg: { role: string }): msg is BashExecutionMessage {
|
|
33
|
-
return msg.role === "bashExecution";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ── formatUserMessage ─────────────────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Format a user message into display lines.
|
|
40
|
-
* Returns null when the message text is empty (caller should skip separator).
|
|
41
|
-
*/
|
|
42
|
-
export function formatUserMessage(
|
|
43
|
-
content: string | unknown[],
|
|
44
|
-
width: number,
|
|
45
|
-
ctx: FormatterContext,
|
|
46
|
-
): string[] | null {
|
|
47
|
-
const { theme, wrapText } = ctx;
|
|
48
|
-
const text = typeof content === "string" ? content : extractText(content);
|
|
49
|
-
if (!text.trim()) return null;
|
|
50
|
-
return [
|
|
51
|
-
theme.fg("accent", "[User]"),
|
|
52
|
-
...wrapText(text.trim(), width),
|
|
53
|
-
];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ── formatBashExecution ───────────────────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Format a bash execution message into display lines.
|
|
60
|
-
* Always returns at least the command line.
|
|
61
|
-
*/
|
|
62
|
-
export function formatBashExecution(
|
|
63
|
-
msg: BashExecutionMessage,
|
|
64
|
-
width: number,
|
|
65
|
-
ctx: FormatterContext,
|
|
66
|
-
): string[] {
|
|
67
|
-
const { theme, wrapText } = ctx;
|
|
68
|
-
const lines: string[] = [
|
|
69
|
-
truncateToWidth(theme.fg("muted", ` $ ${msg.command}`), width),
|
|
70
|
-
];
|
|
71
|
-
const output = msg.output ?? "";
|
|
72
|
-
if (output.trim()) {
|
|
73
|
-
const out = output.length > 500 ? output.slice(0, 500) + "... (truncated)" : output;
|
|
74
|
-
lines.push(...wrapText(out.trim(), width).map(l => theme.fg("dim", l)));
|
|
75
|
-
}
|
|
76
|
-
return lines;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ── formatStreamingIndicator ──────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Format the streaming activity indicator for a running agent.
|
|
83
|
-
* Returns exactly two lines: an empty spacer and the indicator line.
|
|
84
|
-
*/
|
|
85
|
-
export function formatStreamingIndicator(
|
|
86
|
-
activeTools: ReadonlyMap<string, string>,
|
|
87
|
-
responseText: string | undefined,
|
|
88
|
-
width: number,
|
|
89
|
-
theme: Theme,
|
|
90
|
-
): string[] {
|
|
91
|
-
const act = describeActivity(activeTools, responseText);
|
|
92
|
-
return [
|
|
93
|
-
"",
|
|
94
|
-
truncateToWidth(theme.fg("accent", "\u25cd ") + theme.fg("dim", act), width),
|
|
95
|
-
];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ── formatToolResult ────────────────────────────────────────────────────────────
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Format a tool result message into display lines.
|
|
102
|
-
* Returns null when the result text is empty (caller should skip separator).
|
|
103
|
-
*/
|
|
104
|
-
export function formatToolResult(
|
|
105
|
-
content: unknown[],
|
|
106
|
-
width: number,
|
|
107
|
-
ctx: FormatterContext,
|
|
108
|
-
): string[] | null {
|
|
109
|
-
const { theme, wrapText } = ctx;
|
|
110
|
-
const text = extractText(content);
|
|
111
|
-
const truncated = text.length > 500 ? text.slice(0, 500) + "... (truncated)" : text;
|
|
112
|
-
if (!truncated.trim()) return null;
|
|
113
|
-
return [
|
|
114
|
-
theme.fg("dim", "[Result]"),
|
|
115
|
-
...wrapText(truncated.trim(), width).map(l => theme.fg("dim", l)),
|
|
116
|
-
];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
120
|
-
|
|
121
|
-
/** Model/provider attribution for assistant messages. */
|
|
122
|
-
export interface MessageAttribution {
|
|
123
|
-
provider?: string;
|
|
124
|
-
model?: string;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ── formatAssistantMessage ───────────────────────────────────────────────────
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Format an assistant message into display lines.
|
|
131
|
-
* Always returns at least the [Assistant] header line.
|
|
132
|
-
* When attribution is provided, renders as `[Assistant (provider/model)]`.
|
|
133
|
-
*/
|
|
134
|
-
export function formatAssistantMessage(
|
|
135
|
-
content: { type: string; [key: string]: unknown }[],
|
|
136
|
-
width: number,
|
|
137
|
-
ctx: FormatterContext,
|
|
138
|
-
attribution?: MessageAttribution,
|
|
139
|
-
): string[] {
|
|
140
|
-
const { theme, wrapText } = ctx;
|
|
141
|
-
const { textParts, toolNames } = extractAssistantContent(content);
|
|
142
|
-
const label = formatAttributionLabel(attribution);
|
|
143
|
-
const lines: string[] = [theme.bold(`[Assistant${label}]`)];
|
|
144
|
-
if (textParts.length > 0) {
|
|
145
|
-
lines.push(...wrapText(textParts.join("\n").trim(), width));
|
|
146
|
-
}
|
|
147
|
-
for (const name of toolNames) {
|
|
148
|
-
lines.push(truncateToWidth(theme.fg("muted", ` [Tool: ${name}]`), width));
|
|
149
|
-
}
|
|
150
|
-
return lines;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ── formatMessage dispatcher ───────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Dispatch a session message to the appropriate formatter.
|
|
157
|
-
* Returns null for empty/skippable messages and unknown roles.
|
|
158
|
-
*/
|
|
159
|
-
export function formatMessage(
|
|
160
|
-
msg: { role: string; [key: string]: unknown },
|
|
161
|
-
width: number,
|
|
162
|
-
ctx: FormatterContext,
|
|
163
|
-
): string[] | null {
|
|
164
|
-
if (msg.role === "user") {
|
|
165
|
-
return formatUserMessage(msg.content as string | unknown[], width, ctx);
|
|
166
|
-
}
|
|
167
|
-
if (msg.role === "assistant") {
|
|
168
|
-
const attribution: MessageAttribution = {
|
|
169
|
-
provider: msg.provider as string | undefined,
|
|
170
|
-
model: msg.model as string | undefined,
|
|
171
|
-
};
|
|
172
|
-
return formatAssistantMessage(
|
|
173
|
-
msg.content as { type: string; [key: string]: unknown }[],
|
|
174
|
-
width,
|
|
175
|
-
ctx,
|
|
176
|
-
attribution,
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
if (msg.role === "toolResult") {
|
|
180
|
-
return formatToolResult(msg.content as unknown[], width, ctx);
|
|
181
|
-
}
|
|
182
|
-
if (isBashExecution(msg)) {
|
|
183
|
-
return formatBashExecution(msg, width, ctx);
|
|
184
|
-
}
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
189
|
-
|
|
190
|
-
/** Build a `(provider/model)` attribution suffix, or empty string when absent. */
|
|
191
|
-
function formatAttributionLabel(attr?: MessageAttribution): string {
|
|
192
|
-
if (!attr?.provider && !attr?.model) return "";
|
|
193
|
-
if (attr.provider && attr.model) return ` (${attr.provider}/${attr.model})`;
|
|
194
|
-
return ` (${attr.provider ?? attr.model})`;
|
|
195
|
-
}
|