@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.2
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 +128 -0
- package/dist/cli.js +4006 -3966
- package/dist/types/advisor/advise-tool.d.ts +5 -2
- package/dist/types/advisor/config.d.ts +14 -6
- package/dist/types/advisor/runtime.d.ts +20 -11
- package/dist/types/autolearn/controller.d.ts +1 -0
- package/dist/types/cli-commands.d.ts +14 -1
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +32 -7
- package/dist/types/config/settings.d.ts +50 -0
- package/dist/types/cursor.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +5 -2
- package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +18 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
- package/dist/types/lib/xai-http.d.ts +11 -0
- package/dist/types/mcp/transports/stdio.d.ts +13 -1
- package/dist/types/modes/acp/acp-agent.d.ts +3 -0
- package/dist/types/modes/acp/acp-mode.d.ts +6 -2
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/assistant-message.d.ts +2 -0
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +23 -1
- package/dist/types/modes/components/hook-editor.d.ts +16 -2
- package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +4 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
- package/dist/types/modes/print-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
- package/dist/types/modes/warp-events.d.ts +24 -0
- package/dist/types/modes/warp-events.test.d.ts +1 -0
- package/dist/types/plan-mode/model-transition.d.ts +47 -0
- package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
- package/dist/types/registry/agent-lifecycle.d.ts +26 -1
- package/dist/types/sdk.d.ts +13 -2
- package/dist/types/session/agent-session.d.ts +43 -11
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +14 -0
- package/dist/types/session/streaming-output.d.ts +2 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/slash-commands/types.d.ts +4 -4
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/task/label.d.ts +1 -1
- package/dist/types/telemetry-export.d.ts +34 -9
- package/dist/types/tools/approval.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/browser/registry.d.ts +7 -3
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/essential-tools.d.ts +29 -0
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/manage-skill.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/utils/title-generator.d.ts +2 -1
- package/dist/types/web/search/index.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +3 -0
- package/dist/types/web/search/providers/kimi.d.ts +4 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +21 -16
- package/src/advisor/__tests__/advisor.test.ts +1358 -86
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/advise-tool.ts +7 -3
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli/bench-cli.ts +4 -1
- package/src/cli/grep-cli.ts +2 -1
- package/src/cli-commands.ts +57 -23
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +34 -6
- package/src/config/model-resolver.ts +57 -12
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +43 -6
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/codex.ts +14 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/discovery/substitute-plugin-root.ts +23 -7
- package/src/exec/bash-executor.ts +14 -5
- package/src/export/html/template.js +2 -0
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/types.ts +18 -0
- package/src/extensibility/extensions/wrapper.ts +2 -1
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
- package/src/extensibility/plugins/manager.ts +2 -2
- package/src/extensibility/shared-events.ts +6 -0
- package/src/extensibility/utils.ts +91 -25
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
- package/src/internal-urls/agent-protocol.ts +73 -39
- package/src/irc/bus.ts +22 -3
- package/src/launch/broker.ts +3 -2
- package/src/launch/client.ts +2 -2
- package/src/launch/presence.ts +2 -2
- package/src/lib/xai-http.ts +28 -2
- package/src/lsp/client.ts +1 -1
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/transports/stdio.ts +103 -23
- package/src/modes/acp/acp-agent.ts +28 -9
- package/src/modes/acp/acp-event-mapper.ts +8 -0
- package/src/modes/acp/acp-mode.ts +18 -4
- package/src/modes/components/advisor-config.ts +65 -3
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/assistant-message.ts +35 -14
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
- package/src/modes/components/custom-editor.ts +62 -1
- package/src/modes/components/hook-editor.ts +35 -5
- package/src/modes/components/login-dialog.test.ts +56 -0
- package/src/modes/components/login-dialog.ts +7 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/plan-review-overlay.ts +51 -10
- package/src/modes/components/session-selector.ts +10 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/component.ts +2 -4
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/components/tool-execution.test.ts +101 -0
- package/src/modes/components/tool-execution.ts +117 -9
- package/src/modes/components/transcript-container.ts +30 -2
- package/src/modes/controllers/command-controller-shared.ts +1 -1
- package/src/modes/controllers/command-controller.ts +168 -48
- package/src/modes/controllers/event-controller.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -22
- package/src/modes/controllers/input-controller.ts +12 -12
- package/src/modes/controllers/selector-controller.ts +208 -46
- package/src/modes/interactive-mode.ts +205 -65
- package/src/modes/noninteractive-dispose.test.ts +60 -0
- package/src/modes/print-mode.ts +10 -5
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +31 -7
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +10 -0
- package/src/modes/utils/interactive-context-helpers.ts +3 -1
- package/src/modes/utils/transcript-render-helpers.ts +3 -2
- package/src/modes/warp-events.test.ts +794 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/plan-mode/model-transition.test.ts +60 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/sdk.ts +227 -44
- package/src/session/agent-session.ts +1364 -382
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-listing.ts +6 -5
- package/src/session/session-loader.ts +9 -0
- package/src/session/session-manager.ts +48 -0
- package/src/session/session-persistence.ts +11 -0
- package/src/session/streaming-output.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +9 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/slash-commands/types.ts +4 -4
- package/src/system-prompt.ts +2 -2
- package/src/task/executor.ts +1 -1
- package/src/task/label.ts +2 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- package/src/tools/browser/registry.ts +9 -2
- package/src/tools/browser/tab-supervisor.ts +65 -10
- package/src/tools/browser.ts +4 -3
- package/src/tools/essential-tools.ts +45 -0
- package/src/tools/eval.ts +3 -0
- package/src/tools/gh.ts +169 -2
- package/src/tools/image-gen.ts +624 -513
- package/src/tools/index.ts +10 -6
- package/src/tools/manage-skill.ts +5 -3
- package/src/tools/output-meta.ts +7 -0
- package/src/tools/path-utils.ts +8 -5
- package/src/tools/read.ts +48 -3
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/utils/title-generator.ts +15 -4
- package/src/web/search/index.ts +21 -6
- package/src/web/search/providers/base.ts +3 -0
- package/src/web/search/providers/kimi.ts +18 -12
- package/src/web/search/providers/xai.ts +40 -9
- package/src/web/search/types.ts +6 -1
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
} from "@agentclientprotocol/sdk";
|
|
43
43
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
44
44
|
import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
|
|
45
|
-
import { getBlobsDir, isEnoent, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
45
|
+
import { getBlobsDir, isEnoent, logger, type postmortem, VERSION } from "@oh-my-pi/pi-utils";
|
|
46
46
|
import { disableProvider, enableProvider, reset as resetCapabilities } from "../../capability";
|
|
47
47
|
import { Settings } from "../../config/settings";
|
|
48
48
|
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
|
|
@@ -1027,7 +1027,7 @@ export class AcpAgent implements Agent {
|
|
|
1027
1027
|
this.#connection.signal.addEventListener(
|
|
1028
1028
|
"abort",
|
|
1029
1029
|
() => {
|
|
1030
|
-
void this
|
|
1030
|
+
void this.dispose();
|
|
1031
1031
|
},
|
|
1032
1032
|
{ once: true },
|
|
1033
1033
|
);
|
|
@@ -1912,6 +1912,7 @@ export class AcpAgent implements Agent {
|
|
|
1912
1912
|
const projectPath = await resolveActiveProjectRegistryPath(cwd);
|
|
1913
1913
|
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
1914
1914
|
resetCapabilities();
|
|
1915
|
+
await record.session.refreshSkills();
|
|
1915
1916
|
const fileCommands = await loadSlashCommands({ cwd });
|
|
1916
1917
|
record.session.setSlashCommands(fileCommands);
|
|
1917
1918
|
await this.#emitAvailableCommandsUpdate(record);
|
|
@@ -2109,6 +2110,23 @@ export class AcpAgent implements Agent {
|
|
|
2109
2110
|
});
|
|
2110
2111
|
continue;
|
|
2111
2112
|
}
|
|
2113
|
+
if (
|
|
2114
|
+
item.type === "image" &&
|
|
2115
|
+
"data" in item &&
|
|
2116
|
+
typeof item.data === "string" &&
|
|
2117
|
+
"mimeType" in item &&
|
|
2118
|
+
typeof item.mimeType === "string"
|
|
2119
|
+
) {
|
|
2120
|
+
notifications.push({
|
|
2121
|
+
sessionId,
|
|
2122
|
+
update: {
|
|
2123
|
+
sessionUpdate: "agent_message_chunk",
|
|
2124
|
+
content: { type: "image", data: item.data, mimeType: item.mimeType },
|
|
2125
|
+
messageId,
|
|
2126
|
+
},
|
|
2127
|
+
});
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2112
2130
|
if (item.type === "thinking" && "thinking" in item && typeof item.thinking === "string") {
|
|
2113
2131
|
const thinking = canonicalizeMessage(item.thinking);
|
|
2114
2132
|
if (thinking.length === 0) continue;
|
|
@@ -2443,7 +2461,7 @@ export class AcpAgent implements Agent {
|
|
|
2443
2461
|
}
|
|
2444
2462
|
}
|
|
2445
2463
|
|
|
2446
|
-
async #disposeSessionRecord(record: ManagedSessionRecord): Promise<void> {
|
|
2464
|
+
async #disposeSessionRecord(record: ManagedSessionRecord, reason?: postmortem.Reason): Promise<void> {
|
|
2447
2465
|
record.lifetimeUnsubscribe?.();
|
|
2448
2466
|
if (record.mcpManager) {
|
|
2449
2467
|
try {
|
|
@@ -2454,21 +2472,22 @@ export class AcpAgent implements Agent {
|
|
|
2454
2472
|
record.mcpManager = undefined;
|
|
2455
2473
|
}
|
|
2456
2474
|
try {
|
|
2457
|
-
await record.session.dispose();
|
|
2475
|
+
await record.session.dispose({ reason });
|
|
2458
2476
|
} catch (error) {
|
|
2459
2477
|
logger.warn("Failed to dispose ACP session", { error });
|
|
2460
2478
|
}
|
|
2461
2479
|
}
|
|
2462
2480
|
|
|
2463
|
-
async #disposeStandaloneSession(session: AgentSession): Promise<void> {
|
|
2481
|
+
async #disposeStandaloneSession(session: AgentSession, reason?: postmortem.Reason): Promise<void> {
|
|
2464
2482
|
try {
|
|
2465
|
-
await session.dispose();
|
|
2483
|
+
await session.dispose({ reason });
|
|
2466
2484
|
} catch (error) {
|
|
2467
2485
|
logger.warn("Failed to dispose ACP session", { error });
|
|
2468
2486
|
}
|
|
2469
2487
|
}
|
|
2470
2488
|
|
|
2471
|
-
|
|
2489
|
+
/** Dispose every session owned by this ACP connection and await persisted teardown. */
|
|
2490
|
+
async dispose(reason?: postmortem.Reason): Promise<void> {
|
|
2472
2491
|
if (this.#disposePromise) {
|
|
2473
2492
|
await this.#disposePromise;
|
|
2474
2493
|
return;
|
|
@@ -2484,7 +2503,7 @@ export class AcpAgent implements Agent {
|
|
|
2484
2503
|
"ACP agent disposed before queued prompt could run",
|
|
2485
2504
|
);
|
|
2486
2505
|
await this.#cancelPromptForClose(record);
|
|
2487
|
-
await this.#disposeSessionRecord(record);
|
|
2506
|
+
await this.#disposeSessionRecord(record, reason);
|
|
2488
2507
|
} catch (error) {
|
|
2489
2508
|
logger.warn("Failed to clean up ACP session", { sessionId, error });
|
|
2490
2509
|
}
|
|
@@ -2494,7 +2513,7 @@ export class AcpAgent implements Agent {
|
|
|
2494
2513
|
const initialSession = this.#initialSession;
|
|
2495
2514
|
this.#initialSession = undefined;
|
|
2496
2515
|
if (initialSession) {
|
|
2497
|
-
await this.#disposeStandaloneSession(initialSession);
|
|
2516
|
+
await this.#disposeStandaloneSession(initialSession, reason);
|
|
2498
2517
|
}
|
|
2499
2518
|
})();
|
|
2500
2519
|
|
|
@@ -272,6 +272,14 @@ function mapAssistantMessageUpdate(
|
|
|
272
272
|
let text: string;
|
|
273
273
|
const progress = options.getMessageProgress?.(event.message);
|
|
274
274
|
switch (event.assistantMessageEvent.type) {
|
|
275
|
+
case "image_end":
|
|
276
|
+
return [
|
|
277
|
+
toSessionNotification(sessionId, {
|
|
278
|
+
sessionUpdate: "agent_message_chunk",
|
|
279
|
+
content: event.assistantMessageEvent.content,
|
|
280
|
+
messageId: options.getMessageId?.(event.message),
|
|
281
|
+
}),
|
|
282
|
+
];
|
|
275
283
|
case "text_delta":
|
|
276
284
|
sessionUpdate = "agent_message_chunk";
|
|
277
285
|
text = event.assistantMessageEvent.delta;
|
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
import * as stream from "node:stream";
|
|
2
2
|
import { AgentSideConnection, ndJsonStream, type Stream } from "@agentclientprotocol/sdk";
|
|
3
|
+
import { postmortem } from "@oh-my-pi/pi-utils";
|
|
3
4
|
import type { AgentSession } from "../../session/agent-session";
|
|
4
5
|
import { AcpAgent } from "./acp-agent";
|
|
5
6
|
|
|
7
|
+
/** Creates sessions requested by an ACP client. */
|
|
6
8
|
export type AcpSessionFactory = (cwd: string) => Promise<AgentSession>;
|
|
7
9
|
|
|
10
|
+
/** Creates an ACP connection and exposes its agent when process-level teardown must own it. */
|
|
8
11
|
export function createAcpConnection(
|
|
9
12
|
transport: Stream,
|
|
10
13
|
createSession: AcpSessionFactory,
|
|
11
14
|
initialSession?: AgentSession,
|
|
15
|
+
onAgent?: (agent: AcpAgent) => void,
|
|
12
16
|
): AgentSideConnection {
|
|
13
|
-
return new AgentSideConnection(
|
|
17
|
+
return new AgentSideConnection(connection => {
|
|
18
|
+
const agent = new AcpAgent(connection, createSession, initialSession);
|
|
19
|
+
onAgent?.(agent);
|
|
20
|
+
return agent;
|
|
21
|
+
}, transport);
|
|
14
22
|
}
|
|
15
23
|
|
|
16
|
-
|
|
24
|
+
/** Serves ACP over stdio until the peer disconnects, then awaits session teardown before exit. */
|
|
25
|
+
export async function runAcpMode(createSession: AcpSessionFactory, initialSession?: AgentSession): Promise<void> {
|
|
17
26
|
// Humans who run `omp acp` by hand see a silent process and assume it is
|
|
18
27
|
// broken (stdout is the JSON-RPC transport, so nothing may be printed
|
|
19
28
|
// there). When stdin is a TTY no ACP client is attached — say so on stderr
|
|
@@ -25,10 +34,15 @@ export async function runAcpMode(createSession: AcpSessionFactory, initialSessio
|
|
|
25
34
|
"Waiting for protocol frames on stdin; logs: ~/.omp/logs/\n",
|
|
26
35
|
);
|
|
27
36
|
}
|
|
37
|
+
let agent: AcpAgent | undefined;
|
|
38
|
+
postmortem.register("acp-session-teardown", reason => agent?.dispose(reason));
|
|
39
|
+
postmortem.registerStdioDisconnectHandling();
|
|
28
40
|
const input = stream.Writable.toWeb(process.stdout);
|
|
29
41
|
const output = stream.Readable.toWeb(process.stdin);
|
|
30
42
|
const transport = ndJsonStream(input, output);
|
|
31
|
-
const connection = createAcpConnection(transport, createSession, initialSession
|
|
43
|
+
const connection = createAcpConnection(transport, createSession, initialSession, createdAgent => {
|
|
44
|
+
agent = createdAgent;
|
|
45
|
+
});
|
|
32
46
|
await connection.closed;
|
|
33
|
-
|
|
47
|
+
await postmortem.quit(0);
|
|
34
48
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* `save` callback.
|
|
17
17
|
*/
|
|
18
18
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
19
|
-
import type { Model } from "@oh-my-pi/pi-ai";
|
|
19
|
+
import type { Model, UsageReport } from "@oh-my-pi/pi-ai";
|
|
20
20
|
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
21
21
|
import {
|
|
22
22
|
type Component,
|
|
@@ -38,6 +38,9 @@ import {
|
|
|
38
38
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
39
39
|
import { formatModelSelectorValue } from "../../config/model-resolver";
|
|
40
40
|
import type { Settings } from "../../config/settings";
|
|
41
|
+
import type { PerAdvisorStat } from "../../session/agent-session";
|
|
42
|
+
import type { OAuthAccountIdentity } from "../../session/auth-storage";
|
|
43
|
+
import { formatCompactQuota } from "../controllers/command-controller";
|
|
41
44
|
import { getSelectListTheme, theme } from "../theme/theme";
|
|
42
45
|
import { HookEditorComponent } from "./hook-editor";
|
|
43
46
|
import { buildBrowserItems, ModelBrowser, sortModelItems } from "./model-browser";
|
|
@@ -63,6 +66,11 @@ export interface AdvisorConfigCallbacks {
|
|
|
63
66
|
requestRender: () => void;
|
|
64
67
|
/** Surface a transient status/warning line to the user. */
|
|
65
68
|
notify: (message: string) => void;
|
|
69
|
+
/** Live advisor usage stats; lets the preview show tokens/cost per advisor. */
|
|
70
|
+
getAdvisorStats?: () => PerAdvisorStat[];
|
|
71
|
+
getUsageReports?: () => Promise<UsageReport[] | null>;
|
|
72
|
+
/** Resolve the active OAuth identity for quota filtering (per-advisor account stickiness). */
|
|
73
|
+
resolveActiveAccount?: (provider: string, sessionId?: string) => OAuthAccountIdentity | undefined;
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
export interface AdvisorConfigDeps {
|
|
@@ -126,6 +134,8 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
126
134
|
#cb: AdvisorConfigCallbacks;
|
|
127
135
|
#scope: AdvisorConfigScope;
|
|
128
136
|
#doc: WatchdogConfigDoc;
|
|
137
|
+
/** Cached usage reports (quota/window/reset) prefetched on overlay open. */
|
|
138
|
+
#cachedReports: UsageReport[] | null = null;
|
|
129
139
|
#dirty = false;
|
|
130
140
|
|
|
131
141
|
#screen: Screen = "list";
|
|
@@ -157,6 +167,16 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
157
167
|
this.#doc = doc;
|
|
158
168
|
this.#ensureRosterVisible();
|
|
159
169
|
this.#showList();
|
|
170
|
+
// Prefetch usage reports for quota display; non-fatal if unavailable.
|
|
171
|
+
if (callbacks.getUsageReports) {
|
|
172
|
+
void callbacks
|
|
173
|
+
.getUsageReports()
|
|
174
|
+
.then(r => {
|
|
175
|
+
this.#cachedReports = r;
|
|
176
|
+
this.#cb.requestRender();
|
|
177
|
+
})
|
|
178
|
+
.catch(() => {});
|
|
179
|
+
}
|
|
160
180
|
}
|
|
161
181
|
|
|
162
182
|
// ───────────────────────────── render ─────────────────────────────
|
|
@@ -275,6 +295,7 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
275
295
|
const lines = [
|
|
276
296
|
theme.bold(advisor.name || "(unnamed)"),
|
|
277
297
|
"",
|
|
298
|
+
`${theme.fg("dim", "Enabled:")} ${advisor.enabled === false ? "○ off" : "● on"}`,
|
|
278
299
|
`${theme.fg("dim", "Model:")} ${model}`,
|
|
279
300
|
`${theme.fg("dim", "Tools:")} ${tools}`,
|
|
280
301
|
"",
|
|
@@ -282,6 +303,34 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
282
303
|
];
|
|
283
304
|
const instr = advisor.instructions?.trim();
|
|
284
305
|
lines.push(...(instr ? wrap(instr, bodyWidth) : [theme.fg("muted", "(none)")]));
|
|
306
|
+
// Show live usage stats when available from the session.
|
|
307
|
+
const liveStat = this.#cb.getAdvisorStats?.()?.find(s => s.name === (advisor.name || "default"));
|
|
308
|
+
if (liveStat && (liveStat.status === "running" || liveStat.status === "quota_exhausted")) {
|
|
309
|
+
lines.push("", theme.fg("dim", "Usage:"));
|
|
310
|
+
const spendParts: string[] = [
|
|
311
|
+
`${liveStat.tokens.input.toLocaleString()} in`,
|
|
312
|
+
`${liveStat.tokens.output.toLocaleString()} out`,
|
|
313
|
+
];
|
|
314
|
+
if (liveStat.tokens.cacheRead > 0) spendParts.push(`${liveStat.tokens.cacheRead.toLocaleString()} cache`);
|
|
315
|
+
lines.push(theme.fg("dim", ` Tokens: ${spendParts.join(", ")}`));
|
|
316
|
+
if (liveStat.cost > 0) lines.push(theme.fg("dim", ` Cost: $${liveStat.cost.toFixed(4)}`));
|
|
317
|
+
if (liveStat.contextWindow > 0) {
|
|
318
|
+
const pct = Math.round((liveStat.contextTokens / liveStat.contextWindow) * 100);
|
|
319
|
+
lines.push(
|
|
320
|
+
theme.fg(
|
|
321
|
+
"dim",
|
|
322
|
+
` Context: ${liveStat.contextTokens.toLocaleString()}/${liveStat.contextWindow.toLocaleString()} (${pct}%)`,
|
|
323
|
+
),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const quotaProvider =
|
|
328
|
+
(advisor.model?.includes("/") ? advisor.model.split("/")[0] : null) ?? liveStat?.model?.provider;
|
|
329
|
+
if (this.#cachedReports && quotaProvider) {
|
|
330
|
+
const activeAccount = this.#cb.resolveActiveAccount?.(quotaProvider, liveStat?.sessionId);
|
|
331
|
+
const quota = formatCompactQuota(quotaProvider, this.#cachedReports, Date.now(), activeAccount);
|
|
332
|
+
if (quota) lines.push(theme.fg("dim", ` ${quota}`));
|
|
333
|
+
}
|
|
285
334
|
return lines.map(line => truncateToWidth(line, bodyWidth));
|
|
286
335
|
}
|
|
287
336
|
|
|
@@ -311,7 +360,8 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
311
360
|
advisor.name === "default" &&
|
|
312
361
|
!advisor.model?.trim() &&
|
|
313
362
|
advisor.tools === undefined &&
|
|
314
|
-
!advisor.instructions?.trim()
|
|
363
|
+
!advisor.instructions?.trim() &&
|
|
364
|
+
advisor.enabled !== false
|
|
315
365
|
);
|
|
316
366
|
}
|
|
317
367
|
|
|
@@ -325,7 +375,7 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
325
375
|
this.#ensureRosterVisible();
|
|
326
376
|
const items: SelectItem[] = this.#doc.advisors.map((advisor, index) => ({
|
|
327
377
|
value: `advisor:${index}`,
|
|
328
|
-
label: advisor.name || "(unnamed)"
|
|
378
|
+
label: `${advisor.enabled === false ? "○" : "●"} ${advisor.name || "(unnamed)"}`,
|
|
329
379
|
description: this.#advisorSummary(advisor),
|
|
330
380
|
}));
|
|
331
381
|
items.push({ value: "add", label: "+ Add advisor" });
|
|
@@ -395,6 +445,11 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
395
445
|
const toolsDescription = formatAdvisorTools(advisor.tools, "no tools");
|
|
396
446
|
const items: SelectItem[] = [
|
|
397
447
|
{ value: "name", label: "Name", description: advisor.name },
|
|
448
|
+
{
|
|
449
|
+
value: "toggleEnabled",
|
|
450
|
+
label: "Enabled",
|
|
451
|
+
description: advisor.enabled === false ? "○ off" : "● on",
|
|
452
|
+
},
|
|
398
453
|
{ value: "model", label: "Model", description: modelDescription },
|
|
399
454
|
];
|
|
400
455
|
if (advisor.model?.trim()) {
|
|
@@ -414,6 +469,13 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
414
469
|
|
|
415
470
|
#onDetailSelect(index: number, field: string): void {
|
|
416
471
|
switch (field) {
|
|
472
|
+
case "toggleEnabled": {
|
|
473
|
+
const a = this.#doc.advisors[index];
|
|
474
|
+
a.enabled = a.enabled === false ? undefined : false;
|
|
475
|
+
this.#dirty = true;
|
|
476
|
+
this.#showDetail(index);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
417
479
|
case "name":
|
|
418
480
|
this.#showNameEditor(index);
|
|
419
481
|
return;
|
|
@@ -556,7 +556,7 @@ export class AskDialogComponent implements Component {
|
|
|
556
556
|
}
|
|
557
557
|
const isEnter = matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n";
|
|
558
558
|
const isSpace = matchesKey(keyData, "space") || keyData === " ";
|
|
559
|
-
if (!isEnter && !isSpace) return;
|
|
559
|
+
if (!isEnter && !(question.multi && isSpace)) return;
|
|
560
560
|
if (rowItem.kind === "other") {
|
|
561
561
|
void this.#promptForCustomInput(question, state, rowItem);
|
|
562
562
|
return;
|
|
@@ -173,6 +173,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
173
173
|
#lastMessage?: AssistantMessage;
|
|
174
174
|
#toolImagesByCallId = new Map<string, ImageContent[]>();
|
|
175
175
|
#convertedKittyImages = new Map<string, ImageContent>();
|
|
176
|
+
#showImages = true;
|
|
176
177
|
#kittyConversionsInFlight = new Set<string>();
|
|
177
178
|
#transcriptBlockFinalized: boolean;
|
|
178
179
|
/**
|
|
@@ -497,6 +498,15 @@ export class AssistantMessageComponent extends Container {
|
|
|
497
498
|
}
|
|
498
499
|
}
|
|
499
500
|
|
|
501
|
+
/** Toggle rendering for assistant-native and tool-result images. */
|
|
502
|
+
setImagesVisible(visible: boolean): void {
|
|
503
|
+
if (this.#showImages === visible) return;
|
|
504
|
+
this.#showImages = visible;
|
|
505
|
+
if (this.#lastMessage) {
|
|
506
|
+
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
500
510
|
setToolResultImages(toolCallId: string, images: ImageContent[]): void {
|
|
501
511
|
if (!toolCallId) return;
|
|
502
512
|
const validImages = images.filter(img => img.type === "image" && img.data && img.mimeType);
|
|
@@ -514,19 +524,17 @@ export class AssistantMessageComponent extends Container {
|
|
|
514
524
|
this.#toolImagesByCallId.delete(toolCallId);
|
|
515
525
|
} else {
|
|
516
526
|
this.#toolImagesByCallId.set(toolCallId, validImages);
|
|
517
|
-
this.#
|
|
527
|
+
this.#convertImagesForKitty(validImages.map((image, index) => ({ image, key: `${toolCallId}:${index}` })));
|
|
518
528
|
}
|
|
519
529
|
if (this.#lastMessage) {
|
|
520
530
|
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
521
531
|
}
|
|
522
532
|
}
|
|
523
533
|
|
|
524
|
-
#
|
|
534
|
+
#convertImagesForKitty(entries: Array<{ image: ImageContent; key: string }>): void {
|
|
525
535
|
if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
|
|
526
|
-
for (
|
|
527
|
-
|
|
528
|
-
if (!image || image.mimeType === "image/png") continue;
|
|
529
|
-
const key = `${toolCallId}:${index}`;
|
|
536
|
+
for (const { image, key } of entries) {
|
|
537
|
+
if (image.mimeType === "image/png") continue;
|
|
530
538
|
if (this.#convertedKittyImages.has(key) || this.#kittyConversionsInFlight.has(key)) continue;
|
|
531
539
|
this.#kittyConversionsInFlight.add(key);
|
|
532
540
|
new Bun.Image(Buffer.from(image.data, "base64"))
|
|
@@ -550,14 +558,12 @@ export class AssistantMessageComponent extends Container {
|
|
|
550
558
|
}
|
|
551
559
|
}
|
|
552
560
|
|
|
553
|
-
#
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
);
|
|
557
|
-
if (imageEntries.length === 0) return;
|
|
561
|
+
#renderImageEntries(entries: Array<{ image: ImageContent; key: string }>, withLeadingSpacer: boolean): void {
|
|
562
|
+
if (!this.#showImages || entries.length === 0) return;
|
|
563
|
+
this.#convertImagesForKitty(entries);
|
|
558
564
|
|
|
559
|
-
this.#contentContainer.addChild(new Spacer(1));
|
|
560
|
-
for (const { image, key } of
|
|
565
|
+
if (withLeadingSpacer) this.#contentContainer.addChild(new Spacer(1));
|
|
566
|
+
for (const { image, key } of entries) {
|
|
561
567
|
const displayImage =
|
|
562
568
|
TERMINAL.imageProtocol === ImageProtocol.Kitty && image.mimeType !== "image/png"
|
|
563
569
|
? this.#convertedKittyImages.get(key)
|
|
@@ -577,6 +583,13 @@ export class AssistantMessageComponent extends Container {
|
|
|
577
583
|
}
|
|
578
584
|
}
|
|
579
585
|
|
|
586
|
+
#renderToolImages(): void {
|
|
587
|
+
const entries = Array.from(this.#toolImagesByCallId.entries()).flatMap(([toolCallId, images]) =>
|
|
588
|
+
images.map((image, index) => ({ image, key: `${toolCallId}:${index}` })),
|
|
589
|
+
);
|
|
590
|
+
this.#renderImageEntries(entries, true);
|
|
591
|
+
}
|
|
592
|
+
|
|
580
593
|
#appendThinkingExtensions(contentIndex: number, thinkingIndex: number, text: string): void {
|
|
581
594
|
for (const renderer of this.thinkingRenderers) {
|
|
582
595
|
try {
|
|
@@ -620,7 +633,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
620
633
|
|
|
621
634
|
#canFastPath(message: AssistantMessage): boolean {
|
|
622
635
|
for (const content of message.content) {
|
|
623
|
-
if (content.type === "toolCall") return false;
|
|
636
|
+
if (content.type === "toolCall" || content.type === "image") return false;
|
|
624
637
|
}
|
|
625
638
|
if (this.#toolImagesByCallId.size > 0) return false;
|
|
626
639
|
const errorPresentation = resolveAssistantErrorPresentation(message);
|
|
@@ -769,6 +782,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
769
782
|
const hasVisibleContent = message.content.some(
|
|
770
783
|
c =>
|
|
771
784
|
(c.type === "text" && canonicalizeMessage(c.text)) ||
|
|
785
|
+
(c.type === "image" && c.data && c.mimeType) ||
|
|
772
786
|
(!this.hideThinkingBlock &&
|
|
773
787
|
c.type === "thinking" &&
|
|
774
788
|
resolveThinkingDisplay(c, this.proseOnlyThinking).visible),
|
|
@@ -776,6 +790,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
776
790
|
|
|
777
791
|
// Render content in order
|
|
778
792
|
let thinkingIndex = 0;
|
|
793
|
+
let hasRenderedContent = false;
|
|
779
794
|
for (let i = 0; i < message.content.length; i++) {
|
|
780
795
|
const content = message.content[i];
|
|
781
796
|
if (content.type === "text" && canonicalizeMessage(content.text)) {
|
|
@@ -785,6 +800,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
785
800
|
md.transientRenderCache = this.#lastUpdateTransient;
|
|
786
801
|
this.#contentContainer.addChild(md);
|
|
787
802
|
captureItems?.push({ md, contentIndex: i, blockType: "text", lastText: trimmed });
|
|
803
|
+
hasRenderedContent = true;
|
|
788
804
|
} else if (content.type === "thinking" && resolveThinkingDisplay(content, this.proseOnlyThinking).visible) {
|
|
789
805
|
const thinkingText = resolveThinkingDisplay(content, this.proseOnlyThinking).text;
|
|
790
806
|
if (this.hideThinkingBlock) {
|
|
@@ -798,6 +814,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
798
814
|
.some(
|
|
799
815
|
c =>
|
|
800
816
|
(c.type === "text" && canonicalizeMessage(c.text)) ||
|
|
817
|
+
(c.type === "image" && c.data && c.mimeType) ||
|
|
801
818
|
(c.type === "thinking" && resolveThinkingDisplay(c, this.proseOnlyThinking).visible),
|
|
802
819
|
);
|
|
803
820
|
|
|
@@ -810,10 +827,14 @@ export class AssistantMessageComponent extends Container {
|
|
|
810
827
|
this.#contentContainer.addChild(md);
|
|
811
828
|
captureItems?.push({ md, contentIndex: i, blockType: "thinking", lastText: thinkingText });
|
|
812
829
|
this.#appendThinkingExtensions(i, thinkingIndex, thinkingText);
|
|
830
|
+
hasRenderedContent = true;
|
|
813
831
|
thinkingIndex += 1;
|
|
814
832
|
if (hasVisibleContentAfter) {
|
|
815
833
|
this.#contentContainer.addChild(new Spacer(1));
|
|
816
834
|
}
|
|
835
|
+
} else if (content.type === "image" && content.data && content.mimeType) {
|
|
836
|
+
this.#renderImageEntries([{ image: content, key: `native:${i}` }], hasRenderedContent);
|
|
837
|
+
hasRenderedContent ||= this.#showImages;
|
|
817
838
|
}
|
|
818
839
|
}
|
|
819
840
|
|
|
@@ -283,6 +283,7 @@ export class ChatTranscriptBuilder {
|
|
|
283
283
|
this.deps.ui.imageBudget,
|
|
284
284
|
proseOnlyThinking,
|
|
285
285
|
);
|
|
286
|
+
assistantComponent.setImagesVisible(settings.get("terminal.showImages"));
|
|
286
287
|
this.container.addChild(assistantComponent);
|
|
287
288
|
|
|
288
289
|
if (settings.get("display.cacheMissMarker")) {
|
|
@@ -313,6 +314,7 @@ export class ChatTranscriptBuilder {
|
|
|
313
314
|
undefined,
|
|
314
315
|
proseOnlyThinking,
|
|
315
316
|
);
|
|
317
|
+
component.setImagesVisible(settings.get("terminal.showImages"));
|
|
316
318
|
this.container.addChild(component);
|
|
317
319
|
};
|
|
318
320
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { ProcessTerminal, TUI } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { getEditorTheme, initTheme } from "../theme/theme";
|
|
4
|
+
import { CustomEditor } from "./custom-editor";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Regression for issue #4766: plugins written against upstream pi subclass
|
|
8
|
+
* `CustomEditor`/`Editor` and forward `super(tui, theme, keybindings)`. omp's
|
|
9
|
+
* `setEditorComponent` factory contract advertises exactly that arg order, so
|
|
10
|
+
* the base constructor must resolve the real theme by shape (not position) or
|
|
11
|
+
* every render throws `undefined is not an object (evaluating
|
|
12
|
+
* 'this.#theme.symbols.boxRound')`.
|
|
13
|
+
*/
|
|
14
|
+
describe("CustomEditor upstream-pi constructor compatibility (#4766)", () => {
|
|
15
|
+
it("renders when constructed as (tui, theme, keybindings)", async () => {
|
|
16
|
+
await initTheme();
|
|
17
|
+
const tui = new TUI(new ProcessTerminal());
|
|
18
|
+
const editor = new CustomEditor(tui, getEditorTheme(), {});
|
|
19
|
+
editor.setText("run this workflow");
|
|
20
|
+
expect(() => editor.render(80)).not.toThrow();
|
|
21
|
+
// The rounded border glyphs from the resolved theme must reach the frame.
|
|
22
|
+
const frame = editor.render(80).join("\n");
|
|
23
|
+
expect(frame).toContain(getEditorTheme().symbols.boxRound.horizontal);
|
|
24
|
+
// The leading TUI is captured so plugin overrides calling
|
|
25
|
+
// `this.tui.requestRender()` keep working.
|
|
26
|
+
expect(editor.tui).toBe(tui);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("still accepts omp's own (theme) constructor", async () => {
|
|
30
|
+
await initTheme();
|
|
31
|
+
const editor = new CustomEditor(getEditorTheme());
|
|
32
|
+
editor.setText("hello");
|
|
33
|
+
expect(() => editor.render(80)).not.toThrow();
|
|
34
|
+
expect(editor.tui).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
addKeyAliases,
|
|
5
|
+
canonicalKeyId,
|
|
6
|
+
Editor,
|
|
7
|
+
type EditorTheme,
|
|
8
|
+
type KeyId,
|
|
9
|
+
parseKey,
|
|
10
|
+
parseKittySequence,
|
|
11
|
+
TUI,
|
|
12
|
+
} from "@oh-my-pi/pi-tui";
|
|
4
13
|
import { BracketedPasteHandler } from "@oh-my-pi/pi-tui/bracketed-paste";
|
|
5
14
|
import type { AppKeybinding } from "../../config/keybindings";
|
|
6
15
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
@@ -284,6 +293,31 @@ export function extractImagePathFromText(text: string): string | undefined {
|
|
|
284
293
|
return undefined;
|
|
285
294
|
}
|
|
286
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Resolve the {@link EditorTheme} from a `CustomEditor`/`Editor` constructor
|
|
298
|
+
* argument list, tolerating both the omp `(theme)` and upstream-pi
|
|
299
|
+
* `(tui, theme, keybindings)` conventions (see {@link CustomEditor}'s
|
|
300
|
+
* constructor). A real `EditorTheme` is identified structurally — it exposes a
|
|
301
|
+
* `borderColor` function and a `symbols` object — so a `TUI` passed in the first
|
|
302
|
+
* slot is skipped rather than mistaken for the theme.
|
|
303
|
+
*/
|
|
304
|
+
function pickEditorTheme(args: readonly unknown[]): EditorTheme {
|
|
305
|
+
for (const arg of args) {
|
|
306
|
+
if (isEditorTheme(arg)) return arg;
|
|
307
|
+
}
|
|
308
|
+
// Fall back to the first argument so a caller passing a bare theme that
|
|
309
|
+
// somehow fails the shape probe still reaches the base constructor.
|
|
310
|
+
return args[0] as EditorTheme;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function isEditorTheme(value: unknown): value is EditorTheme {
|
|
314
|
+
if (typeof value !== "object" || value === null) return false;
|
|
315
|
+
const candidate = value as Partial<EditorTheme>;
|
|
316
|
+
return (
|
|
317
|
+
typeof candidate.borderColor === "function" && typeof candidate.symbols === "object" && candidate.symbols !== null
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
287
321
|
/**
|
|
288
322
|
* Custom editor that handles configurable app-level shortcuts for coding-agent.
|
|
289
323
|
*/
|
|
@@ -297,6 +331,33 @@ export class CustomEditor extends Editor {
|
|
|
297
331
|
* `undefined` entries are images without a backing reference yet. */
|
|
298
332
|
pendingImageLinks: (string | undefined)[] = [];
|
|
299
333
|
|
|
334
|
+
/**
|
|
335
|
+
* The host {@link TUI}, captured when a plugin constructs this editor through
|
|
336
|
+
* the upstream-pi `(tui, theme, keybindings)` convention. Undefined for omp's
|
|
337
|
+
* own `new CustomEditor(theme)` callers (they drive repaints through the
|
|
338
|
+
* interactive-mode wiring instead). Plugins that call `this.tui.requestRender()`
|
|
339
|
+
* in their overrides read it here (issue #4766).
|
|
340
|
+
*/
|
|
341
|
+
tui?: TUI;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Accept both the omp constructor convention — `new CustomEditor(theme)` —
|
|
345
|
+
* and the upstream-pi `Editor` convention — `new Editor(tui, theme, keybindings)`
|
|
346
|
+
* — that {@link ExtensionUIContext.setEditorComponent}'s factory contract
|
|
347
|
+
* advertises `(tui, theme, keybindings)`. Plugins written against upstream pi
|
|
348
|
+
* subclass `CustomEditor`/`Editor` and forward `super(tui, theme, keybindings)`;
|
|
349
|
+
* without this shim the `TUI` lands in the `theme` slot and every render throws
|
|
350
|
+
* `undefined is not an object (evaluating 'this.#theme.symbols.boxRound')`
|
|
351
|
+
* (issue #4766). We locate the real {@link EditorTheme} among the args by shape
|
|
352
|
+
* (it carries `symbols`/`borderColor`) rather than by position, and capture a
|
|
353
|
+
* leading {@link TUI} so plugin overrides calling `this.tui.requestRender()`
|
|
354
|
+
* keep working.
|
|
355
|
+
*/
|
|
356
|
+
constructor(...args: readonly unknown[]) {
|
|
357
|
+
super(pickEditorTheme(args));
|
|
358
|
+
if (args[0] instanceof TUI) this.tui = args[0];
|
|
359
|
+
}
|
|
360
|
+
|
|
300
361
|
/** Clear the composer draft: optionally commit `historyText` to history, then
|
|
301
362
|
* reset the editor text and all pending draft-image state. The shared tail of
|
|
302
363
|
* every "message submitted" path; pass no argument for a plain discard. */
|