@oh-my-pi/pi-coding-agent 16.4.6 → 16.5.0
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 +88 -0
- package/dist/cli.js +3312 -3254
- package/dist/types/cli/args.d.ts +5 -0
- package/dist/types/cli/gallery-fixtures/shell.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +15 -0
- package/dist/types/config/model-resolver.d.ts +4 -3
- package/dist/types/config/model-roles.d.ts +8 -0
- package/dist/types/config/settings-schema.d.ts +46 -14
- package/dist/types/extensibility/extensions/types.d.ts +1 -1
- package/dist/types/launch/broker.d.ts +2 -0
- package/dist/types/launch/client.d.ts +22 -0
- package/dist/types/launch/paths.d.ts +4 -0
- package/dist/types/launch/presence.d.ts +8 -0
- package/dist/types/launch/protocol.d.ts +170 -0
- package/dist/types/launch/terminal-output.d.ts +7 -0
- package/dist/types/modes/components/agent-hub.d.ts +1 -1
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-browser.d.ts +28 -3
- package/dist/types/modes/components/model-hub.d.ts +0 -10
- package/dist/types/modes/components/model-picker.d.ts +44 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/dist/types/modes/print-mode.d.ts +14 -8
- package/dist/types/modes/print-mode.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/sdk.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +42 -0
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/session/session-entries.d.ts +6 -0
- package/dist/types/thinking.d.ts +2 -2
- package/dist/types/tiny/models.d.ts +1 -1
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +28 -2
- package/dist/types/tools/browser/tab-protocol.d.ts +6 -0
- package/dist/types/tools/browser/tab-worker.d.ts +6 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/launch.d.ts +121 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/terminal-output.d.ts +5 -0
- package/dist/types/vibe/runtime.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +0 -8
- package/package.json +20 -20
- package/src/cli/args.ts +11 -0
- package/src/cli/flag-tables.ts +9 -0
- package/src/cli/gallery-fixtures/shell.ts +82 -1
- package/src/cli.ts +10 -0
- package/src/commands/launch.ts +17 -0
- package/src/config/model-resolver.ts +110 -31
- package/src/config/model-roles.ts +14 -0
- package/src/config/settings-schema.ts +71 -7
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +2 -2
- package/src/eval/__tests__/completion-bridge.test.ts +1 -1
- package/src/eval/completion-bridge.ts +4 -4
- package/src/eval/js/shared/rewrite-imports.ts +31 -13
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/extensions/model-api.ts +1 -1
- package/src/extensibility/extensions/types.ts +1 -1
- package/src/internal-urls/docs-index.ts +4 -4
- package/src/launch/broker.ts +1017 -0
- package/src/launch/client.ts +344 -0
- package/src/launch/paths.ts +17 -0
- package/src/launch/presence.ts +82 -0
- package/src/launch/protocol.ts +386 -0
- package/src/launch/terminal-output.ts +46 -0
- package/src/main.ts +50 -1
- package/src/modes/acp/acp-agent.ts +8 -1
- package/src/modes/components/agent-hub.ts +101 -31
- package/src/modes/components/compaction-summary-message.ts +8 -2
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/model-browser.ts +152 -49
- package/src/modes/components/model-hub.ts +55 -142
- package/src/modes/components/model-picker.ts +233 -0
- package/src/modes/components/plan-review-overlay.ts +7 -0
- package/src/modes/components/snapcompact-shape-preview-doc.md +7 -11
- package/src/modes/components/status-line/component.test.ts +41 -2
- package/src/modes/components/status-line/component.ts +4 -0
- package/src/modes/components/status-line/segments.ts +6 -0
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/welcome.ts +13 -14
- package/src/modes/controllers/command-controller.ts +9 -1
- package/src/modes/controllers/event-controller.ts +31 -32
- package/src/modes/controllers/selector-controller.ts +96 -22
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +20 -3
- package/src/modes/print-mode.test.ts +71 -0
- package/src/modes/print-mode.ts +51 -2
- package/src/modes/theme/theme.ts +9 -0
- package/src/modes/utils/ui-helpers.ts +25 -4
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/scout.md +1 -1
- package/src/prompts/system/plan-yolo-handoff.md +5 -0
- package/src/prompts/system/prewalk-checklist.md +7 -0
- package/src/prompts/system/prewalk-continue.md +1 -0
- package/src/prompts/system/prewalk-plan.md +13 -0
- package/src/prompts/system/system-prompt.md +9 -7
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/bash.md +6 -4
- package/src/prompts/tools/browser.md +4 -4
- package/src/prompts/tools/launch.md +25 -0
- package/src/sdk.ts +8 -2
- package/src/session/agent-session.ts +550 -87
- package/src/session/session-context.test.ts +10 -5
- package/src/session/session-context.ts +25 -8
- package/src/session/session-entries.ts +6 -0
- package/src/slash-commands/builtin-registry.ts +25 -0
- package/src/task/agents.ts +2 -2
- package/src/thinking.ts +10 -3
- package/src/tiny/models.ts +7 -7
- package/src/tools/bash-interactive.ts +5 -8
- package/src/tools/bash.ts +38 -24
- package/src/tools/browser/cmux/cmux-tab.ts +17 -2
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +66 -6
- package/src/tools/browser/tab-protocol.ts +6 -0
- package/src/tools/browser/tab-supervisor.ts +17 -1
- package/src/tools/browser/tab-worker.ts +140 -21
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/index.ts +5 -0
- package/src/tools/inspect-image.ts +2 -2
- package/src/tools/launch.ts +643 -0
- package/src/tools/render-utils.ts +3 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/terminal-output.ts +141 -0
- package/src/tts/speech-enhancer.ts +2 -2
- package/src/utils/image-vision-fallback.ts +3 -3
- package/src/vibe/runtime.ts +2 -2
- package/src/web/search/provider.ts +0 -10
- package/src/web/search/providers/perplexity.ts +18 -2
- package/src/web/search/providers/public.ts +2 -4
- package/src/web/search/types.ts +0 -10
- package/dist/types/web/search/providers/bing.d.ts +0 -14
- package/dist/types/web/search/providers/yahoo.d.ts +0 -14
- package/src/web/search/providers/bing.ts +0 -197
- package/src/web/search/providers/yahoo.ts +0 -179
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test";
|
|
|
2
2
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
4
4
|
import type { CompactionSummaryMessage } from "./messages";
|
|
5
|
-
import { buildSessionContext } from "./session-context";
|
|
5
|
+
import { buildSessionContext, type StrippedToolCallsMarker } from "./session-context";
|
|
6
6
|
import type { SessionEntry } from "./session-entries";
|
|
7
7
|
|
|
8
8
|
const timestamp = "2026-07-09T00:00:00.000Z";
|
|
@@ -129,12 +129,16 @@ function danglingCallIds(messages: AgentMessage[]): string[] {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
describe("buildSessionContext dangling toolCalls", () => {
|
|
132
|
-
it("strips a dangling toolCall
|
|
132
|
+
it("strips a dangling toolCall from the transcript but keeps the turn with a stripped marker", () => {
|
|
133
133
|
const context = buildSessionContext(danglingToolCallEntries, undefined, undefined, { transcript: true });
|
|
134
134
|
|
|
135
135
|
expect(danglingCallIds(context.messages)).toEqual([]);
|
|
136
|
-
// The turn
|
|
137
|
-
|
|
136
|
+
// The turn survives (even content-less) carrying the marker so the TUI
|
|
137
|
+
// renders a placeholder row instead of silently erasing the activity.
|
|
138
|
+
const assistant = context.messages.find(message => message.role === "assistant");
|
|
139
|
+
expect(assistant).toBeDefined();
|
|
140
|
+
expect(assistant?.content).toEqual([]);
|
|
141
|
+
expect((assistant as AgentMessage & StrippedToolCallsMarker).strippedToolCalls).toBe(1);
|
|
138
142
|
});
|
|
139
143
|
|
|
140
144
|
it("keeps a dangling toolCall in transcript mode with keepDanglingToolCalls", () => {
|
|
@@ -146,11 +150,12 @@ describe("buildSessionContext dangling toolCalls", () => {
|
|
|
146
150
|
expect(danglingCallIds(context.messages)).toEqual(["call-1"]);
|
|
147
151
|
});
|
|
148
152
|
|
|
149
|
-
it("always strips dangling toolCalls from the LLM context", () => {
|
|
153
|
+
it("always strips dangling toolCalls from the LLM context and drops the emptied turn", () => {
|
|
150
154
|
const context = buildSessionContext(danglingToolCallEntries, undefined, undefined, {
|
|
151
155
|
keepDanglingToolCalls: true,
|
|
152
156
|
});
|
|
153
157
|
|
|
154
158
|
expect(danglingCallIds(context.messages)).toEqual([]);
|
|
159
|
+
expect(context.messages.some(message => message.role === "assistant")).toBe(false);
|
|
155
160
|
});
|
|
156
161
|
});
|
|
@@ -131,6 +131,16 @@ export interface BuildSessionContextOptions {
|
|
|
131
131
|
keepDanglingToolCalls?: boolean;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Display-only marker set on transcript assistant messages whose dangling
|
|
136
|
+
* `toolCall` blocks were stripped (no paired result on the resolved path —
|
|
137
|
+
* failed/retried turns, results on sibling branches). The TUI renders a
|
|
138
|
+
* placeholder row from it so the turn's activity never silently vanishes.
|
|
139
|
+
*/
|
|
140
|
+
export interface StrippedToolCallsMarker {
|
|
141
|
+
strippedToolCalls?: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
134
144
|
/**
|
|
135
145
|
* Build the session context from entries using tree traversal.
|
|
136
146
|
* If leafId is provided, walks from that entry to root.
|
|
@@ -353,6 +363,7 @@ export function buildSessionContext(
|
|
|
353
363
|
undefined,
|
|
354
364
|
undefined,
|
|
355
365
|
snapcompactHistoryBlocksForContext(snapcompactArchive, options),
|
|
366
|
+
entry.warning,
|
|
356
367
|
),
|
|
357
368
|
);
|
|
358
369
|
} else {
|
|
@@ -385,6 +396,7 @@ export function buildSessionContext(
|
|
|
385
396
|
providerPayload,
|
|
386
397
|
undefined,
|
|
387
398
|
snapcompactHistoryBlocksForContext(snapcompactArchive, options),
|
|
399
|
+
compaction.warning,
|
|
388
400
|
);
|
|
389
401
|
// Agent context (non-transcript): summary first so the LLM sees the
|
|
390
402
|
// compacted context before recent messages.
|
|
@@ -468,10 +480,11 @@ export function buildSessionContext(
|
|
|
468
480
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
469
481
|
const message = messages[i];
|
|
470
482
|
if (message.role !== "assistant") continue;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
483
|
+
let strippedToolCalls = 0;
|
|
484
|
+
for (const block of message.content) {
|
|
485
|
+
if (block.type === "toolCall" && !pairedToolResultIds.has(block.id)) strippedToolCalls++;
|
|
486
|
+
}
|
|
487
|
+
if (strippedToolCalls === 0) continue;
|
|
475
488
|
const normalized = message.content
|
|
476
489
|
.filter(
|
|
477
490
|
block =>
|
|
@@ -483,13 +496,17 @@ export function buildSessionContext(
|
|
|
483
496
|
? { ...block, thinkingSignature: undefined }
|
|
484
497
|
: block,
|
|
485
498
|
);
|
|
486
|
-
if (normalized.length === 0) {
|
|
499
|
+
if (normalized.length === 0 && !options?.transcript) {
|
|
487
500
|
messages.splice(i, 1);
|
|
501
|
+
} else {
|
|
502
|
+
const rewritten = { ...message, content: normalized };
|
|
488
503
|
if (options?.transcript) {
|
|
489
|
-
|
|
504
|
+
// Display transcript: keep the turn (even content-less) and mark
|
|
505
|
+
// how many calls were dropped so the TUI renders a placeholder
|
|
506
|
+
// row instead of silently erasing the turn's activity.
|
|
507
|
+
(rewritten as AgentMessage & StrippedToolCallsMarker).strippedToolCalls = strippedToolCalls;
|
|
490
508
|
}
|
|
491
|
-
|
|
492
|
-
messages[i] = { ...message, content: normalized };
|
|
509
|
+
messages[i] = rewritten;
|
|
493
510
|
}
|
|
494
511
|
}
|
|
495
512
|
}
|
|
@@ -92,6 +92,12 @@ export interface CompactionEntry<T = unknown> extends SessionEntryBase {
|
|
|
92
92
|
preserveData?: Record<string, unknown>;
|
|
93
93
|
/** True if generated by an extension, undefined/false if pi-generated (backward compatible) */
|
|
94
94
|
fromExtension?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Dead-end warning from the post-pass progress guard: the pass completed
|
|
97
|
+
* but freed too little for maintenance to continue. Rendered on the
|
|
98
|
+
* compaction divider.
|
|
99
|
+
*/
|
|
100
|
+
warning?: string;
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
export interface BranchSummaryEntry<T = unknown> extends SessionEntryBase {
|
|
@@ -6,6 +6,7 @@ import { type AutocompleteItem, Spacer } from "@oh-my-pi/pi-tui";
|
|
|
6
6
|
import { APP_NAME, getProjectDir, setProjectDir } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { COLLAB_GUEST_ALLOWED_COMMANDS, CollabGuestLink } from "../collab/guest";
|
|
8
8
|
import { CollabHost } from "../collab/host";
|
|
9
|
+
import { expandRoleAlias, getModelMatchPreferences, resolveCliModel } from "../config/model-resolver";
|
|
9
10
|
import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
|
|
10
11
|
import type { SettingPath, SettingValue } from "../config/settings";
|
|
11
12
|
import { settings } from "../config/settings";
|
|
@@ -460,6 +461,30 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
460
461
|
runtime.ctx.editor.setText("");
|
|
461
462
|
},
|
|
462
463
|
},
|
|
464
|
+
{
|
|
465
|
+
name: "prewalk",
|
|
466
|
+
description: "Switch to a fast/cheap model at the next action (works even without --prewalk)",
|
|
467
|
+
acpDescription: "Prewalk at the next action",
|
|
468
|
+
handle: async (_command, runtime) => {
|
|
469
|
+
const rolePattern = expandRoleAlias("@smol", runtime.settings);
|
|
470
|
+
const resolved = resolveCliModel({
|
|
471
|
+
cliModel: rolePattern,
|
|
472
|
+
modelRegistry: runtime.session.modelRegistry,
|
|
473
|
+
preferences: getModelMatchPreferences(runtime.settings),
|
|
474
|
+
});
|
|
475
|
+
if (resolved.error || !resolved.model) {
|
|
476
|
+
return usage(resolved.error ?? `Model "${rolePattern}" not found`, runtime);
|
|
477
|
+
}
|
|
478
|
+
if (!runtime.session.modelRegistry.hasConfiguredAuth(resolved.model)) {
|
|
479
|
+
return usage(`No API key for ${resolved.model.provider}/${resolved.model.id}`, runtime);
|
|
480
|
+
}
|
|
481
|
+
runtime.session.armPrewalk(resolved.model, resolved.thinkingLevel);
|
|
482
|
+
await runtime.output(
|
|
483
|
+
`Prewalk on: switching to ${resolved.model.provider}/${resolved.model.id} at the next edit/write (todo-gated).`,
|
|
484
|
+
);
|
|
485
|
+
return commandConsumed();
|
|
486
|
+
},
|
|
487
|
+
},
|
|
463
488
|
{
|
|
464
489
|
name: "advisor",
|
|
465
490
|
description: "Toggle the advisor (a second model that reviews each turn and injects notes)",
|
package/src/task/agents.ts
CHANGED
|
@@ -50,7 +50,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
|
|
50
50
|
name: "task",
|
|
51
51
|
description: "General-purpose subagent with full capabilities for delegated multi-step tasks",
|
|
52
52
|
spawns: "*",
|
|
53
|
-
model: "
|
|
53
|
+
model: "@task",
|
|
54
54
|
thinkingLevel: AUTO_THINKING,
|
|
55
55
|
},
|
|
56
56
|
template: taskMd,
|
|
@@ -60,7 +60,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
|
|
60
60
|
frontmatter: {
|
|
61
61
|
name: "sonic",
|
|
62
62
|
description: "Low-reasoning agent for strictly mechanical updates or data collection only",
|
|
63
|
-
model: "
|
|
63
|
+
model: "@smol",
|
|
64
64
|
thinkingLevel: Effort.Medium,
|
|
65
65
|
},
|
|
66
66
|
template: taskMd,
|
package/src/thinking.ts
CHANGED
|
@@ -62,18 +62,25 @@ const THINKING_LEVEL_BY_SELECTOR: Readonly<Record<string, ThinkingLevel>> = {
|
|
|
62
62
|
};
|
|
63
63
|
|
|
64
64
|
function getOwnSelector<T>(selectors: Readonly<Record<string, T>>, value: string | null | undefined): T | undefined {
|
|
65
|
-
|
|
65
|
+
if (value === undefined || value === null) return undefined;
|
|
66
|
+
if (Object.hasOwn(selectors, value)) return selectors[value];
|
|
67
|
+
// Accept unambiguous abbreviations (`xhi` → xhigh, `med` → medium) so every
|
|
68
|
+
// selector surface (`--thinking`, `:suffix`, role values) parses alike.
|
|
69
|
+
// Two-character minimum keeps single letters (`m`) from guessing.
|
|
70
|
+
if (value.length < 2) return undefined;
|
|
71
|
+
const matches = Object.keys(selectors).filter(selector => selector.startsWith(value));
|
|
72
|
+
return matches.length === 1 ? selectors[matches[0]] : undefined;
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
/**
|
|
69
|
-
* Parses a provider-facing effort value.
|
|
76
|
+
* Parses a provider-facing effort value. Accepts unambiguous abbreviations.
|
|
70
77
|
*/
|
|
71
78
|
export function parseEffort(value: string | null | undefined): Effort | undefined {
|
|
72
79
|
return getOwnSelector(EFFORT_BY_SELECTOR, value);
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
/**
|
|
76
|
-
* Parses an agent-local thinking selector.
|
|
83
|
+
* Parses an agent-local thinking selector. Accepts unambiguous abbreviations.
|
|
77
84
|
*/
|
|
78
85
|
export function parseThinkingLevel(value: string | null | undefined): ThinkingLevel | undefined {
|
|
79
86
|
return getOwnSelector(THINKING_LEVEL_BY_SELECTOR, value);
|
package/src/tiny/models.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Default session-title model: the online
|
|
1
|
+
/** Default session-title model: the online @smol path (no local download / on-device inference). */
|
|
2
2
|
export const ONLINE_TINY_TITLE_MODEL_KEY = "online";
|
|
3
3
|
/** Local model the `tiny-models` CLI downloads when none is named. Not the session-title default — that is {@link ONLINE_TINY_TITLE_MODEL_KEY}. */
|
|
4
4
|
export const DEFAULT_TINY_TITLE_LOCAL_MODEL_KEY = "lfm2-700m";
|
|
@@ -87,9 +87,9 @@ void TINY_TITLE_MODEL_VALUES_MATCH_REGISTRY;
|
|
|
87
87
|
export const TINY_TITLE_MODEL_OPTIONS = [
|
|
88
88
|
{
|
|
89
89
|
value: ONLINE_TINY_TITLE_MODEL_KEY,
|
|
90
|
-
label: "Online (TINY role, else
|
|
90
|
+
label: "Online (TINY role, else @smol)",
|
|
91
91
|
description:
|
|
92
|
-
"Online title generation: the TINY model role (set one in /models) when assigned, otherwise the online fallback (commit role, then
|
|
92
|
+
"Online title generation: the TINY model role (set one in /models) when assigned, otherwise the online fallback (commit role, then @smol). No local download or on-device inference.",
|
|
93
93
|
},
|
|
94
94
|
...TINY_TITLE_LOCAL_MODELS.map(model => ({
|
|
95
95
|
value: model.key,
|
|
@@ -194,9 +194,9 @@ void TINY_MEMORY_MODEL_VALUES_MATCH_REGISTRY;
|
|
|
194
194
|
export const TINY_MEMORY_MODEL_OPTIONS = [
|
|
195
195
|
{
|
|
196
196
|
value: ONLINE_MEMORY_MODEL_KEY,
|
|
197
|
-
label: "Online (TINY role, else smol)",
|
|
197
|
+
label: "Online (TINY role, else @smol)",
|
|
198
198
|
description:
|
|
199
|
-
"Use the online model: the TINY role from /models when set, otherwise
|
|
199
|
+
"Use the online model: the TINY role from /models when set, otherwise @smol. No local model download or on-device inference.",
|
|
200
200
|
},
|
|
201
201
|
...TINY_MEMORY_LOCAL_MODELS.map(model => ({
|
|
202
202
|
value: model.key,
|
|
@@ -256,9 +256,9 @@ export type AutoThinkingModelKey = TinyMemoryModelKey;
|
|
|
256
256
|
export const AUTO_THINKING_MODEL_OPTIONS = [
|
|
257
257
|
{
|
|
258
258
|
value: ONLINE_AUTO_THINKING_MODEL_KEY,
|
|
259
|
-
label: "Online (TINY role, else smol)",
|
|
259
|
+
label: "Online (TINY role, else @smol)",
|
|
260
260
|
description:
|
|
261
|
-
"Classify prompt difficulty online with the TINY role model (set one in /models) or
|
|
261
|
+
"Classify prompt difficulty online with the TINY role model (set one in /models) or @smol; no local download or on-device inference.",
|
|
262
262
|
},
|
|
263
263
|
...TINY_MEMORY_LOCAL_MODELS.map(model => ({
|
|
264
264
|
value: model.key,
|
|
@@ -19,6 +19,7 @@ import { OutputSink, type OutputSummary } from "../session/streaming-output";
|
|
|
19
19
|
import { sanitizeWithOptionalSixelPassthrough } from "../utils/sixel";
|
|
20
20
|
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "./output-meta";
|
|
21
21
|
import { formatStatusIcon, replaceTabs } from "./render-utils";
|
|
22
|
+
import { readTerminalRows, styleTerminalRow } from "./terminal-output";
|
|
22
23
|
|
|
23
24
|
export interface BashInteractiveResult extends OutputSummary {
|
|
24
25
|
exitCode: number | undefined;
|
|
@@ -225,14 +226,10 @@ class BashInteractiveOverlayComponent implements Component {
|
|
|
225
226
|
|
|
226
227
|
#readViewport(innerWidth: number, maxContentRows: number): string[] {
|
|
227
228
|
this.#terminal.resize(innerWidth, maxContentRows);
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const line = buffer.getLine(viewportY + i)?.translateToString(true) ?? "";
|
|
233
|
-
visibleLines.push(truncateToWidth(replaceTabs(sanitizeText(line)), innerWidth));
|
|
234
|
-
}
|
|
235
|
-
return visibleLines;
|
|
229
|
+
const viewportY = this.#terminal.buffer.active.viewportY;
|
|
230
|
+
return readTerminalRows(this.#terminal, viewportY, maxContentRows).map(line =>
|
|
231
|
+
truncateToWidth(styleTerminalRow(line, this.uiTheme.getFgAnsi("toolOutput")), innerWidth),
|
|
232
|
+
);
|
|
236
233
|
}
|
|
237
234
|
render(width: number): readonly string[] {
|
|
238
235
|
const safeWidth = Math.max(20, width);
|
package/src/tools/bash.ts
CHANGED
|
@@ -36,12 +36,18 @@ import {
|
|
|
36
36
|
stripRawOutputArtifactNotice,
|
|
37
37
|
} from "./output-meta";
|
|
38
38
|
import { resolveToCwd } from "./path-utils";
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
capPreviewLines,
|
|
41
|
+
DEFAULT_TERMINAL_PREVIEW_LINES,
|
|
42
|
+
formatToolWorkingDirectory,
|
|
43
|
+
previewWindowRows,
|
|
44
|
+
replaceTabs,
|
|
45
|
+
} from "./render-utils";
|
|
40
46
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
41
47
|
import { toolResult } from "./tool-result";
|
|
42
48
|
import { clampTimeout, TOOL_TIMEOUTS } from "./tool-timeouts";
|
|
43
49
|
|
|
44
|
-
export const BASH_DEFAULT_PREVIEW_LINES =
|
|
50
|
+
export const BASH_DEFAULT_PREVIEW_LINES = DEFAULT_TERMINAL_PREVIEW_LINES;
|
|
45
51
|
|
|
46
52
|
const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
47
53
|
const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
|
|
@@ -193,10 +199,9 @@ type ManagedBashJobCompletion =
|
|
|
193
199
|
|
|
194
200
|
interface ManagedBashJobHandle {
|
|
195
201
|
jobId: string;
|
|
196
|
-
label: string;
|
|
197
202
|
completion: Promise<ManagedBashJobCompletion>;
|
|
198
203
|
getLatestText: () => string;
|
|
199
|
-
|
|
204
|
+
stopUpdates: () => void;
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
function normalizeResultOutput(result: BashResult | BashInteractiveResult): string {
|
|
@@ -327,6 +332,10 @@ function formatExitCodeNotice(exitCode: number): string {
|
|
|
327
332
|
return `Command exited with code ${exitCode}`;
|
|
328
333
|
}
|
|
329
334
|
|
|
335
|
+
function formatBackgroundNotice(jobId: string): string {
|
|
336
|
+
return `Backgrounded as job ${jobId}; result will be delivered automatically.`;
|
|
337
|
+
}
|
|
338
|
+
|
|
330
339
|
/**
|
|
331
340
|
* Strip the trailing occurrence of `notice` (plus a single surrounding newline
|
|
332
341
|
* on each side) so the TUI can echo the value via a styled footer label
|
|
@@ -355,6 +364,11 @@ function stripExitCodeNotice(text: string, exitCode: number | undefined): string
|
|
|
355
364
|
return stripTrailingNotice(text, formatExitCodeNotice(exitCode));
|
|
356
365
|
}
|
|
357
366
|
|
|
367
|
+
function stripBackgroundNotice(text: string, async: BashToolDetails["async"] | undefined): string {
|
|
368
|
+
if (async?.state !== "running") return text;
|
|
369
|
+
return stripTrailingNotice(text, formatBackgroundNotice(async.jobId));
|
|
370
|
+
}
|
|
371
|
+
|
|
358
372
|
/**
|
|
359
373
|
* Bash tool implementation.
|
|
360
374
|
*
|
|
@@ -389,6 +403,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
389
403
|
hasGrep: isToolActive("grep", this.session.settings.get("grep.enabled")),
|
|
390
404
|
hasGlob: isToolActive("glob", this.session.settings.get("glob.enabled")),
|
|
391
405
|
hasRead: isToolActive("read", true),
|
|
406
|
+
hasLaunch: isToolActive("launch", this.session.settings.get("launch.enabled")),
|
|
392
407
|
hasEval: isToolActive(
|
|
393
408
|
"eval",
|
|
394
409
|
evalBackends.python || evalBackends.js || evalBackends.ruby || evalBackends.julia,
|
|
@@ -518,7 +533,6 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
518
533
|
|
|
519
534
|
#buildBackgroundStartResult(
|
|
520
535
|
jobId: string,
|
|
521
|
-
label: string,
|
|
522
536
|
previewText: string,
|
|
523
537
|
timeoutSec: number | undefined,
|
|
524
538
|
options: { requestedTimeoutSec?: number; notices?: readonly string[] } = {},
|
|
@@ -542,11 +556,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
542
556
|
if (options.notices?.length) {
|
|
543
557
|
lines.push(...options.notices, "");
|
|
544
558
|
}
|
|
545
|
-
lines.push(
|
|
546
|
-
lines.push("Result will be delivered automatically when complete.");
|
|
547
|
-
lines.push(
|
|
548
|
-
`You can use \`job\` to poll until complete, but prefer to continue with another task in the meanwhile if it's not blocking.`,
|
|
549
|
-
);
|
|
559
|
+
lines.push(formatBackgroundNotice(jobId));
|
|
550
560
|
return {
|
|
551
561
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
552
562
|
details,
|
|
@@ -567,7 +577,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
567
577
|
|
|
568
578
|
resolvedEnv?: Record<string, string>;
|
|
569
579
|
onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
|
|
570
|
-
|
|
580
|
+
forwardUpdates: boolean;
|
|
571
581
|
}): ManagedBashJobHandle {
|
|
572
582
|
const manager = this.session.asyncJobManager;
|
|
573
583
|
if (!manager) {
|
|
@@ -576,7 +586,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
576
586
|
|
|
577
587
|
const label = options.command.length > 120 ? `${options.command.slice(0, 117)}...` : options.command;
|
|
578
588
|
let latestText = "";
|
|
579
|
-
let
|
|
589
|
+
let forwardUpdates = options.forwardUpdates;
|
|
580
590
|
const completion = Promise.withResolvers<ManagedBashJobCompletion>();
|
|
581
591
|
|
|
582
592
|
const jobId = manager.register(
|
|
@@ -632,11 +642,12 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
632
642
|
},
|
|
633
643
|
{
|
|
634
644
|
ownerId: this.session.getAgentId?.() ?? undefined,
|
|
635
|
-
onProgress: async
|
|
645
|
+
onProgress: async text => {
|
|
636
646
|
latestText = text;
|
|
647
|
+
if (!forwardUpdates) return;
|
|
637
648
|
await options.onUpdate?.({
|
|
638
649
|
content: [{ type: "text", text }],
|
|
639
|
-
details:
|
|
650
|
+
details: {},
|
|
640
651
|
});
|
|
641
652
|
},
|
|
642
653
|
},
|
|
@@ -644,11 +655,10 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
644
655
|
|
|
645
656
|
return {
|
|
646
657
|
jobId,
|
|
647
|
-
label,
|
|
648
658
|
completion: completion.promise,
|
|
649
659
|
getLatestText: () => latestText,
|
|
650
|
-
|
|
651
|
-
|
|
660
|
+
stopUpdates: () => {
|
|
661
|
+
forwardUpdates = false;
|
|
652
662
|
},
|
|
653
663
|
};
|
|
654
664
|
}
|
|
@@ -813,9 +823,9 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
813
823
|
|
|
814
824
|
resolvedEnv,
|
|
815
825
|
onUpdate,
|
|
816
|
-
|
|
826
|
+
forwardUpdates: false,
|
|
817
827
|
});
|
|
818
|
-
return this.#buildBackgroundStartResult(job.jobId,
|
|
828
|
+
return this.#buildBackgroundStartResult(job.jobId, "", timeoutSec, {
|
|
819
829
|
requestedTimeoutSec,
|
|
820
830
|
notices: pendingNotices,
|
|
821
831
|
});
|
|
@@ -851,10 +861,10 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
851
861
|
|
|
852
862
|
resolvedEnv,
|
|
853
863
|
onUpdate,
|
|
854
|
-
startBackgrounded,
|
|
864
|
+
forwardUpdates: !startBackgrounded,
|
|
855
865
|
});
|
|
856
866
|
if (startBackgrounded) {
|
|
857
|
-
return this.#buildBackgroundStartResult(job.jobId,
|
|
867
|
+
return this.#buildBackgroundStartResult(job.jobId, "", timeoutSec, {
|
|
858
868
|
requestedTimeoutSec,
|
|
859
869
|
notices: pendingNotices,
|
|
860
870
|
});
|
|
@@ -874,9 +884,9 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
874
884
|
autoBgManager.cancel(job.jobId);
|
|
875
885
|
throw new ToolAbortError(job.getLatestText() || "Command aborted");
|
|
876
886
|
}
|
|
877
|
-
job.
|
|
887
|
+
job.stopUpdates();
|
|
878
888
|
autoBgManager.resumeDeliveries([job.jobId]);
|
|
879
|
-
return this.#buildBackgroundStartResult(job.jobId, job.
|
|
889
|
+
return this.#buildBackgroundStartResult(job.jobId, job.getLatestText(), timeoutSec, {
|
|
880
890
|
requestedTimeoutSec,
|
|
881
891
|
notices: pendingNotices,
|
|
882
892
|
});
|
|
@@ -1303,7 +1313,8 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1303
1313
|
) {
|
|
1304
1314
|
return cachedLines;
|
|
1305
1315
|
}
|
|
1306
|
-
const
|
|
1316
|
+
const withoutBackground = stripBackgroundNotice(rawOutput, details?.async);
|
|
1317
|
+
const strippedOutput = stripOutputNotice(withoutBackground, details?.meta);
|
|
1307
1318
|
const withoutExit = stripExitCodeNotice(strippedOutput, details?.exitCode);
|
|
1308
1319
|
const withoutWall = stripWallTimeNotice(withoutExit, details?.wallTimeMs);
|
|
1309
1320
|
const rawOutputArtifact = stripRawOutputArtifactNotice(withoutWall);
|
|
@@ -1317,6 +1328,9 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1317
1328
|
const requestedTimeoutSeconds = details?.requestedTimeoutSeconds;
|
|
1318
1329
|
const wallTimeMs = details?.wallTimeMs;
|
|
1319
1330
|
const statsParts: string[] = [];
|
|
1331
|
+
if (details?.async?.state === "running") {
|
|
1332
|
+
statsParts.push(`Backgrounded: ${details.async.jobId}`);
|
|
1333
|
+
}
|
|
1320
1334
|
if (wallTimeMs !== undefined) {
|
|
1321
1335
|
statsParts.push(`Wall: ${formatWallTimeSeconds(wallTimeMs)}s`);
|
|
1322
1336
|
}
|
|
@@ -12,7 +12,12 @@ import { ToolAbortError, ToolError, throwIfAborted } from "../../tool-errors";
|
|
|
12
12
|
import { type AriaSnapshotOptions, buildAriaSnapshotScript } from "../aria/aria-snapshot";
|
|
13
13
|
import { DEFAULT_VIEWPORT } from "../launch";
|
|
14
14
|
import { extractReadableFromHtml, type ReadableFormat } from "../readable";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
bindBrowserRunFacade,
|
|
17
|
+
resolvePredicateTimeout,
|
|
18
|
+
type WaitPredicateOptions,
|
|
19
|
+
waitForBrowserRun,
|
|
20
|
+
} from "../run-cancellation";
|
|
16
21
|
import { cloneSafe, RunOutput } from "../run-output";
|
|
17
22
|
import type { Observation, ReadyInfo, RunResultOk, ScreenshotResult, SessionSnapshot } from "../tab-protocol";
|
|
18
23
|
import {
|
|
@@ -1352,7 +1357,17 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
|
|
|
1352
1357
|
assert: (cond: unknown, text?: string): void => {
|
|
1353
1358
|
if (!cond) throw new ToolError(text ?? "Assertion failed");
|
|
1354
1359
|
},
|
|
1355
|
-
wait: (
|
|
1360
|
+
wait: (msOrPredicate: number | (() => unknown), waitOpts?: WaitPredicateOptions): Promise<unknown> =>
|
|
1361
|
+
waitForBrowserRun(
|
|
1362
|
+
msOrPredicate,
|
|
1363
|
+
signal,
|
|
1364
|
+
typeof msOrPredicate === "number"
|
|
1365
|
+
? waitOpts
|
|
1366
|
+
: {
|
|
1367
|
+
timeout: resolvePredicateTimeout(opts.timeoutMs, waitOpts?.timeout),
|
|
1368
|
+
interval: waitOpts?.interval,
|
|
1369
|
+
},
|
|
1370
|
+
),
|
|
1356
1371
|
});
|
|
1357
1372
|
|
|
1358
1373
|
const hooks: RuntimeHooks = {
|
|
@@ -121,12 +121,16 @@ async function loadBrowsers(): Promise<typeof BrowsersNs> {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
|
-
*
|
|
125
|
-
* Skipped when a system Chromium (NixOS)
|
|
126
|
-
* The browser is cached under
|
|
124
|
+
* Resolve the Chromium executable puppeteer will launch, lazily downloading it
|
|
125
|
+
* on first use via @puppeteer/browsers. Skipped when a system Chromium (NixOS)
|
|
126
|
+
* or PUPPETEER_EXECUTABLE_PATH is set. The browser is cached under
|
|
127
|
+
* ~/.omp/puppeteer (getPuppeteerDir). Returns undefined when platform
|
|
128
|
+
* detection fails (puppeteer default resolution takes over). Exported so
|
|
129
|
+
* real-browser tests can probe launchability and skip on hosts missing
|
|
130
|
+
* Chrome's system libraries.
|
|
127
131
|
*/
|
|
128
132
|
let chromiumExecutablePromise: Promise<string | undefined> | undefined;
|
|
129
|
-
async function ensureChromiumExecutable(): Promise<string | undefined> {
|
|
133
|
+
export async function ensureChromiumExecutable(): Promise<string | undefined> {
|
|
130
134
|
const sysChrome = resolveSystemChromium();
|
|
131
135
|
if (sysChrome) return sysChrome;
|
|
132
136
|
const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
2
|
-
import { throwIfAborted } from "../tool-errors";
|
|
2
|
+
import { ToolError, throwIfAborted } from "../tool-errors";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Marks a run-scoped promise as observed without changing its behavior for awaited callers.
|
|
@@ -16,12 +16,72 @@ export function markHandled<T>(promise: Promise<T>): Promise<T> {
|
|
|
16
16
|
return promise;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
/** Headroom subtracted from the cell budget so an in-run deadline fires before the opaque whole-cell timeout. */
|
|
20
|
+
export const CELL_BUDGET_SLACK_MS = 1_000;
|
|
21
|
+
|
|
22
|
+
/** Default poll deadline for `wait(predicate)` before clamping to the cell budget. */
|
|
23
|
+
export const DEFAULT_PREDICATE_TIMEOUT_MS = 30_000;
|
|
24
|
+
|
|
25
|
+
/** Options for the predicate form of the run-scoped `wait()` helper. */
|
|
26
|
+
export interface WaitPredicateOptions {
|
|
27
|
+
/** Max time to poll before failing, in ms (default 30s, clamped to the cell budget). */
|
|
28
|
+
timeout?: number;
|
|
29
|
+
/** Poll interval in ms (default 100, floor 10). */
|
|
30
|
+
interval?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Effective `wait(predicate)` deadline for a given cell budget. Always strictly below
|
|
35
|
+
* the cell budget so the named `wait(predicate) timed out` error wins the race against
|
|
36
|
+
* the opaque whole-cell "Browser code execution timed out". `0`/`Infinity` ("disable")
|
|
37
|
+
* map to the largest bounded deadline; negative/NaN garbage falls back to the default.
|
|
38
|
+
*/
|
|
39
|
+
export function resolvePredicateTimeout(cellTimeoutMs: number, explicit?: number): number {
|
|
40
|
+
const budgetBound = Math.max(1, cellTimeoutMs - CELL_BUDGET_SLACK_MS);
|
|
41
|
+
if (explicit === 0 || explicit === Number.POSITIVE_INFINITY) return budgetBound;
|
|
42
|
+
if (explicit !== undefined && Number.isFinite(explicit) && explicit > 0) return Math.min(explicit, budgetBound);
|
|
43
|
+
return Math.min(DEFAULT_PREDICATE_TIMEOUT_MS, budgetBound);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Run-scoped `wait()` helper for evaluated browser code, honoring the owning run's
|
|
48
|
+
* cancellation signal.
|
|
49
|
+
*
|
|
50
|
+
* - `wait(ms)` sleeps for `ms` milliseconds.
|
|
51
|
+
* - `wait(fn, { timeout?, interval? })` polls `fn` (sync or async) until it returns a
|
|
52
|
+
* truthy value and resolves with that value; throws a named `ToolError` on timeout
|
|
53
|
+
* instead of stalling into the whole-cell deadline. Predicate errors propagate.
|
|
54
|
+
*/
|
|
55
|
+
export function waitForBrowserRun(
|
|
56
|
+
msOrPredicate: number | (() => unknown),
|
|
57
|
+
signal: AbortSignal,
|
|
58
|
+
opts?: WaitPredicateOptions,
|
|
59
|
+
): Promise<unknown> {
|
|
60
|
+
const promise = (async (): Promise<unknown> => {
|
|
24
61
|
throwIfAborted(signal);
|
|
62
|
+
if (typeof msOrPredicate === "number") {
|
|
63
|
+
await untilAborted(signal, async () => await Bun.sleep(msOrPredicate));
|
|
64
|
+
throwIfAborted(signal);
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
if (typeof msOrPredicate !== "function") {
|
|
68
|
+
throw new ToolError("wait(...) expects milliseconds (number) or a predicate function to poll");
|
|
69
|
+
}
|
|
70
|
+
const timeout =
|
|
71
|
+
opts?.timeout !== undefined && Number.isFinite(opts.timeout) && opts.timeout > 0
|
|
72
|
+
? opts.timeout
|
|
73
|
+
: DEFAULT_PREDICATE_TIMEOUT_MS;
|
|
74
|
+
const interval = Math.max(opts?.interval ?? 100, 10);
|
|
75
|
+
const deadline = Date.now() + timeout;
|
|
76
|
+
for (;;) {
|
|
77
|
+
const value = await untilAborted(signal, async () => await msOrPredicate());
|
|
78
|
+
throwIfAborted(signal);
|
|
79
|
+
if (value) return value;
|
|
80
|
+
if (Date.now() + interval > deadline) {
|
|
81
|
+
throw new ToolError(`wait(predicate) timed out after ${timeout}ms — predicate never returned truthy`);
|
|
82
|
+
}
|
|
83
|
+
await untilAborted(signal, async () => await Bun.sleep(interval));
|
|
84
|
+
}
|
|
25
85
|
})();
|
|
26
86
|
return markHandled(promise);
|
|
27
87
|
}
|
|
@@ -59,6 +59,12 @@ export type WorkerInitPayload =
|
|
|
59
59
|
safeDir: string;
|
|
60
60
|
targetId: string;
|
|
61
61
|
dialogs?: "accept" | "dismiss";
|
|
62
|
+
/**
|
|
63
|
+
* Post-timeout recycle: before adopting the page, dismiss any open JS dialog and
|
|
64
|
+
* stop a pending navigation so a blocked target cannot stall worker init (which
|
|
65
|
+
* previously force-killed the tab). Never set for first-time Electron attach.
|
|
66
|
+
*/
|
|
67
|
+
recover?: boolean;
|
|
62
68
|
};
|
|
63
69
|
|
|
64
70
|
export type ToolReply = { ok: true; value: unknown } | { ok: false; error: RunErrorPayload };
|