@oh-my-pi/pi-coding-agent 16.0.10 → 16.0.11
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 +35 -0
- package/dist/cli.js +3208 -3199
- 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 +0 -4
- 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/agent-hub.d.ts +6 -1
- package/dist/types/modes/components/agent-transcript-viewer.d.ts +39 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +42 -0
- package/dist/types/modes/controllers/command-controller.d.ts +3 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/registry/agent-registry.d.ts +10 -3
- package/dist/types/session/compact-modes.d.ts +60 -0
- package/dist/types/session/streaming-output.d.ts +0 -2
- package/dist/types/tools/__tests__/json-tree.test.d.ts +1 -0
- 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 +0 -5
- package/src/config/settings.ts +0 -6
- package/src/edit/file-snapshot-store.ts +1 -1
- package/src/edit/renderer.ts +7 -7
- 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/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +63 -0
- package/src/modes/components/agent-hub.ts +97 -920
- package/src/modes/components/agent-transcript-viewer.ts +461 -0
- package/src/modes/components/chat-transcript-builder.ts +462 -0
- package/src/modes/components/diff.ts +12 -35
- package/src/modes/controllers/command-controller.ts +12 -2
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +8 -1
- package/src/modes/controllers/selector-controller.ts +4 -1
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/types.ts +2 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/registry/agent-registry.ts +13 -4
- package/src/sdk.ts +1 -1
- package/src/session/agent-session.ts +92 -3
- package/src/session/compact-modes.ts +105 -0
- package/src/session/session-dump-format.ts +1 -1
- package/src/session/session-history-format.ts +1 -1
- package/src/session/streaming-output.ts +5 -5
- package/src/slash-commands/builtin-registry.ts +16 -4
- 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.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/irc.ts +1 -1
- package/src/tools/json-tree.ts +22 -5
- package/src/tools/read.ts +5 -6
- 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
|
@@ -0,0 +1,462 @@
|
|
|
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 { BranchSummaryMessageComponent } from "./branch-summary-message";
|
|
40
|
+
import { CollabPromptMessageComponent } from "./collab-prompt-message";
|
|
41
|
+
import { CompactionSummaryMessageComponent, createHandoffSummaryMessageComponent } from "./compaction-summary-message";
|
|
42
|
+
import { CustomMessageComponent } from "./custom-message";
|
|
43
|
+
import { EvalExecutionComponent } from "./eval-execution";
|
|
44
|
+
import { type LateDiagnosticsFile, LateDiagnosticsMessageComponent } from "./late-diagnostics-message";
|
|
45
|
+
import { ReadToolGroupComponent, readArgsHaveTarget, readArgsTargetInternalUrl } from "./read-tool-group";
|
|
46
|
+
import { SkillMessageComponent } from "./skill-message";
|
|
47
|
+
import { ToolExecutionComponent } from "./tool-execution";
|
|
48
|
+
import { TranscriptBlock, TranscriptContainer } from "./transcript-container";
|
|
49
|
+
import { createUsageRowBlock } from "./usage-row";
|
|
50
|
+
import { UserMessageComponent } from "./user-message";
|
|
51
|
+
|
|
52
|
+
export interface ChatTranscriptBuilderDeps {
|
|
53
|
+
ui: TUI;
|
|
54
|
+
getTool?: (name: string) => AgentTool | undefined;
|
|
55
|
+
getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
|
|
56
|
+
cwd: string;
|
|
57
|
+
hideThinkingBlock?: () => boolean;
|
|
58
|
+
requestRender: () => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Extracts the plain-text content of a user message (string or text blocks). */
|
|
62
|
+
function userMessageText(message: Extract<AgentMessage, { role: "user" }>): string {
|
|
63
|
+
if (typeof message.content === "string") return message.content;
|
|
64
|
+
return message.content
|
|
65
|
+
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
|
66
|
+
.map(block => block.text)
|
|
67
|
+
.join("");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class ChatTranscriptBuilder {
|
|
71
|
+
readonly container = new TranscriptContainer();
|
|
72
|
+
#pendingTools = new Map<string, ToolExecutionComponent | ReadToolGroupComponent>();
|
|
73
|
+
#readArgs = new Map<string, Record<string, unknown>>();
|
|
74
|
+
#readGroup: ReadToolGroupComponent | null = null;
|
|
75
|
+
#pendingUsage: Usage | undefined;
|
|
76
|
+
#waitingPoll: ToolExecutionComponent | null = null;
|
|
77
|
+
#expandables: Array<{ setExpanded(expanded: boolean): void }> = [];
|
|
78
|
+
#expanded = false;
|
|
79
|
+
|
|
80
|
+
constructor(private readonly deps: ChatTranscriptBuilderDeps) {}
|
|
81
|
+
|
|
82
|
+
/** Whether the transcript currently holds any rendered rows. */
|
|
83
|
+
get isEmpty(): boolean {
|
|
84
|
+
return this.container.children.length === 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Discard all components and rebuild the whole transcript from `entries`. */
|
|
88
|
+
rebuild(entries: SessionMessageEntry[]): void {
|
|
89
|
+
this.reset();
|
|
90
|
+
for (const entry of entries) this.#appendChatMessage(entry.message);
|
|
91
|
+
// Flush the trailing turn's usage row only once its tools are materialized
|
|
92
|
+
// (a read whose result has not arrived stays pending); otherwise the row
|
|
93
|
+
// would sit above its tools. The drain happens here at the end of the pass.
|
|
94
|
+
if (this.#readArgs.size === 0 && this.#pendingTools.size === 0) this.#flushPendingUsage();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Toggle tool-output expansion across every expandable component. */
|
|
98
|
+
setExpanded(expanded: boolean): void {
|
|
99
|
+
this.#expanded = expanded;
|
|
100
|
+
for (const component of this.#expandables) component.setExpanded(expanded);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get expanded(): boolean {
|
|
104
|
+
return this.#expanded;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Tear down components (sealing pending spinners) and clear build state. */
|
|
108
|
+
reset(): void {
|
|
109
|
+
for (const pending of this.#pendingTools.values()) pending.seal();
|
|
110
|
+
this.#pendingTools.clear();
|
|
111
|
+
this.#readArgs.clear();
|
|
112
|
+
this.#readGroup = null;
|
|
113
|
+
this.#pendingUsage = undefined;
|
|
114
|
+
this.#waitingPoll = null;
|
|
115
|
+
this.#expandables = [];
|
|
116
|
+
this.container.dispose();
|
|
117
|
+
this.container.clear();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
dispose(): void {
|
|
121
|
+
this.reset();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#trackExpandable(component: { setExpanded(expanded: boolean): void }): void {
|
|
125
|
+
component.setExpanded(this.#expanded);
|
|
126
|
+
this.#expandables.push(component);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A `job` poll showing all-running is displaced by the next `job` call. */
|
|
130
|
+
#resolveWaitingPoll(nextToolName?: string): void {
|
|
131
|
+
const previous = this.#waitingPoll;
|
|
132
|
+
if (!previous) return;
|
|
133
|
+
this.#waitingPoll = null;
|
|
134
|
+
if (nextToolName === "job" && previous.isDisplaceableBlock()) {
|
|
135
|
+
this.container.removeChild(previous);
|
|
136
|
+
}
|
|
137
|
+
previous.seal();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#ensureReadGroup(): ReadToolGroupComponent {
|
|
141
|
+
if (!this.#readGroup) {
|
|
142
|
+
this.#readGroup = new ReadToolGroupComponent({
|
|
143
|
+
showContentPreview: settings.get("read.toolResultPreview"),
|
|
144
|
+
});
|
|
145
|
+
this.#trackExpandable(this.#readGroup);
|
|
146
|
+
this.container.addChild(this.#readGroup);
|
|
147
|
+
}
|
|
148
|
+
return this.#readGroup;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// The per-turn token-usage row must land below the turn's tool blocks, but
|
|
152
|
+
// normal `read` calls only materialize their group in #appendToolResult. Defer
|
|
153
|
+
// the row: stash it on the assistant message and flush once the turn's tools
|
|
154
|
+
// are placed, sealing the read run so the row sits under it.
|
|
155
|
+
#flushPendingUsage(): void {
|
|
156
|
+
if (!this.#pendingUsage) return;
|
|
157
|
+
this.#readGroup?.seal();
|
|
158
|
+
this.#readGroup = null;
|
|
159
|
+
this.container.addChild(createUsageRowBlock(this.#pendingUsage));
|
|
160
|
+
this.#pendingUsage = undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#appendChatMessage(message: AgentMessage): void {
|
|
164
|
+
if (message.role !== "toolResult") this.#flushPendingUsage();
|
|
165
|
+
switch (message.role) {
|
|
166
|
+
case "assistant":
|
|
167
|
+
this.#appendAssistantMessage(message);
|
|
168
|
+
break;
|
|
169
|
+
case "toolResult":
|
|
170
|
+
this.#appendToolResult(message);
|
|
171
|
+
break;
|
|
172
|
+
case "user":
|
|
173
|
+
case "developer": {
|
|
174
|
+
// A user prompt closes the poll-displacement window, same as the live path.
|
|
175
|
+
if (message.role === "user") this.#resolveWaitingPoll();
|
|
176
|
+
const textContent = message.role === "user" ? userMessageText(message) : "";
|
|
177
|
+
if (textContent) {
|
|
178
|
+
const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
|
|
179
|
+
this.container.addChild(new UserMessageComponent(textContent, isSynthetic));
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case "bashExecution": {
|
|
184
|
+
const component = new BashExecutionComponent(message.command, this.deps.ui, message.excludeFromContext);
|
|
185
|
+
if (message.output) component.appendOutput(message.output);
|
|
186
|
+
component.setComplete(message.exitCode, message.cancelled, { truncation: message.meta?.truncation });
|
|
187
|
+
this.container.addChild(component);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case "pythonExecution": {
|
|
191
|
+
const component = new EvalExecutionComponent(message.code, this.deps.ui, message.excludeFromContext);
|
|
192
|
+
if (message.output) component.appendOutput(message.output);
|
|
193
|
+
component.setComplete(message.exitCode, message.cancelled, { truncation: message.meta?.truncation });
|
|
194
|
+
this.container.addChild(component);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
case "hookMessage":
|
|
198
|
+
case "custom":
|
|
199
|
+
this.#appendCustomMessage(message);
|
|
200
|
+
break;
|
|
201
|
+
case "compactionSummary": {
|
|
202
|
+
const component = new CompactionSummaryMessageComponent(message);
|
|
203
|
+
this.#trackExpandable(component);
|
|
204
|
+
this.container.addChild(component);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case "branchSummary": {
|
|
208
|
+
const component = new BranchSummaryMessageComponent(message);
|
|
209
|
+
this.#trackExpandable(component);
|
|
210
|
+
this.container.addChild(component);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case "fileMention": {
|
|
214
|
+
const block = new TranscriptBlock();
|
|
215
|
+
for (const file of message.files) {
|
|
216
|
+
let suffix: string;
|
|
217
|
+
if (file.skippedReason === "tooLarge") {
|
|
218
|
+
const size = typeof file.byteSize === "number" ? formatBytes(file.byteSize) : "unknown size";
|
|
219
|
+
suffix = `(skipped: ${size})`;
|
|
220
|
+
} else {
|
|
221
|
+
suffix = file.image
|
|
222
|
+
? "(image)"
|
|
223
|
+
: file.lineCount === undefined
|
|
224
|
+
? "(unknown lines)"
|
|
225
|
+
: `(${file.lineCount} lines)`;
|
|
226
|
+
}
|
|
227
|
+
const text = `${theme.fg("dim", `${theme.tree.last} `)}${theme.fg("muted", "Read")} ${theme.fg(
|
|
228
|
+
"accent",
|
|
229
|
+
file.path,
|
|
230
|
+
)} ${theme.fg("dim", suffix)}`;
|
|
231
|
+
// Indent one column to match the transcript's other rows (the viewer renders
|
|
232
|
+
// body rows without an outer gutter; rows own their left pad).
|
|
233
|
+
block.addChild(new Text(text, 1, 0));
|
|
234
|
+
}
|
|
235
|
+
if (block.children.length > 0) this.container.addChild(block);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
default:
|
|
239
|
+
message satisfies never;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#appendAssistantMessage(message: Extract<AgentMessage, { role: "assistant" }>): void {
|
|
244
|
+
const assistantComponent = new AssistantMessageComponent(message, this.deps.hideThinkingBlock?.() ?? false, () =>
|
|
245
|
+
this.deps.requestRender(),
|
|
246
|
+
);
|
|
247
|
+
this.container.addChild(assistantComponent);
|
|
248
|
+
|
|
249
|
+
const hasVisibleAssistantContent = message.content.some(
|
|
250
|
+
content =>
|
|
251
|
+
(content.type === "text" && canonicalizeMessage(content.text)) ||
|
|
252
|
+
(content.type === "thinking" && canonicalizeMessage(content.thinking)),
|
|
253
|
+
);
|
|
254
|
+
if (hasVisibleAssistantContent) {
|
|
255
|
+
// New visible turn content closes the current read run (mirrors rebuild).
|
|
256
|
+
this.#readGroup?.seal();
|
|
257
|
+
this.#readGroup = null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const isAbortedSilently = message.stopReason === "aborted" && isSilentAbort(message.errorMessage);
|
|
261
|
+
const hasErrorStop = !isAbortedSilently && (message.stopReason === "aborted" || message.stopReason === "error");
|
|
262
|
+
const errorMessage = hasErrorStop
|
|
263
|
+
? message.stopReason === "aborted"
|
|
264
|
+
? resolveAbortLabel(message.errorMessage)
|
|
265
|
+
: message.errorMessage || "Error"
|
|
266
|
+
: null;
|
|
267
|
+
|
|
268
|
+
for (const content of message.content) {
|
|
269
|
+
if (content.type !== "toolCall") continue;
|
|
270
|
+
this.#resolveWaitingPoll(content.name);
|
|
271
|
+
|
|
272
|
+
if (
|
|
273
|
+
content.name === "read" &&
|
|
274
|
+
readArgsHaveTarget(content.arguments) &&
|
|
275
|
+
!readArgsTargetInternalUrl(content.arguments)
|
|
276
|
+
) {
|
|
277
|
+
if (hasErrorStop && errorMessage) {
|
|
278
|
+
const group = this.#ensureReadGroup();
|
|
279
|
+
group.updateArgs(content.arguments, content.id);
|
|
280
|
+
group.updateResult(
|
|
281
|
+
{ content: [{ type: "text", text: errorMessage }], isError: true },
|
|
282
|
+
false,
|
|
283
|
+
content.id,
|
|
284
|
+
);
|
|
285
|
+
} else {
|
|
286
|
+
const normalizedArgs =
|
|
287
|
+
content.arguments && typeof content.arguments === "object" && !Array.isArray(content.arguments)
|
|
288
|
+
? (content.arguments as Record<string, unknown>)
|
|
289
|
+
: {};
|
|
290
|
+
this.#readArgs.set(content.id, normalizedArgs);
|
|
291
|
+
}
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.#readGroup?.seal();
|
|
296
|
+
this.#readGroup = null;
|
|
297
|
+
const component = new ToolExecutionComponent(
|
|
298
|
+
content.name,
|
|
299
|
+
content.arguments,
|
|
300
|
+
{
|
|
301
|
+
// Images can't be sliced through the scroll viewport; keep them off.
|
|
302
|
+
showImages: false,
|
|
303
|
+
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
304
|
+
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
305
|
+
liveRegion: this.container,
|
|
306
|
+
},
|
|
307
|
+
this.deps.getTool?.(content.name),
|
|
308
|
+
this.deps.ui,
|
|
309
|
+
this.deps.cwd,
|
|
310
|
+
content.id,
|
|
311
|
+
);
|
|
312
|
+
this.#trackExpandable(component);
|
|
313
|
+
this.container.addChild(component);
|
|
314
|
+
|
|
315
|
+
if (hasErrorStop && errorMessage) {
|
|
316
|
+
component.updateResult(
|
|
317
|
+
{ content: [{ type: "text", text: errorMessage }], isError: true },
|
|
318
|
+
false,
|
|
319
|
+
content.id,
|
|
320
|
+
);
|
|
321
|
+
} else {
|
|
322
|
+
this.#pendingTools.set(content.id, component);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.#pendingUsage = settings.get("display.showTokenUsage") ? message.usage : undefined;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#appendToolResult(message: Extract<AgentMessage, { role: "toolResult" }>): void {
|
|
330
|
+
const pending = this.#pendingTools.get(message.toolCallId);
|
|
331
|
+
const isReadGroupResult = message.toolName === "read" && (!pending || pending instanceof ReadToolGroupComponent);
|
|
332
|
+
if (isReadGroupResult) {
|
|
333
|
+
let component = pending;
|
|
334
|
+
if (!component) {
|
|
335
|
+
const group = this.#ensureReadGroup();
|
|
336
|
+
const args = this.#readArgs.get(message.toolCallId);
|
|
337
|
+
if (args) group.updateArgs(args, message.toolCallId);
|
|
338
|
+
component = group;
|
|
339
|
+
}
|
|
340
|
+
component.updateResult(message, false, message.toolCallId);
|
|
341
|
+
this.#pendingTools.delete(message.toolCallId);
|
|
342
|
+
this.#readArgs.delete(message.toolCallId);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (!pending) return;
|
|
346
|
+
pending.updateResult(message, false, message.toolCallId);
|
|
347
|
+
this.#pendingTools.delete(message.toolCallId);
|
|
348
|
+
if (message.toolName === "job" && pending instanceof ToolExecutionComponent && pending.isDisplaceableBlock()) {
|
|
349
|
+
this.#waitingPoll = pending;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#appendCustomMessage(message: Extract<AgentMessage, { role: "custom" | "hookMessage" }>): void {
|
|
354
|
+
if (!message.display) return;
|
|
355
|
+
if (message.customType === "async-result") {
|
|
356
|
+
const details = (
|
|
357
|
+
message as CustomMessage<{
|
|
358
|
+
jobId?: string;
|
|
359
|
+
type?: "bash" | "task";
|
|
360
|
+
label?: string;
|
|
361
|
+
durationMs?: number;
|
|
362
|
+
jobs?: Array<{ jobId?: string; type?: "bash" | "task"; label?: string; durationMs?: number }>;
|
|
363
|
+
}>
|
|
364
|
+
).details;
|
|
365
|
+
const jobs =
|
|
366
|
+
details?.jobs && details.jobs.length > 0
|
|
367
|
+
? details.jobs
|
|
368
|
+
: [
|
|
369
|
+
{
|
|
370
|
+
jobId: details?.jobId,
|
|
371
|
+
type: details?.type,
|
|
372
|
+
label: details?.label,
|
|
373
|
+
durationMs: details?.durationMs,
|
|
374
|
+
},
|
|
375
|
+
];
|
|
376
|
+
const block = new TranscriptBlock();
|
|
377
|
+
for (const job of jobs) {
|
|
378
|
+
const jobId = job.jobId ?? "unknown";
|
|
379
|
+
const typeLabel = job.type ? `[${job.type}]` : "[job]";
|
|
380
|
+
const duration = typeof job.durationMs === "number" ? formatDuration(job.durationMs) : undefined;
|
|
381
|
+
const line = [
|
|
382
|
+
theme.fg("success", `${theme.status.done} Background job completed`),
|
|
383
|
+
theme.fg("dim", typeLabel),
|
|
384
|
+
theme.fg("accent", jobId),
|
|
385
|
+
duration ? theme.fg("dim", `(${duration})`) : undefined,
|
|
386
|
+
]
|
|
387
|
+
.filter(Boolean)
|
|
388
|
+
.join(" ");
|
|
389
|
+
block.addChild(new Text(line, 1, 0));
|
|
390
|
+
}
|
|
391
|
+
this.container.addChild(block);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (message.customType === LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE) {
|
|
395
|
+
const details = (message as CustomMessage<{ files?: LateDiagnosticsFile[] }>).details;
|
|
396
|
+
const component = new LateDiagnosticsMessageComponent(details?.files ?? []);
|
|
397
|
+
this.#trackExpandable(component);
|
|
398
|
+
this.container.addChild(component);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (message.customType === COLLAB_PROMPT_MESSAGE_TYPE) {
|
|
402
|
+
this.container.addChild(new CollabPromptMessageComponent(message as CustomMessage<CollabPromptDetails>));
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE) {
|
|
406
|
+
const component = new SkillMessageComponent(message as CustomMessage<SkillPromptDetails>);
|
|
407
|
+
this.#trackExpandable(component);
|
|
408
|
+
this.container.addChild(component);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (
|
|
412
|
+
message.customType === "irc:incoming" ||
|
|
413
|
+
message.customType === "irc:autoreply" ||
|
|
414
|
+
message.customType === "irc:relay"
|
|
415
|
+
) {
|
|
416
|
+
const details = (
|
|
417
|
+
message as CustomMessage<{ from?: string; to?: string; message?: string; body?: string; replyTo?: string }>
|
|
418
|
+
).details;
|
|
419
|
+
const kind =
|
|
420
|
+
message.customType === "irc:incoming"
|
|
421
|
+
? ("incoming" as const)
|
|
422
|
+
: message.customType === "irc:autoreply"
|
|
423
|
+
? ("autoreply" as const)
|
|
424
|
+
: ("relay" as const);
|
|
425
|
+
const card = createIrcMessageCard(
|
|
426
|
+
{
|
|
427
|
+
kind,
|
|
428
|
+
from: details?.from,
|
|
429
|
+
to: details?.to,
|
|
430
|
+
body: kind === "incoming" ? details?.message : details?.body,
|
|
431
|
+
replyTo: details?.replyTo,
|
|
432
|
+
timestamp: message.timestamp,
|
|
433
|
+
},
|
|
434
|
+
() => this.#expanded,
|
|
435
|
+
theme,
|
|
436
|
+
);
|
|
437
|
+
this.container.addChild(card);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (message.customType === "advisor") {
|
|
441
|
+
const details = (message as CustomMessage<AdvisorMessageDetails>).details;
|
|
442
|
+
this.container.addChild(createAdvisorMessageCard(details, () => this.#expanded, theme));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (message.customType === BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE) {
|
|
446
|
+
this.container.addChild(createBackgroundTanDispatchBlock(message as CustomMessage<unknown>));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const handoffComponent = createHandoffSummaryMessageComponent(message as CustomMessage<unknown>, this.#expanded);
|
|
450
|
+
if (handoffComponent) {
|
|
451
|
+
this.#trackExpandable(handoffComponent);
|
|
452
|
+
this.container.addChild(handoffComponent);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const component = new CustomMessageComponent(
|
|
456
|
+
message as CustomMessage<unknown>,
|
|
457
|
+
this.deps.getMessageRenderer?.(message.customType),
|
|
458
|
+
);
|
|
459
|
+
this.#trackExpandable(component);
|
|
460
|
+
this.container.addChild(component);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
@@ -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
|
}
|
|
@@ -38,6 +38,7 @@ import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
|
|
|
38
38
|
import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
|
|
39
39
|
import type { AsyncJobSnapshotItem } from "../../session/agent-session";
|
|
40
40
|
import type { AuthStorage, OAuthAccountIdentity } from "../../session/auth-storage";
|
|
41
|
+
import type { CompactMode } from "../../session/compact-modes";
|
|
41
42
|
import type { NewSessionOptions } from "../../session/session-entries";
|
|
42
43
|
import { formatShakeSummary, type ShakeMode, type ShakeResult } from "../../session/shake-types";
|
|
43
44
|
import { limitMatchesActiveAccount } from "../../slash-commands/helpers/active-oauth-account";
|
|
@@ -1034,6 +1035,7 @@ export class CommandController {
|
|
|
1034
1035
|
|
|
1035
1036
|
async handleCompactCommand(
|
|
1036
1037
|
customInstructions?: string,
|
|
1038
|
+
mode?: CompactMode,
|
|
1037
1039
|
beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>,
|
|
1038
1040
|
): Promise<CompactionOutcome> {
|
|
1039
1041
|
const entries = this.ctx.sessionManager.getEntries();
|
|
@@ -1044,7 +1046,7 @@ export class CommandController {
|
|
|
1044
1046
|
return "ok";
|
|
1045
1047
|
}
|
|
1046
1048
|
|
|
1047
|
-
return this.executeCompaction(customInstructions, false, beforeFlush);
|
|
1049
|
+
return this.executeCompaction(customInstructions, false, beforeFlush, mode);
|
|
1048
1050
|
}
|
|
1049
1051
|
|
|
1050
1052
|
/**
|
|
@@ -1090,6 +1092,7 @@ export class CommandController {
|
|
|
1090
1092
|
customInstructionsOrOptions?: string | CompactOptions,
|
|
1091
1093
|
isAuto = false,
|
|
1092
1094
|
beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>,
|
|
1095
|
+
mode?: CompactMode,
|
|
1093
1096
|
): Promise<CompactionOutcome> {
|
|
1094
1097
|
if (this.ctx.loadingAnimation) {
|
|
1095
1098
|
this.ctx.loadingAnimation.stop();
|
|
@@ -1111,10 +1114,17 @@ export class CommandController {
|
|
|
1111
1114
|
let outcome: CompactionOutcome = "ok";
|
|
1112
1115
|
try {
|
|
1113
1116
|
const instructions = typeof customInstructionsOrOptions === "string" ? customInstructionsOrOptions : undefined;
|
|
1114
|
-
const
|
|
1117
|
+
const baseOptions =
|
|
1115
1118
|
customInstructionsOrOptions && typeof customInstructionsOrOptions === "object"
|
|
1116
1119
|
? customInstructionsOrOptions
|
|
1117
1120
|
: undefined;
|
|
1121
|
+
// The slash path passes `mode` positionally; the extension path carries
|
|
1122
|
+
// it inside the options object. Either source wins over no mode.
|
|
1123
|
+
const effectiveMode = mode ?? baseOptions?.mode;
|
|
1124
|
+
const options =
|
|
1125
|
+
baseOptions || effectiveMode
|
|
1126
|
+
? { ...baseOptions, ...(effectiveMode ? { mode: effectiveMode } : {}) }
|
|
1127
|
+
: undefined;
|
|
1118
1128
|
await this.ctx.session.compact(instructions, options);
|
|
1119
1129
|
|
|
1120
1130
|
compactingLoader.stop();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
|
|
2
1
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
3
2
|
import { type Component, Loader, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
4
4
|
import { extractTextContent } from "../../commit/utils";
|
|
5
5
|
import { settings } from "../../config/settings";
|
|
6
6
|
import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
|
|
@@ -1529,7 +1529,6 @@ export class InputController {
|
|
|
1529
1529
|
for (const child of this.ctx.chatContainer.children) {
|
|
1530
1530
|
if (child instanceof AssistantMessageComponent) {
|
|
1531
1531
|
child.setHideThinkingBlock(this.ctx.hideThinkingBlock);
|
|
1532
|
-
child.invalidate();
|
|
1533
1532
|
}
|
|
1534
1533
|
}
|
|
1535
1534
|
|
|
@@ -1538,6 +1537,14 @@ export class InputController {
|
|
|
1538
1537
|
this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
|
|
1539
1538
|
}
|
|
1540
1539
|
|
|
1540
|
+
// Every block now carries the new flag, but on ED3-risk terminals the
|
|
1541
|
+
// blocks that scrolled past the live region are frozen snapshots in
|
|
1542
|
+
// committed scrollback — a plain repaint replays them stale, so scrolling
|
|
1543
|
+
// up still shows the old thinking expanded. resetDisplay() retires those
|
|
1544
|
+
// snapshots (it invalidates every block) and forces a full clear + replay
|
|
1545
|
+
// of the whole transcript, matching setToolsExpanded()'s redraw.
|
|
1546
|
+
this.ctx.ui.resetDisplay();
|
|
1547
|
+
|
|
1541
1548
|
this.ctx.showStatus(`Thinking blocks: ${this.ctx.hideThinkingBlock ? "hidden" : "visible"}`);
|
|
1542
1549
|
}
|
|
1543
1550
|
|
|
@@ -320,9 +320,12 @@ export class SelectorController {
|
|
|
320
320
|
for (const child of this.ctx.chatContainer.children) {
|
|
321
321
|
if (child instanceof AssistantMessageComponent) {
|
|
322
322
|
child.setHideThinkingBlock(value as boolean);
|
|
323
|
-
child.invalidate();
|
|
324
323
|
}
|
|
325
324
|
}
|
|
325
|
+
// Full clear + replay so blocks frozen in committed scrollback on
|
|
326
|
+
// ED3-risk terminals retire their stale snapshots too (see
|
|
327
|
+
// InputController.toggleThinkingBlockVisibility).
|
|
328
|
+
this.ctx.ui.resetDisplay();
|
|
326
329
|
break;
|
|
327
330
|
case "tui.tight":
|
|
328
331
|
setTuiTight(value as boolean);
|
|
@@ -82,6 +82,7 @@ import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compa
|
|
|
82
82
|
type: "text",
|
|
83
83
|
};
|
|
84
84
|
import type { AgentSession, AgentSessionEvent, ResolvedRoleModel } from "../session/agent-session";
|
|
85
|
+
import type { CompactMode } from "../session/compact-modes";
|
|
85
86
|
import { HistoryStorage } from "../session/history-storage";
|
|
86
87
|
import type { SessionContext } from "../session/session-context";
|
|
87
88
|
import { getRecentSessions } from "../session/session-listing";
|
|
@@ -2350,7 +2351,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2350
2351
|
// the try/finally is idempotent and kept for the !compactBeforeExecute
|
|
2351
2352
|
// branch.
|
|
2352
2353
|
this.session.setPlanReferencePath(options.planFilePath);
|
|
2353
|
-
compactOutcome = await this.handleCompactCommand(compactionPrompt, outcome =>
|
|
2354
|
+
compactOutcome = await this.handleCompactCommand(compactionPrompt, undefined, outcome =>
|
|
2354
2355
|
this.#applyDeferredPlanModelTransition(outcome, options.executionModel),
|
|
2355
2356
|
);
|
|
2356
2357
|
}
|
|
@@ -3616,9 +3617,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3616
3617
|
|
|
3617
3618
|
handleCompactCommand(
|
|
3618
3619
|
customInstructions?: string,
|
|
3620
|
+
mode?: CompactMode,
|
|
3619
3621
|
beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>,
|
|
3620
3622
|
): Promise<CompactionOutcome> {
|
|
3621
|
-
return this.#commandController.handleCompactCommand(customInstructions, beforeFlush);
|
|
3623
|
+
return this.#commandController.handleCompactCommand(customInstructions, mode, beforeFlush);
|
|
3622
3624
|
}
|
|
3623
3625
|
|
|
3624
3626
|
handleHandoffCommand(customInstructions?: string): Promise<void> {
|