@oh-my-pi/pi-coding-agent 15.11.0 → 15.11.2
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 -2
- package/dist/cli.js +678 -657
- package/dist/types/capability/mcp.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +49 -4
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
- package/dist/types/extensibility/custom-tools/loader.d.ts +2 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +8 -4
- package/dist/types/extensibility/extensions/types.d.ts +2 -2
- package/dist/types/extensibility/hooks/types.d.ts +8 -4
- package/dist/types/irc/bus.d.ts +15 -2
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +6 -1
- package/dist/types/mcp/transports/stdio.d.ts +1 -0
- package/dist/types/mcp/types.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +30 -0
- package/dist/types/modes/theme/theme.d.ts +3 -2
- package/dist/types/session/agent-session.d.ts +17 -3
- package/dist/types/slash-commands/available-commands.d.ts +34 -0
- package/dist/types/task/index.d.ts +3 -3
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/attach.d.ts +4 -4
- package/dist/types/tools/browser/registry.d.ts +1 -0
- package/dist/types/tools/irc.d.ts +3 -2
- package/dist/types/tools/path-utils.d.ts +0 -4
- package/dist/types/tools/render-utils.d.ts +22 -0
- package/package.json +11 -11
- package/src/capability/mcp.ts +1 -0
- package/src/cli/gallery-cli.ts +5 -4
- package/src/config/mcp-schema.json +4 -0
- package/src/config/settings-schema.ts +55 -4
- package/src/edit/renderer.ts +96 -46
- package/src/exec/bash-executor.ts +21 -6
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +6 -1
- package/src/extensibility/custom-commands/loader.ts +3 -1
- package/src/extensibility/custom-commands/types.ts +6 -3
- package/src/extensibility/custom-tools/loader.ts +4 -7
- package/src/extensibility/custom-tools/types.ts +8 -4
- package/src/extensibility/extensions/loader.ts +2 -1
- package/src/extensibility/extensions/types.ts +2 -2
- package/src/extensibility/hooks/loader.ts +3 -1
- package/src/extensibility/hooks/types.ts +8 -4
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/irc/bus.ts +14 -3
- package/src/lsp/defaults.json +6 -0
- package/src/lsp/render.ts +2 -28
- package/src/mcp/manager.ts +3 -0
- package/src/mcp/oauth-discovery.ts +27 -2
- package/src/mcp/oauth-flow.ts +47 -1
- package/src/mcp/transports/stdio.ts +3 -0
- package/src/mcp/types.ts +2 -0
- package/src/memories/index.ts +2 -0
- package/src/modes/acp/acp-agent.ts +4 -67
- package/src/modes/components/assistant-message.ts +15 -0
- package/src/modes/components/btw-panel.ts +5 -1
- package/src/modes/components/mcp-add-wizard.ts +13 -0
- package/src/modes/components/plan-review-overlay.ts +32 -3
- package/src/modes/components/settings-selector.ts +2 -0
- package/src/modes/components/status-line/component.ts +22 -12
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/transcript-container.ts +99 -18
- package/src/modes/components/tree-selector.ts +6 -1
- package/src/modes/controllers/event-controller.ts +28 -4
- package/src/modes/controllers/mcp-command-controller.ts +34 -2
- package/src/modes/controllers/selector-controller.ts +4 -0
- package/src/modes/controllers/streaming-reveal.ts +16 -8
- package/src/modes/controllers/tool-args-reveal.ts +174 -0
- package/src/modes/interactive-mode.ts +41 -2
- package/src/modes/rpc/rpc-client.ts +32 -0
- package/src/modes/rpc/rpc-mode.ts +82 -7
- package/src/modes/rpc/rpc-types.ts +23 -0
- package/src/modes/theme/theme.ts +13 -7
- package/src/modes/utils/ui-helpers.ts +13 -4
- package/src/prompts/memories/consolidation_system.md +4 -0
- package/src/prompts/system/irc-autoreply.md +6 -0
- package/src/prompts/system/irc-incoming.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/task.md +7 -2
- package/src/session/agent-session.ts +120 -10
- package/src/slash-commands/available-commands.ts +105 -0
- package/src/task/index.ts +15 -10
- package/src/task/render.ts +10 -4
- package/src/tools/bash.ts +5 -1
- package/src/tools/browser/attach.ts +26 -7
- package/src/tools/browser/registry.ts +11 -1
- package/src/tools/irc.ts +16 -4
- package/src/tools/job.ts +7 -3
- package/src/tools/path-utils.ts +22 -15
- package/src/tools/render-utils.ts +56 -0
- package/src/tools/write.ts +65 -47
- package/src/web/search/providers/anthropic.ts +29 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Browser, type Page } from "puppeteer-core";
|
|
2
2
|
/**
|
|
3
3
|
* Allocate an unused TCP port on 127.0.0.1 by binding to port 0 and reading
|
|
4
4
|
* back the kernel-assigned port. There's a small race between close and the
|
|
@@ -17,9 +17,9 @@ export declare function findReusableCdp(exe: string, signal?: AbortSignal): Prom
|
|
|
17
17
|
pid: number;
|
|
18
18
|
} | null>;
|
|
19
19
|
/**
|
|
20
|
-
* Pick the best page target on an attached browser.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Pick the best page target on an attached browser. Prefer discoverable page
|
|
21
|
+
* targets first so Chromium/Edge attach flows that hide pages from
|
|
22
|
+
* `browser.pages()` can still return a usable tab.
|
|
23
23
|
*/
|
|
24
24
|
export declare function pickElectronTarget(browser: Browser, matcher?: string): Promise<Page>;
|
|
25
25
|
/**
|
|
@@ -36,6 +36,7 @@ export interface AcquireBrowserOptions {
|
|
|
36
36
|
signal?: AbortSignal;
|
|
37
37
|
}
|
|
38
38
|
export declare function acquireBrowser(kind: BrowserKind, opts: AcquireBrowserOptions): Promise<BrowserHandle>;
|
|
39
|
+
export declare function normalizeConnectedCdpUrl(rawCdpUrl: string): string;
|
|
39
40
|
export declare function holdBrowser(handle: BrowserHandle): void;
|
|
40
41
|
export declare function releaseBrowser(handle: BrowserHandle, opts: {
|
|
41
42
|
kill: boolean;
|
|
@@ -90,12 +90,13 @@ export declare class IrcTool implements AgentTool<typeof ircSchema, IrcDetails>
|
|
|
90
90
|
type IrcRenderArgs = Partial<IrcParams>;
|
|
91
91
|
/**
|
|
92
92
|
* Display-only transcript card for live IRC traffic: `irc:incoming` DMs
|
|
93
|
-
* delivered to this session
|
|
93
|
+
* delivered to this session, `irc:autoreply` side-channel replies sent on
|
|
94
|
+
* this session's behalf, and `irc:relay` observations of agent↔agent
|
|
94
95
|
* traffic. Shares the tool renderer's glyph + quote-border conventions so
|
|
95
96
|
* cards and `irc` tool output look identical in the transcript.
|
|
96
97
|
*/
|
|
97
98
|
export declare function createIrcMessageCard(card: {
|
|
98
|
-
kind: "incoming" | "relay";
|
|
99
|
+
kind: "incoming" | "autoreply" | "relay";
|
|
99
100
|
from?: string;
|
|
100
101
|
to?: string;
|
|
101
102
|
body?: string;
|
|
@@ -124,10 +124,6 @@ export interface ResolvedMultiFindPattern {
|
|
|
124
124
|
targets: ResolvedFindTarget[];
|
|
125
125
|
scopePath: string;
|
|
126
126
|
}
|
|
127
|
-
/**
|
|
128
|
-
* Split a user path into a base path + glob pattern for tools that delegate to
|
|
129
|
-
* APIs accepting separate `path` and `glob` arguments.
|
|
130
|
-
*/
|
|
131
127
|
export declare function parseSearchPath(filePath: string): ParsedSearchPath;
|
|
132
128
|
/**
|
|
133
129
|
* Async sibling of {@link parseSearchPath} that prefers literal interpretation
|
|
@@ -173,6 +173,28 @@ export declare function capParseErrors(errors: string[] | undefined, limit?: num
|
|
|
173
173
|
export declare function createCachedComponent(getExpanded: () => boolean, compute: (width: number, expanded: boolean) => string[], options?: {
|
|
174
174
|
paddingX?: number;
|
|
175
175
|
}): Component;
|
|
176
|
+
/**
|
|
177
|
+
* Single-slot memo for an expensive rendered string (syntax highlighting, diff
|
|
178
|
+
* coloring) keyed by the exact inputs that shape the bytes: theme instance,
|
|
179
|
+
* expanded state, a caller-chosen salt (path/language), and the source content.
|
|
180
|
+
* Field-wise comparison instead of a concatenated key string: a cache hit costs
|
|
181
|
+
* one string value-compare (engines short-circuit on length) and a miss never
|
|
182
|
+
* allocates a key. Comparing the {@link Theme} by reference is sound because
|
|
183
|
+
* theme switches replace the instance wholesale (`setTheme`/`previewTheme`/
|
|
184
|
+
* `setSymbolPreset` in modes/theme/theme.ts) — themes are never mutated in
|
|
185
|
+
* place.
|
|
186
|
+
*/
|
|
187
|
+
export interface RenderedStringCache {
|
|
188
|
+
theme: Theme | null;
|
|
189
|
+
expanded: boolean;
|
|
190
|
+
salt: string;
|
|
191
|
+
content: string;
|
|
192
|
+
value: string;
|
|
193
|
+
}
|
|
194
|
+
export declare function createRenderedStringCache(): RenderedStringCache;
|
|
195
|
+
/** Drop the memo so the next lookup re-renders (e.g. the render function identity changed). */
|
|
196
|
+
export declare function invalidateRenderedStringCache(cache: RenderedStringCache): void;
|
|
197
|
+
export declare function cachedRenderedString(cache: RenderedStringCache | undefined, theme: Theme, expanded: boolean, salt: string, content: string, render: () => string): string;
|
|
176
198
|
/**
|
|
177
199
|
* Append the indented bullet list of parse errors (capped at
|
|
178
200
|
* {@link PARSE_ERRORS_LIMIT}) to `lines`, with an overflow summary line if the
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "15.11.
|
|
4
|
+
"version": "15.11.2",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -47,16 +47,16 @@
|
|
|
47
47
|
"@agentclientprotocol/sdk": "0.22.1",
|
|
48
48
|
"@babel/parser": "^7.29.7",
|
|
49
49
|
"@mozilla/readability": "^0.6.0",
|
|
50
|
-
"@oh-my-pi/hashline": "15.11.
|
|
51
|
-
"@oh-my-pi/omp-stats": "15.11.
|
|
52
|
-
"@oh-my-pi/pi-agent-core": "15.11.
|
|
53
|
-
"@oh-my-pi/pi-ai": "15.11.
|
|
54
|
-
"@oh-my-pi/pi-catalog": "15.11.
|
|
55
|
-
"@oh-my-pi/pi-mnemopi": "15.11.
|
|
56
|
-
"@oh-my-pi/pi-natives": "15.11.
|
|
57
|
-
"@oh-my-pi/pi-tui": "15.11.
|
|
58
|
-
"@oh-my-pi/pi-utils": "15.11.
|
|
59
|
-
"@oh-my-pi/snapcompact": "15.11.
|
|
50
|
+
"@oh-my-pi/hashline": "15.11.2",
|
|
51
|
+
"@oh-my-pi/omp-stats": "15.11.2",
|
|
52
|
+
"@oh-my-pi/pi-agent-core": "15.11.2",
|
|
53
|
+
"@oh-my-pi/pi-ai": "15.11.2",
|
|
54
|
+
"@oh-my-pi/pi-catalog": "15.11.2",
|
|
55
|
+
"@oh-my-pi/pi-mnemopi": "15.11.2",
|
|
56
|
+
"@oh-my-pi/pi-natives": "15.11.2",
|
|
57
|
+
"@oh-my-pi/pi-tui": "15.11.2",
|
|
58
|
+
"@oh-my-pi/pi-utils": "15.11.2",
|
|
59
|
+
"@oh-my-pi/snapcompact": "15.11.2",
|
|
60
60
|
"@opentelemetry/api": "^1.9.1",
|
|
61
61
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
62
62
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
package/src/capability/mcp.ts
CHANGED
|
@@ -36,6 +36,7 @@ export interface MCPServer {
|
|
|
36
36
|
tokenUrl?: string;
|
|
37
37
|
clientId?: string;
|
|
38
38
|
clientSecret?: string;
|
|
39
|
+
resource?: string;
|
|
39
40
|
};
|
|
40
41
|
/** OAuth configuration (clientId, clientSecret, redirectUri, callbackPort, callbackPath) for servers requiring explicit client credentials */
|
|
41
42
|
oauth?: {
|
package/src/cli/gallery-cli.ts
CHANGED
|
@@ -111,10 +111,11 @@ export async function renderGalleryState(
|
|
|
111
111
|
|
|
112
112
|
const tool = fakeToolFor(name, fixture);
|
|
113
113
|
const streamingArgs = state === "streaming" ? (fixture.streamingArgs ?? fixture.args) : fixture.args;
|
|
114
|
-
// The component only calls `requestRender`
|
|
115
|
-
// `imageBudget` is consulted solely
|
|
116
|
-
// disables. A cast avoids
|
|
117
|
-
|
|
114
|
+
// The component only calls `requestRender`/`requestComponentRender` (via
|
|
115
|
+
// its loader) during a static render; `imageBudget` is consulted solely
|
|
116
|
+
// when images render, which the gallery disables. A cast avoids
|
|
117
|
+
// constructing a real terminal.
|
|
118
|
+
const ui = { requestRender() {}, requestComponentRender() {} } as unknown as TUI;
|
|
118
119
|
const component = new ToolExecutionComponent(name, streamingArgs, { showImages: false }, tool, ui, getProjectDir());
|
|
119
120
|
component.setExpanded(expanded);
|
|
120
121
|
|
|
@@ -452,6 +452,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
452
452
|
description: "Use the session name color for the editor border and status line gap",
|
|
453
453
|
},
|
|
454
454
|
},
|
|
455
|
+
|
|
456
|
+
"statusLine.transparent": {
|
|
457
|
+
type: "boolean",
|
|
458
|
+
default: false,
|
|
459
|
+
ui: {
|
|
460
|
+
tab: "appearance",
|
|
461
|
+
label: "Transparent Status Line",
|
|
462
|
+
description:
|
|
463
|
+
"Use the terminal's default background for the status line instead of the theme's `statusLineBg`. Powerline end caps are dropped because they need a contrasting fill to bridge into the surrounding terminal.",
|
|
464
|
+
},
|
|
465
|
+
},
|
|
455
466
|
"tools.artifactSpillThreshold": {
|
|
456
467
|
type: "number",
|
|
457
468
|
default: 50,
|
|
@@ -668,7 +679,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
668
679
|
ui: {
|
|
669
680
|
tab: "appearance",
|
|
670
681
|
label: "Smooth Streaming",
|
|
671
|
-
description: "Reveal assistant text smoothly while
|
|
682
|
+
description: "Reveal assistant text and streamed tool input smoothly while chunks arrive",
|
|
672
683
|
},
|
|
673
684
|
},
|
|
674
685
|
|
|
@@ -1082,6 +1093,46 @@ export const SETTINGS_SCHEMA = {
|
|
|
1082
1093
|
ui: { tab: "interaction", label: "Collapse Changelog", description: "Show condensed changelog after updates" },
|
|
1083
1094
|
},
|
|
1084
1095
|
|
|
1096
|
+
"magicKeywords.enabled": {
|
|
1097
|
+
type: "boolean",
|
|
1098
|
+
default: true,
|
|
1099
|
+
ui: {
|
|
1100
|
+
tab: "interaction",
|
|
1101
|
+
label: "Magic Keywords",
|
|
1102
|
+
description: "Enable hidden notices for standalone ultrathink, orchestrate, and workflowz keywords",
|
|
1103
|
+
},
|
|
1104
|
+
},
|
|
1105
|
+
|
|
1106
|
+
"magicKeywords.ultrathink": {
|
|
1107
|
+
type: "boolean",
|
|
1108
|
+
default: true,
|
|
1109
|
+
ui: {
|
|
1110
|
+
tab: "interaction",
|
|
1111
|
+
label: "Ultrathink Keyword",
|
|
1112
|
+
description: "Let standalone ultrathink request maximum automatic thinking and append its hidden notice",
|
|
1113
|
+
},
|
|
1114
|
+
},
|
|
1115
|
+
|
|
1116
|
+
"magicKeywords.orchestrate": {
|
|
1117
|
+
type: "boolean",
|
|
1118
|
+
default: true,
|
|
1119
|
+
ui: {
|
|
1120
|
+
tab: "interaction",
|
|
1121
|
+
label: "Orchestrate Keyword",
|
|
1122
|
+
description: "Let standalone orchestrate append its hidden multi-agent orchestration notice",
|
|
1123
|
+
},
|
|
1124
|
+
},
|
|
1125
|
+
|
|
1126
|
+
"magicKeywords.workflow": {
|
|
1127
|
+
type: "boolean",
|
|
1128
|
+
default: true,
|
|
1129
|
+
ui: {
|
|
1130
|
+
tab: "interaction",
|
|
1131
|
+
label: "Workflow Keyword",
|
|
1132
|
+
description: "Let standalone workflowz append its hidden eval workflow notice",
|
|
1133
|
+
},
|
|
1134
|
+
},
|
|
1135
|
+
|
|
1085
1136
|
// Notifications
|
|
1086
1137
|
"completion.notify": {
|
|
1087
1138
|
type: "enum",
|
|
@@ -2501,11 +2552,11 @@ export const SETTINGS_SCHEMA = {
|
|
|
2501
2552
|
// Async jobs
|
|
2502
2553
|
"async.enabled": {
|
|
2503
2554
|
type: "boolean",
|
|
2504
|
-
default:
|
|
2555
|
+
default: true,
|
|
2505
2556
|
ui: {
|
|
2506
2557
|
tab: "tools",
|
|
2507
2558
|
label: "Async Execution",
|
|
2508
|
-
description: "Enable async bash commands",
|
|
2559
|
+
description: "Enable async bash commands and background task execution",
|
|
2509
2560
|
},
|
|
2510
2561
|
},
|
|
2511
2562
|
|
|
@@ -2758,7 +2809,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
2758
2809
|
tab: "tasks",
|
|
2759
2810
|
label: "Batch Task Calls",
|
|
2760
2811
|
description:
|
|
2761
|
-
"Switch the task tool to its batch shape: one call carries { agent, context, tasks[] } — one subagent per item (with per-item isolation) and a required shared context prepended to every assignment.
|
|
2812
|
+
"Switch the task tool to its batch shape: one call carries { agent, context, tasks[] } — one subagent per item (with per-item isolation) and a required shared context prepended to every assignment. With async.enabled=true, each spawn runs as an independent background agent with the normal idle/parked lifecycle; otherwise the call blocks for merged results. Disable to restore the flat single-spawn schema.",
|
|
2762
2813
|
},
|
|
2763
2814
|
},
|
|
2764
2815
|
|
package/src/edit/renderer.ts
CHANGED
|
@@ -12,13 +12,17 @@ import { renderDiff as renderDiffColored } from "../modes/components/diff";
|
|
|
12
12
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
13
13
|
import type { OutputMeta } from "../tools/output-meta";
|
|
14
14
|
import {
|
|
15
|
+
cachedRenderedString,
|
|
16
|
+
createRenderedStringCache,
|
|
15
17
|
formatDiagnostics,
|
|
16
18
|
formatExpandHint,
|
|
17
19
|
formatStatusIcon,
|
|
18
20
|
getDiffStats,
|
|
19
21
|
getLspBatchRequest,
|
|
22
|
+
invalidateRenderedStringCache,
|
|
20
23
|
type LspBatchRequest,
|
|
21
24
|
PREVIEW_LIMITS,
|
|
25
|
+
type RenderedStringCache,
|
|
22
26
|
replaceTabs,
|
|
23
27
|
shortenPath,
|
|
24
28
|
truncateDiffByHunk,
|
|
@@ -138,6 +142,26 @@ export interface EditRenderContext {
|
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
const EDIT_STREAMING_PREVIEW_LINES = 12;
|
|
145
|
+
|
|
146
|
+
function plainDiffRender(diffText: string): string {
|
|
147
|
+
return diffText;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Lazily grown per-file preview cache slots: the file count of a streaming
|
|
152
|
+
* multi-file patch is discovered mid-stream, so a fixed-size array would
|
|
153
|
+
* silently bypass caching for late files.
|
|
154
|
+
*/
|
|
155
|
+
function previewCacheAt(caches: RenderedStringCache[] | undefined, index: number): RenderedStringCache | undefined {
|
|
156
|
+
if (!caches) return undefined;
|
|
157
|
+
let cache = caches[index];
|
|
158
|
+
if (cache === undefined) {
|
|
159
|
+
cache = createRenderedStringCache();
|
|
160
|
+
caches[index] = cache;
|
|
161
|
+
}
|
|
162
|
+
return cache;
|
|
163
|
+
}
|
|
164
|
+
|
|
141
165
|
const CALL_TEXT_PREVIEW_LINES = 6;
|
|
142
166
|
const CALL_TEXT_PREVIEW_WIDTH = 80;
|
|
143
167
|
|
|
@@ -313,7 +337,6 @@ function renderPlainTextPreview(text: string, uiTheme: Theme, filePath?: string)
|
|
|
313
337
|
}
|
|
314
338
|
return preview.trimEnd();
|
|
315
339
|
}
|
|
316
|
-
|
|
317
340
|
function formatStreamingDiff(
|
|
318
341
|
diff: string,
|
|
319
342
|
rawPath: string,
|
|
@@ -321,26 +344,30 @@ function formatStreamingDiff(
|
|
|
321
344
|
expanded: boolean,
|
|
322
345
|
label = "streaming",
|
|
323
346
|
spinnerFrame?: number,
|
|
347
|
+
cache?: RenderedStringCache,
|
|
324
348
|
): string {
|
|
325
349
|
if (!diff) return "";
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
350
|
+
let text = cachedRenderedString(cache, uiTheme, expanded, rawPath, diff, () => {
|
|
351
|
+
// Collapsed uses a "Cursor" tail window: pin the last
|
|
352
|
+
// EDIT_STREAMING_PREVIEW_LINES rows to the bottom so freshly streamed changes
|
|
353
|
+
// stay on screen. The whole-file diff is recomputed on every streamed chunk
|
|
354
|
+
// and its Myers alignment is not monotonic in payload length, so a hunk-aware
|
|
355
|
+
// window stutters as rows move between hunks. Expanded deliberately lifts that
|
|
356
|
+
// cap for the approval-time full view.
|
|
357
|
+
const allLines = diff.replace(/\n+$/u, "").split("\n");
|
|
358
|
+
const hiddenLines = expanded ? 0 : Math.max(0, allLines.length - EDIT_STREAMING_PREVIEW_LINES);
|
|
359
|
+
const visible = hiddenLines > 0 ? allLines.slice(hiddenLines) : allLines;
|
|
360
|
+
let rendered = "\n\n";
|
|
361
|
+
if (hiddenLines > 0) {
|
|
362
|
+
const hiddenHunks = getDiffStats(allLines.slice(0, hiddenLines).join("\n")).hunks;
|
|
363
|
+
const remainder: string[] = [];
|
|
364
|
+
if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
|
|
365
|
+
remainder.push(`${hiddenLines} more lines`);
|
|
366
|
+
rendered += `${uiTheme.fg("dim", `… (${remainder.join(", ")} above)`)}\n`;
|
|
367
|
+
}
|
|
368
|
+
rendered += renderDiffColored(visible.join("\n"), { filePath: rawPath });
|
|
369
|
+
return rendered;
|
|
370
|
+
});
|
|
344
371
|
// The animated glyph rides this trailing line — inside the transcript's
|
|
345
372
|
// volatile-tail holdback — never the block header: an animating head row
|
|
346
373
|
// pins the native-scrollback commit boundary at the top of the block, so a
|
|
@@ -360,9 +387,11 @@ function formatMultiFileStreamingDiff(
|
|
|
360
387
|
uiTheme: Theme,
|
|
361
388
|
expanded: boolean,
|
|
362
389
|
spinnerFrame?: number,
|
|
390
|
+
caches?: RenderedStringCache[],
|
|
363
391
|
): string {
|
|
364
392
|
const parts: string[] = [];
|
|
365
|
-
for (
|
|
393
|
+
for (let index = 0; index < previews.length; index++) {
|
|
394
|
+
const preview = previews[index]!;
|
|
366
395
|
if (!preview.diff && !preview.error) continue;
|
|
367
396
|
const header = uiTheme.fg("dim", `\n\n── ${shortenPath(preview.path)} ──`);
|
|
368
397
|
if (preview.error) {
|
|
@@ -373,9 +402,10 @@ function formatMultiFileStreamingDiff(
|
|
|
373
402
|
// Only the last file's preview carries the animated streaming glyph;
|
|
374
403
|
// earlier files have settled and must stay byte-stable so their rows
|
|
375
404
|
// can commit to native scrollback mid-stream.
|
|
376
|
-
const isLast =
|
|
405
|
+
const isLast = index === previews.length - 1;
|
|
406
|
+
const cache = previewCacheAt(caches, index);
|
|
377
407
|
parts.push(
|
|
378
|
-
`${header}${formatStreamingDiff(preview.diff, preview.path, uiTheme, expanded, "preview", isLast ? spinnerFrame : undefined)}`,
|
|
408
|
+
`${header}${formatStreamingDiff(preview.diff, preview.path, uiTheme, expanded, "preview", isLast ? spinnerFrame : undefined, cache)}`,
|
|
379
409
|
);
|
|
380
410
|
}
|
|
381
411
|
}
|
|
@@ -389,16 +419,18 @@ function getCallPreview(
|
|
|
389
419
|
renderContext: EditRenderContext | undefined,
|
|
390
420
|
expanded: boolean,
|
|
391
421
|
spinnerFrame?: number,
|
|
422
|
+
caches?: RenderedStringCache[],
|
|
392
423
|
): string {
|
|
393
424
|
const multi = renderContext?.perFileDiffPreview;
|
|
394
425
|
if (multi && multi.length > 1 && multi.some(p => p.diff || p.error)) {
|
|
395
|
-
return formatMultiFileStreamingDiff(multi, uiTheme, expanded, spinnerFrame);
|
|
426
|
+
return formatMultiFileStreamingDiff(multi, uiTheme, expanded, spinnerFrame, caches);
|
|
396
427
|
}
|
|
428
|
+
const cache = previewCacheAt(caches, 0);
|
|
397
429
|
if (args.previewDiff) {
|
|
398
|
-
return formatStreamingDiff(args.previewDiff, rawPath, uiTheme, expanded, "preview", spinnerFrame);
|
|
430
|
+
return formatStreamingDiff(args.previewDiff, rawPath, uiTheme, expanded, "preview", spinnerFrame, cache);
|
|
399
431
|
}
|
|
400
432
|
if (args.diff && args.op) {
|
|
401
|
-
return formatStreamingDiff(args.diff, rawPath, uiTheme, expanded, "streaming", spinnerFrame);
|
|
433
|
+
return formatStreamingDiff(args.diff, rawPath, uiTheme, expanded, "streaming", spinnerFrame, cache);
|
|
402
434
|
}
|
|
403
435
|
if (args.diff) {
|
|
404
436
|
return renderPlainTextPreview(args.diff, uiTheme, rawPath);
|
|
@@ -492,30 +524,32 @@ function formatDiffStatsSuffix(diff: string, uiTheme: Theme): string {
|
|
|
492
524
|
].filter(value => value !== undefined);
|
|
493
525
|
return ` ${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${stats.join(uiTheme.fg("dim", "/"))}${uiTheme.fg("dim", uiTheme.format.bracketRight)}`;
|
|
494
526
|
}
|
|
495
|
-
|
|
496
527
|
function renderDiffSection(
|
|
497
528
|
diff: string,
|
|
498
529
|
rawPath: string,
|
|
499
530
|
expanded: boolean,
|
|
500
531
|
uiTheme: Theme,
|
|
501
532
|
renderDiffFn: (t: string, o?: { filePath?: string }) => string,
|
|
533
|
+
cache?: RenderedStringCache,
|
|
502
534
|
): string {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
535
|
+
return cachedRenderedString(cache, uiTheme, expanded, rawPath, diff, () => {
|
|
536
|
+
const {
|
|
537
|
+
text: truncatedDiff,
|
|
538
|
+
hiddenHunks,
|
|
539
|
+
hiddenLines,
|
|
540
|
+
} = expanded
|
|
541
|
+
? { text: diff, hiddenHunks: 0, hiddenLines: 0 }
|
|
542
|
+
: truncateDiffByHunk(diff, PREVIEW_LIMITS.DIFF_COLLAPSED_HUNKS, PREVIEW_LIMITS.DIFF_COLLAPSED_LINES);
|
|
543
|
+
|
|
544
|
+
let text = `\n${renderDiffFn(truncatedDiff, { filePath: rawPath })}`;
|
|
545
|
+
if (!expanded && (hiddenHunks > 0 || hiddenLines > 0)) {
|
|
546
|
+
const remainder: string[] = [];
|
|
547
|
+
if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
|
|
548
|
+
if (hiddenLines > 0) remainder.push(`${hiddenLines} more lines`);
|
|
549
|
+
text += uiTheme.fg("toolOutput", `\n… (${remainder.join(", ")}) ${formatExpandHint(uiTheme)}`);
|
|
550
|
+
}
|
|
551
|
+
return text;
|
|
552
|
+
});
|
|
519
553
|
}
|
|
520
554
|
|
|
521
555
|
function wrapEditRendererLine(line: string, width: number): string[] {
|
|
@@ -574,6 +608,7 @@ export const editToolRenderer = {
|
|
|
574
608
|
if (Array.isArray(editArgs.edits)) {
|
|
575
609
|
fileCount = countEditFiles(editArgs.edits);
|
|
576
610
|
}
|
|
611
|
+
const callPreviewCaches: RenderedStringCache[] = [];
|
|
577
612
|
return framedBlock(uiTheme, width => {
|
|
578
613
|
// Static pending icon, never the animated glyph: the header is the
|
|
579
614
|
// head row of the framed block, and native-scrollback commits are
|
|
@@ -588,7 +623,15 @@ export const editToolRenderer = {
|
|
|
588
623
|
rename,
|
|
589
624
|
extraSuffix: fileCount > 1 ? uiTheme.fg("dim", ` (+${fileCount - 1} more)`) : undefined,
|
|
590
625
|
});
|
|
591
|
-
let body = getCallPreview(
|
|
626
|
+
let body = getCallPreview(
|
|
627
|
+
editArgs,
|
|
628
|
+
rawPath,
|
|
629
|
+
uiTheme,
|
|
630
|
+
renderContext,
|
|
631
|
+
options.expanded,
|
|
632
|
+
options?.spinnerFrame,
|
|
633
|
+
callPreviewCaches,
|
|
634
|
+
);
|
|
592
635
|
if (applyPatchSummary?.error) {
|
|
593
636
|
body += `\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error, rawPath), Math.max(1, width - 2)))}`;
|
|
594
637
|
}
|
|
@@ -652,11 +695,18 @@ function renderSingleFileResult(
|
|
|
652
695
|
(result.content?.find(c => c.type === "text")?.text ?? "")
|
|
653
696
|
: "";
|
|
654
697
|
|
|
698
|
+
let diffSectionRenderDiffFn: ((t: string, o?: { filePath?: string }) => string) | undefined;
|
|
699
|
+
const diffSectionCache = createRenderedStringCache();
|
|
700
|
+
|
|
655
701
|
return framedBlock(uiTheme, width => {
|
|
656
702
|
const { expanded, renderContext } = options;
|
|
657
703
|
const editDiffPreview = renderContext?.editDiffPreview;
|
|
658
|
-
const renderDiffFn = renderContext?.renderDiff ??
|
|
704
|
+
const renderDiffFn = renderContext?.renderDiff ?? plainDiffRender;
|
|
659
705
|
|
|
706
|
+
if (diffSectionRenderDiffFn !== renderDiffFn) {
|
|
707
|
+
diffSectionRenderDiffFn = renderDiffFn;
|
|
708
|
+
invalidateRenderedStringCache(diffSectionCache);
|
|
709
|
+
}
|
|
660
710
|
const firstChangedLine =
|
|
661
711
|
(editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
|
|
662
712
|
(details && !isError ? details.firstChangedLine : undefined);
|
|
@@ -681,11 +731,11 @@ function renderSingleFileResult(
|
|
|
681
731
|
if (isError) {
|
|
682
732
|
if (errorText) body = uiTheme.fg("error", replaceTabs(errorText, rawPath));
|
|
683
733
|
} else if (details?.diff) {
|
|
684
|
-
body = renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn);
|
|
734
|
+
body = renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn, diffSectionCache);
|
|
685
735
|
} else if (editDiffPreview) {
|
|
686
736
|
if ("error" in editDiffPreview) body = uiTheme.fg("error", replaceTabs(editDiffPreview.error, rawPath));
|
|
687
737
|
else if (editDiffPreview.diff)
|
|
688
|
-
body = renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn);
|
|
738
|
+
body = renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn, diffSectionCache);
|
|
689
739
|
}
|
|
690
740
|
if (details?.diagnostics) {
|
|
691
741
|
body += formatDiagnostics(details.diagnostics, expanded, uiTheme, (fp: string) =>
|
|
@@ -56,6 +56,8 @@ export interface BashResult {
|
|
|
56
56
|
const shellSessions = new Map<string, Shell>();
|
|
57
57
|
const brokenShellSessions = new Set<string>();
|
|
58
58
|
const shellSessionQuarantines = new Map<string, Promise<unknown>>();
|
|
59
|
+
/** Session keys with a command currently in flight on the persistent Shell. */
|
|
60
|
+
const shellSessionsInUse = new Set<string>();
|
|
59
61
|
|
|
60
62
|
function quarantineShellSession(
|
|
61
63
|
sessionKey: string,
|
|
@@ -223,8 +225,14 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
223
225
|
shellSessions.delete(sessionKey);
|
|
224
226
|
}
|
|
225
227
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
+
// A persistent Shell runs one command at a time (the native session is a
|
|
229
|
+
// mutex-guarded queue and `abort()` kills every in-flight run on it). When
|
|
230
|
+
// parallel bash calls overlap on the same key, the first one owns the
|
|
231
|
+
// persistent session; the rest degrade to isolated one-shot shells — the
|
|
232
|
+
// same path quarantined sessions take.
|
|
233
|
+
const sessionBusy = shellSessionsInUse.has(sessionKey);
|
|
234
|
+
let shellSession = persistentSessionBroken || sessionBusy ? undefined : shellSessions.get(sessionKey);
|
|
235
|
+
if (!shellSession && !persistentSessionBroken && !sessionBusy) {
|
|
228
236
|
shellSession = new Shell({
|
|
229
237
|
sessionEnv: shellEnv,
|
|
230
238
|
snapshotPath: snapshotPath ?? undefined,
|
|
@@ -232,6 +240,10 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
232
240
|
});
|
|
233
241
|
shellSessions.set(sessionKey, shellSession);
|
|
234
242
|
}
|
|
243
|
+
const ownsPersistentSession = shellSession !== undefined;
|
|
244
|
+
if (ownsPersistentSession) {
|
|
245
|
+
shellSessionsInUse.add(sessionKey);
|
|
246
|
+
}
|
|
235
247
|
const userSignal = options?.signal;
|
|
236
248
|
const runAbortController = new AbortController();
|
|
237
249
|
let abortCleanupPromise: Promise<void> | undefined;
|
|
@@ -393,10 +405,13 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
393
405
|
if (userSignal) {
|
|
394
406
|
userSignal.removeEventListener("abort", abortHandler);
|
|
395
407
|
}
|
|
396
|
-
if (
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
408
|
+
if (ownsPersistentSession) {
|
|
409
|
+
shellSessionsInUse.delete(sessionKey);
|
|
410
|
+
if (resetSession || options?.sessionKey?.includes(":async:")) {
|
|
411
|
+
// `:async:` keys are per-job (jobId is unique), so the Shell would
|
|
412
|
+
// otherwise stay in the process-global map forever after completion.
|
|
413
|
+
shellSessions.delete(sessionKey);
|
|
414
|
+
}
|
|
400
415
|
}
|
|
401
416
|
}
|
|
402
417
|
}
|