@oh-my-pi/pi-coding-agent 17.1.7 → 17.2.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 +127 -0
- package/dist/{CHANGELOG-5k4dq4g6.md → CHANGELOG-be1f2t8h.md} +127 -0
- package/dist/cli.js +9554 -12379
- package/dist/template-8vdv6xb4.js +1671 -0
- package/dist/template-f8wx9vfn.css +1355 -0
- package/dist/template-qat058wr.html +55 -0
- package/dist/tool-views.generated-jdfmzwmn.js +35 -0
- package/dist/types/advisor/advise-tool.d.ts +0 -7
- package/dist/types/advisor/runtime.d.ts +8 -11
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/async/job-manager.d.ts +11 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- package/dist/types/cleanse/agent.d.ts +19 -0
- package/dist/types/cleanse/balance.d.ts +7 -0
- package/dist/types/cleanse/checkers.d.ts +13 -0
- package/dist/types/cleanse/index.d.ts +16 -0
- package/dist/types/cleanse/loop.d.ts +16 -0
- package/dist/types/cleanse/parsers.d.ts +13 -0
- package/dist/types/cleanse/progress.d.ts +14 -0
- package/dist/types/cleanse/types.d.ts +64 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/session-picker.d.ts +14 -7
- package/dist/types/collab/guest.d.ts +21 -2
- package/dist/types/commands/cleanse.d.ts +23 -0
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +63 -8
- package/dist/types/cursor-bridge-tools.d.ts +54 -0
- package/dist/types/cursor.d.ts +193 -8
- package/dist/types/edit/edit-clipboard.d.ts +19 -0
- package/dist/types/edit/hashline/diff.d.ts +9 -1
- package/dist/types/edit/index.d.ts +8 -1
- package/dist/types/edit/streaming.d.ts +8 -1
- package/dist/types/export/html/index.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +19 -2
- package/dist/types/extensibility/extensions/types.d.ts +25 -1
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
- package/dist/types/internal-urls/local-protocol.d.ts +3 -2
- package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
- package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
- package/dist/types/launch/terminal-output-worker.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +33 -4
- package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
- package/dist/types/modes/components/read-tool-group.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +6 -0
- package/dist/types/modes/components/status-line/component.d.ts +23 -0
- package/dist/types/modes/components/tool-execution.d.ts +16 -7
- package/dist/types/modes/components/usage-row.d.ts +2 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +8 -5
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/types/modes/theme/theme.d.ts +12 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-events.d.ts +2 -0
- package/dist/types/session/agent-session-types.d.ts +36 -1
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/async-job-delivery.d.ts +8 -0
- package/dist/types/session/claude-session-store.d.ts +13 -0
- package/dist/types/session/codex-session-store.d.ts +14 -0
- package/dist/types/session/credential-pin.d.ts +58 -0
- package/dist/types/session/foreign-session-import.d.ts +15 -0
- package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
- package/dist/types/session/foreign-session-store.d.ts +23 -0
- package/dist/types/session/model-controls.d.ts +3 -3
- package/dist/types/session/session-advisors.d.ts +47 -2
- package/dist/types/session/session-entries.d.ts +19 -1
- package/dist/types/session/session-handoff.d.ts +4 -1
- package/dist/types/session/session-loader.d.ts +2 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-tools.d.ts +25 -1
- package/dist/types/session/turn-recovery.d.ts +10 -4
- package/dist/types/system-prompt.d.ts +2 -1
- package/dist/types/task/executor.d.ts +11 -2
- package/dist/types/task/isolation-ownership.d.ts +34 -0
- package/dist/types/task/render.d.ts +7 -0
- package/dist/types/thinking.d.ts +18 -6
- package/dist/types/tools/acp-bridge.d.ts +28 -5
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
- package/dist/types/tools/browser/tab-worker.d.ts +1 -4
- package/dist/types/tools/grep.d.ts +17 -1
- package/dist/types/tools/hub/launch.d.ts +5 -1
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/path-utils.d.ts +19 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +2 -1
- package/dist/types/tui/output-block.d.ts +5 -5
- package/dist/types/utils/changelog.d.ts +19 -0
- package/dist/types/utils/git.d.ts +10 -0
- package/dist/types/utils/jj.d.ts +11 -4
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +16 -12
- package/scripts/bundle-dist.ts +3 -1
- package/src/advisor/advise-tool.ts +0 -11
- package/src/advisor/runtime.ts +138 -24
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/async/job-manager.ts +30 -7
- package/src/auto-thinking/classifier.ts +48 -10
- package/src/cleanse/agent.ts +226 -0
- package/src/cleanse/balance.ts +79 -0
- package/src/cleanse/checkers.ts +996 -0
- package/src/cleanse/index.ts +190 -0
- package/src/cleanse/loop.ts +51 -0
- package/src/cleanse/parsers.ts +726 -0
- package/src/cleanse/progress.ts +50 -0
- package/src/cleanse/prompts/assignment.md +47 -0
- package/src/cleanse/types.ts +72 -0
- package/src/cli/args.ts +6 -0
- package/src/cli/flag-tables.ts +2 -0
- package/src/cli/gallery-fixtures/fs.ts +25 -0
- package/src/cli/session-picker.ts +34 -17
- package/src/cli/ttsr-cli.ts +19 -1
- package/src/cli/update-cli.ts +3 -0
- package/src/cli/usage-cli.ts +29 -4
- package/src/cli/worktree-cli.ts +28 -11
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +11 -1
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -0
- package/src/commands/cleanse.ts +45 -0
- package/src/commands/launch.ts +6 -0
- package/src/config/model-registry.ts +103 -25
- package/src/config/settings-schema.ts +65 -6
- package/src/config/settings.ts +25 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +463 -12
- package/src/discovery/claude-plugins.ts +144 -34
- package/src/edit/edit-clipboard.ts +23 -0
- package/src/edit/hashline/diff.ts +49 -10
- package/src/edit/hashline/execute.ts +38 -6
- package/src/edit/hashline/filesystem.ts +27 -3
- package/src/edit/index.ts +12 -2
- package/src/edit/renderer.ts +4 -4
- package/src/edit/streaming.ts +17 -2
- package/src/export/html/index.ts +17 -10
- package/src/export/html/template.js +1 -1
- package/src/extensibility/extensions/runner.ts +70 -0
- package/src/extensibility/extensions/types.ts +30 -0
- package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
- package/src/internal-urls/local-protocol.ts +3 -2
- package/src/internal-urls/mcp-protocol.ts +4 -1
- package/src/launch/broker.ts +14 -4
- package/src/launch/terminal-output-worker-client.ts +53 -0
- package/src/launch/terminal-output-worker-protocol.ts +11 -0
- package/src/launch/terminal-output-worker.ts +23 -0
- package/src/lsp/client.ts +18 -3
- package/src/lsp/index.ts +4 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +127 -56
- package/src/mcp/manager.ts +120 -18
- package/src/modes/acp/acp-agent.ts +27 -18
- package/src/modes/acp/acp-event-mapper.ts +38 -4
- package/src/modes/components/chat-transcript-builder.ts +30 -11
- package/src/modes/components/codex-reset-fireworks.ts +369 -0
- package/src/modes/components/read-tool-group.ts +159 -17
- package/src/modes/components/session-selector.ts +19 -3
- package/src/modes/components/settings-selector.ts +9 -1
- package/src/modes/components/status-line/component.ts +381 -54
- package/src/modes/components/tool-execution.ts +78 -31
- package/src/modes/components/usage-row.ts +10 -5
- package/src/modes/controllers/event-controller.ts +26 -4
- package/src/modes/controllers/mcp-command-controller.ts +68 -3
- package/src/modes/controllers/selector-controller.ts +94 -36
- package/src/modes/controllers/tan-command-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +67 -64
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- package/src/modes/rpc/rpc-client.ts +18 -0
- package/src/modes/rpc/rpc-mode.ts +16 -1
- package/src/modes/rpc/rpc-types.ts +11 -0
- package/src/modes/theme/theme.ts +151 -6
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +38 -21
- package/src/prompts/goals/guided-goal-interview.md +41 -6
- package/src/prompts/system/auto-thinking-difficulty.md +4 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/system/vibe-mode-active.md +4 -1
- package/src/prompts/system/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/grep.md +7 -6
- package/src/sdk.ts +178 -15
- package/src/session/agent-session-events.ts +1 -0
- package/src/session/agent-session-types.ts +40 -1
- package/src/session/agent-session.ts +330 -179
- package/src/session/async-job-delivery.ts +8 -0
- package/src/session/claude-session-store.ts +426 -0
- package/src/session/codex-session-store.ts +673 -0
- package/src/session/credential-pin.ts +93 -0
- package/src/session/foreign-session-import.ts +52 -0
- package/src/session/foreign-session-jsonl.ts +29 -0
- package/src/session/foreign-session-store.ts +26 -0
- package/src/session/model-controls.ts +15 -7
- package/src/session/session-advisors.ts +142 -23
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- package/src/session/session-history-format.ts +31 -6
- package/src/session/session-loader.ts +25 -9
- package/src/session/session-maintenance.ts +59 -11
- package/src/session/session-manager.ts +61 -0
- package/src/session/session-tools.ts +107 -5
- package/src/session/settings-stream-fn.ts +1 -0
- package/src/session/turn-recovery.ts +97 -51
- package/src/slash-commands/builtin-registry.ts +136 -5
- package/src/system-prompt.ts +3 -2
- package/src/task/executor.ts +17 -4
- package/src/task/isolation-ownership.ts +106 -0
- package/src/task/render.ts +20 -4
- package/src/task/worktree.ts +8 -0
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/ask.ts +3 -3
- package/src/tools/ast-edit.ts +9 -2
- package/src/tools/browser/cmux/cmux-tab.ts +9 -14
- package/src/tools/browser/tab-worker.ts +12 -35
- package/src/tools/browser.ts +7 -2
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/gh-renderer.ts +3 -3
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +38 -5
- package/src/tools/path-utils.ts +88 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/write.ts +44 -3
- package/src/tools/xdev.ts +39 -14
- package/src/tui/code-cell.ts +4 -4
- package/src/tui/output-block.ts +25 -8
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/utils/shell-snapshot-fn-env.sh +5 -2
- package/src/web/search/providers/codex.ts +34 -0
- package/src/web/search/render.ts +2 -2
- package/src/web/search/types.ts +1 -1
- package/dist/types/goals/guided-setup.d.ts +0 -30
- package/src/goals/guided-setup.ts +0 -171
- package/src/prompts/goals/guided-goal-system.md +0 -33
package/src/export/html/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as fs from "node:fs
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentState } from "@oh-my-pi/pi-agent-core";
|
|
4
4
|
import { APP_NAME, isEnoent } from "@oh-my-pi/pi-utils";
|
|
@@ -7,31 +7,38 @@ import type { SessionEntry, SessionHeader } from "../../session/session-entries"
|
|
|
7
7
|
import { loadEntriesFromFile } from "../../session/session-loader";
|
|
8
8
|
import { SessionManager } from "../../session/session-manager";
|
|
9
9
|
import type { ExportThemeNames } from "./args";
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
10
|
+
import templateCssPath from "./template.css" with { type: "file" };
|
|
11
|
+
import templateHtmlPath from "./template.html" with { type: "file" };
|
|
12
|
+
import templateJsPath from "./template.js" with { type: "file" };
|
|
13
13
|
// Pre-built React tool renderers: built by `gen:tool-views` (`bun run gen:tool-views`),
|
|
14
14
|
// run automatically by root `prepare` on install and by `prepack` at publish.
|
|
15
|
-
import
|
|
15
|
+
import toolViewsJsPath from "./tool-views.generated.js" with { type: "file" };
|
|
16
16
|
import { webExportThemeVars } from "./web-palette";
|
|
17
17
|
|
|
18
18
|
export { type ExportThemeNames, parseExportArgs } from "./args";
|
|
19
19
|
|
|
20
20
|
let cachedTemplate: string | undefined;
|
|
21
|
+
/** Resolve a Bun file-loader value without parsing Windows drive letters as URL schemes. */
|
|
22
|
+
export function resolveBundledHtmlAssetPath(assetPath: string, moduleDir: string = import.meta.dir): string {
|
|
23
|
+
if (path.isAbsolute(assetPath) || path.win32.isAbsolute(assetPath)) return assetPath;
|
|
24
|
+
return path.resolve(moduleDir, assetPath);
|
|
25
|
+
}
|
|
21
26
|
|
|
22
27
|
/** Compose the standalone export template: minified CSS, tool renderers, and viewer JS inlined. */
|
|
23
28
|
export function getTemplate(): string {
|
|
24
29
|
if (cachedTemplate) return cachedTemplate;
|
|
30
|
+
const templateCss = fs.readFileSync(resolveBundledHtmlAssetPath(templateCssPath), "utf8");
|
|
31
|
+
const templateHtml = fs.readFileSync(resolveBundledHtmlAssetPath(templateHtmlPath as unknown as string), "utf8");
|
|
32
|
+
const templateJs = fs.readFileSync(resolveBundledHtmlAssetPath(templateJsPath), "utf8");
|
|
33
|
+
const toolViewsJs = fs.readFileSync(resolveBundledHtmlAssetPath(toolViewsJsPath), "utf8");
|
|
25
34
|
const minifiedCss = templateCss
|
|
26
35
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
27
36
|
.replace(/\s+/g, " ")
|
|
28
37
|
.replace(/\s*([{}:;,])\s*/g, "$1")
|
|
29
38
|
.trim();
|
|
30
39
|
// Function replacements so `$'`, `$&`, `$$`, etc. inside the embedded
|
|
31
|
-
// CSS/JS are not interpreted as substitution patterns.
|
|
32
|
-
|
|
33
|
-
// every *.html import as HTMLBundle (TS can't vary types by import attribute).
|
|
34
|
-
cachedTemplate = (templateHtml as unknown as string)
|
|
40
|
+
// CSS/JS are not interpreted as substitution patterns.
|
|
41
|
+
cachedTemplate = templateHtml
|
|
35
42
|
.replace("<template-css/>", () => `<style>${minifiedCss}</style>`)
|
|
36
43
|
.replace("<template-tool-views/>", () => `<script>${toolViewsJs}</script>`)
|
|
37
44
|
.replace("<template-js/>", () => `<script>${templateJs}</script>`);
|
|
@@ -207,7 +214,7 @@ async function collectSubSessionsFromDir(
|
|
|
207
214
|
): Promise<void> {
|
|
208
215
|
let names: string[];
|
|
209
216
|
try {
|
|
210
|
-
names = await fs.readdir(dir);
|
|
217
|
+
names = await fs.promises.readdir(dir);
|
|
211
218
|
} catch (err) {
|
|
212
219
|
if (isEnoent(err)) return;
|
|
213
220
|
throw err;
|
|
@@ -395,7 +395,7 @@
|
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
// Apply filter mode
|
|
398
|
-
const isSettingsEntry = ['label', 'custom', 'model_change', 'thinking_level_change', 'mode_change', 'ttsr_injection', 'session_init'].includes(entry.type);
|
|
398
|
+
const isSettingsEntry = ['label', 'custom', 'model_change', 'thinking_level_change', 'mode_change', 'ttsr_injection', 'session_init', 'credential_pin'].includes(entry.type);
|
|
399
399
|
let passesFilter = true;
|
|
400
400
|
|
|
401
401
|
switch (filterMode) {
|
|
@@ -10,6 +10,7 @@ import type { Settings } from "../../config/settings";
|
|
|
10
10
|
import type { LocalProtocolOptions } from "../../internal-urls/local-protocol";
|
|
11
11
|
import type { MemoryRuntimeContext } from "../../memory-backend";
|
|
12
12
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
13
|
+
import type { AsyncJobSnapshot } from "../../session/agent-session";
|
|
13
14
|
import type { SessionManager } from "../../session/session-manager";
|
|
14
15
|
import type { BranchHandler, NavigateTreeHandler, NewSessionHandler } from "../session-handler-types";
|
|
15
16
|
import { ManagedTimers } from "./managed-timers";
|
|
@@ -40,6 +41,7 @@ import type {
|
|
|
40
41
|
ExtensionUIDialogOptions,
|
|
41
42
|
InputEvent,
|
|
42
43
|
InputEventResult,
|
|
44
|
+
McpNotificationEvent,
|
|
43
45
|
MessageRenderer,
|
|
44
46
|
RegisteredCommand,
|
|
45
47
|
RegisteredTool,
|
|
@@ -218,6 +220,14 @@ async function raceHandlerWithTimeout<T>(
|
|
|
218
220
|
|
|
219
221
|
const MAX_PENDING_CREDENTIAL_DISABLED = 32;
|
|
220
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Buffer cap for `mcp_notification` events received before {@link ExtensionRunner.initialize}
|
|
225
|
+
* has run. Sized to match the manager-side buffer in `MCPManager.NOTIFICATION_BUFFER_CAP` so
|
|
226
|
+
* the two layers can't drop different amounts of the same burst — the pipe drains, or it
|
|
227
|
+
* spills, but it does so consistently at both ends. Drop-oldest under pressure.
|
|
228
|
+
*/
|
|
229
|
+
const MAX_PENDING_MCP_NOTIFICATIONS = 100;
|
|
230
|
+
|
|
221
231
|
/**
|
|
222
232
|
* Events handled by the generic emit() method.
|
|
223
233
|
* Events with dedicated emitXxx() methods are excluded for stronger type safety.
|
|
@@ -327,6 +337,7 @@ export class ExtensionRunner {
|
|
|
327
337
|
#getContextUsageFn: () => ContextUsage | undefined = () => undefined;
|
|
328
338
|
#compactFn: (instructionsOrOptions?: string | CompactOptions) => Promise<void> = async () => {};
|
|
329
339
|
#getSystemPromptFn: () => string[] = () => [];
|
|
340
|
+
#getAsyncJobSnapshotFn: () => AsyncJobSnapshot | null = () => null;
|
|
330
341
|
#newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });
|
|
331
342
|
#branchHandler: BranchHandler = async () => ({ cancelled: false });
|
|
332
343
|
#navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
|
|
@@ -345,6 +356,19 @@ export class ExtensionRunner {
|
|
|
345
356
|
*/
|
|
346
357
|
#pendingCredentialDisabled: CredentialDisabledEvent[] = [];
|
|
347
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Buffer for `mcp_notification` events received via {@link emitMcpNotification} before
|
|
361
|
+
* {@link initialize} has run. Two-layer race: `MCPManager` also buffers frames until
|
|
362
|
+
* its first `addNotificationListener` subscriber attaches, but the sdk.ts bridge is
|
|
363
|
+
* registered inside `createAgentSession` — BEFORE the mode controller calls
|
|
364
|
+
* `ExtensionRunner.initialize()`. Without this second buffer, the manager's drain
|
|
365
|
+
* arrives at the bridge → the bridge calls `emitMcpNotification` → the runner drops
|
|
366
|
+
* the frame because `#initialized === false`, and the frame evaporates a second time.
|
|
367
|
+
* Bounded at {@link MAX_PENDING_MCP_NOTIFICATIONS}; oldest entries are dropped under
|
|
368
|
+
* pressure. Drained in {@link initialize} once the runtime/UI context is wired.
|
|
369
|
+
*/
|
|
370
|
+
#pendingMcpNotifications: Array<Omit<McpNotificationEvent, "type">> = [];
|
|
371
|
+
|
|
348
372
|
/**
|
|
349
373
|
* Timers scheduled by extensions through the sanctioned `ctx.setInterval` /
|
|
350
374
|
* `ctx.setTimeout` helpers. Callbacks run with the same isolation as handler
|
|
@@ -392,9 +416,11 @@ export class ExtensionRunner {
|
|
|
392
416
|
getMemory?: () => MemoryRuntimeContext | undefined,
|
|
393
417
|
private readonly settings?: Settings,
|
|
394
418
|
private readonly localProtocolOptions?: LocalProtocolOptions,
|
|
419
|
+
getAsyncJobSnapshot?: () => AsyncJobSnapshot | null,
|
|
395
420
|
) {
|
|
396
421
|
this.#uiContext = noOpUIContext;
|
|
397
422
|
this.#getMemoryFn = getMemory;
|
|
423
|
+
this.#getAsyncJobSnapshotFn = getAsyncJobSnapshot ?? (() => null);
|
|
398
424
|
}
|
|
399
425
|
|
|
400
426
|
initialize(
|
|
@@ -457,6 +483,23 @@ export class ExtensionRunner {
|
|
|
457
483
|
});
|
|
458
484
|
}
|
|
459
485
|
});
|
|
486
|
+
|
|
487
|
+
// Drain events buffered by emitMcpNotification() before initialize ran, using the
|
|
488
|
+
// same deferred-microtask ordering as the credential-disabled drain above so any
|
|
489
|
+
// onError listener registered synchronously after initialize() still catches
|
|
490
|
+
// handler errors during flush.
|
|
491
|
+
const pendingMcp = this.#pendingMcpNotifications.splice(0);
|
|
492
|
+
queueMicrotask(() => {
|
|
493
|
+
for (const event of pendingMcp) {
|
|
494
|
+
this.emit({ type: "mcp_notification", ...event }).catch((error: unknown) => {
|
|
495
|
+
logger.warn("mcp_notification handler threw during initialize flush", {
|
|
496
|
+
server: event.server,
|
|
497
|
+
method: event.method,
|
|
498
|
+
error: error instanceof Error ? error.message : String(error),
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
});
|
|
460
503
|
}
|
|
461
504
|
|
|
462
505
|
/**
|
|
@@ -484,6 +527,32 @@ export class ExtensionRunner {
|
|
|
484
527
|
await this.emit({ type: "credential_disabled", ...event });
|
|
485
528
|
}
|
|
486
529
|
|
|
530
|
+
/**
|
|
531
|
+
* Forward an MCP server notification to extension handlers.
|
|
532
|
+
*
|
|
533
|
+
* If {@link initialize} has not yet run, the notification is buffered and replayed
|
|
534
|
+
* once initialize wires the runtime/UI context. Matches the credential-disabled
|
|
535
|
+
* deferral above: the sdk.ts bridge registers `MCPManager.addNotificationListener`
|
|
536
|
+
* inside `createAgentSession` — BEFORE the mode controller calls `initialize()` on
|
|
537
|
+
* this runner — so notification frames drained by the manager (either fresh
|
|
538
|
+
* arrivals or replay from its own startup buffer) can reach us pre-init. Without
|
|
539
|
+
* this buffer they would evaporate for a second time here.
|
|
540
|
+
*
|
|
541
|
+
* Bounded at {@link MAX_PENDING_MCP_NOTIFICATIONS}; oldest entries drop under
|
|
542
|
+
* pressure. Never throws; per-handler errors are routed through {@link onError}
|
|
543
|
+
* via {@link emit}'s normal isolation.
|
|
544
|
+
*/
|
|
545
|
+
async emitMcpNotification(event: Omit<McpNotificationEvent, "type">): Promise<void> {
|
|
546
|
+
if (!this.#initialized) {
|
|
547
|
+
if (this.#pendingMcpNotifications.length >= MAX_PENDING_MCP_NOTIFICATIONS) {
|
|
548
|
+
this.#pendingMcpNotifications.shift();
|
|
549
|
+
}
|
|
550
|
+
this.#pendingMcpNotifications.push(event);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
await this.emit({ type: "mcp_notification", ...event });
|
|
554
|
+
}
|
|
555
|
+
|
|
487
556
|
/** Emits a session stop pass that can be cancelled with the active settle signal. */
|
|
488
557
|
async emitSessionStop(event: Omit<SessionStopEvent, "type">): Promise<SessionStopEventResult | undefined> {
|
|
489
558
|
if (event.signal.aborted) return undefined;
|
|
@@ -666,6 +735,7 @@ export class ExtensionRunner {
|
|
|
666
735
|
ui: this.#uiContext,
|
|
667
736
|
getContextUsage: () => this.#getContextUsageFn(),
|
|
668
737
|
compact: instructionsOrOptions => this.#compactFn(instructionsOrOptions),
|
|
738
|
+
getAsyncJobSnapshot: () => this.#getAsyncJobSnapshotFn(),
|
|
669
739
|
hasUI: this.hasUI(),
|
|
670
740
|
cwd: this.cwd,
|
|
671
741
|
sessionManager: this.sessionManager,
|
|
@@ -49,6 +49,7 @@ import type { LocalProtocolOptions } from "../../internal-urls/local-protocol";
|
|
|
49
49
|
import type { MemoryRuntimeContext } from "../../memory-backend";
|
|
50
50
|
import type { CustomEditor } from "../../modes/components/custom-editor";
|
|
51
51
|
import type { Theme } from "../../modes/theme/theme";
|
|
52
|
+
import type { AsyncJobSnapshot } from "../../session/agent-session";
|
|
52
53
|
import type { CompactMode } from "../../session/compact-modes";
|
|
53
54
|
import type { CustomMessage, CustomMessagePayload } from "../../session/messages";
|
|
54
55
|
import type { ReadonlySessionManager, SessionManager } from "../../session/session-manager";
|
|
@@ -415,6 +416,8 @@ export interface ExtensionContext {
|
|
|
415
416
|
ui: ExtensionUIContext;
|
|
416
417
|
/** Get current context usage for the active model. */
|
|
417
418
|
getContextUsage(): ContextUsage | undefined;
|
|
419
|
+
/** Get a read-only snapshot of async jobs owned by this session. */
|
|
420
|
+
getAsyncJobSnapshot(): AsyncJobSnapshot | null;
|
|
418
421
|
/** Compact the session context (interactive mode shows UI). */
|
|
419
422
|
compact(instructionsOrOptions?: string | CompactOptions): Promise<void>;
|
|
420
423
|
/** Whether UI is available (false in print/RPC mode) */
|
|
@@ -713,6 +716,31 @@ export interface CredentialDisabledEvent {
|
|
|
713
716
|
disabledCause: string;
|
|
714
717
|
}
|
|
715
718
|
|
|
719
|
+
// ============================================================================
|
|
720
|
+
// MCP Events
|
|
721
|
+
// ============================================================================
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Fired for every JSON-RPC notification received from a connected MCP server,
|
|
725
|
+
* AFTER the runtime's own handling of known list/update methods. Unknown or
|
|
726
|
+
* server-custom methods are delivered too — extensions can bridge them into
|
|
727
|
+
* session behavior by inspecting `method`/`params` and injecting a follow-up
|
|
728
|
+
* via `pi.sendMessage(..., { deliverAs })` or `pi.sendUserMessage(...)`.
|
|
729
|
+
*/
|
|
730
|
+
export interface McpNotificationEvent {
|
|
731
|
+
type: "mcp_notification";
|
|
732
|
+
/**
|
|
733
|
+
* Server name as declared in the MCP config (raw, unsanitized). Note this
|
|
734
|
+
* differs from the sanitized prefix used in `mcp__<sanitized_server>_<tool>`
|
|
735
|
+
* tool names — filter by this raw name, not by tool-name prefix matching.
|
|
736
|
+
*/
|
|
737
|
+
server: string;
|
|
738
|
+
/** JSON-RPC method (e.g. `notifications/tools/list_changed`, or server-custom). */
|
|
739
|
+
method: string;
|
|
740
|
+
/** JSON-RPC params, opaque to the runtime. */
|
|
741
|
+
params: unknown;
|
|
742
|
+
}
|
|
743
|
+
|
|
716
744
|
// ============================================================================
|
|
717
745
|
// User Bash Events
|
|
718
746
|
// ============================================================================
|
|
@@ -941,6 +969,7 @@ export type ExtensionEvent =
|
|
|
941
969
|
| TodoReminderEvent
|
|
942
970
|
| GoalUpdatedEvent
|
|
943
971
|
| CredentialDisabledEvent
|
|
972
|
+
| McpNotificationEvent
|
|
944
973
|
| UserBashEvent
|
|
945
974
|
| UserPythonEvent
|
|
946
975
|
| InputEvent
|
|
@@ -1133,6 +1162,7 @@ export interface ExtensionAPI {
|
|
|
1133
1162
|
on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): void;
|
|
1134
1163
|
on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): void;
|
|
1135
1164
|
on(event: "user_python", handler: ExtensionHandler<UserPythonEvent, UserPythonEventResult>): void;
|
|
1165
|
+
on(event: "mcp_notification", handler: ExtensionHandler<McpNotificationEvent>): void;
|
|
1136
1166
|
|
|
1137
1167
|
// =========================================================================
|
|
1138
1168
|
// Tool Registration
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* `types.ts` via the `export *` below — pi-ai still exports both as types,
|
|
20
20
|
* only the runtime `Type` builder and `StringEnum()` helper were removed.
|
|
21
21
|
*/
|
|
22
|
-
import type { Api, Model } from "@oh-my-pi/pi-ai";
|
|
22
|
+
import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
|
|
23
23
|
import type { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
24
24
|
import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
25
25
|
import {
|
|
@@ -80,6 +80,33 @@ export function clampThinkingLevel<TApi extends Api>(model: Model<TApi>, level:
|
|
|
80
80
|
return clampThinkingLevelForModel(model, level) ?? "off";
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Provider-error classification patterns ported verbatim from historical pi-ai
|
|
85
|
+
* (`@earendil-works/pi-ai` `utils/retry.ts`). Legacy extensions call
|
|
86
|
+
* {@link isRetryableAssistantError} to decide whether to restart a failed
|
|
87
|
+
* assistant turn, so the wording tables must match the upstream semantics they
|
|
88
|
+
* were authored against rather than OMP's own `Error`-based classifiers.
|
|
89
|
+
*/
|
|
90
|
+
const NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN =
|
|
91
|
+
/GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i;
|
|
92
|
+
const RETRYABLE_PROVIDER_ERROR_PATTERN =
|
|
93
|
+
/overloaded|rate.?limit|too many requests|429|500|502|503|504|524|service.?unavailable|server.?error|internal.?error|provider.?returned.?error|network.?error|connection.?error|connection.?refused|connection.?lost|other side closed|fetch failed|getaddrinfo|ENOTFOUND|EAI_AGAIN|upstream.?connect|reset before headers|socket hang up|socket connection was closed|timed? out|timeout|terminated|websocket.?closed|websocket.?error|ended without|stream ended before message_stop|stream ended before a terminal response event|http2 request did not get a response|retry delay|you can retry your request|try your request again|please retry your request|ResourceExhausted/i;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Compatibility implementation of historical pi-ai's `isRetryableAssistantError`.
|
|
97
|
+
*
|
|
98
|
+
* Classifies whether a failed assistant message looks like a transient provider
|
|
99
|
+
* or transport error so legacy extensions can decide if the last assistant turn
|
|
100
|
+
* should be restarted. Account/quota limits are treated as non-retryable. This
|
|
101
|
+
* does not implement any retry policy; callers own budget, backoff, and reporting.
|
|
102
|
+
*/
|
|
103
|
+
export function isRetryableAssistantError(message: AssistantMessage): boolean {
|
|
104
|
+
if (message.stopReason !== "error" || !message.errorMessage) return false;
|
|
105
|
+
const errorMessage = message.errorMessage;
|
|
106
|
+
if (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false;
|
|
107
|
+
return RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);
|
|
108
|
+
}
|
|
109
|
+
|
|
83
110
|
export * from "@oh-my-pi/pi-ai";
|
|
84
111
|
/**
|
|
85
112
|
* Compatibility re-exports for catalog symbols that pi-ai historically exposed
|
|
@@ -93,3 +120,21 @@ export * from "@oh-my-pi/pi-ai";
|
|
|
93
120
|
export { calculateCost, getBundledModel, getBundledModels, getBundledProviders, modelsAreEqual, Type };
|
|
94
121
|
export const getModel = getBundledModel;
|
|
95
122
|
export const getModels = getBundledModels;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Compatibility re-exports for runtime helpers that upstream
|
|
126
|
+
* `@earendil-works/pi-ai` exposed from its package root but omp's
|
|
127
|
+
* `@oh-my-pi/pi-ai` barrel no longer forwards. Each symbol still exists in the
|
|
128
|
+
* host graph — only its root re-export was dropped — so bridging it here keeps
|
|
129
|
+
* legacy extensions importing it from the pi-ai root resolving through Bun's
|
|
130
|
+
* static named-export check (e.g. `omp plugin install pi-blackhole`).
|
|
131
|
+
*
|
|
132
|
+
* This is the full set derived from an audit of the upstream root surface: the
|
|
133
|
+
* error-classification predicate `isContextOverflow` (now under
|
|
134
|
+
* `@oh-my-pi/pi-ai/error`) and the JSON-repair helpers that omp relocated to
|
|
135
|
+
* `@oh-my-pi/pi-utils`. Upstream root symbols with no omp equivalent are
|
|
136
|
+
* intentionally not shimmed — the package has diverged and there is nothing to
|
|
137
|
+
* forward.
|
|
138
|
+
*/
|
|
139
|
+
export { isContextOverflow } from "@oh-my-pi/pi-ai/error";
|
|
140
|
+
export { parseJsonWithRepair, parseStreamingJson, repairJson } from "@oh-my-pi/pi-utils";
|
|
@@ -17,6 +17,7 @@ import * as fs from "node:fs";
|
|
|
17
17
|
import * as path from "node:path";
|
|
18
18
|
import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
19
19
|
import { type AuthCredential, SqliteAuthCredentialStore, type TSchema } from "@oh-my-pi/pi-ai";
|
|
20
|
+
import { piEscapeRegexLiteral, piJoinPath } from "@oh-my-pi/pi-ai/providers/cursor-pi-args";
|
|
20
21
|
import { getKeybindings, type Keybinding, Text } from "@oh-my-pi/pi-tui";
|
|
21
22
|
import {
|
|
22
23
|
getAgentDbPath,
|
|
@@ -298,16 +299,6 @@ function lineRangePath(readPath: string, offset: number | undefined, limit: numb
|
|
|
298
299
|
return `${readPath}:${start}-${end}`;
|
|
299
300
|
}
|
|
300
301
|
|
|
301
|
-
function escapeRegexLiteral(value: string): string {
|
|
302
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function joinLegacyGlob(searchPath: string, pattern: string): string {
|
|
306
|
-
if (path.isAbsolute(pattern)) return pattern;
|
|
307
|
-
if (!searchPath || searchPath === ".") return pattern;
|
|
308
|
-
return path.join(searchPath, pattern);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
302
|
function normalizeLegacyLimit(limit: number | undefined, fallback: number): number {
|
|
312
303
|
if (limit === undefined || !Number.isFinite(limit)) return fallback;
|
|
313
304
|
return Math.max(1, Math.floor(limit));
|
|
@@ -512,7 +503,7 @@ export function createGrepToolDefinition(cwd: string, options?: GrepToolOptions)
|
|
|
512
503
|
renderResult: legacyRenderResult,
|
|
513
504
|
execute: (toolCallId, params, signal, onUpdate) => {
|
|
514
505
|
const rawPattern = stringField(params, "pattern") ?? "";
|
|
515
|
-
const pattern = booleanField(params, "literal") ?
|
|
506
|
+
const pattern = booleanField(params, "literal") ? piEscapeRegexLiteral(rawPattern) : rawPattern;
|
|
516
507
|
const searchPath = stringField(params, "path") ?? ".";
|
|
517
508
|
const glob = stringField(params, "glob");
|
|
518
509
|
const context = numberField(params, "context");
|
|
@@ -529,7 +520,7 @@ export function createGrepToolDefinition(cwd: string, options?: GrepToolOptions)
|
|
|
529
520
|
toolCallId,
|
|
530
521
|
{
|
|
531
522
|
pattern,
|
|
532
|
-
path: glob ?
|
|
523
|
+
path: glob ? piJoinPath(searchPath, glob) : searchPath,
|
|
533
524
|
case: booleanField(params, "ignoreCase") ? false : undefined,
|
|
534
525
|
},
|
|
535
526
|
signal,
|
|
@@ -587,7 +578,7 @@ export function createFindToolDefinition(cwd: string, options?: FindToolOptions)
|
|
|
587
578
|
}
|
|
588
579
|
return tool.execute(
|
|
589
580
|
toolCallId,
|
|
590
|
-
{ path:
|
|
581
|
+
{ path: piJoinPath(searchPath, pattern), hidden: true, gitignore: true, limit },
|
|
591
582
|
signal,
|
|
592
583
|
onUpdate,
|
|
593
584
|
);
|
|
@@ -1378,6 +1369,14 @@ export function getPackageDir(): string {
|
|
|
1378
1369
|
// check during validation (issue #6583).
|
|
1379
1370
|
export { estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
|
|
1380
1371
|
|
|
1372
|
+
// Same barrel gap for two more legacy package-root exports: pi re-exported the
|
|
1373
|
+
// `CONFIG_DIR_NAME` constant and the CLI parser `parseArgs`. In omp
|
|
1374
|
+
// `CONFIG_DIR_NAME` lives in `@oh-my-pi/pi-utils` and `parseArgs` in
|
|
1375
|
+
// `../cli/args`, neither of which the barrel below forwards, so legacy
|
|
1376
|
+
// extensions importing either fail Bun's static export check during validation.
|
|
1377
|
+
export { CONFIG_DIR_NAME } from "@oh-my-pi/pi-utils";
|
|
1378
|
+
export { parseArgs } from "../cli/args";
|
|
1379
|
+
|
|
1381
1380
|
export * from "../index";
|
|
1382
1381
|
export { formatBytes as formatSize } from "../tools/render-utils";
|
|
1383
1382
|
export { copyToClipboard } from "../utils/clipboard";
|
|
@@ -390,8 +390,9 @@ export class LocalProtocolHandler implements ProtocolHandler {
|
|
|
390
390
|
|
|
391
391
|
/**
|
|
392
392
|
* Install a process-global override that wins over the AgentRegistry-based
|
|
393
|
-
* derivation. Used by SDK consumers that wire
|
|
394
|
-
* `
|
|
393
|
+
* derivation. Used by top-level SDK consumers that wire
|
|
394
|
+
* `localProtocolOptions` on `createAgentSession`; subagents keep their
|
|
395
|
+
* inherited mapping session-bound.
|
|
395
396
|
*/
|
|
396
397
|
static setOverride(value: LocalProtocolOptions | undefined): void {
|
|
397
398
|
LocalProtocolHandler.#override = value;
|
|
@@ -97,7 +97,10 @@ function formatAvailableResources(mcpManager: MCPManager): string {
|
|
|
97
97
|
.getConnectedServers()
|
|
98
98
|
.flatMap(name => {
|
|
99
99
|
const serverResources = mcpManager.getServerResources(name);
|
|
100
|
-
|
|
100
|
+
if (!serverResources) return [];
|
|
101
|
+
const concrete = serverResources.resources.map(r => ` ${r.uri} (${name})`);
|
|
102
|
+
const templates = serverResources.templates.map(t => ` ${t.uriTemplate} (${name}, template)`);
|
|
103
|
+
return [...concrete, ...templates];
|
|
101
104
|
})
|
|
102
105
|
.join("\n");
|
|
103
106
|
return available || " (none)";
|
package/src/launch/broker.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as net from "node:net";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { Process, type PtyRunResult, PtySession } from "@oh-my-pi/pi-natives";
|
|
6
|
-
import { isEexist, isEnoent, logger, postmortem, procmgr, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { isEexist, isEnoent, logger, postmortem, procmgr, sanitizeText, setProcessName } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { hostHasInheritableConsole } from "../eval/py/spawn-options";
|
|
8
8
|
import { truncateHead, truncateHeadBytes, truncateTail, truncateTailBytes } from "../session/streaming-output";
|
|
9
9
|
import { workerEnvFromParent } from "../subprocess/worker-client";
|
|
@@ -103,6 +103,10 @@ function terminalState(state: DaemonSnapshot["state"]): boolean {
|
|
|
103
103
|
return state === "exited" || state === "failed";
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
function settledState(state: DaemonSnapshot["state"]): boolean {
|
|
107
|
+
return terminalState(state) || state === "restarting";
|
|
108
|
+
}
|
|
109
|
+
|
|
106
110
|
/**
|
|
107
111
|
* Order daemons for the `list` response: non-terminal (active) daemons first,
|
|
108
112
|
* oldest to newest, so the process the user is acting on is immediately visible
|
|
@@ -754,7 +758,7 @@ class DaemonBroker {
|
|
|
754
758
|
}
|
|
755
759
|
|
|
756
760
|
async #refreshDetached(record: ManagedDaemon): Promise<void> {
|
|
757
|
-
if (!record.spec.detached ||
|
|
761
|
+
if (!record.spec.detached || settledState(record.snapshot.state)) return;
|
|
758
762
|
const generation = record.generation;
|
|
759
763
|
await this.#readDetachedOutput(record, generation);
|
|
760
764
|
if (generation !== record.generation || record.process) return;
|
|
@@ -791,8 +795,14 @@ class DaemonBroker {
|
|
|
791
795
|
}
|
|
792
796
|
|
|
793
797
|
async #settle(record: ManagedDaemon, generation: number, exitCode?: number, error?: string): Promise<void> {
|
|
794
|
-
|
|
798
|
+
// `restarting` is a settled state (child exited, relaunch timer armed). Any op that
|
|
799
|
+
// runs #refreshDetached on such a record must not re-settle it: re-entry double-counts
|
|
800
|
+
// restartCount and overwrites record.restartTimer, orphaning the armed timer so it fires
|
|
801
|
+
// after stop() and resurrects the daemon (issue #6852).
|
|
802
|
+
if (generation !== record.generation || settledState(record.snapshot.state)) return;
|
|
795
803
|
await this.#readDetachedOutput(record, generation);
|
|
804
|
+
// The output read yields, so a concurrent refresh may settle this generation first.
|
|
805
|
+
if (generation !== record.generation || settledState(record.snapshot.state)) return;
|
|
796
806
|
record.process = undefined;
|
|
797
807
|
record.input = undefined;
|
|
798
808
|
record.pty = undefined;
|
|
@@ -1110,7 +1120,7 @@ export async function startDaemonBrokerFromEnvironment(): Promise<void> {
|
|
|
1110
1120
|
await fs.mkdir(runtimeDir, { recursive: true, mode: 0o700 });
|
|
1111
1121
|
const lease = await acquireBrokerLease(runtimeDir);
|
|
1112
1122
|
if (!lease) return;
|
|
1113
|
-
|
|
1123
|
+
setProcessName("omp daemon broker");
|
|
1114
1124
|
const token = (await Bun.file(path.join(runtimeDir, TOKEN_FILE)).text()).trim();
|
|
1115
1125
|
if (!token) throw new Error("Daemon broker token is empty");
|
|
1116
1126
|
const broker = new DaemonBroker(projectDir, runtimeDir, token, idleGraceMs);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { workerHostEntry } from "@oh-my-pi/pi-utils/worker-host";
|
|
2
|
+
import type { TerminalOutputOptions } from "./terminal-output";
|
|
3
|
+
import {
|
|
4
|
+
TERMINAL_OUTPUT_WORKER_ARG,
|
|
5
|
+
type TerminalOutputWorkerRequest,
|
|
6
|
+
type TerminalOutputWorkerResult,
|
|
7
|
+
} from "./terminal-output-worker-protocol";
|
|
8
|
+
|
|
9
|
+
/** Replay legacy broker PTY bytes without evaluating xterm in the client process. */
|
|
10
|
+
export async function renderTerminalOutputIsolated(
|
|
11
|
+
output: string,
|
|
12
|
+
options: TerminalOutputOptions,
|
|
13
|
+
): Promise<string[] | undefined> {
|
|
14
|
+
const hostEntry = workerHostEntry();
|
|
15
|
+
const worker = hostEntry
|
|
16
|
+
? new Worker(hostEntry, { type: "module", argv: [TERMINAL_OUTPUT_WORKER_ARG] })
|
|
17
|
+
: new Worker(new URL("./terminal-output-worker.ts", import.meta.url).href, { type: "module" });
|
|
18
|
+
const pending = Promise.withResolvers<string[] | undefined>();
|
|
19
|
+
const onMessage = (event: MessageEvent<TerminalOutputWorkerResult>): void => {
|
|
20
|
+
if (event.data.ok) pending.resolve(event.data.rows);
|
|
21
|
+
else pending.reject(new Error(event.data.error));
|
|
22
|
+
};
|
|
23
|
+
const onError = (event: ErrorEvent): void => {
|
|
24
|
+
pending.reject(event.error instanceof Error ? event.error : new Error(event.message));
|
|
25
|
+
};
|
|
26
|
+
const onClose = (): void => {
|
|
27
|
+
pending.reject(new Error("Terminal output worker exited before responding"));
|
|
28
|
+
};
|
|
29
|
+
worker.addEventListener("message", onMessage);
|
|
30
|
+
worker.addEventListener("error", onError);
|
|
31
|
+
worker.addEventListener("close", onClose);
|
|
32
|
+
try {
|
|
33
|
+
const request: TerminalOutputWorkerRequest = { output, options };
|
|
34
|
+
worker.postMessage(request);
|
|
35
|
+
return await pending.promise;
|
|
36
|
+
} finally {
|
|
37
|
+
worker.removeEventListener("message", onMessage);
|
|
38
|
+
worker.removeEventListener("error", onError);
|
|
39
|
+
worker.removeEventListener("close", onClose);
|
|
40
|
+
worker.terminate();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Distribution smoke for source, npm-bundle, and compiled worker routing. */
|
|
45
|
+
export async function smokeTestTerminalOutputWorker(): Promise<void> {
|
|
46
|
+
const rows = await renderTerminalOutputIsolated("old\r\x1b[2K\x1b[1;32mready\x1b[0m", {
|
|
47
|
+
head: false,
|
|
48
|
+
maxRows: 10,
|
|
49
|
+
});
|
|
50
|
+
if (rows?.length !== 1 || rows[0] !== "\x1b[0m\x1b[1;38;5;2mready") {
|
|
51
|
+
throw new Error("terminal output worker smoke mismatch");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TerminalOutputOptions } from "./terminal-output";
|
|
2
|
+
|
|
3
|
+
/** Hidden CLI selector for legacy PTY replay outside the client process. */
|
|
4
|
+
export const TERMINAL_OUTPUT_WORKER_ARG = "__omp_worker_terminal_output";
|
|
5
|
+
|
|
6
|
+
export interface TerminalOutputWorkerRequest {
|
|
7
|
+
output: string;
|
|
8
|
+
options: TerminalOutputOptions;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type TerminalOutputWorkerResult = { ok: true; rows: string[] | undefined } | { ok: false; error: string };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { parentPort } from "node:worker_threads";
|
|
2
|
+
import { consumeWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
|
|
3
|
+
import { renderTerminalOutput } from "./terminal-output";
|
|
4
|
+
import type { TerminalOutputWorkerRequest, TerminalOutputWorkerResult } from "./terminal-output-worker-protocol";
|
|
5
|
+
|
|
6
|
+
if (!parentPort) throw new Error("terminal-output-worker: missing parentPort");
|
|
7
|
+
|
|
8
|
+
const port = parentPort;
|
|
9
|
+
const inbox = consumeWorkerInbox();
|
|
10
|
+
const handle = async (message: unknown): Promise<void> => {
|
|
11
|
+
const request = message as TerminalOutputWorkerRequest;
|
|
12
|
+
let result: TerminalOutputWorkerResult;
|
|
13
|
+
try {
|
|
14
|
+
result = { ok: true, rows: await renderTerminalOutput(request.output, request.options) };
|
|
15
|
+
} catch (error) {
|
|
16
|
+
result = { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
17
|
+
}
|
|
18
|
+
port.postMessage(result);
|
|
19
|
+
port.close();
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (inbox) inbox.bind(message => void handle(message));
|
|
23
|
+
else port.on("message", message => void handle(message));
|
package/src/lsp/client.ts
CHANGED
|
@@ -27,6 +27,7 @@ const fileOperationLocks = new Map<string, Promise<void>>();
|
|
|
27
27
|
/** Negative cache of recent init failures so a broken server fails fast instead of re-spawning per call. */
|
|
28
28
|
const INIT_FAILURE_BACKOFF_MS = 3 * 60 * 1000;
|
|
29
29
|
const initFailures = new Map<string, { at: number; message: string }>();
|
|
30
|
+
const READER_EXIT_GRACE_MS = 100;
|
|
30
31
|
|
|
31
32
|
// Idle timeout configuration (disabled by default)
|
|
32
33
|
let idleTimeoutMs: number | null = null;
|
|
@@ -303,6 +304,7 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
303
304
|
|
|
304
305
|
const framer = new MessageFramer(Buffer.from(client.messageBuffer));
|
|
305
306
|
|
|
307
|
+
let readerFailed = false;
|
|
306
308
|
try {
|
|
307
309
|
while (true) {
|
|
308
310
|
const { done, value } = await reader.read();
|
|
@@ -391,6 +393,7 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
391
393
|
}
|
|
392
394
|
}
|
|
393
395
|
} catch (err) {
|
|
396
|
+
readerFailed = true;
|
|
394
397
|
// Connection closed or error - reject all pending requests
|
|
395
398
|
for (const pending of Array.from(client.pendingRequests.values())) {
|
|
396
399
|
pending.reject(new Error(`LSP connection closed: ${err}`));
|
|
@@ -401,6 +404,9 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
401
404
|
client.messageBuffer = framer.remainder();
|
|
402
405
|
reader.releaseLock();
|
|
403
406
|
client.isReading = false;
|
|
407
|
+
if (!readerFailed && client.proc.exitCode === null) {
|
|
408
|
+
await waitForExit(client, READER_EXIT_GRACE_MS);
|
|
409
|
+
}
|
|
404
410
|
// Reader exited while the server process is still alive (unrecoverable
|
|
405
411
|
// read error or bad stream state): nothing will route responses anymore,
|
|
406
412
|
// so tear the client down — the next call respawns instead of timing out.
|
|
@@ -676,6 +682,15 @@ const PROJECT_LOAD_TIMEOUT_MS = 15_000;
|
|
|
676
682
|
const SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
677
683
|
const EXIT_TIMEOUT_MS = 1_000;
|
|
678
684
|
|
|
685
|
+
function clientKey(config: ServerConfig, cwd: string): string {
|
|
686
|
+
return `${config.command}:${cwd}`;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** Allow an explicit user reload to retry a matching initialization failure immediately. */
|
|
690
|
+
export function clearInitializationFailure(config: ServerConfig, cwd: string): void {
|
|
691
|
+
initFailures.delete(clientKey(config, cwd));
|
|
692
|
+
}
|
|
693
|
+
|
|
679
694
|
/**
|
|
680
695
|
* Get or create an LSP client for the given server configuration and working directory.
|
|
681
696
|
* @param config - Server configuration
|
|
@@ -692,7 +707,7 @@ export async function getOrCreateClient(
|
|
|
692
707
|
initTimeoutMs?: number,
|
|
693
708
|
signal?: AbortSignal,
|
|
694
709
|
): Promise<LspClient> {
|
|
695
|
-
const key =
|
|
710
|
+
const key = clientKey(config, cwd);
|
|
696
711
|
|
|
697
712
|
// Check if client already exists
|
|
698
713
|
const existingClient = clients.get(key);
|
|
@@ -865,13 +880,13 @@ export async function getActiveOrPendingClient(
|
|
|
865
880
|
signal?: AbortSignal,
|
|
866
881
|
): Promise<LspClient | undefined> {
|
|
867
882
|
throwIfAborted(signal);
|
|
868
|
-
const client = clients.get(
|
|
883
|
+
const client = clients.get(clientKey(config, cwd));
|
|
869
884
|
if (client) {
|
|
870
885
|
client.lastActivity = Date.now();
|
|
871
886
|
return client;
|
|
872
887
|
}
|
|
873
888
|
|
|
874
|
-
const pending = clientLocks.get(
|
|
889
|
+
const pending = clientLocks.get(clientKey(config, cwd));
|
|
875
890
|
if (!pending) return undefined;
|
|
876
891
|
try {
|
|
877
892
|
return await untilAborted(signal, pending);
|