@gotgenes/pi-subagents 17.5.0 → 18.0.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.
@@ -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
- }