@oh-my-pi/pi-coding-agent 15.10.1 → 15.10.3
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 +113 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +7 -1
- package/dist/types/cli/startup-cwd.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-provider-priority.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/config/settings.d.ts +7 -2
- package/dist/types/debug/report-bundle.d.ts +3 -0
- package/dist/types/edit/file-snapshot-store.d.ts +18 -10
- package/dist/types/edit/index.d.ts +0 -1
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -1
- package/dist/types/lsp/client.d.ts +10 -0
- package/dist/types/lsp/index.d.ts +0 -5
- package/dist/types/main.d.ts +14 -9
- package/dist/types/mcp/tool-bridge.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +0 -9
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/late-diagnostics-message.d.ts +20 -0
- package/dist/types/modes/components/read-tool-group.d.ts +6 -0
- package/dist/types/modes/components/session-selector.d.ts +16 -7
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +0 -18
- package/dist/types/modes/controllers/event-controller.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/magic-keywords.d.ts +1 -1
- package/dist/types/modes/markdown-prose.d.ts +1 -1
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/modes/workflow.d.ts +3 -3
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/messages.d.ts +11 -8
- package/dist/types/session/session-manager.d.ts +5 -2
- package/dist/types/session/yield-queue.d.ts +10 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/tools/eval-render.d.ts +0 -1
- package/dist/types/tools/eval.d.ts +8 -0
- package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
- package/dist/types/tools/github-cache.d.ts +12 -0
- package/dist/types/tools/index.d.ts +31 -0
- package/dist/types/tools/path-utils.d.ts +13 -1
- package/dist/types/tools/read.d.ts +2 -1
- package/dist/types/tools/render-utils.d.ts +3 -1
- package/dist/types/tools/renderers.d.ts +0 -15
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/write.d.ts +0 -2
- package/dist/types/tools/yield.d.ts +8 -0
- package/dist/types/tui/code-cell.d.ts +0 -2
- package/dist/types/tui/hyperlink.d.ts +5 -7
- package/dist/types/tui/output-block.d.ts +0 -18
- package/package.json +9 -9
- package/src/cli/args.ts +3 -1
- package/src/cli/dry-balance-cli.ts +2 -4
- package/src/cli/gallery-cli.ts +4 -0
- package/src/cli/gallery-fixtures/codeintel.ts +0 -1
- package/src/cli/gallery-fixtures/fs.ts +68 -1
- package/src/cli/gallery-fixtures/types.ts +8 -1
- package/src/cli/startup-cwd.ts +68 -0
- package/src/commands/launch.ts +3 -0
- package/src/commit/agentic/agent.ts +1 -0
- package/src/commit/model-selection.ts +3 -2
- package/src/config/model-provider-priority.ts +55 -0
- package/src/config/model-registry.ts +4 -22
- package/src/config/model-resolver.ts +39 -7
- package/src/config/settings.ts +86 -41
- package/src/debug/index.ts +8 -0
- package/src/debug/raw-sse-buffer.ts +7 -4
- package/src/debug/report-bundle.ts +9 -0
- package/src/edit/file-snapshot-store.ts +33 -1
- package/src/edit/hashline/diff.ts +86 -0
- package/src/edit/hashline/execute.ts +14 -1
- package/src/edit/hashline/filesystem.ts +2 -1
- package/src/edit/index.ts +31 -17
- package/src/edit/renderer.ts +116 -31
- package/src/eval/__tests__/llm-bridge.test.ts +20 -0
- package/src/eval/js/context-manager.ts +32 -15
- package/src/eval/js/shared/prelude.txt +26 -10
- package/src/eval/llm-bridge.ts +14 -3
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/executor.ts +23 -11
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/lsp/client.ts +23 -11
- package/src/lsp/config.ts +11 -1
- package/src/lsp/index.ts +189 -61
- package/src/main.ts +144 -78
- package/src/mcp/tool-bridge.ts +2 -0
- package/src/memories/index.ts +2 -2
- package/src/modes/components/assistant-message.ts +3 -15
- package/src/modes/components/custom-editor.ts +143 -111
- package/src/modes/components/late-diagnostics-message.ts +60 -0
- package/src/modes/components/model-selector.ts +59 -13
- package/src/modes/components/oauth-selector.ts +33 -7
- package/src/modes/components/plan-review-overlay.ts +26 -5
- package/src/modes/components/read-tool-group.ts +415 -35
- package/src/modes/components/session-selector.ts +89 -35
- package/src/modes/components/status-line.ts +19 -4
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/tool-execution.ts +7 -49
- package/src/modes/components/transcript-container.ts +108 -32
- package/src/modes/components/user-message.ts +1 -1
- package/src/modes/controllers/event-controller.ts +32 -1
- package/src/modes/controllers/input-controller.ts +56 -9
- package/src/modes/interactive-mode.ts +107 -20
- package/src/modes/magic-keywords.ts +1 -1
- package/src/modes/markdown-prose.ts +1 -1
- package/src/modes/theme/shimmer.ts +20 -9
- package/src/modes/types.ts +7 -0
- package/src/modes/utils/ui-helpers.ts +26 -5
- package/src/modes/workflow.ts +10 -10
- package/src/prompts/system/manual-continue.md +7 -0
- package/src/prompts/system/plan-mode-active.md +56 -72
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +85 -10
- package/src/session/agent-session.ts +42 -15
- package/src/session/auth-storage.ts +2 -0
- package/src/session/messages.ts +21 -14
- package/src/session/session-manager.ts +98 -25
- package/src/session/yield-queue.ts +20 -2
- package/src/task/executor.ts +72 -36
- package/src/task/render.ts +3 -4
- package/src/tiny/title-client.ts +6 -1
- package/src/tools/bash.ts +7 -7
- package/src/tools/browser/tab-supervisor.ts +13 -1
- package/src/tools/browser/tab-worker.ts +33 -4
- package/src/tools/eval-render.ts +4 -23
- package/src/tools/eval.ts +13 -2
- package/src/tools/find.ts +148 -99
- package/src/tools/gh-cache-invalidation.ts +200 -0
- package/src/tools/github-cache.ts +25 -0
- package/src/tools/index.ts +32 -0
- package/src/tools/inspect-image.ts +2 -2
- package/src/tools/path-utils.ts +47 -24
- package/src/tools/plan-mode-guard.ts +52 -7
- package/src/tools/read.ts +41 -20
- package/src/tools/render-utils.ts +3 -1
- package/src/tools/renderers.ts +0 -15
- package/src/tools/search.ts +38 -3
- package/src/tools/ssh.ts +0 -1
- package/src/tools/todo.ts +1 -0
- package/src/tools/write.ts +5 -14
- package/src/tools/yield.ts +10 -1
- package/src/tui/code-cell.ts +1 -6
- package/src/tui/hyperlink.ts +13 -23
- package/src/tui/output-block.ts +2 -97
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/enhanced-paste.ts +30 -2
- package/src/web/search/providers/codex.ts +37 -8
package/src/tui/code-cell.ts
CHANGED
|
@@ -32,8 +32,6 @@ export interface CodeCellOptions {
|
|
|
32
32
|
*/
|
|
33
33
|
codeTail?: boolean;
|
|
34
34
|
expanded?: boolean;
|
|
35
|
-
/** Animate the cell border with a sweeping segment while pending/running. */
|
|
36
|
-
animate?: boolean;
|
|
37
35
|
width: number;
|
|
38
36
|
}
|
|
39
37
|
|
|
@@ -147,10 +145,7 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
|
|
|
147
145
|
sections.push({ label: theme.fg("toolTitle", "Output"), lines: outputLines });
|
|
148
146
|
}
|
|
149
147
|
|
|
150
|
-
return renderOutputBlock(
|
|
151
|
-
{ header: title, headerMeta: meta, state, sections, width, animate: options.animate },
|
|
152
|
-
theme,
|
|
153
|
-
);
|
|
148
|
+
return renderOutputBlock({ header: title, headerMeta: meta, state, sections, width }, theme);
|
|
154
149
|
}
|
|
155
150
|
|
|
156
151
|
export interface MarkdownCellOptions {
|
package/src/tui/hyperlink.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* sequences when the active terminal supports hyperlinks and the user setting
|
|
6
6
|
* permits it. Falls back to plain text when disabled.
|
|
7
7
|
*/
|
|
8
|
+
import * as url from "node:url";
|
|
8
9
|
import { TERMINAL } from "@oh-my-pi/pi-tui";
|
|
9
10
|
import { settings } from "../config/settings";
|
|
10
11
|
import {
|
|
@@ -28,21 +29,12 @@ function buildLinkId(uri: string): string {
|
|
|
28
29
|
return (h >>> 0).toString(16).padStart(8, "0");
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
/** Build a `file://` URI
|
|
32
|
-
function buildFileUri(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const encoded = normalized
|
|
38
|
-
.split("/")
|
|
39
|
-
.map(segment => encodeURIComponent(segment))
|
|
40
|
-
.join("/");
|
|
41
|
-
const params: string[] = [];
|
|
42
|
-
if (opts?.line !== undefined) params.push(`line=${opts.line}`);
|
|
43
|
-
if (opts?.col !== undefined) params.push(`col=${opts.col}`);
|
|
44
|
-
const query = params.length > 0 ? `?${params.join("&")}` : "";
|
|
45
|
-
return `${prefix}${encoded}${query}`;
|
|
32
|
+
/** Build a properly encoded `file://` URI with optional line/col query params. */
|
|
33
|
+
function buildFileUri(filePath: string, opts?: { line?: number; col?: number }): string {
|
|
34
|
+
const uri = url.pathToFileURL(filePath);
|
|
35
|
+
if (opts?.line !== undefined) uri.searchParams.set("line", String(opts.line));
|
|
36
|
+
if (opts?.col !== undefined) uri.searchParams.set("col", String(opts.col));
|
|
37
|
+
return uri.href;
|
|
46
38
|
}
|
|
47
39
|
|
|
48
40
|
/**
|
|
@@ -104,21 +96,19 @@ export function urlHyperlink(url: string, displayText: string): string {
|
|
|
104
96
|
}
|
|
105
97
|
|
|
106
98
|
/**
|
|
107
|
-
* Wrap `displayText` in an OSC 8 hyperlink pointing at
|
|
99
|
+
* Wrap `displayText` in an OSC 8 hyperlink pointing at a filesystem path.
|
|
108
100
|
*
|
|
109
101
|
* Returns `displayText` unchanged when hyperlinks are disabled or when
|
|
110
102
|
* the text already contains an OSC 8 sequence (prevents double-wrapping).
|
|
103
|
+
* Relative paths resolve against the current working directory before URI
|
|
104
|
+
* encoding so the OSC 8 target is always a valid `file://` URL.
|
|
111
105
|
*
|
|
112
|
-
*
|
|
113
|
-
* produce invalid `file://` URIs and are accepted silently to avoid runtime
|
|
114
|
-
* errors in renderer hot paths.
|
|
115
|
-
*
|
|
116
|
-
* @param absPath - Absolute filesystem path
|
|
106
|
+
* @param filePath - Filesystem path
|
|
117
107
|
* @param displayText - Text to render as the hyperlink anchor (may contain ANSI codes)
|
|
118
108
|
* @param opts - Optional line/col position appended as `?line=N&col=M` query params
|
|
119
109
|
*/
|
|
120
|
-
export function fileHyperlink(
|
|
121
|
-
return wrapHyperlink(buildFileUri(
|
|
110
|
+
export function fileHyperlink(filePath: string, displayText: string, opts?: { line?: number; col?: number }): string {
|
|
111
|
+
return wrapHyperlink(buildFileUri(filePath, opts), displayText);
|
|
122
112
|
}
|
|
123
113
|
|
|
124
114
|
/**
|
package/src/tui/output-block.ts
CHANGED
|
@@ -17,8 +17,6 @@ export interface OutputBlockOptions {
|
|
|
17
17
|
width: number;
|
|
18
18
|
applyBg?: boolean;
|
|
19
19
|
contentPaddingLeft?: number;
|
|
20
|
-
/** Animate the border with a sweeping dark segment (pending/running state). */
|
|
21
|
-
animate?: boolean;
|
|
22
20
|
/** Override the state-derived border color. Used for muted "legacy" tool
|
|
23
21
|
* frames that should not visually compete with framed-output tools. */
|
|
24
22
|
borderColor?: ThemeColor;
|
|
@@ -37,59 +35,6 @@ export function isFramedBlockComponent(component: Component): boolean {
|
|
|
37
35
|
return (component as FramedBlockComponent)[FRAMED_BLOCK_COMPONENT] === true;
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
const BORDER_SHIMMER_TICK_MS = 1000 / 30;
|
|
41
|
-
/** Duration of one full left↔right↔left bounce of the bottom-edge segment, in
|
|
42
|
-
* ms. Position is derived from the wall clock against this fixed cycle so a
|
|
43
|
-
* resize only nudges the segment proportionally instead of teleporting it. */
|
|
44
|
-
const BORDER_BOUNCE_MS = 3000;
|
|
45
|
-
/** Length, in border cells, of the moving segment. */
|
|
46
|
-
const BORDER_SEGMENT_LEN = 8;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Monotonic frame counter for animated borders, quantized to the TUI's ~30fps
|
|
50
|
-
* render cap so the cache key advances once per animation frame — fine enough
|
|
51
|
-
* for a smooth segment sweep, coarse enough to coalesce multiple render passes
|
|
52
|
-
* land inside the same frame.
|
|
53
|
-
*/
|
|
54
|
-
export function borderShimmerTick(): number {
|
|
55
|
-
return Math.floor(Date.now() / BORDER_SHIMMER_TICK_MS);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Ease-in-out so the segment decelerates into and accelerates out of each wall. */
|
|
59
|
-
function easeInOutQuad(t: number): number {
|
|
60
|
-
return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Column of the travelling segment's center on the bottom edge for a box of
|
|
65
|
-
* inner width `W` at time `now`. The segment bounces left → right → left across
|
|
66
|
-
* the bottom border: a triangle wave over one full there-and-back cycle, eased
|
|
67
|
-
* per leg so it slows as it nears each wall before reversing. Position is
|
|
68
|
-
* derived from the wall clock against a fixed cycle, so a resize shifts the
|
|
69
|
-
* center proportionally — no reset.
|
|
70
|
-
*/
|
|
71
|
-
export function borderSegmentHeadCol(W: number, now: number): number {
|
|
72
|
-
if (W <= 1) return 0;
|
|
73
|
-
const phase = (((now % BORDER_BOUNCE_MS) + BORDER_BOUNCE_MS) % BORDER_BOUNCE_MS) / BORDER_BOUNCE_MS;
|
|
74
|
-
// Triangle: 0→1 rightward over the first half, 1→0 leftward over the second.
|
|
75
|
-
const leg = phase < 0.5 ? phase * 2 : 2 - phase * 2;
|
|
76
|
-
return easeInOutQuad(leg) * (W - 1);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Scale a truecolor foreground escape toward black by `factor`. Returns
|
|
81
|
-
* undefined for 256-color escapes (no RGB to scale) so callers fall back to a
|
|
82
|
-
* dimmer theme color.
|
|
83
|
-
*/
|
|
84
|
-
function darkenFgAnsi(ansi: string, factor: number): string | undefined {
|
|
85
|
-
const m = /38;2;(\d+);(\d+);(\d+)/.exec(ansi);
|
|
86
|
-
if (!m) return undefined;
|
|
87
|
-
const r = Math.round(Number(m[1]) * factor);
|
|
88
|
-
const g = Math.round(Number(m[2]) * factor);
|
|
89
|
-
const b = Math.round(Number(m[3]) * factor);
|
|
90
|
-
return `\x1b[38;2;${r};${g};${b}m`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
38
|
type BlockRow =
|
|
94
39
|
| { kind: "bar"; leftChar: string; rightChar: string; label?: string; meta?: string }
|
|
95
40
|
| { kind: "bottom"; leftChar: string; rightChar: string }
|
|
@@ -135,8 +80,7 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
135
80
|
const contentWidth = Math.max(0, lineWidth - visibleWidth(v) - contentPaddingLeft - visibleWidth(v));
|
|
136
81
|
const contentLeftPadding = contentPaddingLeft > 0 ? padding(contentPaddingLeft) : "";
|
|
137
82
|
|
|
138
|
-
// ── Layout pass: collect row descriptors
|
|
139
|
-
// known before the moving segment is positioned. ──
|
|
83
|
+
// ── Layout pass: collect row descriptors before emitting the bordered lines. ──
|
|
140
84
|
const rows: BlockRow[] = [];
|
|
141
85
|
rows.push({
|
|
142
86
|
kind: "bar",
|
|
@@ -185,39 +129,6 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
185
129
|
rows.push({ kind: "bottom", leftChar: theme.boxSharp.bottomLeft, rightChar: theme.boxSharp.bottomRight });
|
|
186
130
|
|
|
187
131
|
const H = rows.length;
|
|
188
|
-
const W = lineWidth;
|
|
189
|
-
const animate = (options.animate ?? false) && (state === "running" || state === "pending") && W >= 2 && H >= 2;
|
|
190
|
-
|
|
191
|
-
// ── Segment geometry: one dark run bounces left ↔ right along the bottom
|
|
192
|
-
// edge only. The top, interior separators, and side borders stay the flat
|
|
193
|
-
// accent color. ──
|
|
194
|
-
const segLen = animate ? Math.min(BORDER_SEGMENT_LEN, W) : 0;
|
|
195
|
-
const head = animate ? borderSegmentHeadCol(W, Date.now()) : 0;
|
|
196
|
-
const segHalf = segLen / 2;
|
|
197
|
-
const segAnsi = animate ? (darkenFgAnsi(theme.getFgAnsi(borderColor), 0.4) ?? theme.getFgAnsi("borderMuted")) : "";
|
|
198
|
-
const seg = (text: string) => `${segAnsi}${text}\x1b[39m`;
|
|
199
|
-
|
|
200
|
-
// A bottom-edge column is lit when it lies within half a segment of the
|
|
201
|
-
// travelling center.
|
|
202
|
-
const isLit = (col: number): boolean => Math.abs(col - head) < segHalf;
|
|
203
|
-
// Color a run of bottom-edge glyphs starting at column `startCol`, grouping
|
|
204
|
-
// consecutive same-state cells so each run emits a single escape pair.
|
|
205
|
-
const colorEdge = (glyphs: string, startCol: number): string => {
|
|
206
|
-
let out = "";
|
|
207
|
-
let runLit: boolean | null = null;
|
|
208
|
-
let buf = "";
|
|
209
|
-
for (let i = 0; i < glyphs.length; i++) {
|
|
210
|
-
const lit = isLit(startCol + i);
|
|
211
|
-
if (lit !== runLit) {
|
|
212
|
-
if (runLit !== null) out += (runLit ? seg : border)(buf);
|
|
213
|
-
buf = "";
|
|
214
|
-
runLit = lit;
|
|
215
|
-
}
|
|
216
|
-
buf += glyphs[i];
|
|
217
|
-
}
|
|
218
|
-
if (runLit !== null) out += (runLit ? seg : border)(buf);
|
|
219
|
-
return out;
|
|
220
|
-
};
|
|
221
132
|
|
|
222
133
|
const renderBar = (row: { leftChar: string; rightChar: string; label?: string; meta?: string }): string => {
|
|
223
134
|
const leftGlyphs = `${row.leftChar}${cap}`;
|
|
@@ -245,11 +156,7 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
245
156
|
const rightGlyph = row.rightChar;
|
|
246
157
|
const fillCount = Math.max(0, lineWidth - visibleWidth(leftGlyphs) - visibleWidth(rightGlyph));
|
|
247
158
|
const fillGlyphs = h.repeat(fillCount);
|
|
248
|
-
|
|
249
|
-
const leftStr = colorEdge(leftGlyphs, 0);
|
|
250
|
-
const fillStr = colorEdge(fillGlyphs, visibleWidth(leftGlyphs));
|
|
251
|
-
const rightStr = colorEdge(rightGlyph, lineWidth - visibleWidth(rightGlyph));
|
|
252
|
-
return `${leftStr}${fillStr}${rightStr}`;
|
|
159
|
+
return `${border(leftGlyphs)}${border(fillGlyphs)}${border(rightGlyph)}`;
|
|
253
160
|
};
|
|
254
161
|
|
|
255
162
|
const renderContent = (inner: string): string => `${border(v)}${contentLeftPadding}${inner}${border(v)}`;
|
|
@@ -302,8 +209,6 @@ export class CachedOutputBlock {
|
|
|
302
209
|
h.optional(options.state);
|
|
303
210
|
h.optional(options.borderColor);
|
|
304
211
|
h.bool(options.applyBg ?? true);
|
|
305
|
-
h.bool(options.animate ?? false);
|
|
306
|
-
if (options.animate) h.u32(borderShimmerTick());
|
|
307
212
|
if (options.sections) {
|
|
308
213
|
for (const s of options.sections) {
|
|
309
214
|
h.optional(s.label);
|
|
@@ -8,7 +8,7 @@ import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
|
8
8
|
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
9
9
|
|
|
10
10
|
import type { ModelRegistry } from "../config/model-registry";
|
|
11
|
-
import { resolveModelRoleValue } from "../config/model-resolver";
|
|
11
|
+
import { getModelMatchPreferences, resolveModelRoleValue } from "../config/model-resolver";
|
|
12
12
|
import type { Settings } from "../config/settings";
|
|
13
13
|
import MODEL_PRIO from "../priority.json" with { type: "json" };
|
|
14
14
|
import commitSystemPrompt from "../prompts/system/commit-message-system.md" with { type: "text" };
|
|
@@ -51,7 +51,7 @@ function getSmolModelCandidates(
|
|
|
51
51
|
candidates.push({ model, thinkingLevel });
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
const matchPreferences =
|
|
54
|
+
const matchPreferences = getModelMatchPreferences(settings);
|
|
55
55
|
const configuredSmol = resolveModelRoleValue(settings.getModelRole("smol"), availableModels, {
|
|
56
56
|
settings,
|
|
57
57
|
matchPreferences,
|
|
@@ -7,6 +7,8 @@ const PASTE_EVENT_NAME_BASE64 = Buffer.from("Paste event", "utf8").toString("bas
|
|
|
7
7
|
|
|
8
8
|
const IMAGE_MIME_PRIORITY = ["image/png", "image/jpeg", "image/webp", "image/gif"] as const;
|
|
9
9
|
const TEXT_MIME_TYPE = "text/plain";
|
|
10
|
+
/** Kitty's "give me the list of available MIME types" sentinel — see `TARGETS_MIME` in `kitty/clipboard.py`. */
|
|
11
|
+
const MIME_LISTING_TARGET = ".";
|
|
10
12
|
|
|
11
13
|
type PasteReadKind = "image" | "text";
|
|
12
14
|
|
|
@@ -18,6 +20,7 @@ export interface Osc5522Packet {
|
|
|
18
20
|
interface PasteListingState {
|
|
19
21
|
phase: "listing";
|
|
20
22
|
mimes: string[];
|
|
23
|
+
kittyDotPayload?: true;
|
|
21
24
|
pw?: string;
|
|
22
25
|
loc?: string;
|
|
23
26
|
}
|
|
@@ -144,6 +147,25 @@ export class EnhancedPasteController {
|
|
|
144
147
|
if (!mimeType) return;
|
|
145
148
|
|
|
146
149
|
if (state.phase === "listing") {
|
|
150
|
+
// Kitty (as of writing) implements the "list available MIME types"
|
|
151
|
+
// response shape by sending a single DATA packet with `mime="."` and
|
|
152
|
+
// the available types packed into the payload as a whitespace-
|
|
153
|
+
// separated list (see `fulfill_read_request` in
|
|
154
|
+
// kovidgoyal/kitty:kitty/clipboard.py). The 5522-mode ancillary
|
|
155
|
+
// spec instead encodes each type as its own DATA packet with an
|
|
156
|
+
// empty payload. Support both — fall through to the per-packet
|
|
157
|
+
// form when the dot sentinel has no payload, or when the packet
|
|
158
|
+
// already names a concrete MIME type.
|
|
159
|
+
if (mimeType === MIME_LISTING_TARGET) {
|
|
160
|
+
if (!packet.payload) return;
|
|
161
|
+
const listing = decodeBase64Utf8(packet.payload);
|
|
162
|
+
if (!listing) return;
|
|
163
|
+
state.kittyDotPayload = true;
|
|
164
|
+
for (const candidate of listing.split(/\s+/)) {
|
|
165
|
+
if (candidate && candidate !== MIME_LISTING_TARGET) state.mimes.push(candidate);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
147
169
|
state.mimes.push(mimeType);
|
|
148
170
|
return;
|
|
149
171
|
}
|
|
@@ -192,11 +214,17 @@ export class EnhancedPasteController {
|
|
|
192
214
|
chunks: [],
|
|
193
215
|
};
|
|
194
216
|
|
|
195
|
-
const
|
|
217
|
+
const encodedMime = Buffer.from(selected.mimeType, "utf8").toString("base64");
|
|
218
|
+
const metadata = ["type=read"];
|
|
196
219
|
if (state.loc) metadata.push(`loc=${state.loc}`);
|
|
197
220
|
if (state.pw) {
|
|
198
221
|
metadata.push(`pw=${state.pw}`, `name=${PASTE_EVENT_NAME_BASE64}`);
|
|
199
222
|
}
|
|
200
|
-
|
|
223
|
+
if (state.kittyDotPayload) {
|
|
224
|
+
this.#handlers.write(`${OSC5522_PREFIX}${metadata.join(":")};${encodedMime}${OSC_TERMINATOR_BEL}`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
metadata.push(`mime=${encodedMime}`);
|
|
228
|
+
this.#handlers.write(`${OSC5522_PREFIX}${metadata.join(":")}${OSC_TERMINATOR_BEL}`);
|
|
201
229
|
}
|
|
202
230
|
}
|
|
@@ -114,8 +114,34 @@ interface CodexResponse {
|
|
|
114
114
|
usage?: CodexUsage;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Known Codex "image placeholder" answers — short prose the assistant emits in
|
|
119
|
+
* place of a real answer when it produced a screenshot instead of text. These
|
|
120
|
+
* carry no information, so callers treat them as non-answers and advance the
|
|
121
|
+
* chain to a provider that returns text. Extend by adding the normalized
|
|
122
|
+
* literal below; no regex tuning required.
|
|
123
|
+
*/
|
|
124
|
+
const IMAGE_PLACEHOLDER_ANSWERS: ReadonlySet<string> = new Set([
|
|
125
|
+
"see attached image",
|
|
126
|
+
"attached image",
|
|
127
|
+
"see the attached image",
|
|
128
|
+
"see image",
|
|
129
|
+
"see image above",
|
|
130
|
+
"image above",
|
|
131
|
+
"see image below",
|
|
132
|
+
"image below",
|
|
133
|
+
]);
|
|
134
|
+
|
|
117
135
|
function isImagePlaceholderAnswer(text: string): boolean {
|
|
118
|
-
|
|
136
|
+
// Strip surrounding brackets/quotes and trailing punctuation, lowercase,
|
|
137
|
+
// then match against the known-placeholder set.
|
|
138
|
+
const normalized = text
|
|
139
|
+
.trim()
|
|
140
|
+
.replace(/^[[("'`*_]+/, "")
|
|
141
|
+
.replace(/[\])"'`*_.!?]+$/, "")
|
|
142
|
+
.trim()
|
|
143
|
+
.toLowerCase();
|
|
144
|
+
return IMAGE_PLACEHOLDER_ANSWERS.has(normalized);
|
|
119
145
|
}
|
|
120
146
|
|
|
121
147
|
function addSource(sources: SearchSource[], source: SearchSource): void {
|
|
@@ -423,15 +449,18 @@ async function callCodexSearch(
|
|
|
423
449
|
|
|
424
450
|
const finalAnswer = answerParts.join("\n\n").trim();
|
|
425
451
|
const streamedAnswer = streamedAnswerParts.join("").trim();
|
|
426
|
-
|
|
452
|
+
// Throw to advance the chain whenever Codex emitted nothing but image
|
|
453
|
+
// placeholder prose — including the case where the streamed delta itself
|
|
454
|
+
// is the placeholder (the model occasionally streams the same text it
|
|
455
|
+
// publishes as the final output_text).
|
|
456
|
+
const finalIsPlaceholder = finalAnswer.length > 0 && isImagePlaceholderAnswer(finalAnswer);
|
|
457
|
+
const streamedIsPlaceholder = streamedAnswer.length > 0 && isImagePlaceholderAnswer(streamedAnswer);
|
|
458
|
+
const hasFinalText = finalAnswer.length > 0 && !finalIsPlaceholder;
|
|
459
|
+
const hasStreamedText = streamedAnswer.length > 0 && !streamedIsPlaceholder;
|
|
460
|
+
if (!hasFinalText && !hasStreamedText && sources.length === 0) {
|
|
427
461
|
throw new SearchProviderError("codex", "Codex returned image-only response", 502);
|
|
428
462
|
}
|
|
429
|
-
const answer =
|
|
430
|
-
finalAnswer.length > 0 && !isImagePlaceholderAnswer(finalAnswer)
|
|
431
|
-
? finalAnswer
|
|
432
|
-
: streamedAnswer.length > 0
|
|
433
|
-
? streamedAnswer
|
|
434
|
-
: finalAnswer;
|
|
463
|
+
const answer = hasFinalText ? finalAnswer : hasStreamedText ? streamedAnswer : "";
|
|
435
464
|
|
|
436
465
|
// Fallback: when Codex omits url_citation annotations, scrape markdown links
|
|
437
466
|
// and bare URLs from the synthesized answer so callers still receive sources.
|