@oh-my-pi/pi-coding-agent 16.0.2 → 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 +45 -0
- package/README.md +0 -1
- package/dist/cli.js +217 -276
- 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 +8 -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/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/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/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -50
- 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.ts +2 -2
- package/src/commands/install.ts +3 -3
- package/src/config/model-resolver.ts +28 -11
- package/src/config/settings-schema.ts +0 -11
- 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/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/goals/runtime.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +4 -6
- 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 +246 -78
- package/src/session/artifacts.ts +19 -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/task/executor.ts +18 -2
- 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/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/main.ts
CHANGED
|
@@ -55,6 +55,7 @@ import type { PrintModeOptions } from "./modes/print-mode";
|
|
|
55
55
|
import { CURRENT_SETUP_VERSION } from "./modes/setup-version";
|
|
56
56
|
import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
|
|
57
57
|
import type { SubmittedUserInput } from "./modes/types";
|
|
58
|
+
import { AgentLifecycleManager } from "./registry/agent-lifecycle";
|
|
58
59
|
import {
|
|
59
60
|
type CreateAgentSessionOptions,
|
|
60
61
|
type CreateAgentSessionResult,
|
|
@@ -68,6 +69,7 @@ import { resolveResumableSession, type SessionInfo } from "./session/session-lis
|
|
|
68
69
|
import { SessionManager } from "./session/session-manager";
|
|
69
70
|
import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
|
|
70
71
|
import { discoverTitleSystemPromptFile, resolvePromptInput } from "./system-prompt";
|
|
72
|
+
import { createPersistedSubagentReviverFactory } from "./task/persisted-revive";
|
|
71
73
|
import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
|
|
72
74
|
import { AUTO_THINKING } from "./thinking";
|
|
73
75
|
import type { LspStartupServerInfo } from "./tools";
|
|
@@ -1262,6 +1264,24 @@ export async function runRootCommand(
|
|
|
1262
1264
|
eventBus,
|
|
1263
1265
|
preloadedExtensions: extensionsResult,
|
|
1264
1266
|
});
|
|
1267
|
+
|
|
1268
|
+
// Cold-revive support: a `parked` subagent ref restored from disk (Agent Hub
|
|
1269
|
+
// scan, collab mirror, resumed process) has a sessionFile but no in-memory
|
|
1270
|
+
// reviver, so `ensureLive` (IRC sends, hub focus) would refuse it. Install a
|
|
1271
|
+
// factory — bound to THIS top-level session — that rebuilds the subagent from
|
|
1272
|
+
// its persisted JSONL (see persisted-revive.ts). Scoped to the non-ACP
|
|
1273
|
+
// bootstrap: ACP keeps several concurrent top-level sessions and a single
|
|
1274
|
+
// process-global factory must not be clobbered by the most recent one.
|
|
1275
|
+
AgentLifecycleManager.global().setPersistedSubagentReviverFactory(
|
|
1276
|
+
createPersistedSubagentReviverFactory({
|
|
1277
|
+
session,
|
|
1278
|
+
authStorage,
|
|
1279
|
+
modelRegistry,
|
|
1280
|
+
settings: settingsInstance,
|
|
1281
|
+
enableLsp: sessionOptions.enableLsp ?? true,
|
|
1282
|
+
}),
|
|
1283
|
+
Math.trunc(Number(settingsInstance.get("task.agentIdleTtlMs") ?? 420_000) || 0),
|
|
1284
|
+
);
|
|
1265
1285
|
if (parsedArgs.apiKey && !sessionOptions.model && session.model) {
|
|
1266
1286
|
authStorage.setRuntimeApiKey(session.model.provider, parsedArgs.apiKey);
|
|
1267
1287
|
}
|
package/src/mcp/render.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
JSON_TREE_SCALAR_LEN_EXPANDED,
|
|
19
19
|
renderJsonTreeLines,
|
|
20
20
|
} from "../tools/json-tree";
|
|
21
|
+
import { formatStyledTruncationWarning, stripOutputNotice } from "../tools/output-meta";
|
|
21
22
|
import { formatExpandHint, truncateToWidth } from "../tools/render-utils";
|
|
22
23
|
import { renderStatusLine } from "../tui";
|
|
23
24
|
import type { MCPToolDetails } from "./tool-bridge";
|
|
@@ -78,7 +79,14 @@ export function renderMCPResult(
|
|
|
78
79
|
|
|
79
80
|
// Output section
|
|
80
81
|
const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
81
|
-
|
|
82
|
+
// Strip the LLM-facing spill notice before parsing/rendering: a spilled
|
|
83
|
+
// result appends `[Showing… artifact://N]` to the body, which would break
|
|
84
|
+
// JSON detection and bury the recovery link. Surface it as a styled warning
|
|
85
|
+
// instead, mirroring the built-in read/bash/ssh/browser renderers.
|
|
86
|
+
const trimmedOutput = stripOutputNotice(textContent, result.details?.meta).trimEnd();
|
|
87
|
+
const truncationWarning = result.details?.meta?.truncation
|
|
88
|
+
? formatStyledTruncationWarning(result.details.meta, theme)
|
|
89
|
+
: null;
|
|
82
90
|
|
|
83
91
|
if (!trimmedOutput) {
|
|
84
92
|
lines.push(theme.fg("dim", "(no output)"));
|
|
@@ -104,6 +112,7 @@ export function renderMCPResult(
|
|
|
104
112
|
} else if (tree.truncated) {
|
|
105
113
|
lines.push(theme.fg("dim", "…"));
|
|
106
114
|
}
|
|
115
|
+
if (truncationWarning) lines.push(truncationWarning);
|
|
107
116
|
return new Text(lines.join("\n"), 0, 0);
|
|
108
117
|
}
|
|
109
118
|
} catch {
|
|
@@ -128,5 +137,6 @@ export function renderMCPResult(
|
|
|
128
137
|
lines.push(formatExpandHint(theme, expanded, true));
|
|
129
138
|
}
|
|
130
139
|
|
|
140
|
+
if (truncationWarning) lines.push(truncationWarning);
|
|
131
141
|
return new Text(lines.join("\n"), 0, 0);
|
|
132
142
|
}
|
package/src/mcp/tool-bridge.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
RenderResultOptions,
|
|
16
16
|
} from "../extensibility/custom-tools/types";
|
|
17
17
|
import type { Theme } from "../modes/theme/theme";
|
|
18
|
+
import type { OutputMeta } from "../tools/output-meta";
|
|
18
19
|
import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
|
|
19
20
|
import { callTool } from "./client";
|
|
20
21
|
import { renderMCPCall, renderMCPResult } from "./render";
|
|
@@ -71,6 +72,8 @@ export interface MCPToolDetails {
|
|
|
71
72
|
provider?: string;
|
|
72
73
|
/** Provider display name (e.g., "Claude Code", "MCP Config") */
|
|
73
74
|
providerName?: string;
|
|
75
|
+
/** Structured output metadata (set by the spill wrapper when output is truncated to an artifact). */
|
|
76
|
+
meta?: OutputMeta;
|
|
74
77
|
}
|
|
75
78
|
/**
|
|
76
79
|
* Format MCP content for LLM consumption.
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { afterEach, beforeAll, describe, expect, it, vi } from "bun:test";
|
|
1
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "bun:test";
|
|
2
2
|
import { $ } from "bun";
|
|
3
3
|
import { getEditorTheme, initTheme } from "../theme/theme";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
CustomEditor,
|
|
6
|
+
SPACE_HOLD_MECHANICAL_RUN,
|
|
7
|
+
SPACE_HOLD_RELEASE_MS,
|
|
8
|
+
SPACE_REPEAT_MAX_GAP_MS,
|
|
9
|
+
} from "./custom-editor";
|
|
5
10
|
|
|
6
11
|
function makeEditor() {
|
|
7
12
|
const editor = new CustomEditor(getEditorTheme());
|
|
@@ -12,8 +17,26 @@ function makeEditor() {
|
|
|
12
17
|
return { editor, events };
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
/** A gap below SPACE_REPEAT_MAX_GAP_MS — looks like OS key auto-repeat (a held bar). */
|
|
21
|
+
const REPEAT_GAP_MS = 30;
|
|
22
|
+
/** A gap above the threshold — looks like a deliberate keypress. */
|
|
23
|
+
const TAP_GAP_MS = SPACE_REPEAT_MAX_GAP_MS + 80;
|
|
24
|
+
|
|
25
|
+
/** Feed `count` spaces `gapMs` apart on the fake clock. The first space of a run has no prior
|
|
26
|
+
* space, so its gap is effectively infinite and it always reads as a deliberate tap. */
|
|
27
|
+
function feedSpaces(editor: CustomEditor, count: number, gapMs: number): void {
|
|
28
|
+
for (let i = 0; i < count; i++) {
|
|
29
|
+
vi.advanceTimersByTime(gapMs);
|
|
30
|
+
editor.handleInput(" ");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Feed spaces at explicit per-press gaps (ms) on the fake clock — for simulating an irregular cadence. */
|
|
35
|
+
function feedGaps(editor: CustomEditor, gaps: number[]): void {
|
|
36
|
+
for (const gapMs of gaps) {
|
|
37
|
+
vi.advanceTimersByTime(gapMs);
|
|
38
|
+
editor.handleInput(" ");
|
|
39
|
+
}
|
|
17
40
|
}
|
|
18
41
|
|
|
19
42
|
async function decorateInFreshProcess(text: string): Promise<string> {
|
|
@@ -47,29 +70,32 @@ describe("CustomEditor space-hold push-to-talk", () => {
|
|
|
47
70
|
await initTheme();
|
|
48
71
|
});
|
|
49
72
|
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
vi.useFakeTimers();
|
|
75
|
+
});
|
|
76
|
+
|
|
50
77
|
afterEach(() => {
|
|
51
78
|
vi.useRealTimers();
|
|
52
79
|
});
|
|
53
80
|
|
|
54
|
-
it("
|
|
81
|
+
it("types deliberate space taps without triggering, even several in a row", () => {
|
|
55
82
|
const { editor, events } = makeEditor();
|
|
56
|
-
|
|
57
|
-
expect(editor.getText()).toBe("
|
|
83
|
+
feedSpaces(editor, 3, TAP_GAP_MS);
|
|
84
|
+
expect(editor.getText()).toBe(" ");
|
|
58
85
|
expect(events).toEqual([]);
|
|
59
86
|
});
|
|
60
87
|
|
|
61
|
-
it("
|
|
62
|
-
vi.useFakeTimers();
|
|
88
|
+
it("recognizes a held bar from a steady fast cadence and tracks back the burst", () => {
|
|
63
89
|
const { editor, events } = makeEditor();
|
|
64
90
|
editor.handleInput("h");
|
|
65
91
|
editor.handleInput("i");
|
|
66
|
-
//
|
|
67
|
-
// leaving only the pre-burst text
|
|
68
|
-
|
|
92
|
+
// Metronomic auto-repeat: the few pre-burst spaces typed are tracked back out when the hold is
|
|
93
|
+
// recognized, leaving only the pre-burst text.
|
|
94
|
+
feedSpaces(editor, SPACE_HOLD_MECHANICAL_RUN + 2, REPEAT_GAP_MS);
|
|
69
95
|
expect(editor.getText()).toBe("hi");
|
|
70
96
|
expect(events).toEqual(["start"]);
|
|
71
97
|
// Continued auto-repeat while the bar is held is swallowed: no spam, no re-trigger.
|
|
72
|
-
|
|
98
|
+
feedSpaces(editor, 5, REPEAT_GAP_MS);
|
|
73
99
|
expect(editor.getText()).toBe("hi");
|
|
74
100
|
expect(events).toEqual(["start"]);
|
|
75
101
|
// An idle gap with no further repeats means the bar was released -> stop + transcribe.
|
|
@@ -77,20 +103,39 @@ describe("CustomEditor space-hold push-to-talk", () => {
|
|
|
77
103
|
expect(events).toEqual(["start", "end"]);
|
|
78
104
|
});
|
|
79
105
|
|
|
106
|
+
it("does not trigger when the space bar is smashed at an irregular cadence", () => {
|
|
107
|
+
const { editor, events } = makeEditor();
|
|
108
|
+
// Fast but jittery, the way a human mashes — not the metronomic delta of OS auto-repeat.
|
|
109
|
+
const gaps = [40, 95, 45, 100, 35, 90, 50, 105];
|
|
110
|
+
feedGaps(editor, gaps);
|
|
111
|
+
expect(events).toEqual([]);
|
|
112
|
+
// Nothing is eaten: every smashed space still types a real space.
|
|
113
|
+
expect(editor.getText()).toBe(" ".repeat(gaps.length));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("does not trigger on steady but slow spacing", () => {
|
|
117
|
+
const { editor, events } = makeEditor();
|
|
118
|
+
// Even cadence, but slower than auto-repeat: consistent deltas alone must not start recording.
|
|
119
|
+
feedSpaces(editor, 6, TAP_GAP_MS);
|
|
120
|
+
expect(events).toEqual([]);
|
|
121
|
+
expect(editor.getText()).toBe(" ".repeat(6));
|
|
122
|
+
});
|
|
123
|
+
|
|
80
124
|
it("does not trigger when a non-space breaks the run", () => {
|
|
81
125
|
const { editor, events } = makeEditor();
|
|
82
|
-
|
|
126
|
+
// Each partial run climbs the mechanical counter one short of the threshold; the non-space
|
|
127
|
+
// resets it so they never combine into a hold.
|
|
128
|
+
feedSpaces(editor, 3, REPEAT_GAP_MS);
|
|
83
129
|
editor.handleInput("x");
|
|
84
|
-
|
|
130
|
+
feedSpaces(editor, 3, REPEAT_GAP_MS);
|
|
85
131
|
expect(events).toEqual([]);
|
|
86
|
-
expect(editor.getText()).toBe(`${" ".repeat(SPACE_HOLD_THRESHOLD)}x${" ".repeat(SPACE_HOLD_THRESHOLD)}`);
|
|
87
132
|
});
|
|
88
133
|
|
|
89
134
|
it("leaves the space bar typing normally when the gesture is disabled", () => {
|
|
90
135
|
const { editor, events } = makeEditor();
|
|
91
136
|
editor.sttHoldEnabled = () => false;
|
|
92
|
-
|
|
93
|
-
expect(editor.getText()).toBe(" ".repeat(
|
|
137
|
+
feedSpaces(editor, 8, REPEAT_GAP_MS);
|
|
138
|
+
expect(editor.getText()).toBe(" ".repeat(8));
|
|
94
139
|
expect(events).toEqual([]);
|
|
95
140
|
});
|
|
96
141
|
});
|
|
@@ -62,14 +62,34 @@ const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i;
|
|
|
62
62
|
const BRACKETED_IMAGE_PATH_BOUNDARY_REGEX = /\.(?:png|jpe?g|gif|webp)(?=$|["']?\s)/gi;
|
|
63
63
|
const SHELL_ESCAPED_PATH_CHAR_REGEX = /\\([\\\s'"()[\]{}&;<>|?*!$`])/g;
|
|
64
64
|
|
|
65
|
-
/**
|
|
66
|
-
*
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
/** Max gap (ms) between two spaces for the later one to count as OS key auto-repeat rather than a
|
|
66
|
+
* deliberate press. OS auto-repeat is fast; a deliberate tap (even a fast one) is slower. */
|
|
67
|
+
export const SPACE_REPEAT_MAX_GAP_MS = 120;
|
|
68
|
+
/** Two consecutive inter-space gaps are "mechanical" (machine-driven auto-repeat) when both are
|
|
69
|
+
* within {@link SPACE_REPEAT_MAX_GAP_MS} and differ by no more than this — an absolute jitter floor
|
|
70
|
+
* or, for slower repeat rates, {@link SPACE_REPEAT_JITTER_RATIO} of the smaller gap. OS key-repeat
|
|
71
|
+
* is metronomic; a human smashing the bar is fast but irregular, so its deltas never stay this
|
|
72
|
+
* steady. */
|
|
73
|
+
export const SPACE_REPEAT_JITTER_MS = 18;
|
|
74
|
+
export const SPACE_REPEAT_JITTER_RATIO = 0.35;
|
|
75
|
+
/** Consecutive mechanical (fast + steady) deltas that confirm the space bar is held and start
|
|
76
|
+
* recording. Needs a sustained metronomic cadence, so jittery smashing and deliberate taps never
|
|
77
|
+
* reach it. */
|
|
78
|
+
export const SPACE_HOLD_MECHANICAL_RUN = 2;
|
|
69
79
|
/** Idle gap (ms) after the last repeated space that counts as the space bar being released, ending
|
|
70
80
|
* the push-to-talk recording. Must comfortably exceed the OS key-repeat interval. */
|
|
71
81
|
export const SPACE_HOLD_RELEASE_MS = 250;
|
|
72
82
|
|
|
83
|
+
/** Whether two consecutive inter-space gaps look machine-driven: both within the auto-repeat band
|
|
84
|
+
* and steady enough (small absolute or proportional difference). OS key-repeat is metronomic, so
|
|
85
|
+
* its successive deltas match closely; human smashing is fast but irregular and deliberate taps are
|
|
86
|
+
* too slow, so neither passes. */
|
|
87
|
+
function gapsAreMechanical(gap: number, prevGap: number): boolean {
|
|
88
|
+
if (gap > SPACE_REPEAT_MAX_GAP_MS || prevGap > SPACE_REPEAT_MAX_GAP_MS) return false;
|
|
89
|
+
const tolerance = Math.max(SPACE_REPEAT_JITTER_MS, Math.min(gap, prevGap) * SPACE_REPEAT_JITTER_RATIO);
|
|
90
|
+
return Math.abs(gap - prevGap) <= tolerance;
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
function isPastedPathSeparator(char: string | undefined): boolean {
|
|
74
94
|
return char === undefined || char === " " || char === "\t" || char === "\r" || char === "\n";
|
|
75
95
|
}
|
|
@@ -266,8 +286,14 @@ export class CustomEditor extends Editor {
|
|
|
266
286
|
/** Custom key handlers from extensions and non-built-in app actions. */
|
|
267
287
|
#customKeyHandlers = new Map<KeyId, () => void>();
|
|
268
288
|
#customMatchKeys = new Map<string, () => void>();
|
|
269
|
-
/**
|
|
289
|
+
/** Spaces actually inserted in the current run; tracked back out when a hold is recognized. */
|
|
270
290
|
#spaceRunInserted = 0;
|
|
291
|
+
/** Consecutive "mechanical" deltas (fast + steady); a sustained run of these confirms a held bar. */
|
|
292
|
+
#mechanicalRun = 0;
|
|
293
|
+
/** Inter-space gap (ms) of the previous space pair, compared against the next to judge steadiness. */
|
|
294
|
+
#prevSpaceGap: number | undefined;
|
|
295
|
+
/** Monotonic timestamp (ms) of the last space, to measure the gap to the next one. */
|
|
296
|
+
#lastSpaceAt = Number.NEGATIVE_INFINITY;
|
|
271
297
|
/** True while a recognized space-hold push-to-talk recording is in progress. */
|
|
272
298
|
#spaceHoldActive = false;
|
|
273
299
|
/** Idle timer that fires `onSpaceHoldEnd` once repeated spaces stop arriving. */
|
|
@@ -334,9 +360,12 @@ export class CustomEditor extends Editor {
|
|
|
334
360
|
}
|
|
335
361
|
|
|
336
362
|
/** Drive the space-hold push-to-talk state machine. Returns true when the gesture consumed the
|
|
337
|
-
* input so it must not reach normal editing.
|
|
338
|
-
*
|
|
339
|
-
*
|
|
363
|
+
* input so it must not reach normal editing. A held space bar emits OS auto-repeat: a *steady*
|
|
364
|
+
* stream of spaces at a fixed fast interval. We watch the inter-space deltas and only recognize a
|
|
365
|
+
* hold once {@link SPACE_HOLD_MECHANICAL_RUN} consecutive deltas are "mechanical" — both
|
|
366
|
+
* auto-repeat-fast and near-identical (see {@link gapsAreMechanical}). Smashing the bar is fast
|
|
367
|
+
* but jittery and deliberate taps are too slow, so neither escalates and both keep typing real
|
|
368
|
+
* spaces; the few spaces typed before a real hold is recognized are tracked back out. */
|
|
340
369
|
#handleSpaceHold(data: string, canonical: string | undefined): boolean {
|
|
341
370
|
const isSpace = canonical === "space";
|
|
342
371
|
if (this.#spaceHoldActive) {
|
|
@@ -350,21 +379,40 @@ export class CustomEditor extends Editor {
|
|
|
350
379
|
return false;
|
|
351
380
|
}
|
|
352
381
|
if (!isSpace) {
|
|
353
|
-
this.#
|
|
382
|
+
this.#resetSpaceRun();
|
|
354
383
|
return false;
|
|
355
384
|
}
|
|
356
385
|
if (!this.#spaceHoldGestureEnabled()) return false;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
this.#
|
|
360
|
-
|
|
386
|
+
const now = performance.now();
|
|
387
|
+
const gap = now - this.#lastSpaceAt;
|
|
388
|
+
const prevGap = this.#prevSpaceGap;
|
|
389
|
+
this.#lastSpaceAt = now;
|
|
390
|
+
this.#prevSpaceGap = gap;
|
|
391
|
+
if (prevGap === undefined || !gapsAreMechanical(gap, prevGap)) {
|
|
392
|
+
// First space, a deliberate tap, or jittery smashing: not a steady machine cadence yet, so
|
|
393
|
+
// type a real space and reset the mechanical run.
|
|
394
|
+
this.#mechanicalRun = 0;
|
|
395
|
+
super.handleInput(data);
|
|
396
|
+
this.#spaceRunInserted++;
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
// Steady fast repeat: swallow it. Once the cadence has held for SPACE_HOLD_MECHANICAL_RUN
|
|
400
|
+
// deltas it's a held bar — track back the few pre-burst spaces already typed and start.
|
|
401
|
+
if (++this.#mechanicalRun >= SPACE_HOLD_MECHANICAL_RUN) {
|
|
361
402
|
this.deleteBeforeCursor(this.#spaceRunInserted);
|
|
362
|
-
this.#
|
|
403
|
+
this.#resetSpaceRun();
|
|
363
404
|
this.#beginSpaceHold();
|
|
364
405
|
}
|
|
365
406
|
return true;
|
|
366
407
|
}
|
|
367
408
|
|
|
409
|
+
#resetSpaceRun(): void {
|
|
410
|
+
this.#spaceRunInserted = 0;
|
|
411
|
+
this.#mechanicalRun = 0;
|
|
412
|
+
this.#prevSpaceGap = undefined;
|
|
413
|
+
this.#lastSpaceAt = Number.NEGATIVE_INFINITY;
|
|
414
|
+
}
|
|
415
|
+
|
|
368
416
|
#beginSpaceHold(): void {
|
|
369
417
|
this.#spaceHoldActive = true;
|
|
370
418
|
this.#armSpaceHoldReleaseTimer();
|
|
@@ -383,7 +431,7 @@ export class CustomEditor extends Editor {
|
|
|
383
431
|
#endSpaceHold(): void {
|
|
384
432
|
if (!this.#spaceHoldActive) return;
|
|
385
433
|
this.#spaceHoldActive = false;
|
|
386
|
-
this.#
|
|
434
|
+
this.#resetSpaceRun();
|
|
387
435
|
if (this.#spaceHoldTimer) {
|
|
388
436
|
clearTimeout(this.#spaceHoldTimer);
|
|
389
437
|
this.#spaceHoldTimer = undefined;
|
|
@@ -84,9 +84,9 @@ export class CommandController {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
handleDumpCommand(
|
|
87
|
+
handleDumpCommand() {
|
|
88
88
|
try {
|
|
89
|
-
const formatted = this.ctx.session.formatSessionAsText(
|
|
89
|
+
const formatted = this.ctx.session.formatSessionAsText();
|
|
90
90
|
if (!formatted) {
|
|
91
91
|
this.ctx.showError("No messages to dump yet.");
|
|
92
92
|
return;
|
|
@@ -708,21 +708,25 @@ export class InputController {
|
|
|
708
708
|
// No input waiter: the main loop is between turns (post-turn
|
|
709
709
|
// epilogue, retry backoff, or a scheduled continue) with the agent
|
|
710
710
|
// momentarily idle. The editor already cleared itself on Enter, so
|
|
711
|
-
// falling through here would silently swallow the message.
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
//
|
|
711
|
+
// falling through here would silently swallow the message. Submit a
|
|
712
|
+
// real prompt directly; if a background turn starts in the gap,
|
|
713
|
+
// `streamingBehavior: "steer"` preserves the typed-message queueing
|
|
714
|
+
// semantics instead of throwing AgentBusyError.
|
|
715
715
|
this.ctx.editor.imageLinks = undefined;
|
|
716
716
|
const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
|
|
717
717
|
this.ctx.pendingImages = [];
|
|
718
718
|
this.ctx.pendingImageLinks = [];
|
|
719
719
|
try {
|
|
720
|
-
await this.ctx.withLocalSubmission(
|
|
721
|
-
|
|
722
|
-
|
|
720
|
+
await this.ctx.withLocalSubmission(
|
|
721
|
+
text,
|
|
722
|
+
() => this.ctx.session.prompt(text, { streamingBehavior: "steer", images }),
|
|
723
|
+
{
|
|
724
|
+
imageCount: images?.length ?? 0,
|
|
725
|
+
},
|
|
726
|
+
);
|
|
723
727
|
} catch (error) {
|
|
724
728
|
// Don't lose the message: hand the text and images back to the
|
|
725
|
-
// editor so the user can retry (e.g.
|
|
729
|
+
// editor so the user can retry (e.g. prompt dispatch rejecting an
|
|
726
730
|
// extension command).
|
|
727
731
|
this.ctx.editor.setText(text);
|
|
728
732
|
if (images && images.length > 0) {
|
|
@@ -994,7 +998,9 @@ export class InputController {
|
|
|
994
998
|
|
|
995
999
|
restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
|
|
996
1000
|
this.ctx.locallySubmittedUserSignatures.clear();
|
|
997
|
-
|
|
1001
|
+
// On Esc (abort) drop non-user internal steers so the post-abort drain can't
|
|
1002
|
+
// auto-resume; plain Alt+Up dequeue preserves them for the continuing stream.
|
|
1003
|
+
const { steering, followUp } = this.ctx.session.clearQueue({ forInterrupt: options?.abort });
|
|
998
1004
|
// Messages typed while compacting live in `compactionQueuedMessages`, not the
|
|
999
1005
|
// agent queue `clearQueue()` drains — but the pending bar shows the same
|
|
1000
1006
|
// "Alt+Up to edit" hint for them (ui-helpers `updatePendingMessagesDisplay`).
|
|
@@ -1216,11 +1216,20 @@ export class SelectorController {
|
|
|
1216
1216
|
...this.ctx.keybindings.getKeys("app.session.observe"),
|
|
1217
1217
|
];
|
|
1218
1218
|
let hub: AgentHubOverlayComponent | undefined;
|
|
1219
|
-
let overlayHandle: OverlayHandle | undefined;
|
|
1220
1219
|
|
|
1220
|
+
// Render the hub inline in the editor slot — the same anchored region
|
|
1221
|
+
// every other selector (model, session, tree, the `ask` tool) uses —
|
|
1222
|
+
// rather than a floating overlay. A non-fullscreen overlay composited over
|
|
1223
|
+
// a live transcript strands a stale copy in native scrollback every time a
|
|
1224
|
+
// running subagent's progress grows the frame and scrolls the window; the
|
|
1225
|
+
// hub is opened mid-run, so those copies stacked into a wall of duplicate
|
|
1226
|
+
// "Agent Hub" frames bleeding the task tree behind them. As an editor-slot
|
|
1227
|
+
// component it rides the normal append-only commit path: the transcript
|
|
1228
|
+
// commits above it exactly once and the hub repaints in place.
|
|
1221
1229
|
const done = () => {
|
|
1222
1230
|
hub?.dispose();
|
|
1223
|
-
|
|
1231
|
+
this.ctx.editorContainer.clear();
|
|
1232
|
+
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
1224
1233
|
this.ctx.ui.setFocus(this.ctx.editor);
|
|
1225
1234
|
this.ctx.ui.requestRender();
|
|
1226
1235
|
};
|
|
@@ -1251,12 +1260,8 @@ export class SelectorController {
|
|
|
1251
1260
|
return;
|
|
1252
1261
|
}
|
|
1253
1262
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
width: "100%",
|
|
1257
|
-
maxHeight: "100%",
|
|
1258
|
-
margin: 0,
|
|
1259
|
-
});
|
|
1263
|
+
this.ctx.editorContainer.clear();
|
|
1264
|
+
this.ctx.editorContainer.addChild(hub);
|
|
1260
1265
|
this.ctx.ui.setFocus(hub);
|
|
1261
1266
|
this.ctx.ui.requestRender();
|
|
1262
1267
|
}
|
|
@@ -151,6 +151,7 @@ import type { ObservableSession } from "./session-observer-registry";
|
|
|
151
151
|
import { SessionObserverRegistry } from "./session-observer-registry";
|
|
152
152
|
import { runProviderSetupWizard } from "./setup-wizard/lazy";
|
|
153
153
|
import { interruptHint } from "./shared";
|
|
154
|
+
import { clearMermaidCache } from "./theme/mermaid-cache";
|
|
154
155
|
import { type ShimmerPalette, shimmerEnabled, shimmerSegments, shimmerText } from "./theme/shimmer";
|
|
155
156
|
import type { Theme } from "./theme/theme";
|
|
156
157
|
import {
|
|
@@ -854,6 +855,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
854
855
|
onThemeChange(() => {
|
|
855
856
|
this.#clearWorkingMessageAccentCache();
|
|
856
857
|
clearRenderCache();
|
|
858
|
+
clearMermaidCache();
|
|
857
859
|
this.ui.invalidate();
|
|
858
860
|
this.updateEditorBorderColor();
|
|
859
861
|
this.ui.requestRender();
|
|
@@ -3382,8 +3384,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3382
3384
|
return this.#commandController.handleExportCommand(text);
|
|
3383
3385
|
}
|
|
3384
3386
|
|
|
3385
|
-
handleDumpCommand(
|
|
3386
|
-
return this.#commandController.handleDumpCommand(
|
|
3387
|
+
handleDumpCommand() {
|
|
3388
|
+
return this.#commandController.handleDumpCommand();
|
|
3387
3389
|
}
|
|
3388
3390
|
|
|
3389
3391
|
handleAdvisorDumpCommand(isRaw?: boolean) {
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
matchesKey,
|
|
4
|
+
type OverlayFocusOwner,
|
|
5
|
+
padding,
|
|
6
|
+
parseSgrMouse,
|
|
7
|
+
truncateToWidth,
|
|
8
|
+
visibleWidth,
|
|
9
|
+
} from "@oh-my-pi/pi-tui";
|
|
2
10
|
import { APP_NAME } from "@oh-my-pi/pi-utils";
|
|
3
11
|
import { gradientLogo, PI_LOGO } from "../components/welcome";
|
|
4
12
|
import { theme } from "../theme/theme";
|
|
@@ -53,7 +61,7 @@ function dissolveFrames(from: string[], to: string[], progress: number, height:
|
|
|
53
61
|
return out;
|
|
54
62
|
}
|
|
55
63
|
|
|
56
|
-
export class SetupWizardComponent implements Component {
|
|
64
|
+
export class SetupWizardComponent implements Component, OverlayFocusOwner {
|
|
57
65
|
#phase: WizardPhase = "splash";
|
|
58
66
|
#phaseStartedAt = performance.now();
|
|
59
67
|
#sceneIndex = 0;
|
|
@@ -63,6 +71,7 @@ export class SetupWizardComponent implements Component {
|
|
|
63
71
|
#disposed = false;
|
|
64
72
|
/** Screen row where the active scene's body began in the last rendered frame. */
|
|
65
73
|
#bodyRowStart = 0;
|
|
74
|
+
#sceneFocusTarget: Component | undefined;
|
|
66
75
|
|
|
67
76
|
constructor(
|
|
68
77
|
readonly ctx: InteractiveModeContext,
|
|
@@ -87,6 +96,11 @@ export class SetupWizardComponent implements Component {
|
|
|
87
96
|
this.#activeScene?.invalidate?.();
|
|
88
97
|
}
|
|
89
98
|
|
|
99
|
+
ownsOverlayFocusTarget(component: Component): boolean {
|
|
100
|
+
if (this.#sceneFocusTarget !== component) return false;
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
90
104
|
handleInput(data: string): void {
|
|
91
105
|
if (this.#phase === "done") return;
|
|
92
106
|
if (data.startsWith("\x1b[<")) {
|
|
@@ -260,12 +274,19 @@ export class SetupWizardComponent implements Component {
|
|
|
260
274
|
ctx: this.ctx,
|
|
261
275
|
requestRender: () => this.ctx.ui.requestRender(),
|
|
262
276
|
finish: (_result: SetupSceneResult) => this.#finishScene(),
|
|
263
|
-
setFocus: component =>
|
|
264
|
-
|
|
277
|
+
setFocus: component => {
|
|
278
|
+
this.#sceneFocusTarget = component ?? undefined;
|
|
279
|
+
this.ctx.ui.setFocus(component);
|
|
280
|
+
},
|
|
281
|
+
restoreFocus: () => {
|
|
282
|
+
this.#sceneFocusTarget = undefined;
|
|
283
|
+
this.ctx.ui.setFocus(this);
|
|
284
|
+
},
|
|
265
285
|
};
|
|
266
286
|
this.#activeScene = scene.mount(host);
|
|
267
287
|
this.#phase = targetPhase;
|
|
268
288
|
this.#phaseStartedAt = performance.now();
|
|
289
|
+
this.#sceneFocusTarget = undefined;
|
|
269
290
|
this.ctx.ui.setFocus(this);
|
|
270
291
|
void this.#activeScene.onMount?.();
|
|
271
292
|
this.ctx.ui.requestRender();
|
|
@@ -288,6 +309,7 @@ export class SetupWizardComponent implements Component {
|
|
|
288
309
|
}
|
|
289
310
|
|
|
290
311
|
#unmountActiveScene(): void {
|
|
312
|
+
this.#sceneFocusTarget = undefined;
|
|
291
313
|
this.#activeScene?.onUnmount?.();
|
|
292
314
|
this.#activeScene?.dispose?.();
|
|
293
315
|
this.#activeScene = undefined;
|
|
@@ -1,24 +1,87 @@
|
|
|
1
|
-
import { renderMermaidAsciiSafe } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { type MermaidAsciiRenderOptions, renderMermaidAsciiSafe } from "@oh-my-pi/pi-utils";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Options controlling how fenced Mermaid source is resolved to terminal ASCII.
|
|
5
|
+
* Extends the raw render options (theme, color mode, spacing, `useAscii`) with a
|
|
6
|
+
* viewport-fitting hint.
|
|
7
|
+
*/
|
|
8
|
+
export interface MermaidResolveOptions extends MermaidAsciiRenderOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Maximum display width (terminal columns) the diagram should occupy. A
|
|
11
|
+
* layout that overflows this width is re-rendered in the perpendicular
|
|
12
|
+
* orientation — a wide horizontal chain collapses to a tall vertical column
|
|
13
|
+
* (which the terminal can scroll), and a wide vertical fan-out collapses to a
|
|
14
|
+
* tall horizontal column. Omit to keep the source's own layout regardless of
|
|
15
|
+
* width.
|
|
16
|
+
*/
|
|
17
|
+
maxWidth?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Memoizes rendered ASCII (and failures) keyed on the render options + the
|
|
21
|
+
// layout-direction variant + source. Width selection happens per call against
|
|
22
|
+
// the cached renders, so a terminal resize re-decides without re-rendering.
|
|
3
23
|
const cache = new Map<string, string | null>();
|
|
4
24
|
|
|
5
|
-
|
|
6
|
-
|
|
25
|
+
/** Widest rendered row in display columns (ANSI- and CJK-aware). */
|
|
26
|
+
function asciiDisplayWidth(ascii: string): number {
|
|
27
|
+
let max = 0;
|
|
28
|
+
for (const line of ascii.split("\n")) {
|
|
29
|
+
const width = Bun.stringWidth(line);
|
|
30
|
+
if (width > max) max = width;
|
|
31
|
+
}
|
|
32
|
+
return max;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderVariant(
|
|
36
|
+
source: string,
|
|
37
|
+
baseOptions: MermaidAsciiRenderOptions,
|
|
38
|
+
baseKey: string,
|
|
39
|
+
direction: "TD" | "LR" | null,
|
|
40
|
+
): string | null {
|
|
41
|
+
const key = `${baseKey}\x00${direction ?? ""}\x00${source}`;
|
|
42
|
+
const cached = cache.get(key);
|
|
43
|
+
if (cached !== undefined) return cached;
|
|
44
|
+
|
|
45
|
+
const ascii = renderMermaidAsciiSafe(source, direction ? { ...baseOptions, direction } : baseOptions);
|
|
46
|
+
cache.set(key, ascii);
|
|
47
|
+
return ascii;
|
|
7
48
|
}
|
|
8
49
|
|
|
9
50
|
/**
|
|
10
51
|
* Resolve mermaid ASCII from fenced block source text.
|
|
11
52
|
* Returns null when rendering fails, while memoizing failures to avoid repeated work.
|
|
12
53
|
*/
|
|
13
|
-
export function resolveMermaidAscii(source: string): string | null {
|
|
14
|
-
const normalizedSource =
|
|
15
|
-
if (
|
|
16
|
-
return cache.get(normalizedSource) ?? null;
|
|
17
|
-
}
|
|
54
|
+
export function resolveMermaidAscii(source: string, options?: MermaidResolveOptions): string | null {
|
|
55
|
+
const normalizedSource = source.replace(/\r\n?/g, "\n").trim();
|
|
56
|
+
if (!normalizedSource) return null;
|
|
18
57
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
58
|
+
const { maxWidth, ...rest } = options ?? {};
|
|
59
|
+
// Default to uncolored output; callers opt into a themed palette explicitly.
|
|
60
|
+
const baseOptions: MermaidAsciiRenderOptions = { colorMode: "none", ...rest };
|
|
61
|
+
const baseKey = JSON.stringify(baseOptions);
|
|
62
|
+
|
|
63
|
+
const base = renderVariant(normalizedSource, baseOptions, baseKey, null);
|
|
64
|
+
if (base === null) return null;
|
|
65
|
+
if (maxWidth === undefined) return base;
|
|
66
|
+
|
|
67
|
+
let best = base;
|
|
68
|
+
let bestWidth = asciiDisplayWidth(base);
|
|
69
|
+
if (bestWidth <= maxWidth) return base;
|
|
70
|
+
|
|
71
|
+
// The as-authored layout overflows. Render both forced orientations and keep
|
|
72
|
+
// the narrowest (clipping at the call site handles any residual overflow).
|
|
73
|
+
// Re-rendering the already-authored orientation is a cache hit, so this stays
|
|
74
|
+
// cheap, and one of the two will be the perpendicular fit.
|
|
75
|
+
for (const direction of ["TD", "LR"] as const) {
|
|
76
|
+
const variant = renderVariant(normalizedSource, baseOptions, baseKey, direction);
|
|
77
|
+
if (variant === null) continue;
|
|
78
|
+
const variantWidth = asciiDisplayWidth(variant);
|
|
79
|
+
if (variantWidth < bestWidth) {
|
|
80
|
+
best = variant;
|
|
81
|
+
bestWidth = variantWidth;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return best;
|
|
22
85
|
}
|
|
23
86
|
|
|
24
87
|
/**
|