@oh-my-pi/pi-coding-agent 16.0.10 → 16.1.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 +57 -0
- package/dist/cli.js +3344 -3371
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/transcript-recorder.d.ts +52 -0
- package/dist/types/commit/agentic/agent.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +14 -8
- package/dist/types/edit/file-snapshot-store.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +7 -0
- package/dist/types/modes/components/__tests__/skill-message.test.d.ts +1 -0
- package/dist/types/modes/components/agent-hub.d.ts +6 -1
- package/dist/types/modes/components/agent-transcript-viewer.d.ts +39 -0
- package/dist/types/modes/components/assistant-message.d.ts +8 -0
- package/dist/types/modes/components/cache-invalidation-marker.d.ts +34 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +42 -0
- package/dist/types/modes/components/compaction-summary-message.d.ts +14 -1
- package/dist/types/modes/components/index.d.ts +0 -1
- package/dist/types/modes/components/message-frame.d.ts +6 -4
- package/dist/types/modes/controllers/command-controller.d.ts +3 -2
- package/dist/types/modes/interactive-mode.d.ts +4 -2
- package/dist/types/modes/theme/theme.d.ts +7 -1
- package/dist/types/modes/types.d.ts +9 -2
- package/dist/types/registry/agent-registry.d.ts +10 -3
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +20 -1
- package/dist/types/session/compact-modes.d.ts +60 -0
- package/dist/types/session/session-context.d.ts +7 -0
- package/dist/types/session/session-dump-format.d.ts +1 -0
- package/dist/types/session/streaming-output.d.ts +0 -2
- package/dist/types/session/tool-choice-queue.d.ts +14 -0
- package/dist/types/system-prompt.d.ts +3 -3
- package/dist/types/tools/__tests__/json-tree.test.d.ts +1 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/resolve.d.ts +15 -5
- package/package.json +12 -12
- package/src/advisor/index.ts +1 -0
- package/src/advisor/transcript-recorder.ts +136 -0
- package/src/cli/stats-cli.ts +2 -11
- package/src/collab/host.ts +25 -13
- package/src/commit/agentic/agent.ts +2 -1
- package/src/commit/agentic/tools/git-file-diff.ts +2 -2
- package/src/commit/changelog/index.ts +1 -1
- package/src/commit/map-reduce/map-phase.ts +1 -1
- package/src/commit/map-reduce/utils.ts +1 -1
- package/src/config/settings-schema.ts +16 -9
- package/src/config/settings.ts +0 -6
- package/src/debug/log-viewer.ts +4 -4
- package/src/debug/raw-sse.ts +4 -4
- package/src/edit/file-snapshot-store.ts +1 -1
- package/src/edit/renderer.ts +9 -9
- package/src/eval/js/tool-bridge.ts +3 -2
- package/src/eval/py/prelude.py +3 -2
- package/src/export/html/tool-views.generated.js +28 -28
- package/src/extensibility/extensions/types.ts +7 -0
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/history-protocol.ts +8 -3
- package/src/irc/bus.ts +8 -0
- package/src/lsp/index.ts +2 -2
- package/src/lsp/render.ts +7 -7
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +63 -0
- package/src/modes/components/__tests__/skill-message.test.ts +92 -0
- package/src/modes/components/agent-dashboard.ts +1 -1
- package/src/modes/components/agent-hub.ts +97 -920
- package/src/modes/components/agent-transcript-viewer.ts +461 -0
- package/src/modes/components/assistant-message.ts +21 -0
- package/src/modes/components/cache-invalidation-marker.ts +84 -0
- package/src/modes/components/chat-transcript-builder.ts +476 -0
- package/src/modes/components/compaction-summary-message.ts +29 -1
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/diff.ts +12 -35
- package/src/modes/components/dynamic-border.ts +1 -1
- package/src/modes/components/extensions/extension-dashboard.ts +1 -1
- package/src/modes/components/extensions/inspector-panel.ts +5 -5
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/index.ts +0 -1
- package/src/modes/components/message-frame.ts +10 -6
- package/src/modes/components/model-selector.ts +2 -2
- package/src/modes/components/overlay-box.ts +10 -9
- package/src/modes/components/skill-message.ts +39 -19
- package/src/modes/components/tiny-title-download-progress.ts +1 -1
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/command-controller.ts +12 -2
- package/src/modes/controllers/event-controller.ts +15 -1
- package/src/modes/controllers/input-controller.ts +8 -1
- package/src/modes/controllers/selector-controller.ts +11 -1
- package/src/modes/interactive-mode.ts +13 -3
- package/src/modes/theme/theme.ts +14 -0
- package/src/modes/types.ts +9 -2
- package/src/modes/utils/ui-helpers.ts +20 -2
- package/src/prompts/steering/user-interjection.md +3 -4
- package/src/prompts/tools/read.md +1 -1
- package/src/registry/agent-registry.ts +13 -4
- package/src/sdk.ts +9 -7
- package/src/session/agent-session.ts +182 -16
- package/src/session/compact-modes.ts +105 -0
- package/src/session/messages.ts +7 -9
- package/src/session/session-context.ts +54 -7
- package/src/session/session-dump-format.ts +4 -2
- package/src/session/session-history-format.ts +1 -1
- package/src/session/snapcompact-inline.ts +2 -2
- package/src/session/streaming-output.ts +5 -5
- package/src/session/tool-choice-queue.ts +59 -0
- package/src/slash-commands/builtin-registry.ts +16 -4
- package/src/system-prompt.ts +10 -9
- package/src/task/executor.ts +1 -1
- package/src/task/output-manager.ts +5 -0
- package/src/tools/__tests__/json-tree.test.ts +35 -0
- package/src/tools/approval.ts +1 -1
- package/src/tools/bash-interactive.ts +4 -4
- package/src/tools/bash.ts +0 -1
- package/src/tools/browser.ts +0 -1
- package/src/tools/eval.ts +1 -1
- package/src/tools/gh.ts +1 -1
- package/src/tools/index.ts +4 -0
- package/src/tools/irc.ts +1 -1
- package/src/tools/json-tree.ts +22 -5
- package/src/tools/read.ts +5 -6
- package/src/tools/resolve.ts +66 -41
- package/src/tui/output-block.ts +9 -9
- package/src/web/scrapers/firefox-addons.ts +1 -1
- package/src/web/scrapers/github.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +2 -2
- package/src/web/scrapers/metacpan.ts +2 -2
- package/src/web/scrapers/nvd.ts +2 -2
- package/src/web/scrapers/ollama.ts +1 -1
- package/src/web/scrapers/opencorporates.ts +1 -1
- package/src/web/scrapers/pub-dev.ts +1 -1
- package/src/web/scrapers/repology.ts +1 -1
- package/src/web/scrapers/sourcegraph.ts +1 -1
- package/src/web/scrapers/terraform.ts +6 -6
- package/src/web/scrapers/wikidata.ts +2 -2
- package/src/workspace-tree.ts +1 -1
- package/dist/types/modes/components/branch-summary-message.d.ts +0 -13
- package/src/modes/components/branch-summary-message.ts +0 -46
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds transcript components from persisted session message entries — the
|
|
3
|
+
* file/remote-backed counterpart to {@link UiHelpers.addMessageToChat} (which is
|
|
4
|
+
* bound to the live InteractiveModeContext). Used by the fullscreen transcript
|
|
5
|
+
* viewer ({@link AgentTranscriptViewer}) to render a parked subagent / advisor /
|
|
6
|
+
* collab-guest transcript that has no live session.
|
|
7
|
+
*
|
|
8
|
+
* Unlike the old incremental hub sync, {@link ChatTranscriptBuilder.rebuild}
|
|
9
|
+
* always discards prior components and rebuilds the whole transcript from the
|
|
10
|
+
* supplied entries. Re-rendering a growing transcript is therefore O(n) in the
|
|
11
|
+
* entry count, but it cannot duplicate or misorder rows the way incremental
|
|
12
|
+
* component reuse could.
|
|
13
|
+
*/
|
|
14
|
+
import type { AgentMessage, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
15
|
+
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
16
|
+
import { Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
17
|
+
import { formatBytes, formatDuration } from "@oh-my-pi/pi-utils";
|
|
18
|
+
import type { AdvisorMessageDetails } from "../../advisor";
|
|
19
|
+
import { COLLAB_PROMPT_MESSAGE_TYPE, type CollabPromptDetails } from "../../collab/protocol";
|
|
20
|
+
import { settings } from "../../config/settings";
|
|
21
|
+
import type { MessageRenderer } from "../../extensibility/extensions/types";
|
|
22
|
+
import {
|
|
23
|
+
BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE,
|
|
24
|
+
type CustomMessage,
|
|
25
|
+
isSilentAbort,
|
|
26
|
+
LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE,
|
|
27
|
+
resolveAbortLabel,
|
|
28
|
+
SKILL_PROMPT_MESSAGE_TYPE,
|
|
29
|
+
type SkillPromptDetails,
|
|
30
|
+
} from "../../session/messages";
|
|
31
|
+
import type { SessionMessageEntry } from "../../session/session-entries";
|
|
32
|
+
import { createIrcMessageCard } from "../../tools/irc";
|
|
33
|
+
import { canonicalizeMessage } from "../../utils/thinking-display";
|
|
34
|
+
import { theme } from "../theme/theme";
|
|
35
|
+
import { createAdvisorMessageCard } from "./advisor-message";
|
|
36
|
+
import { AssistantMessageComponent } from "./assistant-message";
|
|
37
|
+
import { createBackgroundTanDispatchBlock } from "./background-tan-message";
|
|
38
|
+
import { BashExecutionComponent } from "./bash-execution";
|
|
39
|
+
import { detectCacheInvalidation } from "./cache-invalidation-marker";
|
|
40
|
+
import { CollabPromptMessageComponent } from "./collab-prompt-message";
|
|
41
|
+
import {
|
|
42
|
+
BranchSummaryMessageComponent,
|
|
43
|
+
CompactionSummaryMessageComponent,
|
|
44
|
+
createHandoffSummaryMessageComponent,
|
|
45
|
+
} from "./compaction-summary-message";
|
|
46
|
+
import { CustomMessageComponent } from "./custom-message";
|
|
47
|
+
import { EvalExecutionComponent } from "./eval-execution";
|
|
48
|
+
import { type LateDiagnosticsFile, LateDiagnosticsMessageComponent } from "./late-diagnostics-message";
|
|
49
|
+
import { ReadToolGroupComponent, readArgsHaveTarget, readArgsTargetInternalUrl } from "./read-tool-group";
|
|
50
|
+
import { SkillMessageComponent } from "./skill-message";
|
|
51
|
+
import { ToolExecutionComponent } from "./tool-execution";
|
|
52
|
+
import { TranscriptBlock, TranscriptContainer } from "./transcript-container";
|
|
53
|
+
import { createUsageRowBlock } from "./usage-row";
|
|
54
|
+
import { UserMessageComponent } from "./user-message";
|
|
55
|
+
|
|
56
|
+
export interface ChatTranscriptBuilderDeps {
|
|
57
|
+
ui: TUI;
|
|
58
|
+
getTool?: (name: string) => AgentTool | undefined;
|
|
59
|
+
getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
|
|
60
|
+
cwd: string;
|
|
61
|
+
hideThinkingBlock?: () => boolean;
|
|
62
|
+
requestRender: () => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Extracts the plain-text content of a user message (string or text blocks). */
|
|
66
|
+
function userMessageText(message: Extract<AgentMessage, { role: "user" }>): string {
|
|
67
|
+
if (typeof message.content === "string") return message.content;
|
|
68
|
+
return message.content
|
|
69
|
+
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
|
70
|
+
.map(block => block.text)
|
|
71
|
+
.join("");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class ChatTranscriptBuilder {
|
|
75
|
+
readonly container = new TranscriptContainer();
|
|
76
|
+
#pendingTools = new Map<string, ToolExecutionComponent | ReadToolGroupComponent>();
|
|
77
|
+
#readArgs = new Map<string, Record<string, unknown>>();
|
|
78
|
+
#readGroup: ReadToolGroupComponent | null = null;
|
|
79
|
+
#pendingUsage: Usage | undefined;
|
|
80
|
+
#lastAssistantUsage: Usage | undefined;
|
|
81
|
+
#waitingPoll: ToolExecutionComponent | null = null;
|
|
82
|
+
#expandables: Array<{ setExpanded(expanded: boolean): void }> = [];
|
|
83
|
+
#expanded = false;
|
|
84
|
+
|
|
85
|
+
constructor(private readonly deps: ChatTranscriptBuilderDeps) {}
|
|
86
|
+
|
|
87
|
+
/** Whether the transcript currently holds any rendered rows. */
|
|
88
|
+
get isEmpty(): boolean {
|
|
89
|
+
return this.container.children.length === 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Discard all components and rebuild the whole transcript from `entries`. */
|
|
93
|
+
rebuild(entries: SessionMessageEntry[]): void {
|
|
94
|
+
this.reset();
|
|
95
|
+
for (const entry of entries) this.#appendChatMessage(entry.message);
|
|
96
|
+
// Flush the trailing turn's usage row only once its tools are materialized
|
|
97
|
+
// (a read whose result has not arrived stays pending); otherwise the row
|
|
98
|
+
// would sit above its tools. The drain happens here at the end of the pass.
|
|
99
|
+
if (this.#readArgs.size === 0 && this.#pendingTools.size === 0) this.#flushPendingUsage();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Toggle tool-output expansion across every expandable component. */
|
|
103
|
+
setExpanded(expanded: boolean): void {
|
|
104
|
+
this.#expanded = expanded;
|
|
105
|
+
for (const component of this.#expandables) component.setExpanded(expanded);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get expanded(): boolean {
|
|
109
|
+
return this.#expanded;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Tear down components (sealing pending spinners) and clear build state. */
|
|
113
|
+
reset(): void {
|
|
114
|
+
for (const pending of this.#pendingTools.values()) pending.seal();
|
|
115
|
+
this.#pendingTools.clear();
|
|
116
|
+
this.#readArgs.clear();
|
|
117
|
+
this.#readGroup = null;
|
|
118
|
+
this.#pendingUsage = undefined;
|
|
119
|
+
this.#lastAssistantUsage = undefined;
|
|
120
|
+
this.#waitingPoll = null;
|
|
121
|
+
this.#expandables = [];
|
|
122
|
+
this.container.dispose();
|
|
123
|
+
this.container.clear();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
dispose(): void {
|
|
127
|
+
this.reset();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#trackExpandable(component: { setExpanded(expanded: boolean): void }): void {
|
|
131
|
+
component.setExpanded(this.#expanded);
|
|
132
|
+
this.#expandables.push(component);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A `job` poll showing all-running is displaced by the next `job` call. */
|
|
136
|
+
#resolveWaitingPoll(nextToolName?: string): void {
|
|
137
|
+
const previous = this.#waitingPoll;
|
|
138
|
+
if (!previous) return;
|
|
139
|
+
this.#waitingPoll = null;
|
|
140
|
+
if (nextToolName === "job" && previous.isDisplaceableBlock()) {
|
|
141
|
+
this.container.removeChild(previous);
|
|
142
|
+
}
|
|
143
|
+
previous.seal();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#ensureReadGroup(): ReadToolGroupComponent {
|
|
147
|
+
if (!this.#readGroup) {
|
|
148
|
+
this.#readGroup = new ReadToolGroupComponent({
|
|
149
|
+
showContentPreview: settings.get("read.toolResultPreview"),
|
|
150
|
+
});
|
|
151
|
+
this.#trackExpandable(this.#readGroup);
|
|
152
|
+
this.container.addChild(this.#readGroup);
|
|
153
|
+
}
|
|
154
|
+
return this.#readGroup;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The per-turn token-usage row must land below the turn's tool blocks, but
|
|
158
|
+
// normal `read` calls only materialize their group in #appendToolResult. Defer
|
|
159
|
+
// the row: stash it on the assistant message and flush once the turn's tools
|
|
160
|
+
// are placed, sealing the read run so the row sits under it.
|
|
161
|
+
#flushPendingUsage(): void {
|
|
162
|
+
if (!this.#pendingUsage) return;
|
|
163
|
+
this.#readGroup?.seal();
|
|
164
|
+
this.#readGroup = null;
|
|
165
|
+
this.container.addChild(createUsageRowBlock(this.#pendingUsage));
|
|
166
|
+
this.#pendingUsage = undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#appendChatMessage(message: AgentMessage): void {
|
|
170
|
+
if (message.role !== "toolResult") this.#flushPendingUsage();
|
|
171
|
+
switch (message.role) {
|
|
172
|
+
case "assistant":
|
|
173
|
+
this.#appendAssistantMessage(message);
|
|
174
|
+
break;
|
|
175
|
+
case "toolResult":
|
|
176
|
+
this.#appendToolResult(message);
|
|
177
|
+
break;
|
|
178
|
+
case "user":
|
|
179
|
+
case "developer": {
|
|
180
|
+
// A user prompt closes the poll-displacement window, same as the live path.
|
|
181
|
+
if (message.role === "user") this.#resolveWaitingPoll();
|
|
182
|
+
const textContent = message.role === "user" ? userMessageText(message) : "";
|
|
183
|
+
if (textContent) {
|
|
184
|
+
const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
|
|
185
|
+
this.container.addChild(new UserMessageComponent(textContent, isSynthetic));
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case "bashExecution": {
|
|
190
|
+
const component = new BashExecutionComponent(message.command, this.deps.ui, message.excludeFromContext);
|
|
191
|
+
if (message.output) component.appendOutput(message.output);
|
|
192
|
+
component.setComplete(message.exitCode, message.cancelled, { truncation: message.meta?.truncation });
|
|
193
|
+
this.container.addChild(component);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case "pythonExecution": {
|
|
197
|
+
const component = new EvalExecutionComponent(message.code, this.deps.ui, message.excludeFromContext);
|
|
198
|
+
if (message.output) component.appendOutput(message.output);
|
|
199
|
+
component.setComplete(message.exitCode, message.cancelled, { truncation: message.meta?.truncation });
|
|
200
|
+
this.container.addChild(component);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case "hookMessage":
|
|
204
|
+
case "custom":
|
|
205
|
+
this.#appendCustomMessage(message);
|
|
206
|
+
break;
|
|
207
|
+
case "compactionSummary": {
|
|
208
|
+
const component = new CompactionSummaryMessageComponent(message);
|
|
209
|
+
this.#trackExpandable(component);
|
|
210
|
+
this.container.addChild(component);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case "branchSummary": {
|
|
214
|
+
const component = new BranchSummaryMessageComponent(message);
|
|
215
|
+
this.#trackExpandable(component);
|
|
216
|
+
this.container.addChild(component);
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
case "fileMention": {
|
|
220
|
+
const block = new TranscriptBlock();
|
|
221
|
+
for (const file of message.files) {
|
|
222
|
+
let suffix: string;
|
|
223
|
+
if (file.skippedReason === "tooLarge") {
|
|
224
|
+
const size = typeof file.byteSize === "number" ? formatBytes(file.byteSize) : "unknown size";
|
|
225
|
+
suffix = `(skipped: ${size})`;
|
|
226
|
+
} else {
|
|
227
|
+
suffix = file.image
|
|
228
|
+
? "(image)"
|
|
229
|
+
: file.lineCount === undefined
|
|
230
|
+
? "(unknown lines)"
|
|
231
|
+
: `(${file.lineCount} lines)`;
|
|
232
|
+
}
|
|
233
|
+
const text = `${theme.fg("dim", `${theme.tree.last} `)}${theme.fg("muted", "Read")} ${theme.fg(
|
|
234
|
+
"accent",
|
|
235
|
+
file.path,
|
|
236
|
+
)} ${theme.fg("dim", suffix)}`;
|
|
237
|
+
// Indent one column to match the transcript's other rows (the viewer renders
|
|
238
|
+
// body rows without an outer gutter; rows own their left pad).
|
|
239
|
+
block.addChild(new Text(text, 1, 0));
|
|
240
|
+
}
|
|
241
|
+
if (block.children.length > 0) this.container.addChild(block);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
default:
|
|
245
|
+
message satisfies never;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#appendAssistantMessage(message: Extract<AgentMessage, { role: "assistant" }>): void {
|
|
250
|
+
const assistantComponent = new AssistantMessageComponent(message, this.deps.hideThinkingBlock?.() ?? false, () =>
|
|
251
|
+
this.deps.requestRender(),
|
|
252
|
+
);
|
|
253
|
+
this.container.addChild(assistantComponent);
|
|
254
|
+
|
|
255
|
+
if (settings.get("display.cacheMissMarker")) {
|
|
256
|
+
const invalidation = detectCacheInvalidation(this.#lastAssistantUsage, message.usage);
|
|
257
|
+
if (invalidation) assistantComponent.setCacheInvalidation(invalidation);
|
|
258
|
+
}
|
|
259
|
+
if (message.usage.cacheRead + message.usage.cacheWrite + message.usage.input > 0) {
|
|
260
|
+
this.#lastAssistantUsage = message.usage;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const hasVisibleAssistantContent = message.content.some(
|
|
264
|
+
content =>
|
|
265
|
+
(content.type === "text" && canonicalizeMessage(content.text)) ||
|
|
266
|
+
(content.type === "thinking" && canonicalizeMessage(content.thinking)),
|
|
267
|
+
);
|
|
268
|
+
if (hasVisibleAssistantContent) {
|
|
269
|
+
// New visible turn content closes the current read run (mirrors rebuild).
|
|
270
|
+
this.#readGroup?.seal();
|
|
271
|
+
this.#readGroup = null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const isAbortedSilently = message.stopReason === "aborted" && isSilentAbort(message.errorMessage);
|
|
275
|
+
const hasErrorStop = !isAbortedSilently && (message.stopReason === "aborted" || message.stopReason === "error");
|
|
276
|
+
const errorMessage = hasErrorStop
|
|
277
|
+
? message.stopReason === "aborted"
|
|
278
|
+
? resolveAbortLabel(message.errorMessage)
|
|
279
|
+
: message.errorMessage || "Error"
|
|
280
|
+
: null;
|
|
281
|
+
|
|
282
|
+
for (const content of message.content) {
|
|
283
|
+
if (content.type !== "toolCall") continue;
|
|
284
|
+
this.#resolveWaitingPoll(content.name);
|
|
285
|
+
|
|
286
|
+
if (
|
|
287
|
+
content.name === "read" &&
|
|
288
|
+
readArgsHaveTarget(content.arguments) &&
|
|
289
|
+
!readArgsTargetInternalUrl(content.arguments)
|
|
290
|
+
) {
|
|
291
|
+
if (hasErrorStop && errorMessage) {
|
|
292
|
+
const group = this.#ensureReadGroup();
|
|
293
|
+
group.updateArgs(content.arguments, content.id);
|
|
294
|
+
group.updateResult(
|
|
295
|
+
{ content: [{ type: "text", text: errorMessage }], isError: true },
|
|
296
|
+
false,
|
|
297
|
+
content.id,
|
|
298
|
+
);
|
|
299
|
+
} else {
|
|
300
|
+
const normalizedArgs =
|
|
301
|
+
content.arguments && typeof content.arguments === "object" && !Array.isArray(content.arguments)
|
|
302
|
+
? (content.arguments as Record<string, unknown>)
|
|
303
|
+
: {};
|
|
304
|
+
this.#readArgs.set(content.id, normalizedArgs);
|
|
305
|
+
}
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
this.#readGroup?.seal();
|
|
310
|
+
this.#readGroup = null;
|
|
311
|
+
const component = new ToolExecutionComponent(
|
|
312
|
+
content.name,
|
|
313
|
+
content.arguments,
|
|
314
|
+
{
|
|
315
|
+
// Images can't be sliced through the scroll viewport; keep them off.
|
|
316
|
+
showImages: false,
|
|
317
|
+
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
318
|
+
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
319
|
+
liveRegion: this.container,
|
|
320
|
+
},
|
|
321
|
+
this.deps.getTool?.(content.name),
|
|
322
|
+
this.deps.ui,
|
|
323
|
+
this.deps.cwd,
|
|
324
|
+
content.id,
|
|
325
|
+
);
|
|
326
|
+
this.#trackExpandable(component);
|
|
327
|
+
this.container.addChild(component);
|
|
328
|
+
|
|
329
|
+
if (hasErrorStop && errorMessage) {
|
|
330
|
+
component.updateResult(
|
|
331
|
+
{ content: [{ type: "text", text: errorMessage }], isError: true },
|
|
332
|
+
false,
|
|
333
|
+
content.id,
|
|
334
|
+
);
|
|
335
|
+
} else {
|
|
336
|
+
this.#pendingTools.set(content.id, component);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this.#pendingUsage = settings.get("display.showTokenUsage") ? message.usage : undefined;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#appendToolResult(message: Extract<AgentMessage, { role: "toolResult" }>): void {
|
|
344
|
+
const pending = this.#pendingTools.get(message.toolCallId);
|
|
345
|
+
const isReadGroupResult = message.toolName === "read" && (!pending || pending instanceof ReadToolGroupComponent);
|
|
346
|
+
if (isReadGroupResult) {
|
|
347
|
+
let component = pending;
|
|
348
|
+
if (!component) {
|
|
349
|
+
const group = this.#ensureReadGroup();
|
|
350
|
+
const args = this.#readArgs.get(message.toolCallId);
|
|
351
|
+
if (args) group.updateArgs(args, message.toolCallId);
|
|
352
|
+
component = group;
|
|
353
|
+
}
|
|
354
|
+
component.updateResult(message, false, message.toolCallId);
|
|
355
|
+
this.#pendingTools.delete(message.toolCallId);
|
|
356
|
+
this.#readArgs.delete(message.toolCallId);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!pending) return;
|
|
360
|
+
pending.updateResult(message, false, message.toolCallId);
|
|
361
|
+
this.#pendingTools.delete(message.toolCallId);
|
|
362
|
+
if (message.toolName === "job" && pending instanceof ToolExecutionComponent && pending.isDisplaceableBlock()) {
|
|
363
|
+
this.#waitingPoll = pending;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#appendCustomMessage(message: Extract<AgentMessage, { role: "custom" | "hookMessage" }>): void {
|
|
368
|
+
if (!message.display) return;
|
|
369
|
+
if (message.customType === "async-result") {
|
|
370
|
+
const details = (
|
|
371
|
+
message as CustomMessage<{
|
|
372
|
+
jobId?: string;
|
|
373
|
+
type?: "bash" | "task";
|
|
374
|
+
label?: string;
|
|
375
|
+
durationMs?: number;
|
|
376
|
+
jobs?: Array<{ jobId?: string; type?: "bash" | "task"; label?: string; durationMs?: number }>;
|
|
377
|
+
}>
|
|
378
|
+
).details;
|
|
379
|
+
const jobs =
|
|
380
|
+
details?.jobs && details.jobs.length > 0
|
|
381
|
+
? details.jobs
|
|
382
|
+
: [
|
|
383
|
+
{
|
|
384
|
+
jobId: details?.jobId,
|
|
385
|
+
type: details?.type,
|
|
386
|
+
label: details?.label,
|
|
387
|
+
durationMs: details?.durationMs,
|
|
388
|
+
},
|
|
389
|
+
];
|
|
390
|
+
const block = new TranscriptBlock();
|
|
391
|
+
for (const job of jobs) {
|
|
392
|
+
const jobId = job.jobId ?? "unknown";
|
|
393
|
+
const typeLabel = job.type ? `[${job.type}]` : "[job]";
|
|
394
|
+
const duration = typeof job.durationMs === "number" ? formatDuration(job.durationMs) : undefined;
|
|
395
|
+
const line = [
|
|
396
|
+
theme.fg("success", `${theme.status.done} Background job completed`),
|
|
397
|
+
theme.fg("dim", typeLabel),
|
|
398
|
+
theme.fg("accent", jobId),
|
|
399
|
+
duration ? theme.fg("dim", `(${duration})`) : undefined,
|
|
400
|
+
]
|
|
401
|
+
.filter(Boolean)
|
|
402
|
+
.join(" ");
|
|
403
|
+
block.addChild(new Text(line, 1, 0));
|
|
404
|
+
}
|
|
405
|
+
this.container.addChild(block);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (message.customType === LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE) {
|
|
409
|
+
const details = (message as CustomMessage<{ files?: LateDiagnosticsFile[] }>).details;
|
|
410
|
+
const component = new LateDiagnosticsMessageComponent(details?.files ?? []);
|
|
411
|
+
this.#trackExpandable(component);
|
|
412
|
+
this.container.addChild(component);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (message.customType === COLLAB_PROMPT_MESSAGE_TYPE) {
|
|
416
|
+
this.container.addChild(new CollabPromptMessageComponent(message as CustomMessage<CollabPromptDetails>));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE) {
|
|
420
|
+
const component = new SkillMessageComponent(message as CustomMessage<SkillPromptDetails>);
|
|
421
|
+
this.#trackExpandable(component);
|
|
422
|
+
this.container.addChild(component);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (
|
|
426
|
+
message.customType === "irc:incoming" ||
|
|
427
|
+
message.customType === "irc:autoreply" ||
|
|
428
|
+
message.customType === "irc:relay"
|
|
429
|
+
) {
|
|
430
|
+
const details = (
|
|
431
|
+
message as CustomMessage<{ from?: string; to?: string; message?: string; body?: string; replyTo?: string }>
|
|
432
|
+
).details;
|
|
433
|
+
const kind =
|
|
434
|
+
message.customType === "irc:incoming"
|
|
435
|
+
? ("incoming" as const)
|
|
436
|
+
: message.customType === "irc:autoreply"
|
|
437
|
+
? ("autoreply" as const)
|
|
438
|
+
: ("relay" as const);
|
|
439
|
+
const card = createIrcMessageCard(
|
|
440
|
+
{
|
|
441
|
+
kind,
|
|
442
|
+
from: details?.from,
|
|
443
|
+
to: details?.to,
|
|
444
|
+
body: kind === "incoming" ? details?.message : details?.body,
|
|
445
|
+
replyTo: details?.replyTo,
|
|
446
|
+
timestamp: message.timestamp,
|
|
447
|
+
},
|
|
448
|
+
() => this.#expanded,
|
|
449
|
+
theme,
|
|
450
|
+
);
|
|
451
|
+
this.container.addChild(card);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (message.customType === "advisor") {
|
|
455
|
+
const details = (message as CustomMessage<AdvisorMessageDetails>).details;
|
|
456
|
+
this.container.addChild(createAdvisorMessageCard(details, () => this.#expanded, theme));
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (message.customType === BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE) {
|
|
460
|
+
this.container.addChild(createBackgroundTanDispatchBlock(message as CustomMessage<unknown>));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const handoffComponent = createHandoffSummaryMessageComponent(message as CustomMessage<unknown>, this.#expanded);
|
|
464
|
+
if (handoffComponent) {
|
|
465
|
+
this.#trackExpandable(handoffComponent);
|
|
466
|
+
this.container.addChild(handoffComponent);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const component = new CustomMessageComponent(
|
|
470
|
+
message as CustomMessage<unknown>,
|
|
471
|
+
this.deps.getMessageRenderer?.(message.customType),
|
|
472
|
+
);
|
|
473
|
+
this.#trackExpandable(component);
|
|
474
|
+
this.container.addChild(component);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box, type Component, Markdown } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
3
|
-
import type { CompactionSummaryMessage, CustomMessage } from "../../session/messages";
|
|
3
|
+
import type { BranchSummaryMessage, CompactionSummaryMessage, CustomMessage } from "../../session/messages";
|
|
4
4
|
|
|
5
5
|
interface SummaryDividerOptions {
|
|
6
6
|
label: () => string;
|
|
@@ -156,6 +156,34 @@ export function createHandoffSummaryMessageComponent(
|
|
|
156
156
|
return component;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* A branch summary collapses a side branch back into the main line. Render it
|
|
161
|
+
* with the same slim divider as `/compact` and handoff rather than a `[branch]`
|
|
162
|
+
* box, so every history-collapse point reads as one consistent banner.
|
|
163
|
+
*/
|
|
164
|
+
export class BranchSummaryMessageComponent implements Component {
|
|
165
|
+
#divider: SummaryDividerComponent;
|
|
166
|
+
|
|
167
|
+
constructor(private readonly message: BranchSummaryMessage) {
|
|
168
|
+
this.#divider = new SummaryDividerComponent({
|
|
169
|
+
label: () => `${theme.icon.branch} branch`,
|
|
170
|
+
detailMarkdown: () => `**Branch summary**\n\n${this.message.summary}`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
setExpanded(expanded: boolean): void {
|
|
175
|
+
this.#divider.setExpanded(expanded);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
invalidate(): void {
|
|
179
|
+
this.#divider.invalidate();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
render(width: number): readonly string[] {
|
|
183
|
+
return this.#divider.render(width);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
159
187
|
function getCustomMessageText(message: CustomMessage<unknown>): string {
|
|
160
188
|
if (typeof message.content === "string") return message.content;
|
|
161
189
|
let firstText: string | undefined;
|
|
@@ -46,12 +46,15 @@ export class CustomMessageComponent extends Container {
|
|
|
46
46
|
}
|
|
47
47
|
this.removeChild(this.#box);
|
|
48
48
|
|
|
49
|
+
// The transcript dispatch routes both `custom` and legacy `hookMessage` roles here:
|
|
50
|
+
// tag hooks with the hook glyph, other injected messages with a neutral package.
|
|
51
|
+
const isHook = (this.message.role as string) === "hookMessage";
|
|
49
52
|
const custom = renderFramedMessage({
|
|
50
53
|
message: this.message,
|
|
51
54
|
box: this.#box,
|
|
52
55
|
expanded: this.#expanded,
|
|
53
56
|
customRenderer: this.customRenderer,
|
|
54
|
-
|
|
57
|
+
icon: isHook ? theme.icon.extensionHook : theme.icon.package,
|
|
55
58
|
});
|
|
56
59
|
|
|
57
60
|
if (custom) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DEFAULT_TAB_WIDTH, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import * as Diff from "diff";
|
|
3
3
|
import { getLanguageFromPath, highlightCode, theme } from "../../modes/theme/theme";
|
|
4
4
|
import { type CodeFrameMarker, formatCodeFrameLine, replaceTabs } from "../../tools/render-utils";
|
|
@@ -13,12 +13,12 @@ const DIM_OFF = "\x1b[22m";
|
|
|
13
13
|
* before the first non-whitespace character; remaining tabs in code
|
|
14
14
|
* content are replaced with spaces (like replaceTabs).
|
|
15
15
|
*/
|
|
16
|
-
function visualizeIndent(text: string
|
|
16
|
+
function visualizeIndent(text: string): string {
|
|
17
17
|
const match = text.match(/^([ \t]+)/);
|
|
18
|
-
if (!match) return replaceTabs(text
|
|
18
|
+
if (!match) return replaceTabs(text);
|
|
19
19
|
const indent = match[1];
|
|
20
20
|
const rest = text.slice(indent.length);
|
|
21
|
-
const tabWidth =
|
|
21
|
+
const tabWidth = DEFAULT_TAB_WIDTH;
|
|
22
22
|
const leftPadding = Math.floor(tabWidth / 2);
|
|
23
23
|
const rightPadding = Math.max(0, tabWidth - leftPadding - 1);
|
|
24
24
|
const tabMarker = `${DIM}${" ".repeat(leftPadding)}→${" ".repeat(rightPadding)}${DIM_OFF}`;
|
|
@@ -30,7 +30,7 @@ function visualizeIndent(text: string, filePath?: string): string {
|
|
|
30
30
|
visible += `${DIM}·${DIM_OFF}`;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
return `${visible}${replaceTabs(rest
|
|
33
|
+
return `${visible}${replaceTabs(rest)}`;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -153,7 +153,7 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
|
|
|
153
153
|
// unicode ellipsis.
|
|
154
154
|
const trimmed = line.trim();
|
|
155
155
|
const isGapRow = trimmed.length === 0 || trimmed === "..." || trimmed === "…";
|
|
156
|
-
result.push(theme.fg("toolDiffContext", isGapRow ? "…" : replaceTabs(line
|
|
156
|
+
result.push(theme.fg("toolDiffContext", isGapRow ? "…" : replaceTabs(line)));
|
|
157
157
|
i++;
|
|
158
158
|
continue;
|
|
159
159
|
}
|
|
@@ -184,47 +184,24 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
|
|
|
184
184
|
replaceTabs(added.content),
|
|
185
185
|
);
|
|
186
186
|
|
|
187
|
-
result.push(
|
|
188
|
-
|
|
189
|
-
"toolDiffRemoved",
|
|
190
|
-
formatLine("-", removed.lineNum, visualizeIndent(removedLine, options.filePath)),
|
|
191
|
-
),
|
|
192
|
-
);
|
|
193
|
-
result.push(
|
|
194
|
-
theme.fg("toolDiffAdded", formatLine("+", added.lineNum, visualizeIndent(addedLine, options.filePath))),
|
|
195
|
-
);
|
|
187
|
+
result.push(theme.fg("toolDiffRemoved", formatLine("-", removed.lineNum, visualizeIndent(removedLine))));
|
|
188
|
+
result.push(theme.fg("toolDiffAdded", formatLine("+", added.lineNum, visualizeIndent(addedLine))));
|
|
196
189
|
} else {
|
|
197
190
|
for (const removed of removedLines) {
|
|
198
191
|
result.push(
|
|
199
|
-
theme.fg(
|
|
200
|
-
"toolDiffRemoved",
|
|
201
|
-
formatLine("-", removed.lineNum, visualizeIndent(removed.content, options.filePath)),
|
|
202
|
-
),
|
|
192
|
+
theme.fg("toolDiffRemoved", formatLine("-", removed.lineNum, visualizeIndent(removed.content))),
|
|
203
193
|
);
|
|
204
194
|
}
|
|
205
195
|
for (const added of addedLines) {
|
|
206
|
-
result.push(
|
|
207
|
-
theme.fg(
|
|
208
|
-
"toolDiffAdded",
|
|
209
|
-
formatLine("+", added.lineNum, visualizeIndent(added.content, options.filePath)),
|
|
210
|
-
),
|
|
211
|
-
);
|
|
196
|
+
result.push(theme.fg("toolDiffAdded", formatLine("+", added.lineNum, visualizeIndent(added.content))));
|
|
212
197
|
}
|
|
213
198
|
}
|
|
214
199
|
} else if (parsed.prefix === "+") {
|
|
215
|
-
result.push(
|
|
216
|
-
theme.fg(
|
|
217
|
-
"toolDiffAdded",
|
|
218
|
-
formatLine("+", parsed.lineNum, visualizeIndent(parsed.content, options.filePath)),
|
|
219
|
-
),
|
|
220
|
-
);
|
|
200
|
+
result.push(theme.fg("toolDiffAdded", formatLine("+", parsed.lineNum, visualizeIndent(parsed.content))));
|
|
221
201
|
i++;
|
|
222
202
|
} else {
|
|
223
203
|
const highlighted = contextHighlights.get(i);
|
|
224
|
-
const content =
|
|
225
|
-
highlighted !== undefined
|
|
226
|
-
? replaceTabs(highlighted, options.filePath)
|
|
227
|
-
: visualizeIndent(parsed.content, options.filePath);
|
|
204
|
+
const content = highlighted !== undefined ? replaceTabs(highlighted) : visualizeIndent(parsed.content);
|
|
228
205
|
result.push(theme.fg("toolDiffContext", formatLine(" ", parsed.lineNum, content)));
|
|
229
206
|
i++;
|
|
230
207
|
}
|
|
@@ -26,7 +26,7 @@ export class DynamicBorder implements Component {
|
|
|
26
26
|
if (this.#cachedLines && this.#cachedWidth === width) {
|
|
27
27
|
return this.#cachedLines;
|
|
28
28
|
}
|
|
29
|
-
const lines = [this.#color(theme.
|
|
29
|
+
const lines = [this.#color(theme.boxRound.horizontal.repeat(Math.max(1, width)))];
|
|
30
30
|
this.#cachedWidth = width;
|
|
31
31
|
this.#cachedLines = lines;
|
|
32
32
|
return lines;
|
|
@@ -380,7 +380,7 @@ class TwoColumnBody implements Component {
|
|
|
380
380
|
// Fill the full body height so the dashboard reads as a full-screen view.
|
|
381
381
|
const numLines = this.maxHeight;
|
|
382
382
|
const combined: string[] = [];
|
|
383
|
-
const separator = theme.fg("dim", ` ${theme.
|
|
383
|
+
const separator = theme.fg("dim", ` ${theme.boxRound.vertical} `);
|
|
384
384
|
|
|
385
385
|
for (let i = 0; i < numLines; i++) {
|
|
386
386
|
const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
|