@oh-my-pi/pi-coding-agent 15.10.7 → 15.10.9
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 +27 -0
- package/dist/types/config/model-registry.d.ts +4 -2
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/extensibility/custom-tools/loader.d.ts +22 -3
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -1
- package/dist/types/extensibility/extensions/index.d.ts +1 -1
- package/dist/types/extensibility/extensions/loader.d.ts +17 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +8 -0
- package/dist/types/mcp/oauth-discovery.d.ts +4 -1
- package/dist/types/mcp/oauth-flow.d.ts +6 -1
- package/dist/types/mcp/transports/stdio.d.ts +12 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -2
- package/dist/types/sdk.d.ts +42 -2
- package/dist/types/task/executor.d.ts +16 -0
- package/dist/types/tools/fetch.d.ts +2 -1
- package/dist/types/tools/index.d.ts +20 -1
- package/dist/types/tools/report-tool-issue.d.ts +5 -0
- package/dist/types/tui/hyperlink.d.ts +8 -0
- package/dist/types/web/kagi.d.ts +2 -1
- package/dist/types/web/parallel.d.ts +3 -0
- package/dist/types/web/search/providers/anthropic.d.ts +2 -1
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/brave.d.ts +2 -1
- package/dist/types/web/search/providers/codex.d.ts +2 -1
- package/dist/types/web/search/providers/exa.d.ts +2 -1
- package/dist/types/web/search/providers/gemini.d.ts +2 -1
- package/dist/types/web/search/providers/jina.d.ts +7 -2
- package/dist/types/web/search/providers/kagi.d.ts +7 -2
- package/dist/types/web/search/providers/kimi.d.ts +7 -2
- package/dist/types/web/search/providers/parallel.d.ts +2 -1
- package/dist/types/web/search/providers/perplexity.d.ts +2 -1
- package/dist/types/web/search/providers/searxng.d.ts +2 -1
- package/dist/types/web/search/providers/synthetic.d.ts +7 -3
- package/dist/types/web/search/providers/tavily.d.ts +2 -1
- package/dist/types/web/search/providers/zai.d.ts +2 -1
- package/package.json +9 -9
- package/src/config/model-registry.ts +13 -7
- package/src/config/model-resolver.ts +57 -2
- package/src/config/settings-schema.ts +6 -0
- package/src/extensibility/custom-tools/loader.ts +43 -19
- package/src/extensibility/custom-tools/types.ts +3 -1
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/loader.ts +29 -6
- package/src/extensibility/plugins/legacy-pi-compat.ts +30 -6
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/mcp/oauth-discovery.ts +8 -3
- package/src/mcp/oauth-flow.ts +12 -5
- package/src/mcp/transports/stdio.ts +139 -3
- package/src/modes/components/assistant-message.ts +28 -6
- package/src/modes/components/custom-editor.ts +69 -9
- package/src/modes/components/transcript-container.ts +77 -25
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/mcp-command-controller.ts +2 -2
- package/src/sdk.ts +138 -56
- package/src/ssh/ssh-executor.ts +60 -4
- package/src/task/executor.ts +19 -0
- package/src/task/index.ts +4 -0
- package/src/tools/fetch.ts +22 -5
- package/src/tools/image-gen.ts +33 -11
- package/src/tools/index.ts +21 -2
- package/src/tools/report-tool-issue.ts +7 -1
- package/src/tui/hyperlink.ts +27 -3
- package/src/web/kagi.ts +5 -2
- package/src/web/parallel.ts +7 -3
- package/src/web/search/providers/anthropic.ts +5 -1
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/brave.ts +5 -2
- package/src/web/search/providers/codex.ts +6 -2
- package/src/web/search/providers/exa.ts +91 -8
- package/src/web/search/providers/gemini.ts +6 -0
- package/src/web/search/providers/jina.ts +15 -5
- package/src/web/search/providers/kagi.ts +9 -2
- package/src/web/search/providers/kimi.ts +18 -4
- package/src/web/search/providers/parallel.ts +6 -2
- package/src/web/search/providers/perplexity.ts +7 -4
- package/src/web/search/providers/searxng.ts +6 -2
- package/src/web/search/providers/synthetic.ts +9 -5
- package/src/web/search/providers/tavily.ts +4 -2
- package/src/web/search/providers/zai.ts +15 -4
|
@@ -5,7 +5,16 @@ import { settings } from "../../config/settings";
|
|
|
5
5
|
import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
|
|
6
6
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
7
7
|
import { resolveAbortLabel, shouldRenderAbortReason } from "../../session/messages";
|
|
8
|
-
import { resolveImageOptions } from "../../tools/render-utils";
|
|
8
|
+
import { getPreviewLines, resolveImageOptions, TRUNCATE_LENGTHS } from "../../tools/render-utils";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Max lines of a turn-ending provider error rendered inline in the transcript.
|
|
12
|
+
* Bounds pathological error bodies — e.g. a proxy 502 whose body is a full HTML
|
|
13
|
+
* page — so they can't flood the scrollback. Blank lines are dropped and each
|
|
14
|
+
* line is width-truncated by {@link getPreviewLines}. Full text is still kept in
|
|
15
|
+
* the persisted session.
|
|
16
|
+
*/
|
|
17
|
+
const MAX_TRANSCRIPT_ERROR_LINES = 8;
|
|
9
18
|
|
|
10
19
|
/**
|
|
11
20
|
* Component that renders a complete assistant message
|
|
@@ -78,6 +87,22 @@ export class AssistantMessageComponent extends Container {
|
|
|
78
87
|
this.#transcriptBlockFinalized = true;
|
|
79
88
|
}
|
|
80
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Render a turn-ending provider error inline. Drops blank lines, clamps the
|
|
92
|
+
* line count to {@link MAX_TRANSCRIPT_ERROR_LINES}, and width-truncates each
|
|
93
|
+
* line so a pathological body — e.g. the HTML page a proxy returns on a 502 —
|
|
94
|
+
* can't flood the transcript. Mirrors {@link ErrorBannerComponent}.
|
|
95
|
+
*/
|
|
96
|
+
#appendErrorBlock(message: string): void {
|
|
97
|
+
const lines = getPreviewLines(message, MAX_TRANSCRIPT_ERROR_LINES, TRUNCATE_LENGTHS.LINE);
|
|
98
|
+
if (lines.length === 0) lines.push("Unknown error");
|
|
99
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
100
|
+
this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${lines[0]}`), 1, 0));
|
|
101
|
+
for (const line of lines.slice(1)) {
|
|
102
|
+
this.#contentContainer.addChild(new Text(theme.fg("error", ` ${line}`), 1, 0));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
81
106
|
setToolResultImages(toolCallId: string, images: ImageContent[]): void {
|
|
82
107
|
if (!toolCallId) return;
|
|
83
108
|
const validImages = images.filter(img => img.type === "image" && img.data && img.mimeType);
|
|
@@ -249,9 +274,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
249
274
|
}
|
|
250
275
|
this.#contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
|
|
251
276
|
} else if (message.stopReason === "error" && !this.#errorPinned) {
|
|
252
|
-
|
|
253
|
-
this.#contentContainer.addChild(new Spacer(1));
|
|
254
|
-
this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
|
277
|
+
this.#appendErrorBlock(message.errorMessage || "Unknown error");
|
|
255
278
|
}
|
|
256
279
|
}
|
|
257
280
|
if (
|
|
@@ -260,8 +283,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
260
283
|
message.stopReason !== "aborted" &&
|
|
261
284
|
message.stopReason !== "error"
|
|
262
285
|
) {
|
|
263
|
-
this.#
|
|
264
|
-
this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${message.errorMessage}`), 1, 0));
|
|
286
|
+
this.#appendErrorBlock(message.errorMessage);
|
|
265
287
|
}
|
|
266
288
|
|
|
267
289
|
// Token usage metadata
|
|
@@ -58,16 +58,72 @@ function buildMatchKeys(keys: readonly KeyId[]): Set<string> {
|
|
|
58
58
|
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
59
59
|
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
60
60
|
const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i;
|
|
61
|
+
const BRACKETED_IMAGE_PATH_BOUNDARY_REGEX = /\.(?:png|jpe?g|gif|webp)(?=$|["']?\s)/gi;
|
|
62
|
+
const SHELL_ESCAPED_PATH_CHAR_REGEX = /\\([\\\s'"()[\]{}&;<>|?*!$`])/g;
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
function isPastedPathSeparator(char: string | undefined): boolean {
|
|
65
|
+
return char === undefined || char === " " || char === "\t" || char === "\r" || char === "\n";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function imagePathBoundaryEnd(payload: string, segmentStart: number, extensionEnd: number): number | undefined {
|
|
69
|
+
const quote = payload[segmentStart];
|
|
70
|
+
const afterExtension = payload[extensionEnd];
|
|
71
|
+
if (quote === '"' || quote === "'") {
|
|
72
|
+
return afterExtension === quote && isPastedPathSeparator(payload[extensionEnd + 1])
|
|
73
|
+
? extensionEnd + 1
|
|
74
|
+
: undefined;
|
|
75
|
+
}
|
|
76
|
+
if (isPastedPathSeparator(afterExtension)) return extensionEnd;
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizePastedImagePath(path: string): string {
|
|
81
|
+
const trimmed = path.trim();
|
|
82
|
+
const first = trimmed[0];
|
|
83
|
+
const last = trimmed[trimmed.length - 1];
|
|
84
|
+
const unquoted =
|
|
85
|
+
trimmed.length > 1 && (first === '"' || first === "'") && last === first ? trimmed.slice(1, -1) : trimmed;
|
|
86
|
+
return unquoted.replace(SHELL_ESCAPED_PATH_CHAR_REGEX, "$1");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function extractBracketedImagePastePaths(data: string): string[] | undefined {
|
|
63
90
|
if (!data.startsWith(BRACKETED_PASTE_START)) return undefined;
|
|
64
91
|
const endIndex = data.indexOf(BRACKETED_PASTE_END, BRACKETED_PASTE_START.length);
|
|
65
92
|
if (endIndex === -1 || endIndex + BRACKETED_PASTE_END.length !== data.length) return undefined;
|
|
66
93
|
|
|
67
94
|
const pasted = data.slice(BRACKETED_PASTE_START.length, endIndex).trim();
|
|
68
|
-
if (!pasted
|
|
69
|
-
|
|
70
|
-
|
|
95
|
+
if (!pasted) return undefined;
|
|
96
|
+
|
|
97
|
+
const paths: string[] = [];
|
|
98
|
+
let segmentStart = 0;
|
|
99
|
+
BRACKETED_IMAGE_PATH_BOUNDARY_REGEX.lastIndex = 0;
|
|
100
|
+
for (
|
|
101
|
+
let match = BRACKETED_IMAGE_PATH_BOUNDARY_REGEX.exec(pasted);
|
|
102
|
+
match;
|
|
103
|
+
match = BRACKETED_IMAGE_PATH_BOUNDARY_REGEX.exec(pasted)
|
|
104
|
+
) {
|
|
105
|
+
const extensionEnd = match.index + match[0].length;
|
|
106
|
+
const boundaryEnd = imagePathBoundaryEnd(pasted, segmentStart, extensionEnd);
|
|
107
|
+
if (boundaryEnd === undefined) continue;
|
|
108
|
+
|
|
109
|
+
const path = normalizePastedImagePath(pasted.slice(segmentStart, boundaryEnd));
|
|
110
|
+
if (!path || !BRACKETED_IMAGE_PATH_REGEX.test(path)) return undefined;
|
|
111
|
+
paths.push(path);
|
|
112
|
+
|
|
113
|
+
segmentStart = boundaryEnd;
|
|
114
|
+
while (segmentStart < pasted.length && isPastedPathSeparator(pasted[segmentStart])) {
|
|
115
|
+
segmentStart++;
|
|
116
|
+
}
|
|
117
|
+
BRACKETED_IMAGE_PATH_BOUNDARY_REGEX.lastIndex = segmentStart;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (paths.length === 0 || segmentStart !== pasted.length) return undefined;
|
|
121
|
+
return paths;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function extractBracketedImagePastePath(data: string): string | undefined {
|
|
125
|
+
const paths = extractBracketedImagePastePaths(data);
|
|
126
|
+
return paths?.length === 1 ? paths[0] : undefined;
|
|
71
127
|
}
|
|
72
128
|
|
|
73
129
|
/**
|
|
@@ -111,8 +167,8 @@ export class CustomEditor extends Editor {
|
|
|
111
167
|
onCopyPrompt?: () => void;
|
|
112
168
|
/** Called when the configured image-paste shortcut is pressed. */
|
|
113
169
|
onPasteImage?: () => Promise<boolean>;
|
|
114
|
-
/** Called when a bracketed paste contains
|
|
115
|
-
onPasteImagePath?: (path: string) => void
|
|
170
|
+
/** Called when a bracketed paste contains one or more image-file paths. */
|
|
171
|
+
onPasteImagePath?: (path: string) => void | Promise<void>;
|
|
116
172
|
/** Called when the configured raw text-paste shortcut is pressed. */
|
|
117
173
|
onPasteTextRaw?: () => void;
|
|
118
174
|
/** Called when the configured dequeue shortcut is pressed. */
|
|
@@ -188,9 +244,13 @@ export class CustomEditor extends Editor {
|
|
|
188
244
|
return;
|
|
189
245
|
}
|
|
190
246
|
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
193
|
-
|
|
247
|
+
const pastedImagePaths = extractBracketedImagePastePaths(data);
|
|
248
|
+
if (pastedImagePaths && this.onPasteImagePath) {
|
|
249
|
+
void (async () => {
|
|
250
|
+
for (const path of pastedImagePaths) {
|
|
251
|
+
await this.onPasteImagePath?.(path);
|
|
252
|
+
}
|
|
253
|
+
})();
|
|
194
254
|
return;
|
|
195
255
|
}
|
|
196
256
|
|
|
@@ -7,7 +7,11 @@ interface FrozenRender {
|
|
|
7
7
|
lines: string[];
|
|
8
8
|
generation: number;
|
|
9
9
|
appendOnly: boolean;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Frames remaining until a block that rewrote an interior row may re-earn
|
|
12
|
+
* append-only status. `0` means the block is not under rewrite suspicion.
|
|
13
|
+
*/
|
|
14
|
+
volatileCooldown: number;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
interface SnapshotCarrier {
|
|
@@ -51,10 +55,41 @@ function stripPlainBlankEdges(lines: string[]): string[] {
|
|
|
51
55
|
|
|
52
56
|
interface LiveCommitState {
|
|
53
57
|
appendOnly: boolean;
|
|
54
|
-
|
|
58
|
+
volatileCooldown: number;
|
|
55
59
|
safeLength: number;
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Render frames a block must stay clean (static or append-shaped) after an
|
|
64
|
+
* interior rewrite before its rows become committable again. A one-off
|
|
65
|
+
* re-layout (a codespan finalizing across a wrap boundary, a paragraph
|
|
66
|
+
* re-parsed as a heading) only suspends commits briefly — the pinned emitter
|
|
67
|
+
* appends from the stalled high-water mark, so the gap backfills contiguously
|
|
68
|
+
* once the block re-earns append-only. Periodic animations (a spinner rewrites
|
|
69
|
+
* its row every few frames) keep resetting the countdown and never re-earn it,
|
|
70
|
+
* so genuinely volatile blocks stay deferred. Frames arrive at most at the
|
|
71
|
+
* TUI's 30 Hz render cadence, so 30 frames ≈ 1s of clean streaming.
|
|
72
|
+
*/
|
|
73
|
+
const VOLATILE_REARM_FRAMES = 30;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Visible-content form of a row: SGR/OSC bytes and trailing pad spaces are
|
|
77
|
+
* write framing, not content. A styled line's closing escape moves when the
|
|
78
|
+
* line stops being the last of its span (a wrapped thinking paragraph growing
|
|
79
|
+
* by one row), and width-padded rows shift their trailing spaces as text
|
|
80
|
+
* grows; both leave the on-screen cells identical and must not count as a
|
|
81
|
+
* rewrite of a committed-candidate row. Committed scrollback rows are written
|
|
82
|
+
* with a full SGR/OSC reset terminator, so escape-placement drift between
|
|
83
|
+
* visually identical renders cannot bleed styles across rows.
|
|
84
|
+
*/
|
|
85
|
+
function normalizeRow(line: string): string {
|
|
86
|
+
return Bun.stripANSI(line).trimEnd();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function rowsVisiblyEqual(prev: string, cur: string): boolean {
|
|
90
|
+
return prev === cur || normalizeRow(prev) === normalizeRow(cur);
|
|
91
|
+
}
|
|
92
|
+
|
|
58
93
|
function hasValidSnapshot(
|
|
59
94
|
snapshot: FrozenRender | undefined,
|
|
60
95
|
width: number,
|
|
@@ -66,14 +101,14 @@ function hasValidSnapshot(
|
|
|
66
101
|
function commonPrefixLength(prev: string[], cur: string[]): number {
|
|
67
102
|
const limit = Math.min(prev.length, cur.length);
|
|
68
103
|
let i = 0;
|
|
69
|
-
while (i < limit && prev[i]
|
|
104
|
+
while (i < limit && rowsVisiblyEqual(prev[i]!, cur[i]!)) i++;
|
|
70
105
|
return i;
|
|
71
106
|
}
|
|
72
107
|
|
|
73
108
|
function commonSuffixLength(prev: string[], cur: string[], prefixLength: number): number {
|
|
74
109
|
const limit = Math.min(prev.length - prefixLength, cur.length - prefixLength);
|
|
75
110
|
let i = 0;
|
|
76
|
-
while (i < limit && prev[prev.length - 1 - i]
|
|
111
|
+
while (i < limit && rowsVisiblyEqual(prev[prev.length - 1 - i]!, cur[cur.length - 1 - i]!)) i++;
|
|
77
112
|
return i;
|
|
78
113
|
}
|
|
79
114
|
|
|
@@ -84,42 +119,56 @@ function deriveLiveCommitState(
|
|
|
84
119
|
generation: number,
|
|
85
120
|
): LiveCommitState {
|
|
86
121
|
let appendOnly = false;
|
|
87
|
-
let
|
|
122
|
+
let volatileCooldown = 0;
|
|
88
123
|
if (hasValidSnapshot(previous, width, generation)) {
|
|
89
124
|
appendOnly = previous.appendOnly;
|
|
90
|
-
|
|
125
|
+
volatileCooldown = previous.volatileCooldown;
|
|
91
126
|
|
|
92
127
|
const prefixLength = commonPrefixLength(previous.lines, current);
|
|
93
128
|
const staticRender = prefixLength === previous.lines.length && prefixLength === current.length;
|
|
129
|
+
let cleanFrame = true;
|
|
94
130
|
if (!staticRender) {
|
|
95
131
|
const suffixLength = commonSuffixLength(previous.lines, current, prefixLength);
|
|
96
132
|
// Append-only growth never rewrites a row that may already have scrolled
|
|
97
|
-
// into native scrollback; it only grows the block at/near its tail.
|
|
133
|
+
// into native scrollback; it only grows the block at/near its tail. Four
|
|
98
134
|
// shapes qualify: a pure bottom append, an insertion above stable trailing
|
|
99
|
-
// chrome (a streaming tool's footer/border),
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
135
|
+
// chrome (a streaming tool's footer/border), an in-place extension of the
|
|
136
|
+
// current line by one streamed token (line count unchanged), and a
|
|
137
|
+
// wrap-shrink of the current line where its last word grew past the wrap
|
|
138
|
+
// column and moved down onto an appended row. The first two preserve every
|
|
139
|
+
// previous row across a matching prefix + suffix; the last two leave a
|
|
140
|
+
// single divergent previous row — the block's in-flight bottom line, which
|
|
141
|
+
// cannot have been committed (commits stop at the viewport top and the
|
|
142
|
+
// bottom line is by definition on screen). Any other divergent interior
|
|
143
|
+
// row means the block re-laid-out committed-candidate content — a rewrite,
|
|
144
|
+
// which suspends commits until the block re-earns append-only.
|
|
105
145
|
const preservedEveryRow = prefixLength + suffixLength >= previous.lines.length;
|
|
106
|
-
|
|
146
|
+
let tailExtendedInPlace = false;
|
|
147
|
+
if (
|
|
148
|
+
!preservedEveryRow &&
|
|
107
149
|
prefixLength + suffixLength === previous.lines.length - 1 &&
|
|
108
|
-
prefixLength < current.length
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
150
|
+
prefixLength < current.length
|
|
151
|
+
) {
|
|
152
|
+
const prevTail = normalizeRow(previous.lines[prefixLength]!);
|
|
153
|
+
const curTail = normalizeRow(current[prefixLength]!);
|
|
154
|
+
tailExtendedInPlace =
|
|
155
|
+
curTail.startsWith(prevTail) || (current.length > previous.lines.length && prevTail.startsWith(curTail));
|
|
156
|
+
}
|
|
157
|
+
if ((preservedEveryRow || tailExtendedInPlace) && current.length >= previous.lines.length) {
|
|
158
|
+
if (volatileCooldown === 0) appendOnly = true;
|
|
159
|
+
} else {
|
|
160
|
+
cleanFrame = false;
|
|
114
161
|
appendOnly = false;
|
|
162
|
+
volatileCooldown = VOLATILE_REARM_FRAMES;
|
|
115
163
|
}
|
|
116
164
|
}
|
|
165
|
+
if (cleanFrame && volatileCooldown > 0) volatileCooldown--;
|
|
117
166
|
}
|
|
118
167
|
|
|
119
168
|
return {
|
|
120
169
|
appendOnly,
|
|
121
|
-
|
|
122
|
-
safeLength:
|
|
170
|
+
volatileCooldown,
|
|
171
|
+
safeLength: appendOnly ? current.length : 0,
|
|
123
172
|
};
|
|
124
173
|
}
|
|
125
174
|
|
|
@@ -163,8 +212,11 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
163
212
|
#nativeScrollbackLiveRegionStart: number | undefined;
|
|
164
213
|
// Local line index up to which the leading run of live blocks is safe to
|
|
165
214
|
// commit. Finalized blocks contribute their full frozen body; still-live
|
|
166
|
-
// blocks contribute only
|
|
167
|
-
//
|
|
215
|
+
// blocks contribute only while their render has been observed growing
|
|
216
|
+
// without visibly rewriting a previously rendered interior row (escape
|
|
217
|
+
// placement and pad drift are ignored). A rewrite suspends the block's
|
|
218
|
+
// contribution until it re-earns append-only via VOLATILE_REARM_FRAMES
|
|
219
|
+
// clean frames; the pinned emitter then backfills the stalled gap.
|
|
168
220
|
#nativeScrollbackCommitSafeEnd: number | undefined;
|
|
169
221
|
|
|
170
222
|
override invalidate(): void {
|
|
@@ -265,7 +317,7 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
265
317
|
lines: contribution,
|
|
266
318
|
generation: this.#generation,
|
|
267
319
|
appendOnly: liveCommitState?.appendOnly ?? false,
|
|
268
|
-
|
|
320
|
+
volatileCooldown: liveCommitState?.volatileCooldown ?? 0,
|
|
269
321
|
};
|
|
270
322
|
}
|
|
271
323
|
}
|
|
@@ -191,7 +191,7 @@ export class InputController {
|
|
|
191
191
|
this.ctx.keybindings.getKeys("app.clipboard.pasteImage"),
|
|
192
192
|
);
|
|
193
193
|
this.ctx.editor.onPasteImage = () => this.handleImagePaste();
|
|
194
|
-
this.ctx.editor.onPasteImagePath = path =>
|
|
194
|
+
this.ctx.editor.onPasteImagePath = path => this.handleImagePathPaste(path);
|
|
195
195
|
this.ctx.editor.setActionKeys(
|
|
196
196
|
"app.clipboard.pasteTextRaw",
|
|
197
197
|
this.ctx.keybindings.getKeys("app.clipboard.pasteTextRaw"),
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
import type { MCPAuthConfig, MCPServerConfig, MCPServerConnection } from "../../mcp/types";
|
|
37
37
|
import type { OAuthCredential } from "../../session/auth-storage";
|
|
38
38
|
import { shortenPath } from "../../tools/render-utils";
|
|
39
|
-
import {
|
|
39
|
+
import { urlHyperlinkAlways } from "../../tui";
|
|
40
40
|
import { openPath } from "../../utils/open";
|
|
41
41
|
import { ChatBlock } from "../components/chat-block";
|
|
42
42
|
import { MCPAddWizard } from "../components/mcp-add-wizard";
|
|
@@ -63,7 +63,7 @@ export class MCPAuthorizationLinkPrompt implements Component {
|
|
|
63
63
|
invalidate(): void {}
|
|
64
64
|
|
|
65
65
|
render(_width: number): string[] {
|
|
66
|
-
const link =
|
|
66
|
+
const link = urlHyperlinkAlways(this.#url, "Click here to authorize");
|
|
67
67
|
return [
|
|
68
68
|
` ${theme.fg("success", "Open authorization URL:")}`,
|
|
69
69
|
` ${theme.fg("accent", link)}`,
|
package/src/sdk.ts
CHANGED
|
@@ -62,10 +62,11 @@ import {
|
|
|
62
62
|
type LoadedCustomCommand,
|
|
63
63
|
loadCustomCommands as loadCustomCommandsInternal,
|
|
64
64
|
} from "./extensibility/custom-commands";
|
|
65
|
-
import {
|
|
65
|
+
import { discoverCustomToolPaths, loadCustomTools, type ToolPathWithSource } from "./extensibility/custom-tools";
|
|
66
66
|
import type { CustomTool, CustomToolContext, CustomToolSessionEvent } from "./extensibility/custom-tools/types";
|
|
67
67
|
import {
|
|
68
68
|
discoverAndLoadExtensions,
|
|
69
|
+
discoverExtensionPaths,
|
|
69
70
|
type ExtensionContext,
|
|
70
71
|
type ExtensionFactory,
|
|
71
72
|
ExtensionRunner,
|
|
@@ -337,10 +338,41 @@ export interface CreateAgentSessionOptions {
|
|
|
337
338
|
/** Disable extension discovery (explicit paths still load). */
|
|
338
339
|
disableExtensionDiscovery?: boolean;
|
|
339
340
|
/**
|
|
340
|
-
* Pre-loaded extensions (skips file discovery
|
|
341
|
-
*
|
|
341
|
+
* Pre-loaded extensions (skips file discovery and the per-session factory
|
|
342
|
+
* call). Used by the CLI when extensions are loaded early to parse custom
|
|
343
|
+
* flags — the same process owns the returned instances, so reusing them is
|
|
344
|
+
* safe.
|
|
345
|
+
*
|
|
346
|
+
* NEVER pass this across session boundaries (e.g. parent → subagent).
|
|
347
|
+
* `Extension` instances close over a parent-bound `ExtensionAPI` (cwd,
|
|
348
|
+
* eventBus, runtime), and reusing them would route tools/handlers/commands
|
|
349
|
+
* back through the parent. For subagents, forward
|
|
350
|
+
* {@link preloadedExtensionPaths} instead.
|
|
351
|
+
*
|
|
352
|
+
* @internal
|
|
342
353
|
*/
|
|
343
354
|
preloadedExtensions?: LoadExtensionsResult;
|
|
355
|
+
/**
|
|
356
|
+
* Pre-discovered extension source paths. When provided, the filesystem-scan
|
|
357
|
+
* inside `discoverExtensionPaths()` is skipped — the session still calls
|
|
358
|
+
* `loadExtensions()` itself so each `Extension` is bound to THIS session's
|
|
359
|
+
* `ExtensionAPI` (cwd, eventBus, runtime).
|
|
360
|
+
*
|
|
361
|
+
* This is the safe pass-through for parent → subagent forwarding.
|
|
362
|
+
*/
|
|
363
|
+
preloadedExtensionPaths?: string[];
|
|
364
|
+
/**
|
|
365
|
+
* Pre-discovered custom-tool source paths from `.omp/tools/`, `.claude/tools/`,
|
|
366
|
+
* plugins, etc. When provided, the filesystem-scan inside
|
|
367
|
+
* `discoverCustomToolPaths()` is skipped — subagents inherit the parent's
|
|
368
|
+
* scan result and call `loadCustomTools()` themselves so each session binds
|
|
369
|
+
* tools to its OWN `CustomToolAPI` (cwd, exec, pushPendingAction, UI).
|
|
370
|
+
*
|
|
371
|
+
* Forwarding the loaded `LoadedCustomTool[]` instances directly would reuse
|
|
372
|
+
* the parent's session-bound API and route tool execution back through the
|
|
373
|
+
* parent — wrong for isolated tasks and for pending-action routing.
|
|
374
|
+
*/
|
|
375
|
+
preloadedCustomToolPaths?: ToolPathWithSource[];
|
|
344
376
|
|
|
345
377
|
/** Shared event bus for tool/extension communication. Default: creates new bus. */
|
|
346
378
|
eventBus?: EventBus;
|
|
@@ -565,6 +597,26 @@ export async function discoverExtensions(cwd?: string): Promise<LoadExtensionsRe
|
|
|
565
597
|
return discoverAndLoadExtensions([], resolvedCwd);
|
|
566
598
|
}
|
|
567
599
|
|
|
600
|
+
/**
|
|
601
|
+
* Path-only counterpart of {@link loadSessionExtensions}: the FS-heavy scan
|
|
602
|
+
* without the per-session module load. Subagents reuse the parent's path list
|
|
603
|
+
* (cached on {@link ToolSession.extensionPaths}) and rebuild Extension
|
|
604
|
+
* instances themselves so each session's `ExtensionAPI` (cwd, eventBus,
|
|
605
|
+
* runtime) is its own.
|
|
606
|
+
*/
|
|
607
|
+
export async function discoverSessionExtensionPaths(
|
|
608
|
+
options: Pick<CreateAgentSessionOptions, "disableExtensionDiscovery" | "additionalExtensionPaths">,
|
|
609
|
+
cwd: string,
|
|
610
|
+
settings: Settings,
|
|
611
|
+
): Promise<string[]> {
|
|
612
|
+
if (options.disableExtensionDiscovery) {
|
|
613
|
+
return options.additionalExtensionPaths ?? [];
|
|
614
|
+
}
|
|
615
|
+
const configuredPaths = [...(options.additionalExtensionPaths ?? []), ...(settings.get("extensions") ?? [])];
|
|
616
|
+
const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
|
|
617
|
+
return discoverExtensionPaths(configuredPaths, cwd, disabledExtensionIds);
|
|
618
|
+
}
|
|
619
|
+
|
|
568
620
|
/**
|
|
569
621
|
* Load the discovered/configured extensions for a session — everything {@link
|
|
570
622
|
* createAgentSession} would load except the inline factory extensions it appends
|
|
@@ -580,23 +632,8 @@ export async function loadSessionExtensions(
|
|
|
580
632
|
settings: Settings,
|
|
581
633
|
eventBus: EventBus,
|
|
582
634
|
): Promise<LoadExtensionsResult> {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const configuredPaths = options.additionalExtensionPaths ?? [];
|
|
586
|
-
result = await logger.time("loadExtensions", loadExtensions, configuredPaths, cwd, eventBus);
|
|
587
|
-
} else {
|
|
588
|
-
// Merge CLI extension paths with settings extension paths.
|
|
589
|
-
const configuredPaths = [...(options.additionalExtensionPaths ?? []), ...(settings.get("extensions") ?? [])];
|
|
590
|
-
const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
|
|
591
|
-
result = await logger.time(
|
|
592
|
-
"discoverAndLoadExtensions",
|
|
593
|
-
discoverAndLoadExtensions,
|
|
594
|
-
configuredPaths,
|
|
595
|
-
cwd,
|
|
596
|
-
eventBus,
|
|
597
|
-
disabledExtensionIds,
|
|
598
|
-
);
|
|
599
|
-
}
|
|
635
|
+
const paths = await discoverSessionExtensionPaths(options, cwd, settings);
|
|
636
|
+
const result = await logger.time("loadExtensions", loadExtensions, paths, cwd, eventBus);
|
|
600
637
|
for (const { path, error } of result.errors) {
|
|
601
638
|
logger.error("Failed to load extension", { path, error });
|
|
602
639
|
}
|
|
@@ -1193,23 +1230,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1193
1230
|
}
|
|
1194
1231
|
|
|
1195
1232
|
// Discover rules and bucket them in one pass to avoid repeated scans over large rule sets.
|
|
1196
|
-
const { ttsrManager, rulebookRules, alwaysApplyRules } = await logger.time(
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1233
|
+
const { ttsrManager, rulebookRules, alwaysApplyRules, allRules } = await logger.time(
|
|
1234
|
+
"discoverTtsrRules",
|
|
1235
|
+
async () => {
|
|
1236
|
+
const { TtsrManager } = await import("./export/ttsr");
|
|
1237
|
+
const ttsrSettings = settings.getGroup("ttsr");
|
|
1238
|
+
const ttsrManager = new TtsrManager(ttsrSettings);
|
|
1239
|
+
const rulesResult =
|
|
1240
|
+
options.rules !== undefined
|
|
1241
|
+
? { items: options.rules, warnings: undefined }
|
|
1242
|
+
: await loadCapability<Rule>(ruleCapability.id, { cwd });
|
|
1243
|
+
const { rulebookRules, alwaysApplyRules } = bucketRules(rulesResult.items, ttsrManager, {
|
|
1244
|
+
builtinRules: ttsrSettings.builtinRules,
|
|
1245
|
+
disabledRules: ttsrSettings.disabledRules,
|
|
1246
|
+
});
|
|
1247
|
+
if (existingSession.injectedTtsrRules.length > 0) {
|
|
1248
|
+
ttsrManager.restoreInjected(existingSession.injectedTtsrRules);
|
|
1249
|
+
}
|
|
1250
|
+
return { ttsrManager, rulebookRules, alwaysApplyRules, allRules: rulesResult.items };
|
|
1251
|
+
},
|
|
1252
|
+
);
|
|
1213
1253
|
|
|
1214
1254
|
// Resolve contextFiles up-front (it's needed before tool creation). The
|
|
1215
1255
|
// workspace tree scan is slow on large repos and we MUST NOT block startup on
|
|
@@ -1331,6 +1371,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1331
1371
|
contextFiles,
|
|
1332
1372
|
workspaceTree: resolvedWorkspaceTree,
|
|
1333
1373
|
skills,
|
|
1374
|
+
rules: allRules,
|
|
1334
1375
|
eventBus,
|
|
1335
1376
|
outputSchema: options.outputSchema,
|
|
1336
1377
|
requireYieldTool: options.requireYieldTool,
|
|
@@ -1514,22 +1555,29 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1514
1555
|
customTools.push(...getSearchTools());
|
|
1515
1556
|
}
|
|
1516
1557
|
|
|
1517
|
-
// Discover
|
|
1558
|
+
// Discover custom tools from `.omp/tools/`, `.claude/tools/`, plugins, etc.
|
|
1559
|
+
// Subagents reuse the parent's scan via `preloadedCustomToolPaths` to skip
|
|
1560
|
+
// the FS walk, but ALWAYS re-call `loadCustomTools` here so factories bind
|
|
1561
|
+
// to THIS session's `CustomToolAPI` (cwd, exec, pushPendingAction, UI).
|
|
1562
|
+
// Forwarding the parent's `LoadedCustomTool[]` directly would route tool
|
|
1563
|
+
// execution back through the parent — wrong for isolated tasks and for
|
|
1564
|
+
// pending-action queueing.
|
|
1518
1565
|
const builtInToolNames = builtinTools.map(t => t.name);
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
cwd,
|
|
1524
|
-
builtInToolNames,
|
|
1525
|
-
action => queueResolveHandler(toolSession, action),
|
|
1566
|
+
const customToolPaths: ToolPathWithSource[] =
|
|
1567
|
+
options.preloadedCustomToolPaths ??
|
|
1568
|
+
(await logger.time("discoverCustomToolPaths", () => discoverCustomToolPaths([], cwd)));
|
|
1569
|
+
const customToolsLoadResult = await logger.time("loadCustomTools", () =>
|
|
1570
|
+
loadCustomTools(customToolPaths, cwd, builtInToolNames, action => queueResolveHandler(toolSession, action)),
|
|
1526
1571
|
);
|
|
1527
|
-
for (const { path, error } of
|
|
1572
|
+
for (const { path, error } of customToolsLoadResult.errors) {
|
|
1528
1573
|
logger.error("Custom tool load failed", { path, error });
|
|
1529
1574
|
}
|
|
1530
|
-
if (
|
|
1531
|
-
customTools.push(...
|
|
1575
|
+
if (customToolsLoadResult.tools.length > 0) {
|
|
1576
|
+
customTools.push(...customToolsLoadResult.tools.map(loaded => loaded.tool));
|
|
1532
1577
|
}
|
|
1578
|
+
// Forward the path list (NOT the loaded tools) to subagents so they
|
|
1579
|
+
// re-bind under their own `CustomToolAPI` while skipping the FS scan.
|
|
1580
|
+
toolSession.customToolPaths = customToolPaths;
|
|
1533
1581
|
|
|
1534
1582
|
const inlineExtensions: ExtensionFactory[] = options.extensions ? [...options.extensions] : [];
|
|
1535
1583
|
inlineExtensions.push((await import("./autoresearch")).createAutoresearchExtension);
|
|
@@ -1537,14 +1585,48 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1537
1585
|
inlineExtensions.push(createCustomToolsExtension(customTools));
|
|
1538
1586
|
}
|
|
1539
1587
|
|
|
1540
|
-
// Load extensions.
|
|
1541
|
-
//
|
|
1542
|
-
//
|
|
1543
|
-
//
|
|
1544
|
-
//
|
|
1545
|
-
//
|
|
1546
|
-
|
|
1547
|
-
|
|
1588
|
+
// Load extensions. Three paths:
|
|
1589
|
+
// 1. `preloadedExtensions` (CLI): caller already loaded — reuse the
|
|
1590
|
+
// Extension instances. Shallow-clone `extensions` so the inline
|
|
1591
|
+
// push below cannot mutate the caller's array. `runtime` is shared
|
|
1592
|
+
// so flag values set pre-creation flow into the live session.
|
|
1593
|
+
// 2. `preloadedExtensionPaths` (subagent): caller resolved paths;
|
|
1594
|
+
// skip the FS scan but always re-call `loadExtensions` here so
|
|
1595
|
+
// each `Extension` binds to THIS session's `ExtensionAPI`
|
|
1596
|
+
// (cwd, eventBus, runtime).
|
|
1597
|
+
// 3. No preload: run the full session discovery.
|
|
1598
|
+
// `disableExtensionDiscovery` is honored implicitly: a caller that set
|
|
1599
|
+
// the flag and pre-resolved the result already reflects that choice.
|
|
1600
|
+
let extensionPaths: string[];
|
|
1601
|
+
let extensionsResult: LoadExtensionsResult;
|
|
1602
|
+
if (options.preloadedExtensions) {
|
|
1603
|
+
extensionsResult = {
|
|
1604
|
+
...options.preloadedExtensions,
|
|
1605
|
+
extensions: [...options.preloadedExtensions.extensions],
|
|
1606
|
+
};
|
|
1607
|
+
// Capture paths for downstream forwarding; filter inline-factory
|
|
1608
|
+
// entries (`<inline-N>`) — those are per-session, not source paths.
|
|
1609
|
+
extensionPaths = extensionsResult.extensions
|
|
1610
|
+
.map(ext => ext.resolvedPath)
|
|
1611
|
+
.filter(p => !p.startsWith("<inline"));
|
|
1612
|
+
} else if (options.preloadedExtensionPaths) {
|
|
1613
|
+
extensionPaths = options.preloadedExtensionPaths;
|
|
1614
|
+
extensionsResult = await logger.time("loadExtensions", loadExtensions, extensionPaths, cwd, eventBus);
|
|
1615
|
+
for (const { path, error } of extensionsResult.errors) {
|
|
1616
|
+
logger.error("Failed to load extension", { path, error });
|
|
1617
|
+
}
|
|
1618
|
+
} else {
|
|
1619
|
+
extensionPaths = await logger.time("discoverSessionExtensionPaths", () =>
|
|
1620
|
+
discoverSessionExtensionPaths(options, cwd, settings),
|
|
1621
|
+
);
|
|
1622
|
+
extensionsResult = await logger.time("loadExtensions", loadExtensions, extensionPaths, cwd, eventBus);
|
|
1623
|
+
for (const { path, error } of extensionsResult.errors) {
|
|
1624
|
+
logger.error("Failed to load extension", { path, error });
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
// Forward the source-path list (NOT the loaded instances) so subagents
|
|
1628
|
+
// rebuild their own session-scoped extensions.
|
|
1629
|
+
toolSession.extensionPaths = extensionPaths;
|
|
1548
1630
|
|
|
1549
1631
|
// Load inline extensions from factories
|
|
1550
1632
|
if (inlineExtensions.length > 0) {
|