@oh-my-pi/pi-coding-agent 16.4.8 → 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 +63 -0
- package/dist/cli.js +3502 -3444
- 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 +17 -2
- 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/status-line/types.d.ts +3 -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/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 +111 -21
- package/src/modes/components/model-hub.ts +23 -116
- package/src/modes/components/model-picker.ts +233 -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/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 +7 -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/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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Gallery fixtures for the shell tools (bash, eval). */
|
|
1
|
+
/** Gallery fixtures for the shell tools (bash, eval, launch). */
|
|
2
2
|
import type { GalleryFixture } from "./types";
|
|
3
3
|
|
|
4
4
|
export const shellFixtures: Record<string, GalleryFixture> = {
|
|
@@ -56,6 +56,87 @@ export const shellFixtures: Record<string, GalleryFixture> = {
|
|
|
56
56
|
},
|
|
57
57
|
},
|
|
58
58
|
|
|
59
|
+
launch: {
|
|
60
|
+
label: "Launch",
|
|
61
|
+
streamingArgs: { op: "start", name: "web" },
|
|
62
|
+
args: {
|
|
63
|
+
op: "start",
|
|
64
|
+
name: "web",
|
|
65
|
+
application: "bun",
|
|
66
|
+
args: ["run", "dev"],
|
|
67
|
+
ready: { log: "Local:.*http", port: 5173, timeout: 30 },
|
|
68
|
+
},
|
|
69
|
+
result: {
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: "Started web: ready pid=51234 uptime=1.2s restarts=0\nReady: Local: http://localhost:5173",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
details: {
|
|
77
|
+
op: "start",
|
|
78
|
+
daemon: {
|
|
79
|
+
name: "web",
|
|
80
|
+
id: "d-1",
|
|
81
|
+
state: "ready",
|
|
82
|
+
pid: 51234,
|
|
83
|
+
createdAt: 0,
|
|
84
|
+
startedAt: Date.now() - 1_200,
|
|
85
|
+
readyAt: Date.now(),
|
|
86
|
+
restartCount: 0,
|
|
87
|
+
outputBytes: 2048,
|
|
88
|
+
readyMatch: "Local: http://localhost:5173/",
|
|
89
|
+
persist: false,
|
|
90
|
+
detached: false,
|
|
91
|
+
},
|
|
92
|
+
timedOut: false,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
errorResult: {
|
|
96
|
+
content: [{ type: "text", text: "start requires application" }],
|
|
97
|
+
isError: true,
|
|
98
|
+
details: { op: "start" },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
launch_logs: {
|
|
103
|
+
label: "Launch",
|
|
104
|
+
renderer: "launch",
|
|
105
|
+
args: { op: "logs", name: "comp-debug", lines: 100, follow: true, cursor: 233_512, timeout: 30 },
|
|
106
|
+
result: {
|
|
107
|
+
content: [
|
|
108
|
+
{
|
|
109
|
+
type: "text",
|
|
110
|
+
text: [
|
|
111
|
+
"Breakpoint 1: 3 locations.",
|
|
112
|
+
"(lldb) run",
|
|
113
|
+
"Process 726 launched: '/tmp/compiler'",
|
|
114
|
+
"frame #0: 0x0000000100012f80 compiler`parse_expression",
|
|
115
|
+
"[comp-debug: ready; cursor=233797]",
|
|
116
|
+
].join("\n"),
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
details: {
|
|
120
|
+
op: "logs",
|
|
121
|
+
cursor: 233_797,
|
|
122
|
+
timedOut: false,
|
|
123
|
+
state: "ready",
|
|
124
|
+
terminalRows: [
|
|
125
|
+
"\x1b[0mBreakpoint 1: 3 locations.",
|
|
126
|
+
"\x1b[0m(lldb) run",
|
|
127
|
+
"\x1b[0mProcess 726 launched: '/tmp/compiler'",
|
|
128
|
+
"\x1b[0mframe #0: 0x0000000100012f80 compiler`parse_expression",
|
|
129
|
+
"\x1b[0m\x1b[1;38;5;2m(lldb)\x1b[0m ",
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
errorResult: {
|
|
134
|
+
content: [{ type: "text", text: "No daemon named web" }],
|
|
135
|
+
isError: true,
|
|
136
|
+
details: { op: "logs" },
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
|
|
59
140
|
eval: {
|
|
60
141
|
label: "Eval",
|
|
61
142
|
streamingArgs: {
|
package/src/cli.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import { declareWorkerHostEntry, installWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
|
|
28
28
|
import { installProfileAlias, resolveProfileAliasCommandFromProcess } from "./cli/profile-alias";
|
|
29
29
|
import { extractProfileFlags } from "./cli/profile-bootstrap";
|
|
30
|
+
import { DAEMON_BROKER_WORKER_ARG } from "./launch/protocol";
|
|
30
31
|
|
|
31
32
|
if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {
|
|
32
33
|
process.stderr.write(
|
|
@@ -78,6 +79,8 @@ async function runSmokeTest(): Promise<void> {
|
|
|
78
79
|
const { smokeTestTtsWorker } = await import("./tts/tts-client");
|
|
79
80
|
const { smokeTestMnemopiEmbedWorker } = await import("./mnemopi/embed-client");
|
|
80
81
|
const { smokeTestJsEvalWorker } = await import("./eval/js/context-manager");
|
|
82
|
+
// Smoke dependencies stay lazy so normal CLI startup does not load worker clients.
|
|
83
|
+
const { smokeTestDaemonBroker } = await import("./launch/client");
|
|
81
84
|
await smokeTestSyncWorker();
|
|
82
85
|
|
|
83
86
|
const statsServer = await startServer(0);
|
|
@@ -97,6 +100,7 @@ async function runSmokeTest(): Promise<void> {
|
|
|
97
100
|
await smokeTestJsEvalWorker();
|
|
98
101
|
await smokeTestTtsWorker();
|
|
99
102
|
await smokeTestMnemopiEmbedWorker();
|
|
103
|
+
await smokeTestDaemonBroker();
|
|
100
104
|
process.stdout.write("smoke-test: ok\n");
|
|
101
105
|
}
|
|
102
106
|
|
|
@@ -167,6 +171,12 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
|
|
|
167
171
|
await runIpcSubprocessWorker(startMnemopiEmbedWorker);
|
|
168
172
|
return true;
|
|
169
173
|
}
|
|
174
|
+
if (arg === DAEMON_BROKER_WORKER_ARG) {
|
|
175
|
+
// Worker selectors must dispatch before the normal command graph loads.
|
|
176
|
+
const { startDaemonBrokerFromEnvironment } = await import("./launch/broker");
|
|
177
|
+
await startDaemonBrokerFromEnvironment();
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
170
180
|
return false;
|
|
171
181
|
}
|
|
172
182
|
|
package/src/commands/launch.ts
CHANGED
|
@@ -34,6 +34,23 @@ export default class Index extends Command {
|
|
|
34
34
|
plan: Flags.string({
|
|
35
35
|
description: "Plan model for architectural planning (or PI_PLAN_MODEL env)",
|
|
36
36
|
}),
|
|
37
|
+
prewalk: Flags.boolean({
|
|
38
|
+
description:
|
|
39
|
+
"Switch from the active model to a fast/cheap model at the first edit/write after the plan's todo list exists (default off; see prewalk.enabled)",
|
|
40
|
+
}),
|
|
41
|
+
"no-prewalk": Flags.boolean({
|
|
42
|
+
description: "Disable prewalk even if prewalk.enabled is set",
|
|
43
|
+
}),
|
|
44
|
+
"prewalk-into": Flags.string({
|
|
45
|
+
description: 'Target model for prewalk (default the "smol" role)',
|
|
46
|
+
}),
|
|
47
|
+
"plan-yolo": Flags.boolean({
|
|
48
|
+
description:
|
|
49
|
+
"Force read-only plan mode at start, auto-approve the plan on the model's first resolve call, then switch to --plan-yolo-into to implement it",
|
|
50
|
+
}),
|
|
51
|
+
"plan-yolo-into": Flags.string({
|
|
52
|
+
description: 'Target model for plan-yolo execution (default the "smol" role)',
|
|
53
|
+
}),
|
|
37
54
|
provider: Flags.string({
|
|
38
55
|
description: "Provider to use (legacy; prefer --model)",
|
|
39
56
|
}),
|
|
@@ -37,7 +37,14 @@ import {
|
|
|
37
37
|
resolveThinkingLevelForModel,
|
|
38
38
|
} from "../thinking";
|
|
39
39
|
import { isAuthenticated, kNoAuth, type ModelRegistry } from "./model-registry";
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
DEFAULT_MODEL_ROLE_ALIAS,
|
|
42
|
+
formatModelRoleAlias,
|
|
43
|
+
LEGACY_MODEL_ROLE_ALIAS_PREFIX,
|
|
44
|
+
MODEL_ROLE_ALIAS_PREFIX,
|
|
45
|
+
MODEL_ROLE_IDS,
|
|
46
|
+
type ModelRole,
|
|
47
|
+
} from "./model-roles";
|
|
41
48
|
import type { Settings } from "./settings";
|
|
42
49
|
|
|
43
50
|
function isKnownProvider(provider: string): provider is KnownProvider {
|
|
@@ -109,8 +116,7 @@ function parseThinkingSuffix(value: string, options?: ThinkingSuffixOptions): Co
|
|
|
109
116
|
* level / `:auto` sentinel); `base` then has the suffix stripped. Otherwise
|
|
110
117
|
* `base` is the input.
|
|
111
118
|
* `minColonIndex` requires the colon to appear strictly after that index —
|
|
112
|
-
* role-alias callers pass
|
|
113
|
-
* as long as the `pi/` prefix.
|
|
119
|
+
* role-alias callers pass the matched alias prefix length.
|
|
114
120
|
*/
|
|
115
121
|
function splitThinkingSuffix(
|
|
116
122
|
pattern: string,
|
|
@@ -850,17 +856,32 @@ export function parseModelPattern(
|
|
|
850
856
|
);
|
|
851
857
|
}
|
|
852
858
|
|
|
853
|
-
const PREFIX_MODEL_ROLE = "pi/";
|
|
854
859
|
const DEFAULT_MODEL_ROLE = "default";
|
|
860
|
+
const MODEL_ROLE_ALIAS_PREFIXES = [MODEL_ROLE_ALIAS_PREFIX, LEGACY_MODEL_ROLE_ALIAS_PREFIX];
|
|
861
|
+
|
|
862
|
+
function isModelRole(role: string): role is ModelRole {
|
|
863
|
+
return (MODEL_ROLE_IDS as string[]).includes(role);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Minimum colon index for splitting a `:<level>` suffix off a role alias, or
|
|
868
|
+
* `undefined` when `value` is not role-alias shaped. Doubles as the slice
|
|
869
|
+
* offset of the role name for prefixed aliases (`@role`, `pi/role`); the bare
|
|
870
|
+
* `*` default alias returns 0 because its colon sits immediately after the
|
|
871
|
+
* one-character token (`*:xhigh`).
|
|
872
|
+
*/
|
|
873
|
+
function modelRoleAliasPrefixLength(value: string): number | undefined {
|
|
874
|
+
if (value === DEFAULT_MODEL_ROLE_ALIAS || value.startsWith(`${DEFAULT_MODEL_ROLE_ALIAS}:`)) return 0;
|
|
875
|
+
return MODEL_ROLE_ALIAS_PREFIXES.find(prefix => value.startsWith(prefix))?.length;
|
|
876
|
+
}
|
|
855
877
|
|
|
856
|
-
function getModelRoleAlias(value: string):
|
|
878
|
+
function getModelRoleAlias(value: string, settings?: Settings): string | undefined {
|
|
857
879
|
const normalized = value.trim();
|
|
858
|
-
|
|
880
|
+
const prefixLength = modelRoleAliasPrefixLength(normalized);
|
|
881
|
+
if (prefixLength === undefined) return undefined;
|
|
859
882
|
|
|
860
|
-
const candidate = normalized.slice(
|
|
861
|
-
|
|
862
|
-
if (candidate === role) return role;
|
|
863
|
-
}
|
|
883
|
+
const candidate = normalized === DEFAULT_MODEL_ROLE_ALIAS ? DEFAULT_MODEL_ROLE : normalized.slice(prefixLength);
|
|
884
|
+
if (isModelRole(candidate) || settings?.getModelRole(candidate) !== undefined) return candidate;
|
|
864
885
|
return undefined;
|
|
865
886
|
}
|
|
866
887
|
|
|
@@ -871,7 +892,14 @@ function normalizeModelPatternList(value: string | string[] | undefined): string
|
|
|
871
892
|
}
|
|
872
893
|
|
|
873
894
|
function isSessionInheritedAgentPattern(value: string): boolean {
|
|
874
|
-
return
|
|
895
|
+
return (
|
|
896
|
+
value === DEFAULT_MODEL_ROLE ||
|
|
897
|
+
value === formatModelRoleAlias(DEFAULT_MODEL_ROLE) ||
|
|
898
|
+
value === DEFAULT_MODEL_ROLE_ALIAS ||
|
|
899
|
+
value === `${LEGACY_MODEL_ROLE_ALIAS_PREFIX}${DEFAULT_MODEL_ROLE}` ||
|
|
900
|
+
value === formatModelRoleAlias("task") ||
|
|
901
|
+
value === `${LEGACY_MODEL_ROLE_ALIAS_PREFIX}task`
|
|
902
|
+
);
|
|
875
903
|
}
|
|
876
904
|
|
|
877
905
|
function shouldInheritDefaultBeforePriority(role: ModelRole): boolean {
|
|
@@ -904,7 +932,7 @@ function resolveDefaultInheritedPatterns(
|
|
|
904
932
|
configuredDefault: string | undefined,
|
|
905
933
|
roleDefaults: string[],
|
|
906
934
|
settings: Settings | undefined,
|
|
907
|
-
visited: Set<
|
|
935
|
+
visited: Set<string>,
|
|
908
936
|
): string[] {
|
|
909
937
|
if (!shouldInheritDefaultBeforePriority(role) || !configuredDefault) return [];
|
|
910
938
|
|
|
@@ -912,13 +940,12 @@ function resolveDefaultInheritedPatterns(
|
|
|
912
940
|
for (const pattern of normalizeModelPatternList(configuredDefault)) {
|
|
913
941
|
const { base: aliasCandidate, level: thinkingLevel } = splitThinkingSuffix(
|
|
914
942
|
pattern,
|
|
915
|
-
|
|
943
|
+
modelRoleAliasPrefixLength(pattern) ?? LEGACY_MODEL_ROLE_ALIAS_PREFIX.length,
|
|
916
944
|
MAX_THINKING_SUFFIX_OPTIONS,
|
|
917
945
|
);
|
|
918
|
-
const aliasRole = getModelRoleAlias(aliasCandidate);
|
|
946
|
+
const aliasRole = getModelRoleAlias(aliasCandidate, settings);
|
|
919
947
|
if (aliasRole === role) {
|
|
920
|
-
// Self-alias (e.g. modelRoles.default = "
|
|
921
|
-
// same unset role; collapse straight to the built-in priority chain.
|
|
948
|
+
// Self-alias (e.g. modelRoles.default = "@smol") would loop back to the
|
|
922
949
|
resolved.push(
|
|
923
950
|
...(thinkingLevel
|
|
924
951
|
? roleDefaults.map(defaultPattern => `${defaultPattern}:${thinkingLevel}`)
|
|
@@ -927,8 +954,7 @@ function resolveDefaultInheritedPatterns(
|
|
|
927
954
|
continue;
|
|
928
955
|
}
|
|
929
956
|
if (aliasRole && !visited.has(aliasRole)) {
|
|
930
|
-
// Cross-role alias (e.g. modelRoles.default = "
|
|
931
|
-
// target role's patterns now so downstream one-layer expanders see
|
|
957
|
+
// Cross-role alias (e.g. modelRoles.default = "@slow"): resolve the
|
|
932
958
|
// concrete model patterns instead of another role alias.
|
|
933
959
|
const recursed = resolveConfiguredRolePattern(pattern, settings, new Set(visited));
|
|
934
960
|
if (recursed && recursed.length > 0) {
|
|
@@ -944,27 +970,29 @@ function resolveDefaultInheritedPatterns(
|
|
|
944
970
|
function resolveConfiguredRolePattern(
|
|
945
971
|
value: string,
|
|
946
972
|
settings?: Settings,
|
|
947
|
-
visited: Set<
|
|
973
|
+
visited: Set<string> = new Set(),
|
|
948
974
|
): string[] | undefined {
|
|
949
975
|
const normalized = value.trim();
|
|
950
976
|
if (!normalized) return undefined;
|
|
951
977
|
|
|
952
978
|
const { base: aliasCandidate, level: thinkingLevel } = splitThinkingSuffix(
|
|
953
979
|
normalized,
|
|
954
|
-
|
|
980
|
+
modelRoleAliasPrefixLength(normalized) ?? LEGACY_MODEL_ROLE_ALIAS_PREFIX.length,
|
|
955
981
|
MAX_THINKING_SUFFIX_OPTIONS,
|
|
956
982
|
);
|
|
957
|
-
const role = getModelRoleAlias(aliasCandidate);
|
|
983
|
+
const role = getModelRoleAlias(aliasCandidate, settings);
|
|
958
984
|
if (!role) return [normalized];
|
|
959
985
|
if (visited.has(role)) return undefined;
|
|
960
986
|
visited.add(role);
|
|
961
987
|
|
|
962
988
|
const configured = settings?.getModelRole(role)?.trim();
|
|
963
989
|
const configuredDefault = settings?.getModelRole(DEFAULT_MODEL_ROLE)?.trim();
|
|
964
|
-
const roleDefaults = rolePriorityDefaults(role);
|
|
990
|
+
const roleDefaults = isModelRole(role) ? rolePriorityDefaults(role) : [];
|
|
965
991
|
const resolved = configured
|
|
966
992
|
? normalizeModelPatternList(configured)
|
|
967
|
-
:
|
|
993
|
+
: isModelRole(role)
|
|
994
|
+
? resolveDefaultInheritedPatterns(role, configuredDefault, roleDefaults, settings, visited)
|
|
995
|
+
: roleDefaults;
|
|
968
996
|
if (resolved.length === 0) {
|
|
969
997
|
resolved.push(...roleDefaults);
|
|
970
998
|
}
|
|
@@ -976,7 +1004,7 @@ function resolveConfiguredRolePattern(
|
|
|
976
1004
|
}
|
|
977
1005
|
|
|
978
1006
|
/**
|
|
979
|
-
* Expand a role alias like "
|
|
1007
|
+
* Expand a role alias like "@smol" to the configured model string.
|
|
980
1008
|
*/
|
|
981
1009
|
export function expandRoleAlias(value: string, settings?: Settings): string {
|
|
982
1010
|
const normalized = value.trim();
|
|
@@ -1014,8 +1042,13 @@ export function resolveAgentModelPatterns(options: AgentModelPatternResolutionOp
|
|
|
1014
1042
|
const singleAgentPattern = normalizedAgentPatterns.length === 1 ? normalizedAgentPatterns[0] : undefined;
|
|
1015
1043
|
const agentInheritsSessionModel = singleAgentPattern ? isSessionInheritedAgentPattern(singleAgentPattern) : false;
|
|
1016
1044
|
if (configuredAgentPatterns.length > 0) {
|
|
1045
|
+
if (
|
|
1046
|
+
singleAgentPattern === formatModelRoleAlias("task") ||
|
|
1047
|
+
singleAgentPattern === `${LEGACY_MODEL_ROLE_ALIAS_PREFIX}task`
|
|
1048
|
+
) {
|
|
1049
|
+
return configuredAgentPatterns;
|
|
1050
|
+
}
|
|
1017
1051
|
if (!agentInheritsSessionModel) return configuredAgentPatterns;
|
|
1018
|
-
if (singleAgentPattern === "pi/task") return configuredAgentPatterns;
|
|
1019
1052
|
}
|
|
1020
1053
|
|
|
1021
1054
|
const fallback =
|
|
@@ -1102,12 +1135,16 @@ export function extractExplicitThinkingSelector(
|
|
|
1102
1135
|
let current = normalized;
|
|
1103
1136
|
while (!visited.has(current)) {
|
|
1104
1137
|
visited.add(current);
|
|
1105
|
-
const
|
|
1138
|
+
const rolePrefixLength = modelRoleAliasPrefixLength(current) ?? LEGACY_MODEL_ROLE_ALIAS_PREFIX.length;
|
|
1139
|
+
const strictSelector = splitThinkingSuffix(current, rolePrefixLength).level;
|
|
1106
1140
|
if (strictSelector) {
|
|
1107
1141
|
return strictSelector;
|
|
1108
1142
|
}
|
|
1109
|
-
const maxSelector = splitThinkingSuffix(current,
|
|
1110
|
-
if (
|
|
1143
|
+
const maxSelector = splitThinkingSuffix(current, rolePrefixLength, MAX_THINKING_SUFFIX_OPTIONS).level;
|
|
1144
|
+
if (
|
|
1145
|
+
maxSelector &&
|
|
1146
|
+
(modelRoleAliasPrefixLength(current) !== undefined || !isLiteralModelSelector(current, options))
|
|
1147
|
+
) {
|
|
1111
1148
|
return maxSelector;
|
|
1112
1149
|
}
|
|
1113
1150
|
const expanded = expandRoleAlias(current, settings).trim();
|
|
@@ -1278,7 +1315,7 @@ export function resolveAdvisorRoleSelection(
|
|
|
1278
1315
|
settings: Settings,
|
|
1279
1316
|
availableModels: Model<Api>[],
|
|
1280
1317
|
): { model: Model<Api>; thinkingLevel?: ConfiguredThinkingLevel } | undefined {
|
|
1281
|
-
const resolved = resolveModelRoleValue(
|
|
1318
|
+
const resolved = resolveModelRoleValue(formatModelRoleAlias("advisor"), availableModels, {
|
|
1282
1319
|
settings,
|
|
1283
1320
|
matchPreferences: getModelMatchPreferences(settings),
|
|
1284
1321
|
});
|
|
@@ -1300,6 +1337,7 @@ export async function resolveModelScope(
|
|
|
1300
1337
|
patterns: string[],
|
|
1301
1338
|
modelRegistry: Pick<ModelRegistry, "getAvailable">,
|
|
1302
1339
|
preferences?: ModelMatchPreferences,
|
|
1340
|
+
settings?: Settings,
|
|
1303
1341
|
): Promise<ScopedModel[]> {
|
|
1304
1342
|
const availableModels = modelRegistry.getAvailable();
|
|
1305
1343
|
const context = buildPreferenceContext(availableModels, preferences);
|
|
@@ -1337,6 +1375,25 @@ export async function resolveModelScope(
|
|
|
1337
1375
|
continue;
|
|
1338
1376
|
}
|
|
1339
1377
|
|
|
1378
|
+
// Role aliases (`@smol`, `pi/slow`) resolve to the role's single concrete
|
|
1379
|
+
// model — not its whole fallback chain — so a role contributes one scope
|
|
1380
|
+
// entry exactly like `--model` would pick. (Bare `*` stays a match-all
|
|
1381
|
+
// glob above; scope semantics, not the default-role alias.)
|
|
1382
|
+
if (settings && modelRoleAliasPrefixLength(pattern) !== undefined) {
|
|
1383
|
+
const resolved = resolveModelRoleValue(pattern, availableModels, { settings, matchPreferences: preferences });
|
|
1384
|
+
if (resolved.warning) logger.warn(resolved.warning);
|
|
1385
|
+
if (!resolved.model) {
|
|
1386
|
+
logger.warn(`No models match pattern "${pattern}"`);
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
if (resolved.thinkingLevel === AUTO_THINKING) {
|
|
1390
|
+
addScopedModel(resolved.model, undefined, false);
|
|
1391
|
+
} else {
|
|
1392
|
+
addScopedModel(resolved.model, resolved.thinkingLevel, resolved.explicitThinkingLevel);
|
|
1393
|
+
}
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1340
1397
|
const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPatternWithContext(
|
|
1341
1398
|
pattern,
|
|
1342
1399
|
availableModels,
|
|
@@ -1386,7 +1443,7 @@ export async function resolveAllowedModels(
|
|
|
1386
1443
|
if (!patterns || patterns.length === 0) {
|
|
1387
1444
|
return available;
|
|
1388
1445
|
}
|
|
1389
|
-
const scoped = await resolveModelScope(patterns, modelRegistry, preferences);
|
|
1446
|
+
const scoped = await resolveModelScope(patterns, modelRegistry, preferences, settings);
|
|
1390
1447
|
if (scoped.length === 0) {
|
|
1391
1448
|
return [];
|
|
1392
1449
|
}
|
|
@@ -1415,6 +1472,7 @@ export async function resolveAllowedModels(
|
|
|
1415
1472
|
export function filterAvailableModelsByEnabledPatterns(
|
|
1416
1473
|
available: Model<Api>[],
|
|
1417
1474
|
patterns: readonly string[],
|
|
1475
|
+
settings?: Settings,
|
|
1418
1476
|
): Model<Api>[] {
|
|
1419
1477
|
if (patterns.length === 0) return available;
|
|
1420
1478
|
|
|
@@ -1432,6 +1490,13 @@ export function filterAvailableModelsByEnabledPatterns(
|
|
|
1432
1490
|
continue;
|
|
1433
1491
|
}
|
|
1434
1492
|
|
|
1493
|
+
// Mirror resolveModelScope: role aliases resolve to the role's model.
|
|
1494
|
+
if (settings && modelRoleAliasPrefixLength(pattern) !== undefined) {
|
|
1495
|
+
const { model } = resolveModelRoleValue(pattern, available, { settings });
|
|
1496
|
+
if (model) addAllowed(model);
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1435
1500
|
const { model } = parseModelPatternWithContext(pattern, available, context);
|
|
1436
1501
|
if (model) {
|
|
1437
1502
|
addAllowed(model);
|
|
@@ -1456,9 +1521,10 @@ export function resolveCliModel(options: {
|
|
|
1456
1521
|
cliProvider?: string;
|
|
1457
1522
|
cliModel?: string;
|
|
1458
1523
|
modelRegistry: CliModelRegistry;
|
|
1524
|
+
settings?: Settings;
|
|
1459
1525
|
preferences?: ModelMatchPreferences;
|
|
1460
1526
|
}): ResolveCliModelResult {
|
|
1461
|
-
const { cliProvider, cliModel, modelRegistry, preferences } = options;
|
|
1527
|
+
const { cliProvider, cliModel, modelRegistry, settings, preferences } = options;
|
|
1462
1528
|
|
|
1463
1529
|
if (!cliModel) {
|
|
1464
1530
|
return { model: undefined, selector: undefined, warning: undefined, error: undefined };
|
|
@@ -1474,6 +1540,19 @@ export function resolveCliModel(options: {
|
|
|
1474
1540
|
};
|
|
1475
1541
|
}
|
|
1476
1542
|
|
|
1543
|
+
if (!cliProvider && modelRoleAliasPrefixLength(cliModel) !== undefined) {
|
|
1544
|
+
const resolved = resolveModelRoleValue(cliModel, availableModels, { settings, matchPreferences: preferences });
|
|
1545
|
+
if (resolved.model) {
|
|
1546
|
+
return {
|
|
1547
|
+
model: resolved.model,
|
|
1548
|
+
selector: formatModelString(resolved.model),
|
|
1549
|
+
thinkingLevel: resolved.thinkingLevel,
|
|
1550
|
+
warning: resolved.warning,
|
|
1551
|
+
error: undefined,
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1477
1556
|
const providerMap = new Map<string, string>();
|
|
1478
1557
|
for (const model of availableModels) {
|
|
1479
1558
|
providerMap.set(model.provider.toLowerCase(), model.provider);
|
|
@@ -5,6 +5,20 @@
|
|
|
5
5
|
import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
|
|
6
6
|
import type { Settings } from "./settings";
|
|
7
7
|
|
|
8
|
+
/** Canonical prefix for a configured model role selector. */
|
|
9
|
+
export const MODEL_ROLE_ALIAS_PREFIX = "@";
|
|
10
|
+
|
|
11
|
+
/** Legacy prefix accepted for backwards-compatible role selectors. */
|
|
12
|
+
export const LEGACY_MODEL_ROLE_ALIAS_PREFIX = "pi/";
|
|
13
|
+
|
|
14
|
+
/** Shorthand selector for the default model role. */
|
|
15
|
+
export const DEFAULT_MODEL_ROLE_ALIAS = "*";
|
|
16
|
+
|
|
17
|
+
/** Format a model role as its canonical selector. */
|
|
18
|
+
export function formatModelRoleAlias(role: string): string {
|
|
19
|
+
return `${MODEL_ROLE_ALIAS_PREFIX}${role}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
export type ModelRole =
|
|
9
23
|
| "default"
|
|
10
24
|
| "smol"
|
|
@@ -117,7 +117,7 @@ export const TAB_METADATA: Record<SettingTab, { label: string; icon: `tab.${stri
|
|
|
117
117
|
*/
|
|
118
118
|
export const TAB_GROUPS: Record<SettingTab, readonly string[]> = {
|
|
119
119
|
appearance: ["Theme", "Status Line", "Display", "Images"],
|
|
120
|
-
model: ["Thinking", "Sampling", "Prompt", "Retry & Fallback", "Advisor", "Vision"],
|
|
120
|
+
model: ["Thinking", "Sampling", "Prompt", "Retry & Fallback", "Advisor", "Prewalk", "Vision"],
|
|
121
121
|
interaction: [
|
|
122
122
|
"Input",
|
|
123
123
|
"Approvals",
|
|
@@ -331,6 +331,25 @@ export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
|
|
|
331
331
|
tool: "write",
|
|
332
332
|
message: "Use the `write` tool instead of echo/cat redirection. It handles encoding and provides confirmation.",
|
|
333
333
|
},
|
|
334
|
+
{
|
|
335
|
+
pattern: "^\\s*nohup\\s+|(?<!&)\\&\\s*$",
|
|
336
|
+
tool: "launch",
|
|
337
|
+
message:
|
|
338
|
+
"Use the `launch` tool instead of nohup or background shell syntax so the process stays observable and managed.",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
pattern:
|
|
342
|
+
"^\\s*(?:(?:bun|npm|pnpm|yarn)\\s+(?:run\\s+)?(?:dev|start)(?:\\s|$)|(?:vite|next\\s+dev|nuxt\\s+dev|nodemon|lldb|gdb|tail\\s+-f)(?:\\s|$)|docker\\s+compose\\s+up(?!.*(?:\\s-d(?:\\s|$)|--detach))(?:\\s|$))",
|
|
343
|
+
tool: "launch",
|
|
344
|
+
message:
|
|
345
|
+
"Use the `launch` tool for services, watchers, and debuggers so other omp instances can observe and control them.",
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
pattern:
|
|
349
|
+
"^\\s*(?:(?:bun|npm|pnpm|yarn)\\s+(?:run\\s+)?\\S+|cargo\\s+watch|watchexec|pytest|vitest|jest|tsc)(?:.|\\n)*(?:--watch|-w)(?:\\s|$)",
|
|
350
|
+
tool: "launch",
|
|
351
|
+
message: "Use the `launch` tool for watch mode so its output, input, and lifecycle stay managed.",
|
|
352
|
+
},
|
|
334
353
|
];
|
|
335
354
|
|
|
336
355
|
export const SETTINGS_SCHEMA = {
|
|
@@ -403,6 +422,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
403
422
|
"Pair a second model (assigned to the 'advisor' role) that passively reviews each turn and injects notes.",
|
|
404
423
|
},
|
|
405
424
|
},
|
|
425
|
+
"prewalk.enabled": {
|
|
426
|
+
type: "boolean",
|
|
427
|
+
default: false,
|
|
428
|
+
ui: {
|
|
429
|
+
tab: "model",
|
|
430
|
+
group: "Prewalk",
|
|
431
|
+
label: "Enable Prewalk",
|
|
432
|
+
description:
|
|
433
|
+
"Start on the active model, then switch to a fast/cheap model (default the 'smol' role) at the first edit/write after the plan nudge's todo list exists — the strong model plans, commits the todos, and starts the implementation before handing off. Overridable per session with --prewalk / --no-prewalk.",
|
|
434
|
+
},
|
|
435
|
+
},
|
|
406
436
|
"advisor.subagents": {
|
|
407
437
|
type: "boolean",
|
|
408
438
|
default: false,
|
|
@@ -867,6 +897,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
867
897
|
description: "Remove the 1-character horizontal padding from the left and right of the terminal output",
|
|
868
898
|
},
|
|
869
899
|
},
|
|
900
|
+
"tui.scrollbackRebuild": {
|
|
901
|
+
type: "boolean",
|
|
902
|
+
default: false,
|
|
903
|
+
ui: {
|
|
904
|
+
tab: "appearance",
|
|
905
|
+
group: "Display",
|
|
906
|
+
label: "Rewrite Scrollback",
|
|
907
|
+
description:
|
|
908
|
+
"Erase and replay terminal scrollback when a block's final form replaces its live preview. When off (default), stale preview copies remain in history and the final content is appended below.",
|
|
909
|
+
},
|
|
910
|
+
},
|
|
870
911
|
|
|
871
912
|
"display.shimmer": {
|
|
872
913
|
type: "enum",
|
|
@@ -918,6 +959,18 @@ export const SETTINGS_SCHEMA = {
|
|
|
918
959
|
},
|
|
919
960
|
},
|
|
920
961
|
|
|
962
|
+
"display.collapseCompacted": {
|
|
963
|
+
type: "boolean",
|
|
964
|
+
default: true,
|
|
965
|
+
ui: {
|
|
966
|
+
tab: "appearance",
|
|
967
|
+
group: "Display",
|
|
968
|
+
label: "Collapse Compacted History",
|
|
969
|
+
description:
|
|
970
|
+
"Collapse pre-compaction history behind the summary divider on the live transcript; disable to keep the full transcript inline with dividers at each compaction point",
|
|
971
|
+
},
|
|
972
|
+
},
|
|
973
|
+
|
|
921
974
|
showHardwareCursor: {
|
|
922
975
|
type: "boolean",
|
|
923
976
|
default: true, // will be computed based on platform if undefined
|
|
@@ -1239,7 +1292,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1239
1292
|
textVerbosity: {
|
|
1240
1293
|
type: "enum",
|
|
1241
1294
|
values: ["low", "medium", "high"] as const,
|
|
1242
|
-
default: "
|
|
1295
|
+
default: "medium",
|
|
1243
1296
|
ui: {
|
|
1244
1297
|
tab: "model",
|
|
1245
1298
|
group: "Sampling",
|
|
@@ -1247,8 +1300,8 @@ export const SETTINGS_SCHEMA = {
|
|
|
1247
1300
|
description: "OpenAI Responses and Codex response verbosity (low, medium, or high)",
|
|
1248
1301
|
options: [
|
|
1249
1302
|
{ value: "low", label: "Low", description: "Prefer concise responses" },
|
|
1250
|
-
{ value: "medium", label: "Medium", description: "Balance brevity and detail" },
|
|
1251
|
-
{ value: "high", label: "High", description: "Prefer detailed responses
|
|
1303
|
+
{ value: "medium", label: "Medium", description: "Balance brevity and detail (default)" },
|
|
1304
|
+
{ value: "high", label: "High", description: "Prefer detailed responses" },
|
|
1252
1305
|
],
|
|
1253
1306
|
},
|
|
1254
1307
|
},
|
|
@@ -2597,14 +2650,14 @@ export const SETTINGS_SCHEMA = {
|
|
|
2597
2650
|
group: "Mnemopi",
|
|
2598
2651
|
label: "Mnemopi LLM Mode",
|
|
2599
2652
|
description:
|
|
2600
|
-
"Use no LLM, the online tiny model (the TINY role from /models, else
|
|
2653
|
+
"Use no LLM, the online tiny model (the TINY role from /models, else @smol), or a remote OpenAI-compatible endpoint",
|
|
2601
2654
|
condition: "mnemopiActive",
|
|
2602
2655
|
options: [
|
|
2603
2656
|
{ value: "none", label: "None", description: "Disable Mnemopi LLM-backed extraction" },
|
|
2604
2657
|
{
|
|
2605
2658
|
value: "smol",
|
|
2606
2659
|
label: "Online (tiny)",
|
|
2607
|
-
description: "Use the online tiny model (the TINY role from /models, else
|
|
2660
|
+
description: "Use the online tiny model (the TINY role from /models, else @smol)",
|
|
2608
2661
|
},
|
|
2609
2662
|
{ value: "remote", label: "Remote", description: "Use the Mnemopi remote LLM settings below" },
|
|
2610
2663
|
],
|
|
@@ -3518,6 +3571,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
3518
3571
|
},
|
|
3519
3572
|
},
|
|
3520
3573
|
|
|
3574
|
+
"launch.enabled": {
|
|
3575
|
+
type: "boolean",
|
|
3576
|
+
default: true,
|
|
3577
|
+
ui: {
|
|
3578
|
+
tab: "tools",
|
|
3579
|
+
group: "Available Tools",
|
|
3580
|
+
label: "Launch",
|
|
3581
|
+
description: "Enable the launch tool for supervising shared long-running project processes",
|
|
3582
|
+
},
|
|
3583
|
+
},
|
|
3584
|
+
|
|
3521
3585
|
"speechgen.enabled": {
|
|
3522
3586
|
type: "boolean",
|
|
3523
3587
|
default: false,
|
|
@@ -4547,7 +4611,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
4547
4611
|
group: "Tiny Model",
|
|
4548
4612
|
label: "Tiny Model",
|
|
4549
4613
|
description:
|
|
4550
|
-
"Session-title model: online (the TINY role from /models, else
|
|
4614
|
+
"Session-title model: online (the TINY role from /models, else @smol) by default, or a local on-device model",
|
|
4551
4615
|
options: TINY_TITLE_MODEL_OPTIONS,
|
|
4552
4616
|
},
|
|
4553
4617
|
},
|
package/src/edit/renderer.ts
CHANGED
|
@@ -423,22 +423,25 @@ function formatStreamingDiff(
|
|
|
423
423
|
cache?: RenderedStringCache,
|
|
424
424
|
): string {
|
|
425
425
|
if (!diff) return "";
|
|
426
|
-
// Clamp the
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
426
|
+
// Clamp the tail to the viewport so a tall or fast-growing diff cannot
|
|
427
|
+
// outgrow the live window. Otherwise its mutating rows scroll above the
|
|
428
|
+
// native-scrollback commit boundary mid-stream and freeze into immutable
|
|
429
|
+
// history as a stale preview snapshot; the finalize repair then recommits
|
|
430
|
+
// the final render below it — a duplicated block on the tape. Collapsed
|
|
431
|
+
// gets a short fixed tail; expanded widens it to the viewport-sized window,
|
|
432
|
+
// never unbounded. The budget is VISUAL rows (a long wrapped line counts
|
|
431
433
|
// for more than one) at the framed block's inner width (border only —
|
|
432
|
-
// contentPaddingLeft is 0); only the visible suffix is syntax-colored, so
|
|
433
|
-
// cheap raw-line wrap walk keeps the per-chunk cost bounded.
|
|
434
|
-
// are in the cache salt so a resize re-slices.
|
|
434
|
+
// contentPaddingLeft is 0); only the visible suffix is syntax-colored, so
|
|
435
|
+
// the cheap raw-line wrap walk keeps the per-chunk cost bounded.
|
|
436
|
+
// innerWidth/budget are in the cache salt so a resize re-slices.
|
|
435
437
|
const innerWidth = Math.max(1, width - 2);
|
|
436
|
-
const budget = expanded ?
|
|
438
|
+
const budget = expanded ? previewWindowRows() : Math.min(EDIT_STREAMING_PREVIEW_LINES, previewWindowRows());
|
|
437
439
|
let text = cachedRenderedString(cache, uiTheme, expanded, `${rawPath}:${innerWidth}:${budget}`, diff, () => {
|
|
438
440
|
// "Cursor" tail window: pin the last rows to the bottom so freshly streamed
|
|
439
441
|
// changes stay on screen. The whole-file diff is recomputed every chunk and
|
|
440
442
|
// its Myers alignment is not monotonic in payload length, so a hunk-aware
|
|
441
|
-
// window stutters as rows move between hunks. Expanded
|
|
443
|
+
// window stutters as rows move between hunks. Expanded widens the window
|
|
444
|
+
// to the viewport; the full diff appears once the result finalizes.
|
|
442
445
|
const allLines = diff.replace(/\n+$/u, "").split("\n");
|
|
443
446
|
let visualUsed = 0;
|
|
444
447
|
let cut = allLines.length;
|