@oh-my-pi/pi-coding-agent 16.0.3 → 16.0.5
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 +49 -0
- package/dist/cli.js +697 -337
- package/dist/types/advisor/advise-tool.d.ts +9 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +6 -0
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +92 -3
- package/dist/types/edit/file-snapshot-store.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +5 -2
- package/dist/types/extensibility/extensions/types.d.ts +8 -7
- package/dist/types/extensibility/shared-events.d.ts +22 -1
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +1 -1
- package/dist/types/modes/components/status-line/context-thresholds.d.ts +0 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -0
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +20 -0
- package/dist/types/session/session-persistence.d.ts +4 -0
- package/dist/types/tools/read.d.ts +1 -0
- package/dist/types/tui/code-cell.d.ts +2 -0
- package/dist/types/utils/image-vision-fallback.d.ts +28 -0
- package/dist/types/web/search/providers/base.d.ts +1 -0
- package/dist/types/web/search/providers/gemini.d.ts +1 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +59 -0
- package/src/advisor/advise-tool.ts +13 -0
- package/src/cli/args.ts +4 -0
- package/src/cli/bench-cli.ts +30 -7
- package/src/cli/flag-tables.ts +9 -0
- package/src/collab/host.ts +2 -2
- package/src/commands/launch.ts +6 -0
- package/src/config/settings-schema.ts +85 -3
- package/src/edit/file-snapshot-store.ts +12 -3
- package/src/eval/py/runner.py +44 -0
- package/src/extensibility/extensions/runner.ts +20 -2
- package/src/extensibility/extensions/types.ts +16 -5
- package/src/extensibility/shared-events.ts +24 -0
- package/src/internal-urls/docs-index.generated.ts +81 -81
- package/src/main.ts +18 -9
- package/src/modes/components/branch-summary-message.ts +1 -0
- package/src/modes/components/collab-prompt-message.ts +9 -7
- package/src/modes/components/compaction-summary-message.ts +1 -0
- package/src/modes/components/custom-message.ts +1 -0
- package/src/modes/components/footer.ts +6 -5
- package/src/modes/components/hook-message.ts +1 -0
- package/src/modes/components/read-tool-group.ts +9 -3
- package/src/modes/components/skill-message.ts +1 -0
- package/src/modes/components/status-line/component.ts +131 -14
- package/src/modes/components/status-line/context-thresholds.ts +0 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/todo-reminder.ts +1 -0
- package/src/modes/components/ttsr-notification.ts +1 -0
- package/src/modes/components/user-message.ts +6 -6
- package/src/modes/controllers/event-controller.ts +2 -7
- package/src/modes/controllers/selector-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/modes/utils/context-usage.ts +28 -15
- package/src/prompts/system/system-prompt.md +2 -0
- package/src/prompts/tools/image-attachment-describe-system.md +8 -0
- package/src/prompts/tools/image-attachment-describe.md +10 -0
- package/src/sdk.ts +14 -18
- package/src/session/agent-session.ts +571 -235
- package/src/session/session-loader.ts +19 -32
- package/src/session/session-persistence.ts +27 -11
- package/src/ssh/connection-manager.ts +3 -2
- package/src/task/executor.ts +1 -1
- package/src/tools/image-gen.ts +67 -25
- package/src/tools/read.ts +54 -6
- package/src/tui/code-cell.ts +44 -3
- package/src/utils/image-vision-fallback.ts +197 -0
- package/src/web/search/index.ts +12 -0
- package/src/web/search/providers/base.ts +1 -0
- package/src/web/search/providers/gemini.ts +56 -18
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
AdvisorRuntime,
|
|
13
13
|
type AdvisorRuntimeHost,
|
|
14
14
|
formatAdvisorBatchContent,
|
|
15
|
+
isAdvisorInterruptImmuneTurnActive,
|
|
15
16
|
isInterruptingSeverity,
|
|
16
17
|
resolveAdvisorDeliveryChannel,
|
|
17
18
|
} from "..";
|
|
@@ -124,6 +125,44 @@ describe("advisor", () => {
|
|
|
124
125
|
expect(isInterruptingSeverity(undefined)).toBe(false);
|
|
125
126
|
});
|
|
126
127
|
|
|
128
|
+
it("keeps the interrupt-immune turn fence half-open for the configured window", () => {
|
|
129
|
+
expect(
|
|
130
|
+
isAdvisorInterruptImmuneTurnActive({
|
|
131
|
+
completedTurns: 4,
|
|
132
|
+
immuneTurnStart: undefined,
|
|
133
|
+
immuneTurns: 2,
|
|
134
|
+
}),
|
|
135
|
+
).toBe(false);
|
|
136
|
+
expect(
|
|
137
|
+
isAdvisorInterruptImmuneTurnActive({
|
|
138
|
+
completedTurns: 4,
|
|
139
|
+
immuneTurnStart: 5,
|
|
140
|
+
immuneTurns: 0,
|
|
141
|
+
}),
|
|
142
|
+
).toBe(false);
|
|
143
|
+
expect(
|
|
144
|
+
isAdvisorInterruptImmuneTurnActive({
|
|
145
|
+
completedTurns: 4,
|
|
146
|
+
immuneTurnStart: 5,
|
|
147
|
+
immuneTurns: 2,
|
|
148
|
+
}),
|
|
149
|
+
).toBe(true);
|
|
150
|
+
expect(
|
|
151
|
+
isAdvisorInterruptImmuneTurnActive({
|
|
152
|
+
completedTurns: 6,
|
|
153
|
+
immuneTurnStart: 5,
|
|
154
|
+
immuneTurns: 2,
|
|
155
|
+
}),
|
|
156
|
+
).toBe(true);
|
|
157
|
+
expect(
|
|
158
|
+
isAdvisorInterruptImmuneTurnActive({
|
|
159
|
+
completedTurns: 7,
|
|
160
|
+
immuneTurnStart: 5,
|
|
161
|
+
immuneTurns: 2,
|
|
162
|
+
}),
|
|
163
|
+
).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
127
166
|
it("wraps each note in an advisory tag with severity as an attribute and escapes the body", () => {
|
|
128
167
|
const content = formatAdvisorBatchContent([
|
|
129
168
|
{ note: "first note" },
|
|
@@ -688,6 +727,26 @@ describe("advisor", () => {
|
|
|
688
727
|
}
|
|
689
728
|
});
|
|
690
729
|
|
|
730
|
+
it("routes interrupting notes to the aside queue during immune turns without overriding preservation", () => {
|
|
731
|
+
expect(
|
|
732
|
+
resolveAdvisorDeliveryChannel({
|
|
733
|
+
severity: "concern",
|
|
734
|
+
autoResumeSuppressed: false,
|
|
735
|
+
streaming: true,
|
|
736
|
+
aborting: false,
|
|
737
|
+
interruptImmuneTurnActive: true,
|
|
738
|
+
}),
|
|
739
|
+
).toBe("aside");
|
|
740
|
+
expect(
|
|
741
|
+
resolveAdvisorDeliveryChannel({
|
|
742
|
+
severity: "blocker",
|
|
743
|
+
autoResumeSuppressed: true,
|
|
744
|
+
streaming: false,
|
|
745
|
+
aborting: false,
|
|
746
|
+
interruptImmuneTurnActive: true,
|
|
747
|
+
}),
|
|
748
|
+
).toBe("preserve");
|
|
749
|
+
});
|
|
691
750
|
it("preserves an interrupting note while suppressed AND idle (no auto-resume of a stopped run)", () => {
|
|
692
751
|
for (const severity of ["concern", "blocker"] as const) {
|
|
693
752
|
expect(
|
|
@@ -68,6 +68,15 @@ export function isInterruptingSeverity(severity: AdvisorSeverity | undefined): b
|
|
|
68
68
|
|
|
69
69
|
/** How an advisor note is routed to the primary. */
|
|
70
70
|
export type AdvisorDeliveryChannel = "aside" | "steer" | "preserve";
|
|
71
|
+
/** Half-open turn-count fence for the post-interrupt cooldown. */
|
|
72
|
+
export function isAdvisorInterruptImmuneTurnActive(opts: {
|
|
73
|
+
completedTurns: number;
|
|
74
|
+
immuneTurnStart: number | undefined;
|
|
75
|
+
immuneTurns: number;
|
|
76
|
+
}): boolean {
|
|
77
|
+
if (opts.immuneTurnStart === undefined || opts.immuneTurns <= 0) return false;
|
|
78
|
+
return opts.completedTurns < opts.immuneTurnStart + opts.immuneTurns;
|
|
79
|
+
}
|
|
71
80
|
|
|
72
81
|
/**
|
|
73
82
|
* Decide how one advisor note reaches the primary agent.
|
|
@@ -84,15 +93,19 @@ export type AdvisorDeliveryChannel = "aside" | "steer" | "preserve";
|
|
|
84
93
|
* auto-resume anything, so it is delivered live. Parking it during an active
|
|
85
94
|
* run instead strands it (it never reaches the running agent) and the withheld
|
|
86
95
|
* notes dump as one burst at the next user prompt — the bug this guards.
|
|
96
|
+
* - During the post-interrupt immune-turn window, further `concern`/`blocker`
|
|
97
|
+
* notes are downgraded to asides; suppression preservation still wins.
|
|
87
98
|
*/
|
|
88
99
|
export function resolveAdvisorDeliveryChannel(opts: {
|
|
89
100
|
severity: AdvisorSeverity | undefined;
|
|
90
101
|
autoResumeSuppressed: boolean;
|
|
91
102
|
streaming: boolean;
|
|
92
103
|
aborting: boolean;
|
|
104
|
+
interruptImmuneTurnActive?: boolean;
|
|
93
105
|
}): AdvisorDeliveryChannel {
|
|
94
106
|
if (!isInterruptingSeverity(opts.severity)) return "aside";
|
|
95
107
|
if (opts.autoResumeSuppressed && (opts.aborting || !opts.streaming)) return "preserve";
|
|
108
|
+
if (opts.interruptImmuneTurnActive) return "aside";
|
|
96
109
|
return "steer";
|
|
97
110
|
}
|
|
98
111
|
|
package/src/cli/args.ts
CHANGED
|
@@ -28,11 +28,13 @@ export interface Args {
|
|
|
28
28
|
smol?: string;
|
|
29
29
|
slow?: string;
|
|
30
30
|
plan?: string;
|
|
31
|
+
maxTime?: number;
|
|
31
32
|
apiKey?: string;
|
|
32
33
|
systemPrompt?: string;
|
|
33
34
|
appendSystemPrompt?: string;
|
|
34
35
|
thinking?: Effort;
|
|
35
36
|
hideThinking?: boolean;
|
|
37
|
+
advisor?: boolean;
|
|
36
38
|
continue?: boolean;
|
|
37
39
|
resume?: string | true;
|
|
38
40
|
help?: boolean;
|
|
@@ -194,6 +196,8 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
|
|
|
194
196
|
result.noPty = true;
|
|
195
197
|
} else if (arg === "--hide-thinking") {
|
|
196
198
|
result.hideThinking = true;
|
|
199
|
+
} else if (arg === "--advisor") {
|
|
200
|
+
result.advisor = true;
|
|
197
201
|
} else if (arg === "--print" || arg === "-p") {
|
|
198
202
|
result.print = true;
|
|
199
203
|
} else if (arg === "--no-extensions") {
|
package/src/cli/bench-cli.ts
CHANGED
|
@@ -17,7 +17,12 @@ import { formatDuration, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import type { ApiKeyResolverModel } from "../config/api-key-resolver";
|
|
19
19
|
import { type CanonicalModelQueryOptions, ModelRegistry } from "../config/model-registry";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
formatModelSelectorValue,
|
|
22
|
+
formatModelString,
|
|
23
|
+
getModelMatchPreferences,
|
|
24
|
+
resolveCliModel,
|
|
25
|
+
} from "../config/model-resolver";
|
|
21
26
|
import { Settings } from "../config/settings";
|
|
22
27
|
import benchPrompt from "../prompts/bench.md" with { type: "text" };
|
|
23
28
|
import { discoverAuthStorage } from "../sdk";
|
|
@@ -144,9 +149,15 @@ function isFirstTokenEvent(event: AssistantMessageEvent): boolean {
|
|
|
144
149
|
* latency does not dilute throughput. Falls back to total duration when the
|
|
145
150
|
* response arrived as a single chunk (TTFT ~ duration).
|
|
146
151
|
*/
|
|
147
|
-
function computeTokensPerSecond(
|
|
152
|
+
export function computeTokensPerSecond(
|
|
153
|
+
outputTokens: number,
|
|
154
|
+
durationMs: number,
|
|
155
|
+
ttftMs: number,
|
|
156
|
+
deltaChunkCount: number,
|
|
157
|
+
): number {
|
|
148
158
|
const decodeMs = durationMs - ttftMs;
|
|
149
|
-
|
|
159
|
+
// Fall back to total duration when the response arrived as a single chunk/non-streaming.
|
|
160
|
+
const windowMs = decodeMs > 0 && deltaChunkCount >= 2 ? decodeMs : durationMs;
|
|
150
161
|
return windowMs > 0 ? (outputTokens * 1000) / windowMs : 0;
|
|
151
162
|
}
|
|
152
163
|
|
|
@@ -193,10 +204,17 @@ async function runBenchRequest(
|
|
|
193
204
|
headers: model.provider === "openrouter" ? { "X-OpenRouter-Cache": "false" } : undefined,
|
|
194
205
|
});
|
|
195
206
|
let message: AssistantMessage | undefined;
|
|
207
|
+
let deltaChunkCount = 0;
|
|
196
208
|
for await (const event of stream) {
|
|
197
209
|
if (firstTokenAt === undefined && isFirstTokenEvent(event)) {
|
|
198
210
|
firstTokenAt = now();
|
|
199
211
|
}
|
|
212
|
+
if (
|
|
213
|
+
(event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta") &&
|
|
214
|
+
event.delta.length > 0
|
|
215
|
+
) {
|
|
216
|
+
deltaChunkCount++;
|
|
217
|
+
}
|
|
200
218
|
if (event.type === "error") {
|
|
201
219
|
return { ok: false, error: event.error.errorMessage ?? "request failed" };
|
|
202
220
|
}
|
|
@@ -218,7 +236,7 @@ async function runBenchRequest(
|
|
|
218
236
|
ttftMs,
|
|
219
237
|
durationMs,
|
|
220
238
|
outputTokens,
|
|
221
|
-
tokensPerSecond: computeTokensPerSecond(outputTokens, durationMs, ttftMs),
|
|
239
|
+
tokensPerSecond: computeTokensPerSecond(outputTokens, durationMs, ttftMs, deltaChunkCount),
|
|
222
240
|
};
|
|
223
241
|
} catch (error) {
|
|
224
242
|
return { ok: false, error: getErrorMessage(error) };
|
|
@@ -244,6 +262,10 @@ function buildModelReport(
|
|
|
244
262
|
return { selector, model: formatModelString(model), thinking, results, average };
|
|
245
263
|
}
|
|
246
264
|
|
|
265
|
+
function formatBenchModelLabel(report: BenchModelReport): string {
|
|
266
|
+
return formatModelSelectorValue(report.model, report.thinking);
|
|
267
|
+
}
|
|
268
|
+
|
|
247
269
|
function formatMs(ms: number): string {
|
|
248
270
|
return formatDuration(Math.max(0, Math.round(ms)));
|
|
249
271
|
}
|
|
@@ -264,7 +286,7 @@ export function formatBenchTable(summary: BenchSummary): string {
|
|
|
264
286
|
return b.average.tokensPerSecond - a.average.tokensPerSecond;
|
|
265
287
|
});
|
|
266
288
|
const rows = ranked.map(report => ({
|
|
267
|
-
model: report
|
|
289
|
+
model: formatBenchModelLabel(report),
|
|
268
290
|
ttft: report.average ? formatMs(report.average.ttftMs) : "-",
|
|
269
291
|
tps: report.average ? `${report.average.tokensPerSecond.toFixed(1)}/s` : "-",
|
|
270
292
|
tokens: report.average ? String(Math.round(report.average.outputTokens)) : "-",
|
|
@@ -382,8 +404,9 @@ export async function runBenchCommand(command: BenchCommandArgs, deps: BenchDepe
|
|
|
382
404
|
const reports: BenchModelReport[] = [];
|
|
383
405
|
for (const { selector, model, thinking } of targets) {
|
|
384
406
|
if (!json) {
|
|
385
|
-
const
|
|
386
|
-
|
|
407
|
+
const resolvedModel = formatModelSelectorValue(formatModelString(model), thinking);
|
|
408
|
+
const resolvedNote = selector === resolvedModel ? "" : chalk.dim(` (${selector})`);
|
|
409
|
+
writeStdout(`${chalk.bold(resolvedModel)}${resolvedNote}\n`);
|
|
387
410
|
}
|
|
388
411
|
const results: BenchRunResult[] = [];
|
|
389
412
|
for (let index = 0; index < runs; index++) {
|
package/src/cli/flag-tables.ts
CHANGED
|
@@ -120,6 +120,14 @@ export const STRING_SETTERS: Record<string, StringSetter> = {
|
|
|
120
120
|
"--plan": (result, value) => {
|
|
121
121
|
result.plan = value;
|
|
122
122
|
},
|
|
123
|
+
"--max-time": (result, value, deps) => {
|
|
124
|
+
const seconds = Number(value);
|
|
125
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
126
|
+
result.maxTime = seconds;
|
|
127
|
+
} else {
|
|
128
|
+
deps.logger.warn("Invalid seconds passed to --max-time", { value });
|
|
129
|
+
}
|
|
130
|
+
},
|
|
123
131
|
"--api-key": (result, value) => {
|
|
124
132
|
result.apiKey = value;
|
|
125
133
|
},
|
|
@@ -260,6 +268,7 @@ export const VALUELESS_FLAGS: ReadonlySet<string> = new Set([
|
|
|
260
268
|
"--no-lsp",
|
|
261
269
|
"--no-pty",
|
|
262
270
|
"--hide-thinking",
|
|
271
|
+
"--advisor",
|
|
263
272
|
"--print",
|
|
264
273
|
"--no-extensions",
|
|
265
274
|
"--no-skills",
|
package/src/collab/host.ts
CHANGED
|
@@ -415,7 +415,7 @@ export class CollabHost {
|
|
|
415
415
|
// render exactly the same anchored, provider-real count the host's own
|
|
416
416
|
// status line shows.
|
|
417
417
|
const breakdown = this.#ctx.statusLine.getCachedContextBreakdown();
|
|
418
|
-
const tokens = breakdown.usedTokens;
|
|
418
|
+
const tokens = breakdown.usedTokens ?? 0;
|
|
419
419
|
return {
|
|
420
420
|
isStreaming: session.isStreaming,
|
|
421
421
|
isAborting: session.isAborting,
|
|
@@ -427,7 +427,7 @@ export class CollabHost {
|
|
|
427
427
|
contextUsage: {
|
|
428
428
|
tokens,
|
|
429
429
|
contextWindow: breakdown.contextWindow,
|
|
430
|
-
percent:
|
|
430
|
+
percent: breakdown.contextWindow > 0 ? (tokens / breakdown.contextWindow) * 100 : 0,
|
|
431
431
|
},
|
|
432
432
|
participants: this.participants,
|
|
433
433
|
};
|
package/src/commands/launch.ts
CHANGED
|
@@ -106,6 +106,9 @@ export default class Index extends Command {
|
|
|
106
106
|
"hide-thinking": Flags.boolean({
|
|
107
107
|
description: "Hide thinking blocks in TUI output (display only, does not disable model thinking)",
|
|
108
108
|
}),
|
|
109
|
+
advisor: Flags.boolean({
|
|
110
|
+
description: "Enable the advisor runtime (passively reviews each turn and injects notes)",
|
|
111
|
+
}),
|
|
109
112
|
hook: Flags.string({
|
|
110
113
|
description: "Load a hook/extension file (can be used multiple times)",
|
|
111
114
|
multiple: true,
|
|
@@ -133,6 +136,9 @@ export default class Index extends Command {
|
|
|
133
136
|
"no-title": Flags.boolean({
|
|
134
137
|
description: "Disable title auto-generation",
|
|
135
138
|
}),
|
|
139
|
+
"max-time": Flags.string({
|
|
140
|
+
description: "Stop the session after this many seconds",
|
|
141
|
+
}),
|
|
136
142
|
// `--auto-approve` / `--yolo`: declared here so oclif's auto-generated `--help` lists it.
|
|
137
143
|
// Runtime parsing happens in `cli/args.ts parseArgs` (line 176 in that file) — `runRootCommand`
|
|
138
144
|
// consumes the manual-parser output, not these oclif flag values. If you rename or remove
|
|
@@ -106,7 +106,7 @@ export const TAB_METADATA: Record<SettingTab, { label: string; icon: `tab.${stri
|
|
|
106
106
|
*/
|
|
107
107
|
export const TAB_GROUPS: Record<SettingTab, readonly string[]> = {
|
|
108
108
|
appearance: ["Theme", "Status Line", "Display", "Images"],
|
|
109
|
-
model: ["Thinking", "Sampling", "Prompt", "Retry & Fallback", "Advisor"],
|
|
109
|
+
model: ["Thinking", "Sampling", "Prompt", "Retry & Fallback", "Advisor", "Vision"],
|
|
110
110
|
interaction: [
|
|
111
111
|
"Input",
|
|
112
112
|
"Approvals",
|
|
@@ -117,6 +117,7 @@ export const TAB_GROUPS: Record<SettingTab, readonly string[]> = {
|
|
|
117
117
|
"Startup & Updates",
|
|
118
118
|
"Power (macOS)",
|
|
119
119
|
"Agent",
|
|
120
|
+
"Git",
|
|
120
121
|
],
|
|
121
122
|
context: ["General", "Compaction", "Rules (TTSR)", "Experimental"],
|
|
122
123
|
memory: ["General", "Auto-Learn", "Mnemopi", "Hindsight"],
|
|
@@ -414,7 +415,36 @@ export const SETTINGS_SCHEMA = {
|
|
|
414
415
|
"Pause the main agent for up to 30 seconds if the advisor falls behind by this many turns. Off disables catch-up delays.",
|
|
415
416
|
},
|
|
416
417
|
},
|
|
418
|
+
"advisor.immuneTurns": {
|
|
419
|
+
type: "number",
|
|
420
|
+
default: 1,
|
|
421
|
+
ui: {
|
|
422
|
+
tab: "model",
|
|
423
|
+
group: "Advisor",
|
|
424
|
+
label: "Advisor Immune Turns",
|
|
425
|
+
description:
|
|
426
|
+
"After an advisor concern or blocker interrupts, route further concerns/blockers non-interruptingly for this many primary turns.",
|
|
427
|
+
options: [
|
|
428
|
+
{ value: "0", label: "0 turns", description: "Allow every concern/blocker to interrupt." },
|
|
429
|
+
{ value: "1", label: "1 turn", description: "Default." },
|
|
430
|
+
{ value: "2", label: "2 turns" },
|
|
431
|
+
{ value: "3", label: "3 turns" },
|
|
432
|
+
{ value: "4", label: "4 turns" },
|
|
433
|
+
{ value: "5", label: "5 turns" },
|
|
434
|
+
],
|
|
435
|
+
},
|
|
436
|
+
},
|
|
417
437
|
shellPath: { type: "string", default: undefined },
|
|
438
|
+
"git.enabled": {
|
|
439
|
+
type: "boolean",
|
|
440
|
+
default: true,
|
|
441
|
+
ui: {
|
|
442
|
+
tab: "interaction",
|
|
443
|
+
group: "Git",
|
|
444
|
+
label: "Enable Git Integration",
|
|
445
|
+
description: "Show git branch, status, and PR information in the TUI and watch repository metadata.",
|
|
446
|
+
},
|
|
447
|
+
},
|
|
418
448
|
|
|
419
449
|
extensions: { type: "array", default: EMPTY_STRING_ARRAY },
|
|
420
450
|
|
|
@@ -712,6 +742,18 @@ export const SETTINGS_SCHEMA = {
|
|
|
712
742
|
},
|
|
713
743
|
},
|
|
714
744
|
|
|
745
|
+
"images.describeForTextModels": {
|
|
746
|
+
type: "boolean",
|
|
747
|
+
default: true,
|
|
748
|
+
ui: {
|
|
749
|
+
tab: "model",
|
|
750
|
+
group: "Vision",
|
|
751
|
+
label: "Describe Images for Text Models",
|
|
752
|
+
description:
|
|
753
|
+
"When an image is attached to a model without vision support, save it under local:// and inject a description from a vision-capable model instead of dropping it",
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
|
|
715
757
|
"tui.maxInlineImageColumns": {
|
|
716
758
|
type: "number",
|
|
717
759
|
default: 100,
|
|
@@ -757,6 +799,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
757
799
|
"Wrap paths and URLs in OSC 8 hyperlinks for terminal-native click-to-open (auto: detect support; off: never; always: unconditional)",
|
|
758
800
|
},
|
|
759
801
|
},
|
|
802
|
+
"tui.tight": {
|
|
803
|
+
type: "boolean",
|
|
804
|
+
default: false,
|
|
805
|
+
ui: {
|
|
806
|
+
tab: "appearance",
|
|
807
|
+
group: "Display",
|
|
808
|
+
label: "Tight Layout",
|
|
809
|
+
description: "Remove the 1-character horizontal padding from the left and right of the terminal output",
|
|
810
|
+
},
|
|
811
|
+
},
|
|
760
812
|
// Display rendering
|
|
761
813
|
"display.tabWidth": {
|
|
762
814
|
type: "number",
|
|
@@ -1505,7 +1557,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1505
1557
|
// Context promotion
|
|
1506
1558
|
"contextPromotion.enabled": {
|
|
1507
1559
|
type: "boolean",
|
|
1508
|
-
default:
|
|
1560
|
+
default: false,
|
|
1509
1561
|
ui: {
|
|
1510
1562
|
tab: "context",
|
|
1511
1563
|
group: "General",
|
|
@@ -1769,6 +1821,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1769
1821
|
"qwen3",
|
|
1770
1822
|
"gemini",
|
|
1771
1823
|
"gemma",
|
|
1824
|
+
"minimax",
|
|
1772
1825
|
] as const,
|
|
1773
1826
|
default: "auto",
|
|
1774
1827
|
ui: {
|
|
@@ -1795,6 +1848,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1795
1848
|
{ value: "qwen3", label: "Qwen3", description: "Use the Qwen3 owned dialect." },
|
|
1796
1849
|
{ value: "gemini", label: "Gemini", description: "Use the Gemini owned dialect." },
|
|
1797
1850
|
{ value: "gemma", label: "Gemma", description: "Use the Gemma owned dialect." },
|
|
1851
|
+
{ value: "minimax", label: "MiniMax", description: "Use the MiniMax owned dialect." },
|
|
1798
1852
|
],
|
|
1799
1853
|
},
|
|
1800
1854
|
},
|
|
@@ -2658,7 +2712,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
2658
2712
|
group: "Read Summaries",
|
|
2659
2713
|
label: "Read Summary Unfold Ceiling",
|
|
2660
2714
|
description:
|
|
2661
|
-
"Hard ceiling on summary size while BFS-unfolding. An unfold
|
|
2715
|
+
"Hard ceiling on summary size while BFS-unfolding. An unfold whose revealed lines would exceed this is skipped (that span stays folded) and unfolding continues with the remaining spans.",
|
|
2662
2716
|
},
|
|
2663
2717
|
},
|
|
2664
2718
|
|
|
@@ -3857,6 +3911,34 @@ export const SETTINGS_SCHEMA = {
|
|
|
3857
3911
|
description: "Providers that web_search should never use, even as fallbacks",
|
|
3858
3912
|
},
|
|
3859
3913
|
},
|
|
3914
|
+
"providers.antigravityEndpoint": {
|
|
3915
|
+
type: "enum",
|
|
3916
|
+
values: ["auto", "production", "sandbox"] as const,
|
|
3917
|
+
default: "auto",
|
|
3918
|
+
ui: {
|
|
3919
|
+
tab: "providers",
|
|
3920
|
+
group: "Services",
|
|
3921
|
+
label: "Antigravity Endpoint Mode",
|
|
3922
|
+
description: "Endpoint routing strategy for google-antigravity providers (chat, search, image, discovery)",
|
|
3923
|
+
options: [
|
|
3924
|
+
{
|
|
3925
|
+
value: "auto",
|
|
3926
|
+
label: "Auto",
|
|
3927
|
+
description: "Try production endpoint, fail over to sandbox on 5xx/429",
|
|
3928
|
+
},
|
|
3929
|
+
{
|
|
3930
|
+
value: "production",
|
|
3931
|
+
label: "Production Only",
|
|
3932
|
+
description: "Force production endpoint only",
|
|
3933
|
+
},
|
|
3934
|
+
{
|
|
3935
|
+
value: "sandbox",
|
|
3936
|
+
label: "Sandbox Only",
|
|
3937
|
+
description: "Force sandbox endpoint only",
|
|
3938
|
+
},
|
|
3939
|
+
],
|
|
3940
|
+
},
|
|
3941
|
+
},
|
|
3860
3942
|
"providers.image": {
|
|
3861
3943
|
type: "enum",
|
|
3862
3944
|
values: ["auto", "openai", "antigravity", "xai", "gemini", "openrouter"] as const,
|
|
@@ -116,6 +116,17 @@ export function parseSeenLinesFromHashlineBody(body: string): number[] {
|
|
|
116
116
|
return seen;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/** Merge explicit 1-indexed displayed lines into a recorded hashline snapshot. */
|
|
120
|
+
export function recordSeenLines(
|
|
121
|
+
session: FileSnapshotStoreOwner,
|
|
122
|
+
absolutePath: string,
|
|
123
|
+
tag: string,
|
|
124
|
+
lines: readonly number[],
|
|
125
|
+
): void {
|
|
126
|
+
if (lines.length === 0) return;
|
|
127
|
+
getFileSnapshotStore(session).recordSeenLines(canonicalSnapshotKey(absolutePath), tag, lines);
|
|
128
|
+
}
|
|
129
|
+
|
|
119
130
|
/**
|
|
120
131
|
* Attach the lines a read displayed to the snapshot it minted, so the patcher
|
|
121
132
|
* can reject edits anchored on lines the model never saw. Best-effort: a no-op
|
|
@@ -128,7 +139,5 @@ export function recordSeenLinesFromBody(
|
|
|
128
139
|
tag: string,
|
|
129
140
|
body: string,
|
|
130
141
|
): void {
|
|
131
|
-
|
|
132
|
-
if (seen.length === 0) return;
|
|
133
|
-
getFileSnapshotStore(session).recordSeenLines(canonicalSnapshotKey(absolutePath), tag, seen);
|
|
142
|
+
recordSeenLines(session, absolutePath, tag, parseSeenLinesFromHashlineBody(body));
|
|
134
143
|
}
|
package/src/eval/py/runner.py
CHANGED
|
@@ -190,6 +190,11 @@ class _RunnerState:
|
|
|
190
190
|
|
|
191
191
|
|
|
192
192
|
_CURRENT_RID: contextvars.ContextVar[str | None] = contextvars.ContextVar("omp_current_rid", default=None)
|
|
193
|
+
_CURRENT_DISPLAYED_MATPLOTLIB_FIGURE_IDS: contextvars.ContextVar[set[int] | None] = contextvars.ContextVar(
|
|
194
|
+
"omp_displayed_matplotlib_figure_ids",
|
|
195
|
+
default=None,
|
|
196
|
+
)
|
|
197
|
+
|
|
193
198
|
|
|
194
199
|
_STATE = _RunnerState()
|
|
195
200
|
|
|
@@ -670,6 +675,36 @@ _REPR_MIMES = [
|
|
|
670
675
|
]
|
|
671
676
|
|
|
672
677
|
|
|
678
|
+
def _is_matplotlib_figure(value: Any) -> bool:
|
|
679
|
+
figure_module = sys.modules.get("matplotlib.figure")
|
|
680
|
+
figure_cls = getattr(figure_module, "Figure", None)
|
|
681
|
+
if isinstance(figure_cls, type) and isinstance(value, figure_cls):
|
|
682
|
+
return True
|
|
683
|
+
|
|
684
|
+
value_type = type(value)
|
|
685
|
+
return value_type.__module__ == "matplotlib.figure" and value_type.__name__ == "Figure"
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _matplotlib_figure_png(value: Any) -> str | None:
|
|
689
|
+
if not _is_matplotlib_figure(value):
|
|
690
|
+
return None
|
|
691
|
+
|
|
692
|
+
savefig = getattr(value, "savefig", None)
|
|
693
|
+
if not callable(savefig):
|
|
694
|
+
return None
|
|
695
|
+
|
|
696
|
+
try:
|
|
697
|
+
buf = io.BytesIO()
|
|
698
|
+
savefig(buf, format="png", bbox_inches="tight")
|
|
699
|
+
except Exception:
|
|
700
|
+
return None
|
|
701
|
+
|
|
702
|
+
displayed_ids = _CURRENT_DISPLAYED_MATPLOTLIB_FIGURE_IDS.get()
|
|
703
|
+
if displayed_ids is not None:
|
|
704
|
+
displayed_ids.add(id(value))
|
|
705
|
+
return base64.b64encode(buf.getvalue()).decode("ascii")
|
|
706
|
+
|
|
707
|
+
|
|
673
708
|
def _coerce_image_bytes(value: Any) -> str:
|
|
674
709
|
if isinstance(value, (bytes, bytearray)):
|
|
675
710
|
return base64.b64encode(bytes(value)).decode("ascii")
|
|
@@ -685,6 +720,10 @@ def _mime_bundle(value: Any) -> dict:
|
|
|
685
720
|
accessors, and always provides ``text/plain``.
|
|
686
721
|
"""
|
|
687
722
|
bundle: dict[str, Any] = {}
|
|
723
|
+
matplotlib_png = _matplotlib_figure_png(value)
|
|
724
|
+
if matplotlib_png is not None:
|
|
725
|
+
bundle["image/png"] = matplotlib_png
|
|
726
|
+
|
|
688
727
|
|
|
689
728
|
mimebundle = getattr(value, "_repr_mimebundle_", None)
|
|
690
729
|
if callable(mimebundle):
|
|
@@ -758,6 +797,9 @@ def _flush_matplotlib_figures() -> None:
|
|
|
758
797
|
for num in fignums:
|
|
759
798
|
try:
|
|
760
799
|
fig = plt.figure(num)
|
|
800
|
+
if id(fig) in (_CURRENT_DISPLAYED_MATPLOTLIB_FIGURE_IDS.get() or set()):
|
|
801
|
+
plt.close(fig)
|
|
802
|
+
continue
|
|
761
803
|
buf = io.BytesIO()
|
|
762
804
|
fig.savefig(buf, format="png", bbox_inches="tight")
|
|
763
805
|
data = base64.b64encode(buf.getvalue()).decode("ascii")
|
|
@@ -978,6 +1020,7 @@ def _start_parent_watchdog() -> None:
|
|
|
978
1020
|
async def _handle_request_async(req: dict) -> None:
|
|
979
1021
|
rid = str(req.get("id"))
|
|
980
1022
|
token = _CURRENT_RID.set(rid)
|
|
1023
|
+
displayed_matplotlib_token = _CURRENT_DISPLAYED_MATPLOTLIB_FIGURE_IDS.set(set())
|
|
981
1024
|
_STATE.capture_rid = rid
|
|
982
1025
|
_STATE.user_ns["__omp_run_id__"] = rid
|
|
983
1026
|
_STATE.cancel_requested = False
|
|
@@ -1046,6 +1089,7 @@ async def _handle_request_async(req: dict) -> None:
|
|
|
1046
1089
|
_STATE.capture_rid = None
|
|
1047
1090
|
_flush_stream_proxies(rid)
|
|
1048
1091
|
_CURRENT_RID.reset(token)
|
|
1092
|
+
_CURRENT_DISPLAYED_MATPLOTLIB_FIGURE_IDS.reset(displayed_matplotlib_token)
|
|
1049
1093
|
|
|
1050
1094
|
|
|
1051
1095
|
def _emit_error(rid: str, exc: BaseException) -> None:
|
|
@@ -46,6 +46,8 @@ import type {
|
|
|
46
46
|
SessionBeforeSwitchResult,
|
|
47
47
|
SessionBeforeTreeResult,
|
|
48
48
|
SessionCompactingResult,
|
|
49
|
+
SessionStopEvent,
|
|
50
|
+
SessionStopEventResult,
|
|
49
51
|
ToolCallEvent,
|
|
50
52
|
ToolCallEventResult,
|
|
51
53
|
ToolResultEvent,
|
|
@@ -135,7 +137,9 @@ type RunnerEmitResult<TEvent extends RunnerEmitEvent> = TEvent extends { type: "
|
|
|
135
137
|
? SessionBeforeTreeResult | undefined
|
|
136
138
|
: TEvent extends { type: "session.compacting" }
|
|
137
139
|
? SessionCompactingResult | undefined
|
|
138
|
-
:
|
|
140
|
+
: TEvent extends { type: "session_stop" }
|
|
141
|
+
? SessionStopEventResult | undefined
|
|
142
|
+
: undefined;
|
|
139
143
|
|
|
140
144
|
export type NewSessionHandler = (options?: {
|
|
141
145
|
parentSession?: string;
|
|
@@ -322,6 +326,10 @@ export class ExtensionRunner {
|
|
|
322
326
|
await this.emit({ type: "credential_disabled", ...event });
|
|
323
327
|
}
|
|
324
328
|
|
|
329
|
+
async emitSessionStop(event: Omit<SessionStopEvent, "type">): Promise<SessionStopEventResult | undefined> {
|
|
330
|
+
return await this.emit({ type: "session_stop", ...event });
|
|
331
|
+
}
|
|
332
|
+
|
|
325
333
|
getUIContext(): ExtensionUIContext {
|
|
326
334
|
return this.#uiContext;
|
|
327
335
|
}
|
|
@@ -588,7 +596,7 @@ export class ExtensionRunner {
|
|
|
588
596
|
|
|
589
597
|
async emit<TEvent extends RunnerEmitEvent>(event: TEvent): Promise<RunnerEmitResult<TEvent>> {
|
|
590
598
|
const ctx = this.createContext();
|
|
591
|
-
let result: SessionBeforeEventResult | SessionCompactingResult | undefined;
|
|
599
|
+
let result: SessionBeforeEventResult | SessionCompactingResult | SessionStopEventResult | undefined;
|
|
592
600
|
|
|
593
601
|
if (this.#isSessionShutdownEvent(event)) {
|
|
594
602
|
const timeoutMs = handlerTimeoutForEvent(event.type);
|
|
@@ -627,6 +635,16 @@ export class ExtensionRunner {
|
|
|
627
635
|
if (event.type === "session.compacting" && handlerResult) {
|
|
628
636
|
result = handlerResult as SessionCompactingResult;
|
|
629
637
|
}
|
|
638
|
+
|
|
639
|
+
if (event.type === "session_stop" && handlerResult) {
|
|
640
|
+
result = handlerResult as SessionStopEventResult;
|
|
641
|
+
const hasContinuationContext =
|
|
642
|
+
(typeof result.additionalContext === "string" && result.additionalContext.length > 0) ||
|
|
643
|
+
(typeof result.reason === "string" && result.reason.length > 0);
|
|
644
|
+
if ((result.continue === true || result.decision === "block") && hasContinuationContext) {
|
|
645
|
+
return result as RunnerEmitResult<TEvent>;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
630
648
|
}
|
|
631
649
|
}
|
|
632
650
|
|
|
@@ -82,6 +82,8 @@ import type {
|
|
|
82
82
|
SessionEvent,
|
|
83
83
|
SessionShutdownEvent,
|
|
84
84
|
SessionStartEvent,
|
|
85
|
+
SessionStopEvent,
|
|
86
|
+
SessionStopEventResult,
|
|
85
87
|
SessionSwitchEvent,
|
|
86
88
|
SessionTreeEvent,
|
|
87
89
|
TodoReminderEvent,
|
|
@@ -274,11 +276,11 @@ export interface ExtensionUIContext {
|
|
|
274
276
|
// ============================================================================
|
|
275
277
|
|
|
276
278
|
export interface ContextUsage {
|
|
277
|
-
/** Estimated context tokens
|
|
278
|
-
tokens: number
|
|
279
|
+
/** Estimated context tokens. */
|
|
280
|
+
tokens: number;
|
|
279
281
|
contextWindow: number;
|
|
280
|
-
/** Context usage as percentage of context window
|
|
281
|
-
percent: number
|
|
282
|
+
/** Context usage as percentage of context window. */
|
|
283
|
+
percent: number;
|
|
282
284
|
}
|
|
283
285
|
|
|
284
286
|
export interface CompactOptions {
|
|
@@ -525,7 +527,14 @@ export interface BeforeAgentStartEvent {
|
|
|
525
527
|
systemPrompt: string[];
|
|
526
528
|
}
|
|
527
529
|
|
|
528
|
-
export type {
|
|
530
|
+
export type {
|
|
531
|
+
AgentEndEvent,
|
|
532
|
+
AgentStartEvent,
|
|
533
|
+
SessionStopEvent,
|
|
534
|
+
SessionStopEventResult,
|
|
535
|
+
TurnEndEvent,
|
|
536
|
+
TurnStartEvent,
|
|
537
|
+
} from "../shared-events";
|
|
529
538
|
|
|
530
539
|
/** Fired when a message starts (user, assistant, or toolResult) */
|
|
531
540
|
export interface MessageStartEvent {
|
|
@@ -802,6 +811,7 @@ export type ExtensionEvent =
|
|
|
802
811
|
| BeforeAgentStartEvent
|
|
803
812
|
| AgentStartEvent
|
|
804
813
|
| AgentEndEvent
|
|
814
|
+
| SessionStopEvent
|
|
805
815
|
| TurnStartEvent
|
|
806
816
|
| TurnEndEvent
|
|
807
817
|
| MessageStartEvent
|
|
@@ -978,6 +988,7 @@ export interface ExtensionAPI {
|
|
|
978
988
|
on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult>): void;
|
|
979
989
|
on(event: "agent_start", handler: ExtensionHandler<AgentStartEvent>): void;
|
|
980
990
|
on(event: "agent_end", handler: ExtensionHandler<AgentEndEvent>): void;
|
|
991
|
+
on(event: "session_stop", handler: ExtensionHandler<SessionStopEvent, SessionStopEventResult>): void;
|
|
981
992
|
on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
|
|
982
993
|
on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent>): void;
|
|
983
994
|
on(event: "message_start", handler: ExtensionHandler<MessageStartEvent>): void;
|