@oh-my-pi/pi-coding-agent 15.10.4 → 15.10.6
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 +74 -0
- package/dist/types/capability/rule-buckets.d.ts +1 -1
- package/dist/types/capability/rule.d.ts +6 -1
- package/dist/types/cli/update-cli.d.ts +11 -1
- package/dist/types/config/model-registry.d.ts +18 -1
- package/dist/types/discovery/at-imports.d.ts +15 -0
- package/dist/types/edit/diff.d.ts +3 -2
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +7 -0
- package/dist/types/eval/js/context-manager.d.ts +1 -0
- package/dist/types/eval/js/executor.d.ts +2 -0
- package/dist/types/eval/js/index.d.ts +1 -1
- package/dist/types/eval/js/shared/helpers.d.ts +6 -0
- package/dist/types/eval/js/shared/runtime.d.ts +5 -0
- package/dist/types/eval/js/worker-protocol.d.ts +6 -0
- package/dist/types/eval/py/executor.d.ts +7 -0
- package/dist/types/eval/py/index.d.ts +1 -1
- package/dist/types/exa/index.d.ts +1 -19
- package/dist/types/exa/mcp-client.d.ts +10 -3
- package/dist/types/exa/types.d.ts +0 -83
- package/dist/types/export/ttsr.d.ts +14 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -1
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +1 -1
- package/dist/types/internal-urls/local-protocol.d.ts +10 -0
- package/dist/types/mcp/oauth-flow.d.ts +2 -2
- package/dist/types/modes/components/custom-editor.d.ts +3 -0
- package/dist/types/modes/components/{status-line.d.ts → status-line/component.d.ts} +2 -32
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +31 -2
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +8 -0
- package/dist/types/modes/image-references.d.ts +8 -3
- package/dist/types/modes/interactive-mode.d.ts +9 -1
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +0 -2
- package/dist/types/task/render.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +15 -0
- package/dist/types/tools/index.d.ts +17 -2
- package/dist/types/tools/render-utils.d.ts +1 -1
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/block-context.d.ts +35 -0
- package/dist/types/utils/git.d.ts +6 -0
- package/dist/types/utils/image-loading.d.ts +12 -0
- package/package.json +29 -9
- package/src/capability/rule-buckets.ts +4 -2
- package/src/capability/rule.ts +10 -1
- package/src/cli/auth-broker-cli.ts +6 -7
- package/src/cli/auth-gateway-cli.ts +4 -3
- package/src/cli/list-models.ts +5 -0
- package/src/cli/update-cli.ts +138 -16
- package/src/commit/agentic/tools/split-commit.ts +8 -1
- package/src/config/model-provider-priority.ts +1 -0
- package/src/config/model-registry.ts +81 -2
- package/src/debug/index.ts +4 -8
- package/src/discovery/at-imports.ts +273 -0
- package/src/discovery/builtin-rules/index.ts +4 -0
- package/src/discovery/builtin-rules/ts-no-test-timers.md +55 -0
- package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +75 -0
- package/src/discovery/helpers.ts +2 -1
- package/src/edit/diff.ts +114 -4
- package/src/edit/hashline/diff.ts +1 -1
- package/src/edit/hashline/execute.ts +1 -1
- package/src/edit/modes/patch.ts +6 -2
- package/src/edit/modes/replace.ts +1 -1
- package/src/edit/renderer.ts +12 -2
- package/src/eval/__tests__/helpers-local-roots.test.ts +58 -0
- package/src/eval/backend.ts +15 -0
- package/src/eval/js/context-manager.ts +4 -2
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/index.ts +7 -1
- package/src/eval/js/shared/helpers.ts +53 -6
- package/src/eval/js/shared/runtime.ts +8 -0
- package/src/eval/js/worker-core.ts +1 -0
- package/src/eval/js/worker-protocol.ts +6 -0
- package/src/eval/py/executor.ts +12 -0
- package/src/eval/py/index.ts +7 -1
- package/src/eval/py/prelude.py +43 -4
- package/src/eval/py/runner.py +1 -0
- package/src/exa/index.ts +1 -26
- package/src/exa/mcp-client.ts +10 -10
- package/src/exa/types.ts +0 -97
- package/src/export/ttsr.ts +122 -1
- package/src/extensibility/extensions/types.ts +8 -1
- package/src/extensibility/legacy-pi-ai-shim.ts +1 -1
- package/src/extensibility/plugins/doctor.ts +1 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +6 -5
- package/src/goals/tools/goal-tool.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/internal-urls/local-protocol.ts +13 -0
- package/src/lsp/render.ts +8 -6
- package/src/mcp/oauth-flow.ts +3 -3
- package/src/mcp/render.ts +7 -1
- package/src/modes/components/agent-dashboard.ts +6 -4
- package/src/modes/components/custom-editor.ts +12 -6
- package/src/modes/components/login-dialog.ts +1 -1
- package/src/modes/components/oauth-selector.ts +4 -4
- package/src/modes/components/read-tool-group.ts +10 -3
- package/src/modes/components/{status-line.ts → status-line/component.ts} +18 -40
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/types.ts +23 -8
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/transcript-container.ts +17 -10
- package/src/modes/components/user-message.ts +6 -3
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/event-controller.ts +8 -0
- package/src/modes/controllers/extension-ui-controller.ts +143 -127
- package/src/modes/controllers/input-controller.ts +60 -11
- package/src/modes/controllers/mcp-command-controller.ts +52 -17
- package/src/modes/controllers/selector-controller.ts +4 -11
- package/src/modes/controllers/ssh-command-controller.ts +2 -2
- package/src/modes/image-references.ts +13 -7
- package/src/modes/interactive-mode.ts +35 -3
- package/src/modes/rpc/rpc-mode.ts +1 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -11
- package/src/modes/theme/theme.ts +95 -1
- package/src/modes/types.ts +3 -1
- package/src/modes/utils/ui-helpers.ts +14 -5
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/eval.md +4 -4
- package/src/sdk.ts +31 -14
- package/src/session/agent-session.ts +290 -196
- package/src/session/session-manager.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +9 -1
- package/src/system-prompt.ts +15 -9
- package/src/task/index.ts +9 -1
- package/src/task/render.ts +36 -14
- package/src/tools/ask.ts +14 -5
- package/src/tools/bash-interactive.ts +1 -1
- package/src/tools/bash.ts +14 -2
- package/src/tools/browser/render.ts +5 -2
- package/src/tools/browser/tab-worker.ts +211 -91
- package/src/tools/debug.ts +5 -2
- package/src/tools/eval-render.ts +6 -3
- package/src/tools/eval.ts +1 -1
- package/src/tools/gh-renderer.ts +29 -15
- package/src/tools/index.ts +32 -4
- package/src/tools/inspect-image-renderer.ts +12 -5
- package/src/tools/job.ts +9 -6
- package/src/tools/memory-render.ts +19 -5
- package/src/tools/read.ts +165 -18
- package/src/tools/render-utils.ts +3 -1
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +1 -1
- package/src/tools/ssh.ts +4 -1
- package/src/tools/todo.ts +8 -1
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/tools/write.ts +1 -1
- package/src/tui/code-cell.ts +1 -1
- package/src/utils/block-context.ts +312 -0
- package/src/utils/git.ts +41 -0
- package/src/utils/image-loading.ts +31 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/render.ts +14 -6
- package/dist/types/exa/factory.d.ts +0 -13
- package/dist/types/exa/render.d.ts +0 -19
- package/dist/types/exa/researcher.d.ts +0 -9
- package/dist/types/exa/search.d.ts +0 -9
- package/dist/types/exa/websets.d.ts +0 -9
- package/src/exa/factory.ts +0 -60
- package/src/exa/render.ts +0 -244
- package/src/exa/researcher.ts +0 -36
- package/src/exa/search.ts +0 -47
- package/src/exa/websets.ts +0 -248
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { enclosingBlockBoundaries } from "@oh-my-pi/pi-natives";
|
|
2
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
3
|
+
|
|
4
|
+
const OPEN_TO_CLOSE: Record<string, string> = {
|
|
5
|
+
"(": ")",
|
|
6
|
+
"[": "]",
|
|
7
|
+
"{": "}",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const CLOSE_TO_OPEN: Record<string, string> = {
|
|
11
|
+
")": "(",
|
|
12
|
+
"]": "[",
|
|
13
|
+
"}": "{",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface LineSpan {
|
|
17
|
+
startLine: number;
|
|
18
|
+
endLine: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Where the source came from, so tree-sitter can pick a grammar. */
|
|
22
|
+
export interface BlockContextSource {
|
|
23
|
+
path?: string;
|
|
24
|
+
lang?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type LineEntry = { kind: "line"; lineNumber: number; text: string; context: boolean } | { kind: "ellipsis" };
|
|
28
|
+
|
|
29
|
+
interface StackEntry {
|
|
30
|
+
opener: string;
|
|
31
|
+
lineNumber: number;
|
|
32
|
+
text: string;
|
|
33
|
+
visible: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type ScannerMode = "code" | "single" | "double" | "template" | "blockComment";
|
|
37
|
+
|
|
38
|
+
function normalizeLineSpans(spans: readonly LineSpan[], totalLines: number): LineSpan[] {
|
|
39
|
+
if (totalLines <= 0) return [];
|
|
40
|
+
const normalized: LineSpan[] = [];
|
|
41
|
+
for (const span of spans) {
|
|
42
|
+
const startLine = Math.max(1, Math.trunc(span.startLine));
|
|
43
|
+
const endLine = Math.min(totalLines, Math.trunc(span.endLine));
|
|
44
|
+
if (endLine < startLine) continue;
|
|
45
|
+
normalized.push({ startLine, endLine });
|
|
46
|
+
}
|
|
47
|
+
if (normalized.length <= 1) return normalized;
|
|
48
|
+
normalized.sort((left, right) => left.startLine - right.startLine || left.endLine - right.endLine);
|
|
49
|
+
const merged: LineSpan[] = [];
|
|
50
|
+
for (const span of normalized) {
|
|
51
|
+
const previous = merged[merged.length - 1];
|
|
52
|
+
if (previous && span.startLine <= previous.endLine + 1) {
|
|
53
|
+
previous.endLine = Math.max(previous.endLine, span.endLine);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
merged.push({ ...span });
|
|
57
|
+
}
|
|
58
|
+
return merged;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function visibleLineNumbers(spans: readonly LineSpan[]): Set<number> {
|
|
62
|
+
const visible = new Set<number>();
|
|
63
|
+
for (const span of spans) {
|
|
64
|
+
for (let line = span.startLine; line <= span.endLine; line++) {
|
|
65
|
+
visible.add(line);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return visible;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasEveryLineVisible(visible: ReadonlySet<number>, totalLines: number): boolean {
|
|
72
|
+
return totalLines > 0 && visible.size >= totalLines;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Collapse a set of visible line numbers into sorted, merged inclusive spans. */
|
|
76
|
+
function visibleSetToSpans(visible: ReadonlySet<number>): LineSpan[] {
|
|
77
|
+
const sorted = [...visible].sort((left, right) => left - right);
|
|
78
|
+
const spans: LineSpan[] = [];
|
|
79
|
+
for (const line of sorted) {
|
|
80
|
+
const previous = spans[spans.length - 1];
|
|
81
|
+
if (previous && line <= previous.endLine + 1) {
|
|
82
|
+
previous.endLine = line;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
spans.push({ startLine: line, endLine: line });
|
|
86
|
+
}
|
|
87
|
+
return spans;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Tree-sitter-backed block boundaries. For each multi-line named node whose
|
|
92
|
+
* span crosses the visible window, the native side returns the boundary line
|
|
93
|
+
* outside that window (closer when the opener is shown, opener when the closer
|
|
94
|
+
* is shown). Returns `null` when the language is unrecognized or the source has
|
|
95
|
+
* a syntax error so the caller can fall back to a lexical bracket scan.
|
|
96
|
+
*/
|
|
97
|
+
function nativeBlockContext(
|
|
98
|
+
fullLines: readonly string[],
|
|
99
|
+
visible: ReadonlySet<number>,
|
|
100
|
+
source: BlockContextSource,
|
|
101
|
+
): Map<number, string> | null {
|
|
102
|
+
if (!source.path && !source.lang) return null;
|
|
103
|
+
const ranges = visibleSetToSpans(visible);
|
|
104
|
+
if (ranges.length === 0) return new Map();
|
|
105
|
+
let boundaries: number[] | null;
|
|
106
|
+
try {
|
|
107
|
+
boundaries = enclosingBlockBoundaries({
|
|
108
|
+
code: fullLines.join("\n"),
|
|
109
|
+
path: source.path,
|
|
110
|
+
lang: source.lang,
|
|
111
|
+
ranges,
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
logger.debug("enclosingBlockBoundaries failed; using lexical bracket fallback", { error });
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (boundaries === null) return null;
|
|
118
|
+
const context = new Map<number, string>();
|
|
119
|
+
for (const lineNumber of boundaries) {
|
|
120
|
+
if (visible.has(lineNumber)) continue;
|
|
121
|
+
context.set(lineNumber, fullLines[lineNumber - 1] ?? "");
|
|
122
|
+
}
|
|
123
|
+
return context;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function findMatchingStackIndex(stack: readonly StackEntry[], opener: string): number {
|
|
127
|
+
for (let index = stack.length - 1; index >= 0; index--) {
|
|
128
|
+
if (stack[index].opener === opener) return index;
|
|
129
|
+
}
|
|
130
|
+
return -1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isHashCommentStart(line: string, index: number): boolean {
|
|
134
|
+
if (line[index] !== "#") return false;
|
|
135
|
+
for (let i = 0; i < index; i++) {
|
|
136
|
+
const ch = line[i];
|
|
137
|
+
if (ch !== " " && ch !== "\t") return false;
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Lexical bracket-matching fallback for sources tree-sitter can't parse
|
|
144
|
+
* (unknown extensions, syntax errors). Pairs `()[]{}` while skipping strings
|
|
145
|
+
* and line/block comments, and reports the matching line when one endpoint is
|
|
146
|
+
* visible and the other is not.
|
|
147
|
+
*/
|
|
148
|
+
function lexicalBracketContext(fullLines: readonly string[], visible: ReadonlySet<number>): Map<number, string> {
|
|
149
|
+
const context = new Map<number, string>();
|
|
150
|
+
const stack: StackEntry[] = [];
|
|
151
|
+
let mode: ScannerMode = "code";
|
|
152
|
+
let escaped = false;
|
|
153
|
+
|
|
154
|
+
for (let lineIndex = 0; lineIndex < fullLines.length; lineIndex++) {
|
|
155
|
+
const lineNumber = lineIndex + 1;
|
|
156
|
+
const line = fullLines[lineIndex] ?? "";
|
|
157
|
+
const lineVisible = visible.has(lineNumber);
|
|
158
|
+
let index = 0;
|
|
159
|
+
while (index < line.length) {
|
|
160
|
+
const ch = line[index];
|
|
161
|
+
const next = index + 1 < line.length ? line[index + 1] : "";
|
|
162
|
+
|
|
163
|
+
if (mode === "blockComment") {
|
|
164
|
+
if (ch === "*" && next === "/") {
|
|
165
|
+
mode = "code";
|
|
166
|
+
index += 2;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
index++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (mode === "single" || mode === "double" || mode === "template") {
|
|
174
|
+
if (escaped) {
|
|
175
|
+
escaped = false;
|
|
176
|
+
index++;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (ch === "\\") {
|
|
180
|
+
escaped = true;
|
|
181
|
+
index++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (
|
|
185
|
+
(mode === "single" && ch === "'") ||
|
|
186
|
+
(mode === "double" && ch === '"') ||
|
|
187
|
+
(mode === "template" && ch === "`")
|
|
188
|
+
) {
|
|
189
|
+
mode = "code";
|
|
190
|
+
}
|
|
191
|
+
index++;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (ch === "/" && next === "/") break;
|
|
196
|
+
if (ch === "/" && next === "*") {
|
|
197
|
+
mode = "blockComment";
|
|
198
|
+
index += 2;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (isHashCommentStart(line, index)) break;
|
|
202
|
+
if (ch === "'") {
|
|
203
|
+
mode = "single";
|
|
204
|
+
escaped = false;
|
|
205
|
+
index++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (ch === '"') {
|
|
209
|
+
mode = "double";
|
|
210
|
+
escaped = false;
|
|
211
|
+
index++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (ch === "`") {
|
|
215
|
+
mode = "template";
|
|
216
|
+
escaped = false;
|
|
217
|
+
index++;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (OPEN_TO_CLOSE[ch]) {
|
|
222
|
+
stack.push({ opener: ch, lineNumber, text: line, visible: lineVisible });
|
|
223
|
+
index++;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const opener = CLOSE_TO_OPEN[ch];
|
|
228
|
+
if (opener) {
|
|
229
|
+
const matchIndex = findMatchingStackIndex(stack, opener);
|
|
230
|
+
if (matchIndex !== -1) {
|
|
231
|
+
const [matched] = stack.splice(matchIndex);
|
|
232
|
+
if (matched) {
|
|
233
|
+
if (lineVisible && !matched.visible) context.set(matched.lineNumber, matched.text);
|
|
234
|
+
if (matched.visible && !lineVisible) context.set(lineNumber, line);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
index++;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (mode === "single" || mode === "double") {
|
|
243
|
+
mode = "code";
|
|
244
|
+
escaped = false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const lineNumber of visible) context.delete(lineNumber);
|
|
249
|
+
return context;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Resolve the off-window boundary lines for a visible window: tree-sitter
|
|
254
|
+
* syntactic spans first (covers brace and indentation languages), falling back
|
|
255
|
+
* to a lexical bracket scan when the grammar is unavailable. Returns a map of
|
|
256
|
+
* `lineNumber → source text` for the lines to surface, never including a line
|
|
257
|
+
* already visible.
|
|
258
|
+
*/
|
|
259
|
+
export function findBlockContextLines(
|
|
260
|
+
fullLines: readonly string[],
|
|
261
|
+
visibleInput: ReadonlySet<number> | readonly number[],
|
|
262
|
+
source: BlockContextSource = {},
|
|
263
|
+
): Map<number, string> {
|
|
264
|
+
const visible = visibleInput instanceof Set ? visibleInput : new Set(visibleInput);
|
|
265
|
+
if (visible.size === 0 || hasEveryLineVisible(visible, fullLines.length)) return new Map();
|
|
266
|
+
return nativeBlockContext(fullLines, visible, source) ?? lexicalBracketContext(fullLines, visible);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Build display entries for `visibleSpans` plus any off-window block-boundary
|
|
271
|
+
* lines, in source order, with `{ kind: "ellipsis" }` markers inserted across
|
|
272
|
+
* non-contiguous gaps. `options.lineText` lets callers substitute display text
|
|
273
|
+
* (e.g. column-truncated lines) for a given line number.
|
|
274
|
+
*/
|
|
275
|
+
export function buildLineEntriesWithBlockContext(
|
|
276
|
+
fullLines: readonly string[],
|
|
277
|
+
visibleSpans: readonly LineSpan[],
|
|
278
|
+
source: BlockContextSource = {},
|
|
279
|
+
options: {
|
|
280
|
+
lineText?: (lineNumber: number, sourceText: string, context: boolean) => string;
|
|
281
|
+
} = {},
|
|
282
|
+
): LineEntry[] {
|
|
283
|
+
const spans = normalizeLineSpans(visibleSpans, fullLines.length);
|
|
284
|
+
const visible = visibleLineNumbers(spans);
|
|
285
|
+
const context = findBlockContextLines(fullLines, visible, source);
|
|
286
|
+
const allLines = new Set<number>(visible);
|
|
287
|
+
for (const lineNumber of context.keys()) allLines.add(lineNumber);
|
|
288
|
+
|
|
289
|
+
const sorted = [...allLines].sort((left, right) => left - right);
|
|
290
|
+
const entries: LineEntry[] = [];
|
|
291
|
+
let previousLine: number | undefined;
|
|
292
|
+
for (const lineNumber of sorted) {
|
|
293
|
+
if (previousLine !== undefined && lineNumber > previousLine + 1) {
|
|
294
|
+
entries.push({ kind: "ellipsis" });
|
|
295
|
+
}
|
|
296
|
+
const sourceText = fullLines[lineNumber - 1] ?? "";
|
|
297
|
+
const isContext = context.has(lineNumber);
|
|
298
|
+
entries.push({
|
|
299
|
+
kind: "line",
|
|
300
|
+
lineNumber,
|
|
301
|
+
text: options.lineText?.(lineNumber, sourceText, isContext) ?? sourceText,
|
|
302
|
+
context: isContext,
|
|
303
|
+
});
|
|
304
|
+
previousLine = lineNumber;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return entries;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function lineEntriesToPlainText(entries: readonly LineEntry[], ellipsis = "…"): string {
|
|
311
|
+
return entries.map(entry => (entry.kind === "ellipsis" ? ellipsis : entry.text)).join("\n");
|
|
312
|
+
}
|
package/src/utils/git.ts
CHANGED
|
@@ -45,6 +45,10 @@ export interface StageHunksOptions {
|
|
|
45
45
|
readonly rawDiff?: string;
|
|
46
46
|
readonly signal?: AbortSignal;
|
|
47
47
|
}
|
|
48
|
+
export interface HunkSelectionValidationError {
|
|
49
|
+
readonly path: string;
|
|
50
|
+
readonly message: string;
|
|
51
|
+
}
|
|
48
52
|
|
|
49
53
|
export interface DiffOptions {
|
|
50
54
|
readonly allowFailure?: boolean;
|
|
@@ -678,6 +682,43 @@ function selectHunks(file: FileHunks, selector: HunkSelection["hunks"]): FileHun
|
|
|
678
682
|
return file.hunks;
|
|
679
683
|
}
|
|
680
684
|
|
|
685
|
+
export function createHunkSelectionValidator(
|
|
686
|
+
rawDiff: string,
|
|
687
|
+
): (selections: readonly HunkSelection[]) => HunkSelectionValidationError[] {
|
|
688
|
+
const fileDiffMap = new Map(parseFileDiffs(rawDiff).map(entry => [entry.filename, entry]));
|
|
689
|
+
return selections => validateHunkSelectionsFromMap(fileDiffMap, selections);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function validateHunkSelectionsFromMap(
|
|
693
|
+
fileDiffMap: ReadonlyMap<string, FileDiff>,
|
|
694
|
+
selections: readonly HunkSelection[],
|
|
695
|
+
): HunkSelectionValidationError[] {
|
|
696
|
+
const errors: HunkSelectionValidationError[] = [];
|
|
697
|
+
|
|
698
|
+
for (const selection of selections) {
|
|
699
|
+
const fileDiff = fileDiffMap.get(selection.path);
|
|
700
|
+
if (!fileDiff) continue;
|
|
701
|
+
if (selection.hunks.type === "all") continue;
|
|
702
|
+
if (fileDiff.isBinary) {
|
|
703
|
+
errors.push({ path: selection.path, message: `Cannot select hunks for binary file ${selection.path}` });
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
const selected = selectHunks(parseFileHunks(fileDiff), selection.hunks);
|
|
707
|
+
if (selected.length === 0) {
|
|
708
|
+
errors.push({ path: selection.path, message: `No hunks selected for ${selection.path}` });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return errors;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export function validateHunkSelections(
|
|
716
|
+
rawDiff: string,
|
|
717
|
+
selections: readonly HunkSelection[],
|
|
718
|
+
): HunkSelectionValidationError[] {
|
|
719
|
+
return createHunkSelectionValidator(rawDiff)(selections);
|
|
720
|
+
}
|
|
721
|
+
|
|
681
722
|
function parseStatusPorcelain(text: string): GitStatusSummary {
|
|
682
723
|
let staged = 0;
|
|
683
724
|
let unstaged = 0;
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { formatBytes, readImageMetadata, SUPPORTED_IMAGE_MIME_TYPES } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { resolveReadPath } from "../tools/path-utils";
|
|
5
|
-
import { formatDimensionNote, resizeImage } from "./image-resize";
|
|
5
|
+
import { formatDimensionNote, type ImageResizeOptions, resizeImage } from "./image-resize";
|
|
6
6
|
|
|
7
7
|
export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
|
|
8
8
|
export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = SUPPORTED_IMAGE_MIME_TYPES;
|
|
@@ -50,6 +50,36 @@ export async function ensureSupportedImageInput(image: ImageContent): Promise<Im
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export interface NormalizeModelContextImagesOptions {
|
|
54
|
+
resize?: ImageResizeOptions;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Normalize image blocks before they enter agent/model context. This keeps
|
|
59
|
+
* provider request construction from having to resize an unbounded batch of
|
|
60
|
+
* large images on the streaming hot path. Images are processed sequentially on
|
|
61
|
+
* purpose: `resizeImage` may fan out multiple encoders for one image, so the
|
|
62
|
+
* outer image batch must stay bounded.
|
|
63
|
+
*/
|
|
64
|
+
export async function normalizeModelContextImages(
|
|
65
|
+
images: ImageContent[] | undefined,
|
|
66
|
+
options?: NormalizeModelContextImagesOptions,
|
|
67
|
+
): Promise<ImageContent[] | undefined> {
|
|
68
|
+
if (!images || images.length === 0) return undefined;
|
|
69
|
+
const normalized: ImageContent[] = [];
|
|
70
|
+
for (const image of images) {
|
|
71
|
+
try {
|
|
72
|
+
const resized = await resizeImage(image, options?.resize);
|
|
73
|
+
normalized.push({ type: "image", data: resized.data, mimeType: resized.mimeType });
|
|
74
|
+
} catch {
|
|
75
|
+
// Preserve existing caller behavior for decode/resize failures: keep the
|
|
76
|
+
// user's image block rather than dropping it from the turn.
|
|
77
|
+
normalized.push(image);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return normalized;
|
|
81
|
+
}
|
|
82
|
+
|
|
53
83
|
export async function loadImageInput(options: LoadImageInputOptions): Promise<LoadedImageInput | null> {
|
|
54
84
|
const maxBytes = options.maxBytes ?? MAX_IMAGE_INPUT_BYTES;
|
|
55
85
|
const resolvedPath = options.resolvedPath ?? resolveReadPath(options.path, options.cwd);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import { type AuthStorage, getBundledModels } from "@oh-my-pi/pi-ai";
|
|
11
|
-
import { decodeJwt } from "@oh-my-pi/pi-ai/
|
|
11
|
+
import { decodeJwt } from "@oh-my-pi/pi-ai/oauth/openai-codex";
|
|
12
12
|
import { $env, readSseJson } from "@oh-my-pi/pi-utils";
|
|
13
13
|
import packageJson from "../../../../package.json" with { type: "json" };
|
|
14
14
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
package/src/web/search/render.ts
CHANGED
|
@@ -117,13 +117,21 @@ export function renderSearchResult(
|
|
|
117
117
|
: searchQueries[0]
|
|
118
118
|
? truncateToWidth(searchQueries[0], 80)
|
|
119
119
|
: undefined;
|
|
120
|
+
const success = sourceCount > 0;
|
|
120
121
|
const header = renderStatusLine(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
success
|
|
123
|
+
? {
|
|
124
|
+
iconOverride: theme.styledSymbol("tool.webSearch", "accent"),
|
|
125
|
+
title: "Web Search",
|
|
126
|
+
description: providerLabel,
|
|
127
|
+
meta: [formatCount("source", sourceCount)],
|
|
128
|
+
}
|
|
129
|
+
: {
|
|
130
|
+
icon: "warning",
|
|
131
|
+
title: "Web Search",
|
|
132
|
+
description: providerLabel,
|
|
133
|
+
meta: [formatCount("source", sourceCount)],
|
|
134
|
+
},
|
|
127
135
|
theme,
|
|
128
136
|
);
|
|
129
137
|
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared factory for creating Exa tools with consistent error handling and response formatting.
|
|
3
|
-
*/
|
|
4
|
-
import type { TSchema } from "@oh-my-pi/pi-ai";
|
|
5
|
-
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
6
|
-
import type { ExaRenderDetails } from "./types";
|
|
7
|
-
/** Creates an Exa tool with standardized API key handling, error wrapping, and optional search response formatting. */
|
|
8
|
-
export declare function createExaTool(name: string, label: string, description: string, parameters: TSchema, mcpToolName: string, options?: {
|
|
9
|
-
/** When true, checks isSearchResponse and formats with formatSearchResults. Default: true */
|
|
10
|
-
formatResponse?: boolean;
|
|
11
|
-
/** Transform params before passing to callExaTool */
|
|
12
|
-
transformParams?: (params: Record<string, unknown>) => Record<string, unknown>;
|
|
13
|
-
}): CustomTool<TSchema, ExaRenderDetails>;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Exa TUI Rendering
|
|
3
|
-
*
|
|
4
|
-
* Tree-based rendering with collapsed/expanded states for Exa search results.
|
|
5
|
-
*/
|
|
6
|
-
import type { Component } from "@oh-my-pi/pi-tui";
|
|
7
|
-
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
8
|
-
import type { Theme } from "../modes/theme/theme";
|
|
9
|
-
import type { ExaRenderDetails } from "./types";
|
|
10
|
-
/** Render Exa result with tree-based layout */
|
|
11
|
-
export declare function renderExaResult(result: {
|
|
12
|
-
content: Array<{
|
|
13
|
-
type: string;
|
|
14
|
-
text?: string;
|
|
15
|
-
}>;
|
|
16
|
-
details?: ExaRenderDetails;
|
|
17
|
-
}, options: RenderResultOptions, uiTheme: Theme): Component;
|
|
18
|
-
/** Render Exa call (query/args preview) */
|
|
19
|
-
export declare function renderExaCall(args: Record<string, unknown>, toolName: string, uiTheme: Theme): Component;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Exa Researcher Tools
|
|
3
|
-
*
|
|
4
|
-
* Async research tasks with polling for completion.
|
|
5
|
-
*/
|
|
6
|
-
import type { TSchema } from "@oh-my-pi/pi-ai";
|
|
7
|
-
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
8
|
-
import type { ExaRenderDetails } from "./types";
|
|
9
|
-
export declare const researcherTools: CustomTool<TSchema, ExaRenderDetails>[];
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Exa Search Tools
|
|
3
|
-
*
|
|
4
|
-
* Basic neural/keyword search, deep research, code search, and URL crawling.
|
|
5
|
-
*/
|
|
6
|
-
import type { TSchema } from "@oh-my-pi/pi-ai";
|
|
7
|
-
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
8
|
-
import type { ExaRenderDetails } from "./types";
|
|
9
|
-
export declare const searchTools: CustomTool<TSchema, ExaRenderDetails>[];
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Exa Websets Tools
|
|
3
|
-
*
|
|
4
|
-
* CRUD operations for websets, items, searches, enrichments, and monitoring.
|
|
5
|
-
*/
|
|
6
|
-
import type { TSchema } from "@oh-my-pi/pi-ai";
|
|
7
|
-
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
8
|
-
import type { ExaRenderDetails } from "./types";
|
|
9
|
-
export declare const websetsTools: CustomTool<TSchema, ExaRenderDetails>[];
|
package/src/exa/factory.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared factory for creating Exa tools with consistent error handling and response formatting.
|
|
3
|
-
*/
|
|
4
|
-
import type { TSchema } from "@oh-my-pi/pi-ai";
|
|
5
|
-
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
6
|
-
import { callExaTool, findApiKey, formatGenericResponse, formatSearchResults, isSearchResponse } from "./mcp-client";
|
|
7
|
-
import type { ExaRenderDetails } from "./types";
|
|
8
|
-
|
|
9
|
-
/** Creates an Exa tool with standardized API key handling, error wrapping, and optional search response formatting. */
|
|
10
|
-
export function createExaTool(
|
|
11
|
-
name: string,
|
|
12
|
-
label: string,
|
|
13
|
-
description: string,
|
|
14
|
-
parameters: TSchema,
|
|
15
|
-
mcpToolName: string,
|
|
16
|
-
options?: {
|
|
17
|
-
/** When true, checks isSearchResponse and formats with formatSearchResults. Default: true */
|
|
18
|
-
formatResponse?: boolean;
|
|
19
|
-
/** Transform params before passing to callExaTool */
|
|
20
|
-
transformParams?: (params: Record<string, unknown>) => Record<string, unknown>;
|
|
21
|
-
},
|
|
22
|
-
): CustomTool<TSchema, ExaRenderDetails> {
|
|
23
|
-
const formatResponse = options?.formatResponse ?? true;
|
|
24
|
-
const transformParams = options?.transformParams;
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
name,
|
|
28
|
-
label,
|
|
29
|
-
description,
|
|
30
|
-
parameters,
|
|
31
|
-
async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
|
|
32
|
-
try {
|
|
33
|
-
const apiKey = findApiKey();
|
|
34
|
-
// Exa MCP endpoint is publicly accessible; API key is optional
|
|
35
|
-
const rawArgs = params as Record<string, unknown>;
|
|
36
|
-
const args = transformParams ? transformParams(rawArgs) : rawArgs;
|
|
37
|
-
const response = await callExaTool(mcpToolName, args, apiKey);
|
|
38
|
-
|
|
39
|
-
if (formatResponse && isSearchResponse(response)) {
|
|
40
|
-
const formatted = formatSearchResults(response);
|
|
41
|
-
return {
|
|
42
|
-
content: [{ type: "text" as const, text: formatted }],
|
|
43
|
-
details: { response, toolName: name },
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
content: [{ type: "text" as const, text: formatGenericResponse(response) }],
|
|
49
|
-
details: { raw: response, toolName: name },
|
|
50
|
-
};
|
|
51
|
-
} catch (error) {
|
|
52
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
-
return {
|
|
54
|
-
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
55
|
-
details: { error: message, toolName: name },
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
}
|