@oh-my-pi/pi-coding-agent 16.0.1 → 16.0.3
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 +70 -0
- package/README.md +0 -1
- package/dist/cli.js +316 -371
- package/dist/types/advisor/advise-tool.d.ts +30 -1
- package/dist/types/commands/install.d.ts +1 -1
- package/dist/types/config/model-resolver.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +0 -10
- package/dist/types/eval/js/shared/runtime.d.ts +1 -0
- package/dist/types/eval/js/worker-core.d.ts +1 -0
- package/dist/types/exec/non-interactive-env.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/goals/runtime.d.ts +0 -1
- package/dist/types/mcp/tool-bridge.d.ts +3 -0
- package/dist/types/modes/components/custom-editor.d.ts +14 -4
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +3 -2
- package/dist/types/modes/theme/mermaid-cache.d.ts +18 -1
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +16 -1
- package/dist/types/sdk.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +20 -8
- package/dist/types/session/messages.d.ts +3 -0
- package/dist/types/session/session-dump-format.d.ts +8 -2
- package/dist/types/session/session-entries.d.ts +4 -0
- package/dist/types/session/session-history-format.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +22 -0
- package/dist/types/stt/downloader.d.ts +5 -5
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/persisted-revive.d.ts +36 -0
- package/dist/types/tiny/models.d.ts +8 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +0 -1
- package/dist/types/utils/markit.d.ts +8 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +156 -12
- package/src/advisor/advise-tool.ts +48 -6
- package/src/advisor/runtime.ts +10 -3
- package/src/auto-thinking/classifier.ts +12 -3
- package/src/cli/args.ts +1 -0
- package/src/cli.ts +2 -2
- package/src/commands/install.ts +3 -3
- package/src/config/model-resolver.ts +63 -12
- package/src/config/settings-schema.ts +0 -11
- package/src/discovery/github.ts +89 -1
- package/src/eval/agent-bridge.ts +2 -0
- package/src/eval/js/context-manager.ts +2 -1
- package/src/eval/js/shared/runtime.ts +189 -15
- package/src/eval/js/worker-core.ts +19 -0
- package/src/exec/bash-executor.ts +2 -2
- package/src/exec/non-interactive-env.ts +71 -0
- package/src/export/html/index.ts +1 -1
- package/src/export/html/tool-views.generated.js +34 -35
- package/src/extensibility/extensions/loader.ts +21 -9
- package/src/extensibility/extensions/runner.ts +17 -1
- package/src/extensibility/plugins/loader.ts +154 -21
- package/src/extensibility/plugins/manager.ts +40 -33
- package/src/goals/runtime.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +9 -11
- package/src/main.ts +20 -0
- package/src/mcp/render.ts +11 -1
- package/src/mcp/tool-bridge.ts +3 -0
- package/src/modes/components/custom-editor.test.ts +63 -18
- package/src/modes/components/custom-editor.ts +63 -15
- package/src/modes/controllers/command-controller.ts +2 -2
- package/src/modes/controllers/input-controller.ts +15 -9
- package/src/modes/controllers/selector-controller.ts +13 -8
- package/src/modes/controllers/tan-command-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/setup-wizard/wizard-overlay.ts +26 -4
- package/src/modes/theme/mermaid-cache.ts +74 -11
- package/src/modes/theme/theme.ts +14 -1
- package/src/modes/types.ts +1 -1
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/registry/agent-lifecycle.ts +60 -8
- package/src/sdk.ts +20 -26
- package/src/session/agent-session.ts +381 -110
- package/src/session/artifacts.ts +19 -1
- package/src/session/messages.ts +1 -1
- package/src/session/session-dump-format.ts +167 -23
- package/src/session/session-entries.ts +4 -0
- package/src/session/session-history-format.ts +37 -3
- package/src/session/session-manager.ts +94 -4
- package/src/slash-commands/builtin-registry.ts +4 -7
- package/src/stt/asr-client.ts +6 -0
- package/src/stt/downloader.ts +13 -6
- package/src/stt/stt-controller.ts +52 -11
- package/src/system-prompt.ts +7 -1
- package/src/task/executor.ts +118 -6
- package/src/task/index.ts +2 -2
- package/src/task/persisted-revive.ts +128 -0
- package/src/tiny/models.ts +10 -0
- package/src/tiny/worker.ts +4 -3
- package/src/tools/builtin-names.ts +0 -1
- package/src/tools/index.ts +0 -4
- package/src/tools/output-meta.ts +17 -3
- package/src/utils/lang-from-path.ts +5 -0
- package/src/utils/markit.ts +24 -1
- package/src/utils/title-generator.ts +4 -4
- package/dist/types/tools/render-mermaid.d.ts +0 -38
- package/src/prompts/tools/render-mermaid.md +0 -9
- package/src/tools/render-mermaid.ts +0 -69
package/src/discovery/github.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Capabilities:
|
|
13
13
|
* - context-files: copilot-instructions.md in .github/ and ~/.copilot/; AGENTS.md in each COPILOT_CUSTOM_INSTRUCTIONS_DIRS
|
|
14
|
-
* -
|
|
14
|
+
* - rules: *.instructions.md under .github/instructions/ and <dir>/.github/instructions/ for each custom dir (applyTo frontmatter)
|
|
15
15
|
* - prompts: *.prompt.md in .github/prompts/ (VS Code Copilot prompt files)
|
|
16
16
|
* - skills: <name>/SKILL.md in .github/skills/ (GitHub Agent Skills layout)
|
|
17
17
|
*/
|
|
@@ -22,10 +22,12 @@ import { type ContextFile, contextFileCapability } from "../capability/context-f
|
|
|
22
22
|
import { readFile } from "../capability/fs";
|
|
23
23
|
import { type Instruction, instructionCapability } from "../capability/instruction";
|
|
24
24
|
import { type Prompt, promptCapability } from "../capability/prompt";
|
|
25
|
+
import { type Rule, ruleCapability } from "../capability/rule";
|
|
25
26
|
import { type Skill, skillCapability } from "../capability/skill";
|
|
26
27
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
27
28
|
|
|
28
29
|
import {
|
|
30
|
+
buildRuleFromMarkdown,
|
|
29
31
|
calculateDepth,
|
|
30
32
|
createSourceMeta,
|
|
31
33
|
getProjectPath,
|
|
@@ -152,6 +154,85 @@ function transformInstruction(name: string, content: string, filePath: string, s
|
|
|
152
154
|
};
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
// =============================================================================
|
|
158
|
+
// Rules
|
|
159
|
+
// =============================================================================
|
|
160
|
+
|
|
161
|
+
async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
162
|
+
const items: Rule[] = [];
|
|
163
|
+
const warnings: string[] = [];
|
|
164
|
+
|
|
165
|
+
const load = async (dir: string, level: "user" | "project") => {
|
|
166
|
+
const applyToWarnings: string[] = [];
|
|
167
|
+
const result = await loadFilesFromDir<Rule>(ctx, dir, PROVIDER_ID, level, {
|
|
168
|
+
extensions: ["md"],
|
|
169
|
+
transform: (name, content, filePath, source) =>
|
|
170
|
+
transformInstructionRule(name, content, filePath, source, applyToWarnings),
|
|
171
|
+
recursive: true,
|
|
172
|
+
});
|
|
173
|
+
items.push(...result.items);
|
|
174
|
+
if (result.warnings) warnings.push(...result.warnings);
|
|
175
|
+
warnings.push(...applyToWarnings);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const instructionsDir = getProjectPath(ctx, "github", "instructions");
|
|
179
|
+
if (instructionsDir) {
|
|
180
|
+
await load(instructionsDir, "project");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const dir of copilotCustomInstructionDirs()) {
|
|
184
|
+
await load(path.join(dir, ".github", "instructions"), "user");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { items, warnings };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function transformInstructionRule(
|
|
191
|
+
name: string,
|
|
192
|
+
content: string,
|
|
193
|
+
filePath: string,
|
|
194
|
+
source: SourceMeta,
|
|
195
|
+
warnings: string[],
|
|
196
|
+
): Rule | null {
|
|
197
|
+
if (!name.endsWith(".instructions.md")) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const { frontmatter } = parseFrontmatter(content, { source: filePath });
|
|
202
|
+
const applyToGlobs = normalizeApplyToGlobs(frontmatter.applyTo);
|
|
203
|
+
if (!applyToGlobs) {
|
|
204
|
+
warnings.push(`Missing applyTo in ${filePath}; loaded without GitHub glob scoping.`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const rule = buildRuleFromMarkdown(name, content, filePath, source, {
|
|
208
|
+
stripNamePattern: /\.instructions\.md$/,
|
|
209
|
+
});
|
|
210
|
+
if (applyToGlobs?.some(isAlwaysApplyGlob)) {
|
|
211
|
+
return { ...rule, alwaysApply: true, globs: undefined };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const description = rule.description ?? describeInstructionRule(applyToGlobs);
|
|
215
|
+
return { ...rule, alwaysApply: false, globs: applyToGlobs, description };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function normalizeApplyToGlobs(value: unknown): string[] | undefined {
|
|
219
|
+
// GitHub documents applyTo as a single comma-separated string (e.g.
|
|
220
|
+
// "**/*.ts,**/*.tsx"); also tolerate a YAML array of such strings.
|
|
221
|
+
const raw = Array.isArray(value) ? value : [value];
|
|
222
|
+
const globs = raw.flatMap(item => (typeof item === "string" ? parseCSV(item) : []));
|
|
223
|
+
return globs.length > 0 ? globs : undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isAlwaysApplyGlob(glob: string): boolean {
|
|
227
|
+
// GitHub treats "*", "**", and "**/*" as matching every file.
|
|
228
|
+
return glob === "*" || glob === "**" || glob === "**/*";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function describeInstructionRule(globs: string[] | undefined): string {
|
|
232
|
+
if (!globs) return "GitHub Copilot instructions without applyTo metadata";
|
|
233
|
+
return `GitHub Copilot instructions for ${globs.join(", ")}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
155
236
|
// =============================================================================
|
|
156
237
|
// Prompts
|
|
157
238
|
// =============================================================================
|
|
@@ -232,6 +313,13 @@ registerProvider(instructionCapability.id, {
|
|
|
232
313
|
load: loadInstructions,
|
|
233
314
|
});
|
|
234
315
|
|
|
316
|
+
registerProvider<Rule>(ruleCapability.id, {
|
|
317
|
+
id: PROVIDER_ID,
|
|
318
|
+
displayName: DISPLAY_NAME,
|
|
319
|
+
description: "Load *.instructions.md from .github/instructions/ as Copilot-scoped rules",
|
|
320
|
+
priority: PRIORITY,
|
|
321
|
+
load: loadRules,
|
|
322
|
+
});
|
|
235
323
|
registerProvider<Skill>(skillCapability.id, {
|
|
236
324
|
id: PROVIDER_ID,
|
|
237
325
|
displayName: DISPLAY_NAME,
|
package/src/eval/agent-bridge.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { resolveAgentModelPatterns } from "../config/model-resolver";
|
|
|
10
10
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
11
11
|
import { MCPManager } from "../mcp/manager";
|
|
12
12
|
import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
|
|
13
|
+
import { MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
13
14
|
import * as taskDiscovery from "../task/discovery";
|
|
14
15
|
import * as taskExecutor from "../task/executor";
|
|
15
16
|
import { AgentOutputManager } from "../task/output-manager";
|
|
@@ -288,6 +289,7 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
|
|
|
288
289
|
parentHindsightSessionState: options.session.getHindsightSessionState?.(),
|
|
289
290
|
parentMnemopiSessionState: options.session.getMnemopiSessionState?.(),
|
|
290
291
|
parentTelemetry: options.session.getTelemetry?.(),
|
|
292
|
+
parentAgentId: options.session.getAgentId?.() ?? MAIN_AGENT_ID,
|
|
291
293
|
// Deliberately omit parentEvalSessionId: the parent's Python kernel is
|
|
292
294
|
// blocked on this bridge call, so sharing the eval session would deadlock
|
|
293
295
|
// (subagent queues behind the parent's in-flight execution, parent waits
|
|
@@ -564,7 +564,7 @@ function spawnInlineWorker(): WorkerHandle {
|
|
|
564
564
|
},
|
|
565
565
|
close: () => {},
|
|
566
566
|
};
|
|
567
|
-
new WorkerCore(workerTransport);
|
|
567
|
+
const core = new WorkerCore(workerTransport);
|
|
568
568
|
return {
|
|
569
569
|
mode: "inline",
|
|
570
570
|
send: msg =>
|
|
@@ -600,6 +600,7 @@ function spawnInlineWorker(): WorkerHandle {
|
|
|
600
600
|
async terminate() {
|
|
601
601
|
hostListeners.clear();
|
|
602
602
|
workerListeners.clear();
|
|
603
|
+
core.dispose();
|
|
603
604
|
},
|
|
604
605
|
};
|
|
605
606
|
}
|
|
@@ -55,6 +55,32 @@ export interface RuntimeOptions {
|
|
|
55
55
|
const BASE64_STRICT_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
56
56
|
const DECIMAL_CSV_RE = /^\d{1,3}(?:,\d{1,3})*$/;
|
|
57
57
|
|
|
58
|
+
const PRELUDE_GLOBAL_KEYS = [
|
|
59
|
+
"__omp_js_prelude_loaded__",
|
|
60
|
+
"console",
|
|
61
|
+
"print",
|
|
62
|
+
"display",
|
|
63
|
+
"tool",
|
|
64
|
+
"completion",
|
|
65
|
+
"output",
|
|
66
|
+
"agent",
|
|
67
|
+
"parallel",
|
|
68
|
+
"pipeline",
|
|
69
|
+
"log",
|
|
70
|
+
"phase",
|
|
71
|
+
"budget",
|
|
72
|
+
"__pool",
|
|
73
|
+
"read",
|
|
74
|
+
"write",
|
|
75
|
+
"append",
|
|
76
|
+
"sort",
|
|
77
|
+
"uniq",
|
|
78
|
+
"counter",
|
|
79
|
+
"diff",
|
|
80
|
+
"tree",
|
|
81
|
+
"env",
|
|
82
|
+
];
|
|
83
|
+
|
|
58
84
|
function isStrictBase64(s: string): boolean {
|
|
59
85
|
if (s.length === 0 || s.length % 4 !== 0) return false;
|
|
60
86
|
return BASE64_STRICT_RE.test(s);
|
|
@@ -125,6 +151,22 @@ function describeDataType(data: unknown): string {
|
|
|
125
151
|
* concern.
|
|
126
152
|
*/
|
|
127
153
|
export class JsRuntime {
|
|
154
|
+
#globalOwner = Symbol("JsRuntime globals");
|
|
155
|
+
#ownedGlobalKeys = new Set<string>();
|
|
156
|
+
#disposed = false;
|
|
157
|
+
#runHookResolver = () => this.#als.getStore()?.hooks;
|
|
158
|
+
|
|
159
|
+
#ownGlobal(key: string): void {
|
|
160
|
+
if (this.#ownedGlobalKeys.has(key)) return;
|
|
161
|
+
claimGlobalKey(key, this.#globalOwner);
|
|
162
|
+
this.#ownedGlobalKeys.add(key);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#activateGlobals(action: string): void {
|
|
166
|
+
if (this.#disposed) throw new Error(`Cannot ${action} on a disposed JS runtime`);
|
|
167
|
+
activateGlobalOwner(this.#globalOwner, this.#ownedGlobalKeys, action);
|
|
168
|
+
}
|
|
169
|
+
|
|
128
170
|
readonly helpers: HelperBundle;
|
|
129
171
|
#cwd: string;
|
|
130
172
|
readonly sessionId: string;
|
|
@@ -153,6 +195,7 @@ export class JsRuntime {
|
|
|
153
195
|
}
|
|
154
196
|
|
|
155
197
|
setCwd(cwd: string): void {
|
|
198
|
+
this.#activateGlobals("set cwd");
|
|
156
199
|
this.#cwd = cwd;
|
|
157
200
|
const session = (globalThis as { __omp_session__?: { cwd?: string } }).__omp_session__;
|
|
158
201
|
if (session) session.cwd = cwd;
|
|
@@ -164,6 +207,7 @@ export class JsRuntime {
|
|
|
164
207
|
* cleanup it wants.
|
|
165
208
|
*/
|
|
166
209
|
setRunScope(scope: Record<string, unknown>): void {
|
|
210
|
+
this.#activateGlobals("set run scope");
|
|
167
211
|
Object.assign(globalThis, scope);
|
|
168
212
|
}
|
|
169
213
|
|
|
@@ -173,6 +217,8 @@ export class JsRuntime {
|
|
|
173
217
|
hooks: RuntimeHooks,
|
|
174
218
|
options: { runId?: string; cwd?: string } = {},
|
|
175
219
|
): Promise<unknown> {
|
|
220
|
+
this.#activateGlobals("run code");
|
|
221
|
+
const leaveRun = enterGlobalRun(this.#globalOwner, "run code");
|
|
176
222
|
const context: RunContext = {
|
|
177
223
|
runId: options.runId ?? crypto.randomUUID(),
|
|
178
224
|
hooks,
|
|
@@ -180,21 +226,25 @@ export class JsRuntime {
|
|
|
180
226
|
finalExpressionSet: false,
|
|
181
227
|
finalExpressionValue: undefined,
|
|
182
228
|
};
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
229
|
+
try {
|
|
230
|
+
return await this.#als.run(context, async () => {
|
|
231
|
+
const wrapped = await wrapCode(code);
|
|
232
|
+
const value = indirectEval(wrapped.source, filename);
|
|
233
|
+
if (wrapped.finalExpressionReturned) {
|
|
234
|
+
const awaited = await awaitMaybePromise(value);
|
|
235
|
+
if (context.finalExpressionSet) {
|
|
236
|
+
const finalValue = context.finalExpressionValue;
|
|
237
|
+
context.finalExpressionSet = false;
|
|
238
|
+
context.finalExpressionValue = undefined;
|
|
239
|
+
return await awaitMaybePromise(finalValue);
|
|
240
|
+
}
|
|
241
|
+
return awaited;
|
|
193
242
|
}
|
|
194
|
-
return
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
243
|
+
return await awaitMaybePromise(value);
|
|
244
|
+
});
|
|
245
|
+
} finally {
|
|
246
|
+
leaveRun();
|
|
247
|
+
}
|
|
198
248
|
}
|
|
199
249
|
|
|
200
250
|
displayValue(value: unknown, hooks: RuntimeHooks | undefined = this.#als.getStore()?.hooks): void {
|
|
@@ -338,13 +388,137 @@ export class JsRuntime {
|
|
|
338
388
|
createRequire,
|
|
339
389
|
fs,
|
|
340
390
|
};
|
|
391
|
+
|
|
392
|
+
const allGlobalKeys = new Set<string>([
|
|
393
|
+
...Object.keys(injected),
|
|
394
|
+
...Object.keys(extraGlobals ?? {}),
|
|
395
|
+
...PRELUDE_GLOBAL_KEYS,
|
|
396
|
+
]);
|
|
397
|
+
|
|
398
|
+
for (const key of allGlobalKeys) {
|
|
399
|
+
this.#ownGlobal(key);
|
|
400
|
+
}
|
|
401
|
+
|
|
341
402
|
Object.assign(globalThis, injected, extraGlobals ?? {});
|
|
342
403
|
// Prelude assigns console bridge + short aliases (`read`, `write`, `tool`, `display`, ...)
|
|
343
404
|
// onto globalThis. Must run after helpers are in place.
|
|
344
405
|
indirectEval(JAVASCRIPT_PRELUDE_SOURCE);
|
|
345
|
-
|
|
406
|
+
for (const key of allGlobalKeys) recordGlobalValue(key, this.#globalOwner);
|
|
407
|
+
RUN_HOOK_RESOLVERS.add(this.#runHookResolver);
|
|
346
408
|
patchStdioOnce();
|
|
347
409
|
}
|
|
410
|
+
|
|
411
|
+
dispose(): void {
|
|
412
|
+
if (this.#disposed) return;
|
|
413
|
+
this.#disposed = true;
|
|
414
|
+
RUN_HOOK_RESOLVERS.delete(this.#runHookResolver);
|
|
415
|
+
for (const key of this.#ownedGlobalKeys) releaseGlobalKey(key, this.#globalOwner);
|
|
416
|
+
this.#ownedGlobalKeys.clear();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
interface GlobalSnapshot {
|
|
421
|
+
exists: boolean;
|
|
422
|
+
value: unknown;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
interface GlobalOwnerEntry {
|
|
426
|
+
owner: symbol;
|
|
427
|
+
value: unknown;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
interface GlobalStack {
|
|
431
|
+
base: GlobalSnapshot;
|
|
432
|
+
entries: GlobalOwnerEntry[];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Inline fallback and cmux tabs can create multiple JsRuntime instances in one Bun realm.
|
|
436
|
+
// Track reserved helper globals by owner so disposing one runtime restores the next active
|
|
437
|
+
// owner (or the original process global after the last owner), not a stale snapshot.
|
|
438
|
+
const GLOBAL_STACKS = new Map<string, GlobalStack>();
|
|
439
|
+
|
|
440
|
+
function snapshotGlobal(key: string): GlobalSnapshot {
|
|
441
|
+
return {
|
|
442
|
+
exists: key in globalThis,
|
|
443
|
+
value: (globalThis as Record<string, unknown>)[key],
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function restoreGlobal(key: string, state: GlobalSnapshot): void {
|
|
448
|
+
if (state.exists) {
|
|
449
|
+
(globalThis as Record<string, unknown>)[key] = state.value;
|
|
450
|
+
} else {
|
|
451
|
+
delete (globalThis as Record<string, unknown>)[key];
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function claimGlobalKey(key: string, owner: symbol): void {
|
|
456
|
+
let stack = GLOBAL_STACKS.get(key);
|
|
457
|
+
if (!stack) {
|
|
458
|
+
stack = { base: snapshotGlobal(key), entries: [] };
|
|
459
|
+
GLOBAL_STACKS.set(key, stack);
|
|
460
|
+
}
|
|
461
|
+
stack.entries.push({ owner, value: (globalThis as Record<string, unknown>)[key] });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function recordGlobalValue(key: string, owner: symbol): void {
|
|
465
|
+
const stack = GLOBAL_STACKS.get(key);
|
|
466
|
+
const entry = stack?.entries.findLast(item => item.owner === owner);
|
|
467
|
+
if (entry) entry.value = (globalThis as Record<string, unknown>)[key];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function releaseGlobalKey(key: string, owner: symbol): void {
|
|
471
|
+
const stack = GLOBAL_STACKS.get(key);
|
|
472
|
+
if (!stack) return;
|
|
473
|
+
const index = stack.entries.findIndex(entry => entry.owner === owner);
|
|
474
|
+
if (index === -1) return;
|
|
475
|
+
const wasTop = index === stack.entries.length - 1;
|
|
476
|
+
stack.entries.splice(index, 1);
|
|
477
|
+
if (!wasTop) return;
|
|
478
|
+
const next = stack.entries.at(-1);
|
|
479
|
+
if (next) {
|
|
480
|
+
(globalThis as Record<string, unknown>)[key] = next.value;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
restoreGlobal(key, stack.base);
|
|
484
|
+
GLOBAL_STACKS.delete(key);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Plain globalThis cannot safely serve two different runtimes at the same instant:
|
|
488
|
+
// helpers dereference reserved globals on every call. Sequential cmux tab revisits
|
|
489
|
+
// re-activate their owner stack; overlapping cross-runtime runs fail explicitly.
|
|
490
|
+
let activeGlobalRunOwner: symbol | null = null;
|
|
491
|
+
let activeGlobalRunDepth = 0;
|
|
492
|
+
|
|
493
|
+
function assertCanUseGlobalOwner(owner: symbol, action: string): void {
|
|
494
|
+
if (activeGlobalRunOwner === null || activeGlobalRunOwner === owner) return;
|
|
495
|
+
throw new Error(`Cannot ${action} while another same-realm JS runtime is running`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function activateGlobalOwner(owner: symbol, keys: Iterable<string>, action: string): void {
|
|
499
|
+
assertCanUseGlobalOwner(owner, action);
|
|
500
|
+
for (const key of keys) {
|
|
501
|
+
const stack = GLOBAL_STACKS.get(key);
|
|
502
|
+
const index = stack?.entries.findIndex(entry => entry.owner === owner) ?? -1;
|
|
503
|
+
if (!stack || index === -1) throw new Error(`Cannot ${action} on a disposed JS runtime`);
|
|
504
|
+
const entry = stack.entries[index];
|
|
505
|
+
stack.entries.splice(index, 1);
|
|
506
|
+
stack.entries.push(entry);
|
|
507
|
+
(globalThis as Record<string, unknown>)[key] = entry.value;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function enterGlobalRun(owner: symbol, action: string): () => void {
|
|
512
|
+
assertCanUseGlobalOwner(owner, action);
|
|
513
|
+
activeGlobalRunOwner = owner;
|
|
514
|
+
activeGlobalRunDepth++;
|
|
515
|
+
let left = false;
|
|
516
|
+
return () => {
|
|
517
|
+
if (left) return;
|
|
518
|
+
left = true;
|
|
519
|
+
activeGlobalRunDepth--;
|
|
520
|
+
if (activeGlobalRunDepth === 0) activeGlobalRunOwner = null;
|
|
521
|
+
};
|
|
348
522
|
}
|
|
349
523
|
|
|
350
524
|
/** Resolvers for each live runtime's active-run hooks (one per JsRuntime instance). */
|
|
@@ -124,9 +124,28 @@ export class WorkerCore {
|
|
|
124
124
|
active.pendingTools.clear();
|
|
125
125
|
}
|
|
126
126
|
this.#runs.clear();
|
|
127
|
+
this.#runtime?.dispose?.();
|
|
127
128
|
this.#runtime = null;
|
|
128
129
|
this.#transport.send({ type: "closed" });
|
|
129
130
|
this.#unsubscribe();
|
|
130
131
|
this.#transport.close();
|
|
131
132
|
}
|
|
133
|
+
|
|
134
|
+
dispose(): void {
|
|
135
|
+
for (const active of this.#runs.values()) {
|
|
136
|
+
for (const pending of active.pendingTools.values()) {
|
|
137
|
+
pending.reject(new ToolError("JS worker closed"));
|
|
138
|
+
}
|
|
139
|
+
active.pendingTools.clear();
|
|
140
|
+
}
|
|
141
|
+
this.#runs.clear();
|
|
142
|
+
this.#runtime?.dispose?.();
|
|
143
|
+
this.#runtime = null;
|
|
144
|
+
this.#unsubscribe();
|
|
145
|
+
try {
|
|
146
|
+
this.#transport.close();
|
|
147
|
+
} catch {
|
|
148
|
+
// Ignore
|
|
149
|
+
}
|
|
150
|
+
}
|
|
132
151
|
}
|
|
@@ -11,7 +11,7 @@ import { Settings, type ShellMinimizerSettings } from "../config/settings";
|
|
|
11
11
|
import { OutputSink } from "../session/streaming-output";
|
|
12
12
|
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
|
|
13
13
|
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
|
|
14
|
-
import {
|
|
14
|
+
import { buildNonInteractiveEnv } from "./non-interactive-env";
|
|
15
15
|
|
|
16
16
|
export interface BashExecutorOptions {
|
|
17
17
|
cwd?: string;
|
|
@@ -184,7 +184,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
184
184
|
const minimizer = buildMinimizerOptions(settings.getGroup("shellMinimizer"));
|
|
185
185
|
|
|
186
186
|
const commandCwd = await resolveShellCwd(options?.cwd);
|
|
187
|
-
const commandEnv = options?.env
|
|
187
|
+
const commandEnv = buildNonInteractiveEnv(options?.env);
|
|
188
188
|
|
|
189
189
|
// Apply command prefix if configured
|
|
190
190
|
const prefixedCommand = prefix ? `${prefix} ${command}` : command;
|
|
@@ -46,3 +46,74 @@ export const NON_INTERACTIVE_ENV: Readonly<Record<string, string>> = {
|
|
|
46
46
|
COMPOSER_NO_INTERACTION: "1",
|
|
47
47
|
CLOUDSDK_CORE_DISABLE_PROMPTS: "1",
|
|
48
48
|
};
|
|
49
|
+
|
|
50
|
+
const WINDOWS_UTF8_ENV_DEFAULT_GROUPS: ReadonlyArray<ReadonlyArray<readonly [key: string, value: string]>> = [
|
|
51
|
+
[
|
|
52
|
+
["PYTHONIOENCODING", "utf-8"],
|
|
53
|
+
["PYTHONUTF8", "1"],
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
["LANG", "C.UTF-8"],
|
|
57
|
+
["LC_ALL", "C.UTF-8"],
|
|
58
|
+
],
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
function hasEnvValue(
|
|
62
|
+
env: Record<string, string | undefined> | undefined,
|
|
63
|
+
key: string,
|
|
64
|
+
platform: NodeJS.Platform,
|
|
65
|
+
): boolean {
|
|
66
|
+
if (!env) return false;
|
|
67
|
+
if (platform !== "win32") return env[key] !== undefined;
|
|
68
|
+
|
|
69
|
+
for (const [existingKey, value] of Object.entries(env)) {
|
|
70
|
+
if (value !== undefined && existingKey.toLowerCase() === key.toLowerCase()) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hasLocaleEnvValue(env: Record<string, string | undefined> | undefined, platform: NodeJS.Platform): boolean {
|
|
78
|
+
if (!env) return false;
|
|
79
|
+
for (const [key, value] of Object.entries(env)) {
|
|
80
|
+
if (value === undefined) continue;
|
|
81
|
+
const normalizedKey = platform === "win32" ? key.toUpperCase() : key;
|
|
82
|
+
if (normalizedKey === "LANG" || normalizedKey.startsWith("LC_")) return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function hasEnvGroupValue(
|
|
88
|
+
env: Record<string, string | undefined> | undefined,
|
|
89
|
+
group: ReadonlyArray<readonly [key: string, value: string]>,
|
|
90
|
+
platform: NodeJS.Platform,
|
|
91
|
+
): boolean {
|
|
92
|
+
if (group.some(([key]) => key === "LC_ALL") && hasLocaleEnvValue(env, platform)) return true;
|
|
93
|
+
for (const [key] of group) {
|
|
94
|
+
if (hasEnvValue(env, key, platform)) return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Builds the per-command environment for non-interactive child processes. */
|
|
100
|
+
export function buildNonInteractiveEnv(
|
|
101
|
+
overrides?: Record<string, string>,
|
|
102
|
+
baseEnv: Record<string, string | undefined> = Bun.env,
|
|
103
|
+
platform: NodeJS.Platform = process.platform,
|
|
104
|
+
): Record<string, string> {
|
|
105
|
+
if (platform !== "win32") {
|
|
106
|
+
return overrides ? { ...NON_INTERACTIVE_ENV, ...overrides } : NON_INTERACTIVE_ENV;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const env: Record<string, string> = { ...NON_INTERACTIVE_ENV };
|
|
110
|
+
for (const group of WINDOWS_UTF8_ENV_DEFAULT_GROUPS) {
|
|
111
|
+
if (hasEnvGroupValue(baseEnv, group, platform) || hasEnvGroupValue(overrides, group, platform)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
for (const [key, value] of group) {
|
|
115
|
+
env[key] = value;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return overrides ? { ...env, ...overrides } : env;
|
|
119
|
+
}
|
package/src/export/html/index.ts
CHANGED
|
@@ -242,7 +242,7 @@ export async function exportFromFile(inputPath: string, options?: ExportOptions
|
|
|
242
242
|
|
|
243
243
|
let sm: SessionManager;
|
|
244
244
|
try {
|
|
245
|
-
sm = await SessionManager.open(inputPath);
|
|
245
|
+
sm = await SessionManager.open(inputPath, undefined, undefined, { suppressBreadcrumb: true });
|
|
246
246
|
} catch (err) {
|
|
247
247
|
if (isEnoent(err)) throw new Error(`File not found: ${inputPath}`);
|
|
248
248
|
throw err;
|