@oh-my-pi/pi-coding-agent 16.0.9 → 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 +58 -0
- package/dist/cli.js +3402 -3443
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/transcript-recorder.d.ts +52 -0
- package/dist/types/collab/host.d.ts +2 -2
- package/dist/types/collab/protocol.d.ts +4 -5
- package/dist/types/commit/agentic/agent.d.ts +1 -1
- package/dist/types/config/model-resolver.d.ts +11 -2
- package/dist/types/config/settings-schema.d.ts +12 -6
- 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/agent-session.d.ts +13 -0
- package/dist/types/session/compact-modes.d.ts +60 -0
- package/dist/types/session/streaming-output.d.ts +0 -2
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/slash-commands/helpers/collab-qrcode.d.ts +13 -0
- package/dist/types/tools/__tests__/json-tree.test.d.ts +1 -0
- package/dist/types/tools/index.d.ts +9 -1
- package/dist/types/utils/image-loading.d.ts +12 -0
- package/dist/types/utils/qrcode.d.ts +48 -0
- package/package.json +12 -12
- package/src/advisor/index.ts +1 -0
- package/src/advisor/transcript-recorder.ts +136 -0
- package/src/cli/args.ts +7 -1
- package/src/cli/stats-cli.ts +2 -11
- package/src/collab/host.ts +29 -17
- package/src/collab/protocol.ts +48 -15
- 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/config-file.ts +1 -1
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-registry.ts +16 -4
- package/src/config/model-resolver.ts +193 -35
- package/src/config/settings-schema.ts +14 -7
- package/src/config/settings.ts +3 -9
- 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 +6 -3
- 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/components/oauth-selector.ts +31 -2
- 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/inspect-image.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/registry/agent-registry.ts +13 -4
- package/src/sdk.ts +27 -8
- package/src/session/agent-session.ts +185 -17
- 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 +45 -15
- package/src/slash-commands/helpers/collab-qrcode.ts +28 -0
- package/src/task/executor.ts +1 -1
- package/src/task/output-manager.ts +5 -0
- package/src/thinking.ts +25 -5
- 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/index.ts +10 -1
- package/src/tools/inspect-image.ts +72 -9
- package/src/tools/irc.ts +1 -1
- package/src/tools/json-tree.ts +22 -5
- package/src/tools/read.ts +5 -6
- package/src/utils/file-mentions.ts +5 -2
- package/src/utils/image-loading.ts +58 -0
- package/src/utils/qrcode.ts +535 -0
- 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
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
Spacer,
|
|
11
11
|
TruncatedText,
|
|
12
12
|
} from "@oh-my-pi/pi-tui";
|
|
13
|
+
import { settings } from "../../config/settings";
|
|
13
14
|
import { theme } from "../../modes/theme/theme";
|
|
14
15
|
import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
15
16
|
import type { AuthStorage, CredentialOriginKind } from "../../session/auth-storage";
|
|
@@ -17,6 +18,20 @@ import { DynamicBorder } from "./dynamic-border";
|
|
|
17
18
|
|
|
18
19
|
const OAUTH_SELECTOR_MAX_VISIBLE = 10;
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Provider ids the user has disabled via settings. `/login` (login mode) hides
|
|
23
|
+
* these so a disabled provider's models stay out of reach end-to-end, mirroring
|
|
24
|
+
* the model picker's `disabledProviders` filtering. Reads the settings singleton
|
|
25
|
+
* defensively: it throws before `Settings.init()`, in which case nothing is disabled.
|
|
26
|
+
*/
|
|
27
|
+
function getDisabledProviderIds(): ReadonlySet<string> {
|
|
28
|
+
try {
|
|
29
|
+
return new Set(settings.get("disabledProviders"));
|
|
30
|
+
} catch {
|
|
31
|
+
return new Set();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
/**
|
|
21
36
|
* Rendered lines before the provider rows: top border, spacer, title, spacer
|
|
22
37
|
* (must mirror the constructor's addChild order).
|
|
@@ -102,8 +117,22 @@ export class OAuthSelectorComponent extends Container {
|
|
|
102
117
|
|
|
103
118
|
#loadProviders(): void {
|
|
104
119
|
const providers = getOAuthProviders();
|
|
105
|
-
this.#
|
|
106
|
-
|
|
120
|
+
if (this.#mode === "logout") {
|
|
121
|
+
// Logout stays unfiltered by `disabledProviders`: a now-disabled
|
|
122
|
+
// provider may still hold stored credentials worth removing.
|
|
123
|
+
this.#allProviders = providers.filter(provider => this.#hasSelectableAuth(provider.id));
|
|
124
|
+
} else {
|
|
125
|
+
const disabled = getDisabledProviderIds();
|
|
126
|
+
// Hide a login entry when either its own id or the provider id it
|
|
127
|
+
// stores credentials under is disabled, so alias logins (e.g.
|
|
128
|
+
// `openai-codex-device` ⇒ `openai-codex`) disappear alongside the
|
|
129
|
+
// model provider they authenticate.
|
|
130
|
+
this.#allProviders = providers.filter(
|
|
131
|
+
provider =>
|
|
132
|
+
!disabled.has(provider.id) &&
|
|
133
|
+
!(provider.storeCredentialsAs && disabled.has(provider.storeCredentialsAs)),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
107
136
|
this.#filteredProviders = this.#allProviders;
|
|
108
137
|
}
|
|
109
138
|
|
|
@@ -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";
|