@oh-my-pi/pi-coding-agent 16.3.14 → 16.4.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 +35 -0
- package/dist/cli.js +3053 -3158
- package/dist/types/advisor/config.d.ts +7 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +28 -15
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +7 -2
- package/dist/types/dap/config.d.ts +13 -1
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/lsp/config.d.ts +7 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/transports/stdio.d.ts +8 -3
- package/dist/types/modes/components/hook-selector.d.ts +2 -0
- package/dist/types/modes/theme/defaults/index.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +3 -2
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +5 -3
- package/dist/types/session/session-context.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +4 -0
- package/dist/types/thinking.d.ts +6 -3
- package/dist/types/utils/file-display-mode.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/config.test.ts +84 -0
- package/src/advisor/config.ts +28 -0
- package/src/cli/args.ts +1 -0
- package/src/cli/flag-tables.ts +3 -0
- package/src/config/model-registry.ts +1 -1
- package/src/config/model-resolver.ts +14 -12
- package/src/config/models-config-schema.ts +3 -2
- package/src/config/settings-schema.ts +5 -2
- package/src/dap/config.ts +136 -39
- package/src/dap/defaults.json +1 -1
- package/src/eval/js/shared/runtime.ts +20 -4
- package/src/eval/js/worker-core.ts +9 -2
- package/src/exec/bash-executor.ts +8 -1
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/config.ts +27 -13
- package/src/lsp/index.ts +114 -29
- package/src/main.ts +21 -1
- package/src/mcp/transports/stdio.test.ts +12 -0
- package/src/mcp/transports/stdio.ts +14 -8
- package/src/modes/components/hook-selector.ts +9 -2
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/input-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/theme/defaults/dark-poimandres.json +6 -5
- package/src/modes/theme/defaults/light-poimandres.json +6 -5
- package/src/modes/theme/theme-schema.json +6 -2
- package/src/modes/theme/theme.ts +24 -18
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/agents/plan.md +2 -2
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/{explore.md → scout.md} +3 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +5 -3
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/debug.md +4 -4
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/task.md +2 -2
- package/src/sdk.ts +68 -45
- package/src/session/agent-session.ts +215 -64
- package/src/session/session-context.test.ts +83 -0
- package/src/session/session-context.ts +1 -0
- package/src/session/session-entries.ts +4 -0
- package/src/session/session-manager.ts +9 -1
- package/src/system-prompt.test.ts +20 -11
- package/src/task/agents.ts +2 -5
- package/src/task/executor.ts +111 -74
- package/src/thinking.ts +16 -7
- package/src/tools/ask.ts +51 -13
- package/src/tools/browser/cmux/cmux-tab.ts +21 -12
- package/src/tools/debug.ts +41 -11
- package/src/utils/file-display-mode.ts +1 -1
- package/src/prompts/agents/tester.md +0 -111
|
@@ -5,6 +5,7 @@ import * as path from "node:path";
|
|
|
5
5
|
import {
|
|
6
6
|
advisorConfigFilePath,
|
|
7
7
|
discoverAdvisorConfigs,
|
|
8
|
+
getOrCreateAdvisorProviderSessionId,
|
|
8
9
|
loadWatchdogConfigFile,
|
|
9
10
|
resolveAdvisorConfigEditPath,
|
|
10
11
|
saveWatchdogConfigFile,
|
|
@@ -86,6 +87,89 @@ describe("slugifyAdvisorName", () => {
|
|
|
86
87
|
});
|
|
87
88
|
});
|
|
88
89
|
|
|
90
|
+
describe("getOrCreateAdvisorProviderSessionId", () => {
|
|
91
|
+
const primarySessionA = "018f8f5d-75b0-7cc6-8a6f-2f1c0b8e4c9d";
|
|
92
|
+
const primarySessionB = "018f8f5d-75b1-7cc6-8a6f-2f1c0b8e4c9d";
|
|
93
|
+
|
|
94
|
+
it("returns the generated UUIDv7 instead of a local advisor label", () => {
|
|
95
|
+
const generated = "0193c8f2-7b1a-7c4d-9e2f-123456789abc";
|
|
96
|
+
|
|
97
|
+
const providerSessionId = getOrCreateAdvisorProviderSessionId(
|
|
98
|
+
new Map<string, string>(),
|
|
99
|
+
primarySessionA,
|
|
100
|
+
"security-advisor",
|
|
101
|
+
() => generated,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(providerSessionId).toBe(generated);
|
|
105
|
+
expect(providerSessionId).not.toContain("-advisor");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("reuses the same generated UUIDv7 for repeated calls with the same primary session and slug", () => {
|
|
109
|
+
const generatedIds = ["0193c8f2-7b1a-7c4d-9e2f-123456789abc", "0193c8f2-7b1b-7c4d-9e2f-123456789abc"];
|
|
110
|
+
let nextGeneratedIdIndex = 0;
|
|
111
|
+
const ids = new Map<string, string>();
|
|
112
|
+
|
|
113
|
+
const first = getOrCreateAdvisorProviderSessionId(ids, primarySessionA, "architecture", () => {
|
|
114
|
+
const generated = generatedIds[nextGeneratedIdIndex];
|
|
115
|
+
if (!generated) throw new Error("unexpected generator call");
|
|
116
|
+
nextGeneratedIdIndex += 1;
|
|
117
|
+
return generated;
|
|
118
|
+
});
|
|
119
|
+
const second = getOrCreateAdvisorProviderSessionId(ids, primarySessionA, "architecture", () => {
|
|
120
|
+
const generated = generatedIds[nextGeneratedIdIndex];
|
|
121
|
+
if (!generated) throw new Error("unexpected generator call");
|
|
122
|
+
nextGeneratedIdIndex += 1;
|
|
123
|
+
return generated;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(first).toBe(generatedIds[0]);
|
|
127
|
+
expect(second).toBe(generatedIds[0]);
|
|
128
|
+
expect(nextGeneratedIdIndex).toBe(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("creates distinct UUIDv7 values for different advisor slugs or primary sessions", () => {
|
|
132
|
+
const generatedIds = [
|
|
133
|
+
"0193c8f2-7b1a-7c4d-9e2f-123456789abc",
|
|
134
|
+
"0193c8f2-7b1b-7c4d-9e2f-123456789abc",
|
|
135
|
+
"0193c8f2-7b1c-7c4d-9e2f-123456789abc",
|
|
136
|
+
];
|
|
137
|
+
let nextGeneratedIdIndex = 0;
|
|
138
|
+
const ids = new Map<string, string>();
|
|
139
|
+
const nextGeneratedId = () => {
|
|
140
|
+
const generated = generatedIds[nextGeneratedIdIndex];
|
|
141
|
+
if (!generated) throw new Error("unexpected generator call");
|
|
142
|
+
nextGeneratedIdIndex += 1;
|
|
143
|
+
return generated;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const architecture = getOrCreateAdvisorProviderSessionId(ids, primarySessionA, "architecture", nextGeneratedId);
|
|
147
|
+
const security = getOrCreateAdvisorProviderSessionId(ids, primarySessionA, "security", nextGeneratedId);
|
|
148
|
+
const architectureForOtherSession = getOrCreateAdvisorProviderSessionId(
|
|
149
|
+
ids,
|
|
150
|
+
primarySessionB,
|
|
151
|
+
"architecture",
|
|
152
|
+
nextGeneratedId,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(architecture).toBe(generatedIds[0]);
|
|
156
|
+
expect(security).toBe(generatedIds[1]);
|
|
157
|
+
expect(architectureForOtherSession).toBe(generatedIds[2]);
|
|
158
|
+
expect(new Set([architecture, security, architectureForOtherSession]).size).toBe(3);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("rejects generated values that are not UUIDv7", () => {
|
|
162
|
+
expect(() =>
|
|
163
|
+
getOrCreateAdvisorProviderSessionId(
|
|
164
|
+
new Map<string, string>(),
|
|
165
|
+
primarySessionA,
|
|
166
|
+
"architecture",
|
|
167
|
+
() => "550e8400-e29b-41d4-a716-446655440000",
|
|
168
|
+
),
|
|
169
|
+
).toThrow("non-UUIDv7");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
89
173
|
describe("WATCHDOG.yml file round-trip", () => {
|
|
90
174
|
let tmp: string;
|
|
91
175
|
beforeEach(async () => {
|
package/src/advisor/config.ts
CHANGED
|
@@ -59,6 +59,34 @@ export function slugifyAdvisorName(name: string): string {
|
|
|
59
59
|
return slug || "advisor";
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
const UUID_V7_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
63
|
+
const ADVISOR_PROVIDER_SESSION_KEY_SEPARATOR = "\u0000";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns a stable provider-facing UUIDv7 for one advisor within one primary session.
|
|
67
|
+
*
|
|
68
|
+
* Codex treats `session_id`/`conversation_id` as a UUID-shaped routing identity,
|
|
69
|
+
* so advisor labels such as `-advisor` stay local-only.
|
|
70
|
+
*/
|
|
71
|
+
export function getOrCreateAdvisorProviderSessionId(
|
|
72
|
+
ids: Map<string, string>,
|
|
73
|
+
primarySessionId: string | undefined,
|
|
74
|
+
slug: string,
|
|
75
|
+
randomSessionId: () => string = () => Bun.randomUUIDv7(),
|
|
76
|
+
): string | undefined {
|
|
77
|
+
if (!primarySessionId) return undefined;
|
|
78
|
+
const key = `${primarySessionId}${ADVISOR_PROVIDER_SESSION_KEY_SEPARATOR}${slug}`;
|
|
79
|
+
const existing = ids.get(key);
|
|
80
|
+
if (existing) return existing;
|
|
81
|
+
|
|
82
|
+
const next = randomSessionId();
|
|
83
|
+
if (!UUID_V7_PATTERN.test(next)) {
|
|
84
|
+
throw new Error("Advisor provider session id generator returned a non-UUIDv7 value");
|
|
85
|
+
}
|
|
86
|
+
ids.set(key, next);
|
|
87
|
+
return next;
|
|
88
|
+
}
|
|
89
|
+
|
|
62
90
|
/** Built tool names, for validating an advisor's `tools` list. */
|
|
63
91
|
const KNOWN_TOOL_NAMES = new Set<string>(BUILTIN_TOOL_NAMES);
|
|
64
92
|
|
package/src/cli/args.ts
CHANGED
package/src/cli/flag-tables.ts
CHANGED
|
@@ -141,6 +141,9 @@ export const STRING_SETTERS: Record<string, StringSetter> = {
|
|
|
141
141
|
"--provider-session-id": (result, value) => {
|
|
142
142
|
result.providerSessionId = value;
|
|
143
143
|
},
|
|
144
|
+
"--prompt-cache-key": (result, value) => {
|
|
145
|
+
result.providerPromptCacheKey = value;
|
|
146
|
+
},
|
|
144
147
|
"--session-dir": (result, value) => {
|
|
145
148
|
result.sessionDir = value;
|
|
146
149
|
},
|
|
@@ -690,7 +690,7 @@ function normalizeSuppressedSelector(
|
|
|
690
690
|
const trimmed = selector.trim();
|
|
691
691
|
if (!trimmed) return trimmed;
|
|
692
692
|
const parsed = parseModelString(trimmed, {
|
|
693
|
-
|
|
693
|
+
allowMaxSuffix: true,
|
|
694
694
|
allowAutoAlias: true,
|
|
695
695
|
isLiteralModelId: (provider, id) => hasLiveModel?.(provider, id) === true,
|
|
696
696
|
});
|
|
@@ -79,23 +79,24 @@ export interface ScopedModel {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
interface ThinkingSuffixOptions {
|
|
82
|
-
|
|
82
|
+
allowMaxSuffix?: boolean;
|
|
83
83
|
allowAutoAlias?: boolean;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
interface ModelStringParseOptions extends ThinkingSuffixOptions {
|
|
87
87
|
isLiteralModelId?: (provider: string, id: string) => boolean;
|
|
88
88
|
}
|
|
89
|
-
//
|
|
90
|
-
// and `:auto` maps to the auto sentinel. Both are gated behind
|
|
91
|
-
// (and the literal-id / exact-match guards on the callers)
|
|
92
|
-
//
|
|
93
|
-
|
|
89
|
+
// Suffix recognition for the model-pattern parser: `:max` is a real thinking
|
|
90
|
+
// level and `:auto` maps to the auto sentinel. Both are gated behind flags
|
|
91
|
+
// (and the literal-id / exact-match guards on the callers) because real model
|
|
92
|
+
// ids end in `:max` (e.g. `glm-4.7:max`) — an ungated split would silently
|
|
93
|
+
// reinterpret them as a thinking suffix.
|
|
94
|
+
const MAX_THINKING_SUFFIX_OPTIONS: ThinkingSuffixOptions = { allowMaxSuffix: true, allowAutoAlias: true };
|
|
94
95
|
|
|
95
96
|
function parseThinkingSuffix(value: string, options?: ThinkingSuffixOptions): ConfiguredThinkingLevel | undefined {
|
|
96
97
|
const level = parseThinkingLevel(value);
|
|
98
|
+
if (level === ThinkingLevel.Max) return options?.allowMaxSuffix === true ? level : undefined;
|
|
97
99
|
if (level !== undefined) return level;
|
|
98
|
-
if (options?.allowMaxAlias === true && value === "max") return ThinkingLevel.XHigh;
|
|
99
100
|
if (options?.allowAutoAlias === true && value === AUTO_THINKING) return AUTO_THINKING;
|
|
100
101
|
return undefined;
|
|
101
102
|
}
|
|
@@ -104,8 +105,9 @@ function parseThinkingSuffix(value: string, options?: ThinkingSuffixOptions): Co
|
|
|
104
105
|
* Split a trailing `:<level>` thinking selector off a model pattern.
|
|
105
106
|
*
|
|
106
107
|
* `level` is set when the suffix parses as a concrete thinking level (or, when
|
|
107
|
-
* the caller opts in via `
|
|
108
|
-
*
|
|
108
|
+
* the caller opts in via `allowMaxSuffix`/`allowAutoAlias`, the guarded `:max`
|
|
109
|
+
* level / `:auto` sentinel); `base` then has the suffix stripped. Otherwise
|
|
110
|
+
* `base` is the input.
|
|
109
111
|
* `minColonIndex` requires the colon to appear strictly after that index —
|
|
110
112
|
* role-alias callers pass `PREFIX_MODEL_ROLE.length` so the base is at least
|
|
111
113
|
* as long as the `pi/` prefix.
|
|
@@ -183,7 +185,7 @@ export function parseModelString(
|
|
|
183
185
|
// Strip strict thinking level suffixes first (e.g. "claude-sonnet-4-6:high" -> id "claude-sonnet-4-6", thinkingLevel "high").
|
|
184
186
|
const strict = splitThinkingSuffix(id);
|
|
185
187
|
if (strict.level) return { provider, id: strict.base, thinkingLevel: strict.level };
|
|
186
|
-
// `max` is a
|
|
188
|
+
// `max` is a real thinking level, but real model IDs can also end in
|
|
187
189
|
// `:max`. Context-aware callers pass a literal lookup so those models win.
|
|
188
190
|
const maxAlias = splitThinkingSuffix(id, -1, options);
|
|
189
191
|
if (maxAlias.level) {
|
|
@@ -239,7 +241,7 @@ function getOpenRouterRouteSuffix(modelId: string): { baseId: string; suffix: st
|
|
|
239
241
|
}
|
|
240
242
|
|
|
241
243
|
const suffix = modelId.slice(colonIdx + 1).trim();
|
|
242
|
-
// `max` is a thinking-level
|
|
244
|
+
// `max` is a thinking-level suffix, never an OpenRouter route suffix, so
|
|
243
245
|
// `openrouter/<id>:max` falls through to the max-aware selector split instead of
|
|
244
246
|
// being cloned into a literal `<id>:max` model id with the reasoning level lost.
|
|
245
247
|
if (!suffix || parseThinkingSuffix(suffix, MAX_THINKING_SUFFIX_OPTIONS)) {
|
|
@@ -766,7 +768,7 @@ function parseModelPatternWithContext(
|
|
|
766
768
|
|
|
767
769
|
// No match - try stripping a valid thinking suffix and recursing.
|
|
768
770
|
// `max` is accepted only after the full pattern failed, so literal model IDs
|
|
769
|
-
// ending in `:max` keep winning over the
|
|
771
|
+
// ending in `:max` keep winning over the thinking suffix.
|
|
770
772
|
const { base, level } = splitThinkingSuffix(pattern, -1, MAX_THINKING_SUFFIX_OPTIONS);
|
|
771
773
|
if (level) {
|
|
772
774
|
const result = parseModelPatternWithContext(base, availableModels, context, options);
|
|
@@ -23,6 +23,7 @@ const ReasoningEffortMapSchema = type({
|
|
|
23
23
|
"medium?": "string",
|
|
24
24
|
"high?": "string",
|
|
25
25
|
"xhigh?": "string",
|
|
26
|
+
"max?": "string",
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
const OpenAICompatFields = {
|
|
@@ -74,13 +75,13 @@ const ApiSchema = type(
|
|
|
74
75
|
'"openai-completions" | "openai-responses" | "openai-codex-responses" | "azure-openai-responses" | "anthropic-messages" | "google-generative-ai" | "google-gemini-cli" | "google-vertex"',
|
|
75
76
|
);
|
|
76
77
|
|
|
77
|
-
const EffortSchema = type('"minimal" | "low" | "medium" | "high" | "xhigh"');
|
|
78
|
+
const EffortSchema = type('"minimal" | "low" | "medium" | "high" | "xhigh" | "max"');
|
|
78
79
|
|
|
79
80
|
const ThinkingControlModeSchema = type(
|
|
80
81
|
'"effort" | "budget" | "google-level" | "anthropic-adaptive" | "anthropic-budget-effort"',
|
|
81
82
|
);
|
|
82
83
|
|
|
83
|
-
const EFFORT_ORDER = ["minimal", "low", "medium", "high", "xhigh"] as const;
|
|
84
|
+
const EFFORT_ORDER = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
84
85
|
|
|
85
86
|
/**
|
|
86
87
|
* Accepts the canonical `efforts` vocabulary plus the legacy
|
|
@@ -936,7 +936,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
936
936
|
// Reasoning and prompts
|
|
937
937
|
defaultThinkingLevel: {
|
|
938
938
|
type: "enum",
|
|
939
|
-
values: [...THINKING_EFFORTS, AUTO_THINKING
|
|
939
|
+
values: [...THINKING_EFFORTS, AUTO_THINKING],
|
|
940
940
|
default: "high",
|
|
941
941
|
ui: {
|
|
942
942
|
tab: "model",
|
|
@@ -4148,7 +4148,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
4148
4148
|
group: "Subagents",
|
|
4149
4149
|
label: "Soft Subagent Request Budget",
|
|
4150
4150
|
description:
|
|
4151
|
-
"Soft per-subagent request budget (assistant requests per run). Crossing it can inject a steering notice when task.softRequestBudgetNotice is enabled; at 1.5x the budget the run is aborted gracefully, salvaging partial output. 0 disables the guard. Bundled
|
|
4151
|
+
"Soft per-subagent request budget (assistant requests per run). Crossing it can inject a steering notice when task.softRequestBudgetNotice is enabled; at 1.5x the budget the run is aborted gracefully, salvaging partial output. 0 disables the guard. Bundled scout/sonic agents use a lower built-in budget.",
|
|
4152
4152
|
options: [
|
|
4153
4153
|
{ value: "0", label: "Disabled" },
|
|
4154
4154
|
{ value: "40", label: "40 requests" },
|
|
@@ -4964,6 +4964,8 @@ export const SETTINGS_SCHEMA = {
|
|
|
4964
4964
|
"thinkingBudgets.high": { type: "number", default: 16384 },
|
|
4965
4965
|
|
|
4966
4966
|
"thinkingBudgets.xhigh": { type: "number", default: 32768 },
|
|
4967
|
+
|
|
4968
|
+
"thinkingBudgets.max": { type: "number", default: 32768 },
|
|
4967
4969
|
} as const;
|
|
4968
4970
|
|
|
4969
4971
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -5180,6 +5182,7 @@ export interface ThinkingBudgetsSettings {
|
|
|
5180
5182
|
medium: number;
|
|
5181
5183
|
high: number;
|
|
5182
5184
|
xhigh: number;
|
|
5185
|
+
max: number;
|
|
5183
5186
|
}
|
|
5184
5187
|
|
|
5185
5188
|
export interface SttSettings {
|
package/src/dap/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { isRecord, logger } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { isRecord, logger, WhichCachePolicy } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { YAML } from "bun";
|
|
6
6
|
import { getConfigDirPaths } from "../config";
|
|
7
7
|
import { getPreloadedPluginRoots } from "../discovery/helpers";
|
|
@@ -9,7 +9,7 @@ import { hasRootMarkers, resolveCommand } from "../lsp/config";
|
|
|
9
9
|
import DEFAULTS from "./defaults.json" with { type: "json" };
|
|
10
10
|
import type { DapAdapterConfig, DapResolvedAdapter } from "./types";
|
|
11
11
|
|
|
12
|
-
const EXTENSIONLESS_DEBUGGER_ORDER = ["gdb", "lldb-dap"]
|
|
12
|
+
const EXTENSIONLESS_DEBUGGER_ORDER: readonly string[] = ["gdb", "lldb-dap"];
|
|
13
13
|
|
|
14
14
|
interface NormalizedConfig {
|
|
15
15
|
adapters: Record<string, unknown>;
|
|
@@ -188,10 +188,18 @@ function resolveAdapterFromConfig(
|
|
|
188
188
|
adapterName: string,
|
|
189
189
|
configs: Record<string, DapAdapterConfig>,
|
|
190
190
|
cwd: string,
|
|
191
|
+
localRoots?: readonly string[],
|
|
191
192
|
): DapResolvedAdapter | null {
|
|
192
193
|
const config = configs[adapterName];
|
|
193
194
|
if (!config) return null;
|
|
194
|
-
const
|
|
195
|
+
const normalizedCommand = normalizeCommandForCwd(config.command, cwd);
|
|
196
|
+
const commandIsBare =
|
|
197
|
+
!path.isAbsolute(config.command) && !config.command.includes("/") && !config.command.includes("\\");
|
|
198
|
+
const resolvedCommand = resolveCommand(normalizedCommand, cwd, {
|
|
199
|
+
cache: WhichCachePolicy.Fresh,
|
|
200
|
+
PATH: process.env.PATH,
|
|
201
|
+
localRoots: commandIsBare ? localRoots : undefined,
|
|
202
|
+
});
|
|
195
203
|
if (!resolvedCommand) return null;
|
|
196
204
|
return {
|
|
197
205
|
name: adapterName,
|
|
@@ -219,33 +227,125 @@ export function getAvailableAdapters(cwd: string): DapResolvedAdapter[] {
|
|
|
219
227
|
.filter((adapter): adapter is DapResolvedAdapter => adapter !== null);
|
|
220
228
|
}
|
|
221
229
|
|
|
222
|
-
|
|
230
|
+
/** Launch adapter selection, including a configured adapter whose command is unavailable. */
|
|
231
|
+
export type LaunchAdapterSelection =
|
|
232
|
+
| { kind: "adapter"; adapter: DapResolvedAdapter }
|
|
233
|
+
| { kind: "unavailable"; adapterName: string; command: string }
|
|
234
|
+
| { kind: "none" };
|
|
235
|
+
|
|
236
|
+
interface LaunchAdapterCandidate {
|
|
237
|
+
name: string;
|
|
238
|
+
rootDir: string | null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function findRootMarkerInLaunchAncestry(
|
|
242
|
+
program: string,
|
|
243
|
+
cwd: string,
|
|
244
|
+
markers: string[],
|
|
245
|
+
programKind: LaunchProgramKind,
|
|
246
|
+
): string | null {
|
|
247
|
+
if (markers.length === 0) return null;
|
|
248
|
+
let dir = programKind === "directory" ? path.resolve(cwd, program) : path.dirname(path.resolve(cwd, program));
|
|
249
|
+
while (true) {
|
|
250
|
+
if (hasRootMarkers(dir, markers)) return dir;
|
|
251
|
+
const parent = path.dirname(dir);
|
|
252
|
+
if (parent === dir) return null;
|
|
253
|
+
dir = parent;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function resolveAdapterForLaunch(
|
|
258
|
+
adapterName: string,
|
|
259
|
+
configs: Record<string, DapAdapterConfig>,
|
|
260
|
+
cwd: string,
|
|
261
|
+
rootDir: string | null,
|
|
262
|
+
): DapResolvedAdapter | null {
|
|
263
|
+
const localRoots = rootDir && rootDir !== cwd ? [rootDir, cwd] : undefined;
|
|
264
|
+
return resolveAdapterFromConfig(adapterName, configs, cwd, localRoots);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function unavailableAdapter(
|
|
268
|
+
candidate: LaunchAdapterCandidate,
|
|
269
|
+
configs: Record<string, DapAdapterConfig>,
|
|
270
|
+
): LaunchAdapterSelection {
|
|
271
|
+
const config = configs[candidate.name];
|
|
272
|
+
if (!config) return { kind: "none" };
|
|
273
|
+
return { kind: "unavailable", adapterName: candidate.name, command: config.command };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function selectAutomaticLaunchAdapter(
|
|
277
|
+
program: string,
|
|
278
|
+
cwd: string,
|
|
279
|
+
programKind: LaunchProgramKind,
|
|
280
|
+
configs: Record<string, DapAdapterConfig>,
|
|
281
|
+
): LaunchAdapterSelection {
|
|
223
282
|
const extension = path.extname(program).toLowerCase();
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
283
|
+
if (extension) {
|
|
284
|
+
const configured: LaunchAdapterCandidate[] = [];
|
|
285
|
+
const available: DapResolvedAdapter[] = [];
|
|
286
|
+
for (const name in configs) {
|
|
287
|
+
const config = configs[name];
|
|
288
|
+
if (!config || !(config.fileTypes ?? []).includes(extension)) continue;
|
|
289
|
+
const rootDir = findRootMarkerInLaunchAncestry(program, cwd, config.rootMarkers ?? [], programKind);
|
|
290
|
+
configured.push({ name, rootDir });
|
|
291
|
+
const adapter = resolveAdapterForLaunch(name, configs, cwd, rootDir);
|
|
292
|
+
if (adapter) available.push(adapter);
|
|
293
|
+
}
|
|
294
|
+
const selected = sortAdaptersForLaunch(program, cwd, programKind, available)[0];
|
|
295
|
+
if (selected) return { kind: "adapter", adapter: selected };
|
|
296
|
+
const rootMatch = configured.find(candidate => candidate.rootDir !== null);
|
|
297
|
+
const unavailable = rootMatch ?? configured[0];
|
|
298
|
+
if (unavailable) return unavailableAdapter(unavailable, configs);
|
|
235
299
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
300
|
+
|
|
301
|
+
const available: DapResolvedAdapter[] = [];
|
|
302
|
+
const rootMatches: LaunchAdapterCandidate[] = [];
|
|
303
|
+
const directoryMatches: LaunchAdapterCandidate[] = [];
|
|
304
|
+
for (const name in configs) {
|
|
305
|
+
const config = configs[name];
|
|
306
|
+
if (!config) continue;
|
|
307
|
+
const rootDir = findRootMarkerInLaunchAncestry(program, cwd, config.rootMarkers ?? [], programKind);
|
|
308
|
+
const candidate = { name, rootDir };
|
|
309
|
+
if (rootDir) {
|
|
310
|
+
rootMatches.push(candidate);
|
|
311
|
+
if (config.acceptsDirectoryProgram === true) directoryMatches.push(candidate);
|
|
312
|
+
}
|
|
313
|
+
if (!EXTENSIONLESS_DEBUGGER_ORDER.includes(name) && !rootDir) continue;
|
|
314
|
+
const adapter = resolveAdapterForLaunch(name, configs, cwd, rootDir);
|
|
315
|
+
if (adapter) available.push(adapter);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (programKind === "directory" && directoryMatches.length > 0) {
|
|
319
|
+
const matchingNames = new Set(directoryMatches.map(candidate => candidate.name));
|
|
320
|
+
const directoryAdapters = available.filter(
|
|
321
|
+
adapter => adapter.acceptsDirectoryProgram && matchingNames.has(adapter.name),
|
|
322
|
+
);
|
|
323
|
+
const selected = sortAdaptersForLaunch(program, cwd, programKind, directoryAdapters)[0];
|
|
324
|
+
if (selected) return { kind: "adapter", adapter: selected };
|
|
325
|
+
const unavailable = directoryMatches[0];
|
|
326
|
+
return unavailable ? unavailableAdapter(unavailable, configs) : { kind: "none" };
|
|
239
327
|
}
|
|
240
|
-
|
|
328
|
+
|
|
329
|
+
const directoryAdapters =
|
|
330
|
+
programKind === "directory" ? available.filter(adapter => adapter.acceptsDirectoryProgram) : available;
|
|
331
|
+
const candidates = directoryAdapters.length > 0 ? directoryAdapters : available;
|
|
332
|
+
const selected = sortAdaptersForLaunch(program, cwd, programKind, candidates)[0];
|
|
333
|
+
if (selected) return { kind: "adapter", adapter: selected };
|
|
334
|
+
const unavailable = rootMatches[0];
|
|
335
|
+
return unavailable ? unavailableAdapter(unavailable, configs) : { kind: "none" };
|
|
241
336
|
}
|
|
242
337
|
|
|
243
|
-
function sortAdaptersForLaunch(
|
|
338
|
+
function sortAdaptersForLaunch(
|
|
339
|
+
program: string,
|
|
340
|
+
cwd: string,
|
|
341
|
+
programKind: LaunchProgramKind,
|
|
342
|
+
adapters: DapResolvedAdapter[],
|
|
343
|
+
): DapResolvedAdapter[] {
|
|
244
344
|
const extension = path.extname(program).toLowerCase();
|
|
245
345
|
const rootAware = adapters.map(adapter => ({
|
|
246
346
|
adapter,
|
|
247
347
|
hasExtensionMatch: extension.length > 0 && adapter.fileTypes.includes(extension),
|
|
248
|
-
hasRootMatch:
|
|
348
|
+
hasRootMatch: findRootMarkerInLaunchAncestry(program, cwd, adapter.rootMarkers, programKind) !== null,
|
|
249
349
|
}));
|
|
250
350
|
rootAware.sort((left, right) => {
|
|
251
351
|
if (left.hasExtensionMatch !== right.hasExtensionMatch) {
|
|
@@ -254,36 +354,33 @@ function sortAdaptersForLaunch(program: string, cwd: string, adapters: DapResolv
|
|
|
254
354
|
if (left.hasRootMatch !== right.hasRootMatch) {
|
|
255
355
|
return left.hasRootMatch ? -1 : 1;
|
|
256
356
|
}
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
);
|
|
263
|
-
const normalizedLeftRank = leftDebuggerRank === -1 ? Number.MAX_SAFE_INTEGER : leftDebuggerRank;
|
|
264
|
-
const normalizedRightRank = rightDebuggerRank === -1 ? Number.MAX_SAFE_INTEGER : rightDebuggerRank;
|
|
265
|
-
if (normalizedLeftRank !== normalizedRightRank) {
|
|
266
|
-
return normalizedLeftRank - normalizedRightRank;
|
|
267
|
-
}
|
|
357
|
+
const leftRank = EXTENSIONLESS_DEBUGGER_ORDER.indexOf(left.adapter.name);
|
|
358
|
+
const rightRank = EXTENSIONLESS_DEBUGGER_ORDER.indexOf(right.adapter.name);
|
|
359
|
+
const normalizedLeftRank = leftRank === -1 ? Number.MAX_SAFE_INTEGER : leftRank;
|
|
360
|
+
const normalizedRightRank = rightRank === -1 ? Number.MAX_SAFE_INTEGER : rightRank;
|
|
361
|
+
const rankDelta = normalizedLeftRank - normalizedRightRank;
|
|
362
|
+
if (rankDelta !== 0) return rankDelta;
|
|
268
363
|
return left.adapter.name.localeCompare(right.adapter.name);
|
|
269
364
|
});
|
|
270
365
|
return rootAware.map(entry => entry.adapter);
|
|
271
366
|
}
|
|
272
367
|
|
|
368
|
+
/** Selects a launch adapter or reports why matching configuration cannot run. */
|
|
273
369
|
export function selectLaunchAdapter(
|
|
274
370
|
program: string,
|
|
275
371
|
cwd: string,
|
|
276
372
|
adapterName?: string,
|
|
277
373
|
programKind: LaunchProgramKind = "file",
|
|
278
|
-
):
|
|
374
|
+
): LaunchAdapterSelection {
|
|
375
|
+
const configs = getAdapterConfigs(cwd);
|
|
279
376
|
if (adapterName) {
|
|
280
|
-
|
|
377
|
+
const config = configs[adapterName];
|
|
378
|
+
if (!config) return { kind: "none" };
|
|
379
|
+
const rootDir = findRootMarkerInLaunchAncestry(program, cwd, config.rootMarkers ?? [], programKind);
|
|
380
|
+
const adapter = resolveAdapterForLaunch(adapterName, configs, cwd, rootDir);
|
|
381
|
+
return adapter ? { kind: "adapter", adapter } : { kind: "unavailable", adapterName, command: config.command };
|
|
281
382
|
}
|
|
282
|
-
|
|
283
|
-
const candidates =
|
|
284
|
-
programKind === "directory" ? matches.filter(adapter => adapter.acceptsDirectoryProgram) : matches;
|
|
285
|
-
const sorted = sortAdaptersForLaunch(program, cwd, candidates.length > 0 ? candidates : matches);
|
|
286
|
-
return sorted[0] ?? null;
|
|
383
|
+
return selectAutomaticLaunchAdapter(program, cwd, programKind, configs);
|
|
287
384
|
}
|
|
288
385
|
|
|
289
386
|
export function selectAttachAdapter(cwd: string, adapterName?: string, port?: number): DapResolvedAdapter | null {
|
package/src/dap/defaults.json
CHANGED
|
@@ -163,6 +163,7 @@ export class JsRuntime {
|
|
|
163
163
|
|
|
164
164
|
readonly helpers: HelperBundle;
|
|
165
165
|
#cwd: string;
|
|
166
|
+
#session: { cwd: string; sessionId: string };
|
|
166
167
|
readonly sessionId: string;
|
|
167
168
|
#env: Map<string, string>;
|
|
168
169
|
#als = new AsyncLocalStorage<RunContext>();
|
|
@@ -171,6 +172,7 @@ export class JsRuntime {
|
|
|
171
172
|
|
|
172
173
|
constructor(opts: RuntimeOptions) {
|
|
173
174
|
this.#cwd = opts.initialCwd;
|
|
175
|
+
this.#session = { cwd: opts.initialCwd, sessionId: opts.sessionId };
|
|
174
176
|
this.sessionId = opts.sessionId;
|
|
175
177
|
this.#env = new Map();
|
|
176
178
|
this.#moduleLoader = new LocalModuleLoader(this.sessionId);
|
|
@@ -189,10 +191,19 @@ export class JsRuntime {
|
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
setCwd(cwd: string): void {
|
|
192
|
-
this.#
|
|
194
|
+
if (this.#disposed) throw new Error("Cannot set cwd on a disposed JS runtime");
|
|
195
|
+
// Always stamp the runtime and session state: WorkerCore/browser/cmux call
|
|
196
|
+
// setCwd from init and pre-run paths that may race another same-realm
|
|
197
|
+
// runtime, and a throw here used to escape the inline-worker microtask
|
|
198
|
+
// path as a fatal unhandledRejection that killed the whole session.
|
|
199
|
+
// #session is the same object saved in this owner's global stack entry,
|
|
200
|
+
// so the new cwd survives deferred activation and is visible to this
|
|
201
|
+
// runtime's next run; run()/setRunScope still assert exclusive ownership.
|
|
193
202
|
this.#cwd = cwd;
|
|
194
|
-
|
|
195
|
-
if (
|
|
203
|
+
this.#session.cwd = cwd;
|
|
204
|
+
if (activeGlobalRunOwner === null || activeGlobalRunOwner === this.#globalOwner) {
|
|
205
|
+
this.#activateGlobals("set cwd");
|
|
206
|
+
}
|
|
196
207
|
}
|
|
197
208
|
|
|
198
209
|
/**
|
|
@@ -318,8 +329,13 @@ export class JsRuntime {
|
|
|
318
329
|
}
|
|
319
330
|
|
|
320
331
|
#install(extraGlobals: Record<string, unknown> | undefined): void {
|
|
332
|
+
// Constructing a runtime while another same-realm runtime is mid-run would
|
|
333
|
+
// silently replace the live runtime's globals (Object.assign + prelude eval
|
|
334
|
+
// below). Fail before any global/stack mutation; WorkerCore reports it as
|
|
335
|
+
// init-failed instead of corrupting the active run.
|
|
336
|
+
assertCanUseGlobalOwner(this.#globalOwner, "initialize a JS runtime");
|
|
321
337
|
const injected: Record<string, unknown> = {
|
|
322
|
-
__omp_session__:
|
|
338
|
+
__omp_session__: this.#session,
|
|
323
339
|
__omp_helpers__: this.helpers,
|
|
324
340
|
__omp_call_tool__: async (name: string, args: unknown) => {
|
|
325
341
|
const hooks = this.#activeHooks("tool");
|
|
@@ -184,8 +184,15 @@ export class WorkerCore {
|
|
|
184
184
|
#handle(msg: WorkerInbound): void {
|
|
185
185
|
switch (msg.type) {
|
|
186
186
|
case "init":
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
try {
|
|
188
|
+
this.#ensureRuntime(msg.snapshot);
|
|
189
|
+
this.#transport.send({ type: "ready" });
|
|
190
|
+
} catch (error) {
|
|
191
|
+
// Inline fallback delivers messages on a microtask. A sync throw
|
|
192
|
+
// from ensureRuntime/setCwd would otherwise become a process-fatal
|
|
193
|
+
// unhandledRejection on the main thread.
|
|
194
|
+
this.#transport.send({ type: "init-failed", error: errorPayload(error) });
|
|
195
|
+
}
|
|
189
196
|
return;
|
|
190
197
|
case "run":
|
|
191
198
|
void this.#runOne(msg.runId, msg.code, msg.filename, msg.snapshot);
|
|
@@ -300,9 +300,16 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
300
300
|
const requestedTimeoutMs = options?.timeout;
|
|
301
301
|
const deadlineTimeoutMs = requestedTimeoutMs === 0 ? undefined : Math.max(1_000, requestedTimeoutMs ?? 300_000);
|
|
302
302
|
const nativeTimeoutMs = requestedTimeoutMs !== undefined && requestedTimeoutMs > 0 ? requestedTimeoutMs : undefined;
|
|
303
|
+
const nativeOwnsTimeout = nativeTimeoutMs !== undefined;
|
|
303
304
|
if (deadlineTimeoutMs !== undefined) {
|
|
304
305
|
timeoutTimer = setTimeout(() => {
|
|
305
|
-
|
|
306
|
+
// Explicit timeouts are already enforced inside pi-natives via
|
|
307
|
+
// `timeoutMs`. Do not also abort the JS AbortSignal here: on Windows,
|
|
308
|
+
// aborting that signal while a piped command is still forwarding output
|
|
309
|
+
// can terminate the Bun host before the native timeout result resolves.
|
|
310
|
+
if (!nativeOwnsTimeout) {
|
|
311
|
+
abortCurrentExecution();
|
|
312
|
+
}
|
|
306
313
|
timeoutDeferred.resolve("timeout");
|
|
307
314
|
}, deadlineTimeoutMs);
|
|
308
315
|
}
|
|
@@ -125,6 +125,10 @@ export interface ExtensionUIDialogOptions {
|
|
|
125
125
|
timeout?: number;
|
|
126
126
|
/** Invoked when the UI times out while waiting for a selection/input */
|
|
127
127
|
onTimeout?: () => void;
|
|
128
|
+
/** Invoked when the UI-managed timeout countdown starts */
|
|
129
|
+
onTimeoutStart?: () => void;
|
|
130
|
+
/** Invoked when user input resets a UI-managed timeout countdown */
|
|
131
|
+
onTimeoutReset?: () => void;
|
|
128
132
|
/** Initial cursor position for select dialogs (0-indexed) */
|
|
129
133
|
initialIndex?: number;
|
|
130
134
|
/** Render an outlined list for select dialogs */
|
|
@@ -176,6 +180,8 @@ export type AutocompleteProviderFactory = (current: AutocompleteProvider) => Aut
|
|
|
176
180
|
// and may be invoked from event handlers that have already taken the agent
|
|
177
181
|
// loop's lock — hooks intentionally cannot.
|
|
178
182
|
export interface ExtensionUIContext {
|
|
183
|
+
/** True when selector timeouts start only after the dialog is presented. */
|
|
184
|
+
timeoutStartsOnPresentation?: boolean;
|
|
179
185
|
/** Show a selector and return the selected label, even when an option also includes a description. */
|
|
180
186
|
select(
|
|
181
187
|
title: string,
|