@oh-my-pi/pi-coding-agent 17.2.10 → 17.2.12
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 +53 -0
- package/dist/{CHANGELOG-rhwzpexa.md → CHANGELOG-k9ghy5sn.md} +53 -0
- package/dist/cli.js +10775 -10762
- package/dist/types/advisor/runtime.d.ts +1 -1
- package/dist/types/capability/mcp.d.ts +11 -0
- package/dist/types/capability/skill.d.ts +6 -0
- package/dist/types/cli/command-help.d.ts +3 -0
- package/dist/types/commands/share.d.ts +25 -0
- package/dist/types/commit/agentic/index.d.ts +3 -1
- package/dist/types/commit/execute.d.ts +32 -0
- package/dist/types/commit/index.d.ts +2 -1
- package/dist/types/commit/pipeline.d.ts +7 -1
- package/dist/types/config/custom-models.d.ts +31 -0
- package/dist/types/config/model-config-values.d.ts +19 -0
- package/dist/types/config/model-patch.d.ts +93 -0
- package/dist/types/config/model-provider-discovery.d.ts +51 -0
- package/dist/types/config/model-registry.d.ts +9 -52
- package/dist/types/config/settings-schema.d.ts +1 -34
- package/dist/types/config/settings.d.ts +5 -3
- package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
- package/dist/types/discovery/agent-plugin-format.d.ts +136 -0
- package/dist/types/discovery/agent-plugins.d.ts +1 -0
- package/dist/types/discovery/contained-path.d.ts +33 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/eval/executor-base.d.ts +8 -2
- package/dist/types/eval/kernel-session-registry.d.ts +60 -0
- package/dist/types/export/share.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/loader.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +3 -2
- package/dist/types/extensibility/skills.d.ts +5 -0
- package/dist/types/lsp/diagnostics.d.ts +96 -0
- package/dist/types/lsp/index.d.ts +7 -128
- package/dist/types/lsp/servers.d.ts +72 -0
- package/dist/types/lsp/tool.d.ts +42 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
- package/dist/types/lsp/writethrough.d.ts +33 -0
- package/dist/types/mcp/transports/header-policy.d.ts +46 -0
- package/dist/types/mcp/types.d.ts +15 -0
- package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
- package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
- package/dist/types/modes/components/agent-hub.d.ts +2 -0
- package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -2
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/theme/color.d.ts +19 -0
- package/dist/types/modes/theme/loader.d.ts +19 -0
- package/dist/types/modes/theme/schema.d.ts +175 -0
- package/dist/types/modes/theme/symbols.d.ts +28 -0
- package/dist/types/modes/theme/theme-class.d.ts +244 -0
- package/dist/types/modes/theme/theme.d.ts +9 -280
- package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
- package/dist/types/registry/agent-registry.d.ts +1 -3
- package/dist/types/secrets/index.d.ts +26 -2
- package/dist/types/secrets/message-transform.d.ts +40 -0
- package/dist/types/secrets/obfuscator.d.ts +0 -108
- package/dist/types/secrets/placeholder-scan.d.ts +95 -0
- package/dist/types/secrets/placeholder.d.ts +94 -0
- package/dist/types/secrets/replacement.d.ts +82 -0
- package/dist/types/session/agent-session-events.d.ts +2 -2
- package/dist/types/session/agent-session-types.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +2 -2
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/prewalk.d.ts +3 -1
- package/dist/types/session/session-handoff.d.ts +3 -3
- package/dist/types/session/session-manager.d.ts +32 -0
- package/dist/types/session/session-provider-boundary.d.ts +1 -1
- package/dist/types/session/session-tools.d.ts +8 -0
- package/dist/types/session/turn-recovery.d.ts +9 -4
- package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
- package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
- package/dist/types/slash-commands/builtin-control.d.ts +2 -0
- package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
- package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
- package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/slash-commands/builtin-session.d.ts +2 -0
- package/dist/types/task/index.d.ts +2 -0
- package/dist/types/tools/gh-common.d.ts +69 -0
- package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
- package/dist/types/tools/gh-pr-diff.d.ts +102 -0
- package/dist/types/tools/gh-run-watch.d.ts +78 -0
- package/dist/types/tools/gh-search.d.ts +61 -0
- package/dist/types/tools/gh-types.d.ts +340 -0
- package/dist/types/tools/gh-view.d.ts +88 -0
- package/dist/types/tools/gh.d.ts +4 -201
- package/dist/types/tools/read-archive.d.ts +16 -0
- package/dist/types/tools/read-format.d.ts +104 -0
- package/dist/types/tools/read-path-resolution.d.ts +19 -0
- package/dist/types/tools/read-pdf-images.d.ts +12 -0
- package/dist/types/tools/read-renderer.d.ts +24 -0
- package/dist/types/tools/read-selector.d.ts +27 -0
- package/dist/types/tools/read-sqlite.d.ts +16 -0
- package/dist/types/tools/read-summary.d.ts +19 -0
- package/dist/types/tools/read.d.ts +1 -23
- package/dist/types/tools/shell-tokenize.d.ts +18 -1
- package/dist/types/utils/changelog.d.ts +0 -5
- package/package.json +13 -13
- package/scripts/legacy-pi-virtual-module.ts +4 -1
- package/src/advisor/runtime.ts +12 -7
- package/src/capability/mcp.ts +11 -0
- package/src/capability/skill.ts +6 -0
- package/src/cli/command-help.ts +4 -0
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli-commands.ts +5 -0
- package/src/commands/commit.ts +16 -3
- package/src/commands/share.ts +71 -0
- package/src/commit/agentic/index.ts +39 -21
- package/src/commit/execute.ts +56 -0
- package/src/commit/index.ts +2 -1
- package/src/commit/pipeline.ts +20 -7
- package/src/config/custom-models.ts +188 -0
- package/src/config/model-config-values.ts +128 -0
- package/src/config/model-discovery.ts +1 -1
- package/src/config/model-patch.ts +252 -0
- package/src/config/model-provider-discovery.ts +132 -0
- package/src/config/model-registry.ts +84 -704
- package/src/config/settings-schema.ts +2 -33
- package/src/config/settings.ts +53 -3
- package/src/debug/raw-sse-buffer.ts +20 -0
- package/src/discovery/agent-plugin-format.ts +551 -0
- package/src/discovery/agent-plugins.ts +341 -0
- package/src/discovery/claude-plugins.ts +21 -5
- package/src/discovery/contained-path.ts +75 -0
- package/src/discovery/helpers.ts +23 -9
- package/src/discovery/index.ts +1 -0
- package/src/discovery/omp-plugins.ts +20 -7
- package/src/edit/hashline/diff.ts +5 -1
- package/src/eval/executor-base.ts +39 -6
- package/src/eval/jl/executor.ts +82 -371
- package/src/eval/kernel-session-registry.ts +398 -0
- package/src/eval/py/executor.ts +53 -261
- package/src/eval/rb/executor.ts +36 -279
- package/src/exec/non-interactive-env.ts +1 -0
- package/src/export/share.ts +2 -1
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/loader.ts +78 -14
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +12 -0
- package/src/extensibility/plugins/marketplace/manager.ts +2 -1
- package/src/extensibility/shared-events.ts +3 -2
- package/src/extensibility/skills.ts +9 -0
- package/src/internal-urls/skill-protocol.ts +14 -0
- package/src/internal-urls/vault-protocol.ts +2 -2
- package/src/launch/client.ts +11 -1
- package/src/launch/protocol.ts +7 -2
- package/src/lsp/diagnostics.ts +516 -0
- package/src/lsp/index.ts +20 -2819
- package/src/lsp/servers.ts +296 -0
- package/src/lsp/tool.ts +1352 -0
- package/src/lsp/workspace-diagnostics.ts +170 -0
- package/src/lsp/writethrough.ts +561 -0
- package/src/main.ts +3 -2
- package/src/mcp/config.ts +3 -0
- package/src/mcp/manager.ts +12 -9
- package/src/mcp/transports/header-policy.ts +95 -0
- package/src/mcp/transports/http.ts +35 -52
- package/src/mcp/transports/sse.ts +20 -27
- package/src/mcp/types.ts +15 -0
- package/src/modes/acp/acp-agent.ts +15 -4
- package/src/modes/components/agent-dashboard.ts +2 -0
- package/src/modes/components/agent-hub-projection.ts +2 -2
- package/src/modes/components/agent-hub-renderer.ts +3 -7
- package/src/modes/components/agent-hub.ts +5 -0
- package/src/modes/components/agent-transcript-viewer.ts +3 -0
- package/src/modes/components/chat-transcript-builder.ts +3 -0
- package/src/modes/components/custom-editor.ts +0 -9
- package/src/modes/components/status-line/component.ts +1 -0
- package/src/modes/components/status-line/segments.ts +6 -4
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/tool-execution.ts +13 -16
- package/src/modes/components/tree-selector.ts +62 -3
- package/src/modes/controllers/command-controller.ts +8 -2
- package/src/modes/controllers/event-controller.ts +15 -15
- package/src/modes/controllers/input-controller.ts +20 -2
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/theme/color.ts +133 -0
- package/src/modes/theme/loader.ts +178 -0
- package/src/modes/theme/schema.ts +263 -0
- package/src/modes/theme/symbols.ts +1000 -0
- package/src/modes/theme/theme-class.ts +611 -0
- package/src/modes/theme/theme.ts +27 -2453
- package/src/modes/theme/tui-adapters.ts +279 -0
- package/src/modes/utils/transcript-render-helpers.ts +4 -2
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/registry/agent-registry.ts +2 -2
- package/src/sdk.ts +6 -49
- package/src/secrets/index.ts +51 -6
- package/src/secrets/message-transform.ts +287 -0
- package/src/secrets/obfuscator.ts +39 -1303
- package/src/secrets/placeholder-scan.ts +506 -0
- package/src/secrets/placeholder.ts +309 -0
- package/src/secrets/replacement.ts +216 -0
- package/src/session/agent-session-events.ts +2 -2
- package/src/session/agent-session-types.ts +6 -0
- package/src/session/agent-session.ts +146 -21
- package/src/session/indexed-session-storage.ts +7 -2
- package/src/session/messages.ts +1 -0
- package/src/session/prewalk.ts +57 -17
- package/src/session/session-advisors.ts +32 -34
- package/src/session/session-context.ts +3 -5
- package/src/session/session-handoff.ts +39 -16
- package/src/session/session-manager.ts +67 -3
- package/src/session/session-provider-boundary.ts +3 -6
- package/src/session/session-tools.ts +37 -3
- package/src/session/todo-tracker.ts +11 -1
- package/src/session/turn-recovery.ts +115 -84
- package/src/slash-commands/builtin-collaboration.ts +504 -0
- package/src/slash-commands/builtin-completions.ts +291 -0
- package/src/slash-commands/builtin-control.ts +78 -0
- package/src/slash-commands/builtin-lifecycle.ts +506 -0
- package/src/slash-commands/builtin-marketplace.ts +567 -0
- package/src/slash-commands/builtin-modes.ts +518 -0
- package/src/slash-commands/builtin-registry.ts +21 -2996
- package/src/slash-commands/builtin-session.ts +592 -0
- package/src/task/executor.ts +17 -14
- package/src/task/index.ts +21 -6
- package/src/tools/bash-skill-urls.ts +15 -0
- package/src/tools/bash.ts +10 -11
- package/src/tools/debug.ts +1 -1
- package/src/tools/gh-common.ts +288 -0
- package/src/tools/gh-pr-checkout.ts +679 -0
- package/src/tools/gh-pr-diff.ts +473 -0
- package/src/tools/gh-run-watch.ts +1015 -0
- package/src/tools/gh-search.ts +500 -0
- package/src/tools/gh-types.ts +379 -0
- package/src/tools/gh-view.ts +612 -0
- package/src/tools/gh.ts +109 -3821
- package/src/tools/jtd-to-json-schema.ts +123 -21
- package/src/tools/read-archive.ts +209 -0
- package/src/tools/read-format.ts +593 -0
- package/src/tools/read-path-resolution.ts +36 -0
- package/src/tools/read-pdf-images.ts +250 -0
- package/src/tools/read-renderer.ts +289 -0
- package/src/tools/read-selector.ts +84 -0
- package/src/tools/read-sqlite.ts +215 -0
- package/src/tools/read-summary.ts +199 -0
- package/src/tools/read.ts +82 -1824
- package/src/tools/shell-tokenize.ts +99 -1
- package/src/utils/changelog.ts +1 -1
- package/src/utils/title-generator.ts +3 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/zai.ts +12 -3
|
@@ -23,7 +23,7 @@ import { formatDefaultToolExecution } from "../../tools/default-renderer";
|
|
|
23
23
|
import { EVAL_DEFAULT_PREVIEW_LINES } from "../../tools/eval";
|
|
24
24
|
import { isWaitingPollDetails } from "../../tools/hub";
|
|
25
25
|
import { formatStatusIcon, replaceTabs, resolveImageOptions } from "../../tools/render-utils";
|
|
26
|
-
import { type FirstResultViewportRepaint, toolRenderers } from "../../tools/renderers";
|
|
26
|
+
import { type FirstResultViewportRepaint, type ToolRenderer, toolRenderers } from "../../tools/renderers";
|
|
27
27
|
import { TODO_STRIKE_TOTAL_FRAMES, type TodoToolDetails } from "../../tools/todo";
|
|
28
28
|
import type { XdevState } from "../../tools/xdev";
|
|
29
29
|
import { isFramedBlockComponent, markFramedBlockComponent, renderStatusLine, WidthAwareText } from "../../tui";
|
|
@@ -226,6 +226,8 @@ export interface ToolExecutionOptions {
|
|
|
226
226
|
/** Session-persistent edit clipboard register, forked per preview frame. */
|
|
227
227
|
clipboard?: Clipboard;
|
|
228
228
|
showImages?: boolean; // default: true (only used if terminal supports images)
|
|
229
|
+
/** Allow the name-keyed renderer registry only when the active tool is the built-in implementation. */
|
|
230
|
+
useBuiltInRenderer?: boolean;
|
|
229
231
|
editFuzzyThreshold?: number;
|
|
230
232
|
editAllowFuzzy?: boolean;
|
|
231
233
|
/** Live-region probe used to settle detached task progress once the block
|
|
@@ -312,6 +314,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
312
314
|
// forcing the common image-free result to re-shape on every resize tick.
|
|
313
315
|
#renderedImageCount = 0;
|
|
314
316
|
#tool?: AgentTool;
|
|
317
|
+
#renderer?: ToolRenderer;
|
|
315
318
|
#ui: ToolExecutionUi;
|
|
316
319
|
#cwd: string;
|
|
317
320
|
#result?: {
|
|
@@ -398,6 +401,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
398
401
|
super();
|
|
399
402
|
this.#toolName = toolName;
|
|
400
403
|
this.#toolLabel = tool?.label ?? toolName;
|
|
404
|
+
this.#renderer = options.useBuiltInRenderer === false ? undefined : toolRenderers[toolName];
|
|
401
405
|
this.#showImages = options.showImages ?? true;
|
|
402
406
|
this.#editFuzzyThreshold = options.editFuzzyThreshold;
|
|
403
407
|
this.#editAllowFuzzy = options.editAllowFuzzy;
|
|
@@ -418,10 +422,9 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
418
422
|
this.#contentBox = new Box(0, 1);
|
|
419
423
|
this.#contentText = new WidthAwareText(contentWidth => this.#renderDefaultCard(contentWidth), 1, 1);
|
|
420
424
|
|
|
421
|
-
// Use Box for custom tools or built-in tools
|
|
422
|
-
const hasRenderer = toolName in toolRenderers;
|
|
425
|
+
// Use Box for custom tools or built-in tools with rich renderers.
|
|
423
426
|
const hasCustomRenderer = !!(tool?.renderCall || tool?.renderResult);
|
|
424
|
-
this.#usesContentBox = hasCustomRenderer ||
|
|
427
|
+
this.#usesContentBox = hasCustomRenderer || this.#renderer !== undefined;
|
|
425
428
|
if (this.#usesContentBox) {
|
|
426
429
|
this.addChild(this.#contentBox);
|
|
427
430
|
} else {
|
|
@@ -668,12 +671,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
668
671
|
const isStreamingArgs = !this.#argsComplete && (isEditLikeToolName(this.#toolName) || this.#toolName === "write");
|
|
669
672
|
const isBackgroundAsyncRunning =
|
|
670
673
|
(this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
|
|
671
|
-
const renderer =
|
|
672
|
-
| {
|
|
673
|
-
animatedPendingPreview?: boolean | ((args: unknown) => boolean);
|
|
674
|
-
animatedPartialResult?: boolean | ((args: unknown) => boolean);
|
|
675
|
-
}
|
|
676
|
-
| undefined;
|
|
674
|
+
const renderer = this.#renderer;
|
|
677
675
|
const pendingAnimation = renderer?.animatedPendingPreview;
|
|
678
676
|
const partialAnimation = renderer?.animatedPartialResult;
|
|
679
677
|
const pendingCallConsumesSpinner =
|
|
@@ -933,7 +931,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
933
931
|
|
|
934
932
|
#rendererFlag(name: "forceResultViewportRepaintOnSettle"): boolean {
|
|
935
933
|
const toolValue = (this.#tool as Record<string, unknown> | undefined)?.[name];
|
|
936
|
-
const rendererValue =
|
|
934
|
+
const rendererValue = this.#renderer?.[name];
|
|
937
935
|
return toolValue === true || (toolValue === undefined && rendererValue === true);
|
|
938
936
|
}
|
|
939
937
|
|
|
@@ -948,8 +946,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
948
946
|
if (this.#result !== undefined) return false;
|
|
949
947
|
const toolValue = (this.#tool as { forceFirstResultViewportRepaint?: FirstResultViewportRepaint } | undefined)
|
|
950
948
|
?.forceFirstResultViewportRepaint;
|
|
951
|
-
const value =
|
|
952
|
-
toolValue !== undefined ? toolValue : toolRenderers[this.#toolName]?.forceFirstResultViewportRepaint;
|
|
949
|
+
const value = toolValue !== undefined ? toolValue : this.#renderer?.forceFirstResultViewportRepaint;
|
|
953
950
|
if (typeof value === "function") return value(this.#args, this.#renderState);
|
|
954
951
|
return value === true;
|
|
955
952
|
}
|
|
@@ -1101,9 +1098,9 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
1101
1098
|
const customFramed = this.#contentBox.children.some(isFramedBlockComponent);
|
|
1102
1099
|
this.#contentBox.setPaddingX(customFramed ? 0 : 1);
|
|
1103
1100
|
this.#contentBox.setBgFn(customFramed ? undefined : stateBgFn);
|
|
1104
|
-
} else if (this.#
|
|
1105
|
-
//
|
|
1106
|
-
const renderer =
|
|
1101
|
+
} else if (this.#renderer) {
|
|
1102
|
+
// The active registry entry is a built-in tool with a rich renderer.
|
|
1103
|
+
const renderer = this.#renderer;
|
|
1107
1104
|
|
|
1108
1105
|
// Clean up previous multi-file boxes
|
|
1109
1106
|
for (const box of this.#multiFileBoxes) {
|
|
@@ -286,12 +286,20 @@ class TreeList implements Component {
|
|
|
286
286
|
|
|
287
287
|
// Apply filter mode
|
|
288
288
|
let passesFilter = true;
|
|
289
|
-
// Entry types hidden in default view (settings/bookkeeping)
|
|
289
|
+
// Entry types hidden in default view (settings/bookkeeping). These carry
|
|
290
|
+
// no conversation content, so the tree only shows them in "all" mode.
|
|
290
291
|
const isSettingsEntry =
|
|
291
292
|
entry.type === "label" ||
|
|
292
293
|
entry.type === "custom" ||
|
|
293
294
|
entry.type === "model_change" ||
|
|
294
|
-
entry.type === "thinking_level_change"
|
|
295
|
+
entry.type === "thinking_level_change" ||
|
|
296
|
+
entry.type === "service_tier_change" ||
|
|
297
|
+
entry.type === "title_change" ||
|
|
298
|
+
entry.type === "credential_pin" ||
|
|
299
|
+
entry.type === "session_init" ||
|
|
300
|
+
entry.type === "ttsr_injection" ||
|
|
301
|
+
entry.type === "mode_change" ||
|
|
302
|
+
entry.type === "reset_boundary";
|
|
295
303
|
|
|
296
304
|
switch (this.#filterMode) {
|
|
297
305
|
case "user-only":
|
|
@@ -390,6 +398,34 @@ class TreeList implements Component {
|
|
|
390
398
|
case "label":
|
|
391
399
|
parts.push("label", entry.label ?? "");
|
|
392
400
|
break;
|
|
401
|
+
case "service_tier_change":
|
|
402
|
+
parts.push("service tier");
|
|
403
|
+
if (entry.serviceTier) {
|
|
404
|
+
const serviceTier = entry.serviceTier;
|
|
405
|
+
for (const family in serviceTier) {
|
|
406
|
+
const tier = serviceTier[family as keyof typeof serviceTier];
|
|
407
|
+
if (tier) parts.push(family, tier);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
case "title_change":
|
|
412
|
+
parts.push("title", entry.title);
|
|
413
|
+
break;
|
|
414
|
+
case "mode_change":
|
|
415
|
+
parts.push("mode", entry.mode);
|
|
416
|
+
break;
|
|
417
|
+
case "credential_pin":
|
|
418
|
+
parts.push("credential pin", entry.provider);
|
|
419
|
+
break;
|
|
420
|
+
case "ttsr_injection":
|
|
421
|
+
parts.push("ttsr injection", ...entry.injectedRules);
|
|
422
|
+
break;
|
|
423
|
+
case "reset_boundary":
|
|
424
|
+
parts.push("reset boundary");
|
|
425
|
+
break;
|
|
426
|
+
case "session_init":
|
|
427
|
+
parts.push("session init");
|
|
428
|
+
break;
|
|
393
429
|
}
|
|
394
430
|
|
|
395
431
|
return parts.join(" ");
|
|
@@ -658,8 +694,31 @@ class TreeList implements Component {
|
|
|
658
694
|
case "label":
|
|
659
695
|
result = theme.fg("dim", `[label: ${entry.label ?? "(cleared)"}]`);
|
|
660
696
|
break;
|
|
697
|
+
case "service_tier_change": {
|
|
698
|
+
// Per-family map, or null when the session went back to the default.
|
|
699
|
+
const tiers = entry.serviceTier
|
|
700
|
+
? Object.entries(entry.serviceTier)
|
|
701
|
+
.map(([family, tier]) => `${family}:${tier}`)
|
|
702
|
+
.join(" ")
|
|
703
|
+
: "(default)";
|
|
704
|
+
result = theme.fg("dim", `[service tier: ${tiers}]`);
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
case "title_change":
|
|
708
|
+
result = theme.fg("dim", `[title: ${normalize(entry.title)}]`);
|
|
709
|
+
break;
|
|
710
|
+
case "mode_change":
|
|
711
|
+
result = theme.fg("dim", `[mode: ${entry.mode}]`);
|
|
712
|
+
break;
|
|
713
|
+
case "credential_pin":
|
|
714
|
+
result = theme.fg("dim", `[credential pin: ${entry.provider}]`);
|
|
715
|
+
break;
|
|
661
716
|
default:
|
|
662
|
-
|
|
717
|
+
// Bookkeeping entries with nothing worth spelling out still get their
|
|
718
|
+
// type. A row that renders to the empty string is worse than a
|
|
719
|
+
// useless one: it draws as a bare bullet with no way to tell what it
|
|
720
|
+
// is or why the tree has a gap in it.
|
|
721
|
+
result = theme.fg("dim", `[${entry.type.replaceAll("_", " ")}]`);
|
|
663
722
|
}
|
|
664
723
|
|
|
665
724
|
return isSelected ? theme.bold(result) : result;
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type UsageReport,
|
|
12
12
|
} from "@oh-my-pi/pi-ai";
|
|
13
13
|
import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
14
|
-
import { formatDuration, Snowflake, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import { formatDuration, logger, Snowflake, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
15
15
|
import { shouldEnableAppendOnlyContext } from "../../config/append-only-context-mode";
|
|
16
16
|
import { type BashResult, isPersistentShellCdCommand } from "../../exec/bash-executor";
|
|
17
17
|
import { type LoadedCustomShare, loadCustomShare } from "../../export/custom-share";
|
|
@@ -1436,9 +1436,15 @@ export class CommandController {
|
|
|
1436
1436
|
}
|
|
1437
1437
|
} catch (error) {
|
|
1438
1438
|
const message = error instanceof Error ? error.message : String(error);
|
|
1439
|
-
|
|
1439
|
+
// `session.handoff()` normalizes genuine cancellations to this exact message; a
|
|
1440
|
+
// provider error (even one named AbortError) is re-thrown verbatim so it surfaces
|
|
1441
|
+
// as a real failure instead of a false "cancelled".
|
|
1442
|
+
if (message === "Handoff cancelled") {
|
|
1440
1443
|
this.ctx.showError("Handoff cancelled");
|
|
1441
1444
|
} else {
|
|
1445
|
+
// Persist the real failure so it is debuggable after the transient
|
|
1446
|
+
// TUI error clears (#7993).
|
|
1447
|
+
logger.error("Handoff failed", { error: message });
|
|
1442
1448
|
this.ctx.showError(`Handoff failed: ${message}`);
|
|
1443
1449
|
}
|
|
1444
1450
|
} finally {
|
|
@@ -1073,6 +1073,7 @@ export class EventController {
|
|
|
1073
1073
|
content.name,
|
|
1074
1074
|
renderArgs,
|
|
1075
1075
|
{
|
|
1076
|
+
useBuiltInRenderer: this.ctx.viewSession.hasBuiltInTool(content.name),
|
|
1076
1077
|
snapshots: getFileSnapshotStore(this.ctx.viewSession),
|
|
1077
1078
|
clipboard: getEditClipboard(this.ctx.viewSession),
|
|
1078
1079
|
showImages: settings.get("terminal.showImages"),
|
|
@@ -1322,6 +1323,7 @@ export class EventController {
|
|
|
1322
1323
|
event.toolName,
|
|
1323
1324
|
event.args,
|
|
1324
1325
|
{
|
|
1326
|
+
useBuiltInRenderer: this.ctx.viewSession.hasBuiltInTool(event.toolName),
|
|
1325
1327
|
snapshots: getFileSnapshotStore(this.ctx.viewSession),
|
|
1326
1328
|
clipboard: getEditClipboard(this.ctx.viewSession),
|
|
1327
1329
|
showImages: settings.get("terminal.showImages"),
|
|
@@ -1920,21 +1922,19 @@ export class EventController {
|
|
|
1920
1922
|
this.ctx.retryLoader = undefined;
|
|
1921
1923
|
this.ctx.statusContainer.disposeChildren();
|
|
1922
1924
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
} else {
|
|
1937
|
-
this.#clearRetrySupersededAssistantComponents();
|
|
1925
|
+
let appliedRetryUpdate = false;
|
|
1926
|
+
for (const retryError of event.retryErrors ?? []) {
|
|
1927
|
+
const component = this.#takeRetrySupersededAssistantComponent(retryError.persistenceKey);
|
|
1928
|
+
if (!component) continue;
|
|
1929
|
+
component.applyRetryRecovery(retryError.retryRecovery);
|
|
1930
|
+
if (this.#pinnedErrorComponent === component) this.#pinnedErrorComponent = undefined;
|
|
1931
|
+
appliedRetryUpdate = true;
|
|
1932
|
+
}
|
|
1933
|
+
if (appliedRetryUpdate || (event.retryErrors?.length ?? 0) > 0) {
|
|
1934
|
+
this.ctx.clearPinnedError();
|
|
1935
|
+
}
|
|
1936
|
+
this.#clearRetrySupersededAssistantComponents();
|
|
1937
|
+
if (!event.success) {
|
|
1938
1938
|
this.ctx.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
|
|
1939
1939
|
}
|
|
1940
1940
|
this.#ensureWorkingLoaderWhileStreaming();
|
|
@@ -13,6 +13,7 @@ import { renderSegmentTrack } from "../../modes/components/segment-track";
|
|
|
13
13
|
import { StrippedToolCallsPlaceholder } from "../../modes/components/stripped-tool-calls-placeholder";
|
|
14
14
|
import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
|
|
15
15
|
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
16
|
+
import { TreeSelectorComponent } from "../../modes/components/tree-selector";
|
|
16
17
|
import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
17
18
|
import { materializeImageReferenceLinks, shiftImageMarkers } from "../../modes/image-references";
|
|
18
19
|
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
@@ -177,6 +178,7 @@ export class InputController {
|
|
|
177
178
|
#focusedPasteListenerInstalled = false;
|
|
178
179
|
#btwBranchListenerInstalled = false;
|
|
179
180
|
#btwCopyListenerInstalled = false;
|
|
181
|
+
#expandToolsListenerInstalled = false;
|
|
180
182
|
// Tap counter for the double-← gesture; reset whenever a quiet gap
|
|
181
183
|
// (>= LEFT_DOUBLE_TAP_MAX_GAP_MS) starts a fresh sequence. See
|
|
182
184
|
// #detectLeftDoubleTap.
|
|
@@ -278,6 +280,24 @@ export class InputController {
|
|
|
278
280
|
return { consume: true };
|
|
279
281
|
});
|
|
280
282
|
}
|
|
283
|
+
if (!this.#expandToolsListenerInstalled) {
|
|
284
|
+
this.#expandToolsListenerInstalled = true;
|
|
285
|
+
// `app.tools.expand` (Ctrl+O) toggles the transcript's tool-output
|
|
286
|
+
// preview. It must fire regardless of focus so a truncated edit stays
|
|
287
|
+
// expandable while an approval prompt / select dialog holds keyboard
|
|
288
|
+
// focus (#7837). Defers when the main transcript is not the active
|
|
289
|
+
// surface (a fullscreen/anchored overlay — agent hub, transcript
|
|
290
|
+
// viewer, log viewer, model picker) or when the focused component
|
|
291
|
+
// rebinds Ctrl+O for its own use (the tree selector's filter cycle).
|
|
292
|
+
this.ctx.ui.addInputListener(data => {
|
|
293
|
+
if (!this.ctx.keybindings.matches(data, "app.tools.expand")) return undefined;
|
|
294
|
+
if (this.ctx.ui.hasOverlay()) return undefined;
|
|
295
|
+
if (this.ctx.ui.getFocused() instanceof TreeSelectorComponent && matchesKey(data, "ctrl+o"))
|
|
296
|
+
return undefined;
|
|
297
|
+
this.toggleToolOutputExpansion();
|
|
298
|
+
return { consume: true };
|
|
299
|
+
});
|
|
300
|
+
}
|
|
281
301
|
this.ctx.editor.onEscape = () => {
|
|
282
302
|
// Side-channel panels are the topmost view. Esc dismisses them before
|
|
283
303
|
// touching loop mode, maintenance, or the underlying main turn.
|
|
@@ -456,8 +476,6 @@ export class InputController {
|
|
|
456
476
|
this.ctx.keybindings.getKeys("app.clipboard.copyPrompt"),
|
|
457
477
|
);
|
|
458
478
|
this.ctx.editor.onCopyPrompt = () => this.handleCopyPrompt();
|
|
459
|
-
this.ctx.editor.setActionKeys("app.tools.expand", this.ctx.keybindings.getKeys("app.tools.expand"));
|
|
460
|
-
this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
|
|
461
479
|
this.ctx.editor.setActionKeys(
|
|
462
480
|
"app.tools.toggleVisibility",
|
|
463
481
|
this.ctx.keybindings.getKeys("app.tools.toggleVisibility"),
|
|
@@ -2026,6 +2026,7 @@ export class SelectorController {
|
|
|
2026
2026
|
remote: this.ctx.collabGuest?.hubRemote,
|
|
2027
2027
|
ui: this.ctx.ui,
|
|
2028
2028
|
getTool: name => this.ctx.session.getToolByName(name),
|
|
2029
|
+
isBuiltInTool: name => this.ctx.session.hasBuiltInTool(name),
|
|
2029
2030
|
getMessageRenderer: type => this.ctx.session.extensionRunner?.getMessageRenderer(type),
|
|
2030
2031
|
cwd: this.ctx.sessionManager.getCwd(),
|
|
2031
2032
|
hideThinkingBlock: () => this.ctx.effectiveHideThinkingBlock,
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ColorMode, ColorValue } from "./schema";
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Color Utilities
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export function detectColorMode(): ColorMode {
|
|
8
|
+
const colorterm = Bun.env.COLORTERM;
|
|
9
|
+
if (colorterm === "truecolor" || colorterm === "24bit") {
|
|
10
|
+
return "truecolor";
|
|
11
|
+
}
|
|
12
|
+
// Windows Terminal supports truecolor
|
|
13
|
+
if (Bun.env.WT_SESSION) {
|
|
14
|
+
return "truecolor";
|
|
15
|
+
}
|
|
16
|
+
const term = Bun.env.TERM || "";
|
|
17
|
+
// Only fall back to 256color for truly limited terminals
|
|
18
|
+
if (term === "dumb" || term === "" || term === "linux") {
|
|
19
|
+
return "256color";
|
|
20
|
+
}
|
|
21
|
+
// Assume truecolor for everything else - virtually all modern terminals support it
|
|
22
|
+
return "truecolor";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function colorToAnsi(color: string, mode: ColorMode): string {
|
|
26
|
+
const format = mode === "truecolor" ? "ansi-16m" : "ansi-256";
|
|
27
|
+
const ansi = Bun.color(color, format);
|
|
28
|
+
if (ansi === null) {
|
|
29
|
+
throw new Error(`Invalid color value: ${color}`);
|
|
30
|
+
}
|
|
31
|
+
return ansi;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function fgAnsi(color: string | number, mode: ColorMode): string {
|
|
35
|
+
if (color === "") return "\x1b[39m";
|
|
36
|
+
if (typeof color === "number") return `\x1b[38;5;${color}m`;
|
|
37
|
+
if (typeof color === "string") {
|
|
38
|
+
return colorToAnsi(color, mode);
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`Invalid color value: ${color}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function bgAnsi(color: string | number, mode: ColorMode): string {
|
|
44
|
+
if (color === "") return "\x1b[49m";
|
|
45
|
+
if (typeof color === "number") return `\x1b[48;5;${color}m`;
|
|
46
|
+
const ansi = colorToAnsi(color, mode);
|
|
47
|
+
return ansi.replace("\x1b[38;", "\x1b[48;");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveVarRefs(
|
|
51
|
+
value: ColorValue,
|
|
52
|
+
vars: Record<string, ColorValue>,
|
|
53
|
+
visited = new Set<string>(),
|
|
54
|
+
): string | number {
|
|
55
|
+
if (typeof value === "number" || value === "" || value.startsWith("#")) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
if (visited.has(value)) {
|
|
59
|
+
throw new Error(`Circular variable reference detected: ${value}`);
|
|
60
|
+
}
|
|
61
|
+
if (!(value in vars)) {
|
|
62
|
+
throw new Error(`Variable reference not found: ${value}`);
|
|
63
|
+
}
|
|
64
|
+
visited.add(value);
|
|
65
|
+
return resolveVarRefs(vars[value], vars, visited);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resolveThemeColors<T extends Record<string, ColorValue>>(
|
|
69
|
+
colors: T,
|
|
70
|
+
vars: Record<string, ColorValue> = {},
|
|
71
|
+
): Record<keyof T, string | number> {
|
|
72
|
+
const resolved: Record<string, string | number> = {};
|
|
73
|
+
for (const [key, value] of Object.entries(colors)) {
|
|
74
|
+
resolved[key] = resolveVarRefs(value, vars);
|
|
75
|
+
}
|
|
76
|
+
return resolved as Record<keyof T, string | number>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a theme color value (hex string or 256-color index) to a CSS hex string.
|
|
81
|
+
* Empty string represents the default terminal color.
|
|
82
|
+
*/
|
|
83
|
+
export function resolveToHex(value: string | number, isLight: boolean): string {
|
|
84
|
+
if (typeof value === "number") return ansi256ToHex(value);
|
|
85
|
+
if (value === "") return isLight ? "#000000" : "#e5e5e7";
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Convert a 256-color index to hex string.
|
|
91
|
+
* Indices 0-15: basic colors (approximate)
|
|
92
|
+
* Indices 16-231: 6x6x6 color cube
|
|
93
|
+
* Indices 232-255: grayscale ramp
|
|
94
|
+
*/
|
|
95
|
+
export function ansi256ToHex(index: number): string {
|
|
96
|
+
// Basic colors (0-15) - approximate common terminal values
|
|
97
|
+
const basicColors = [
|
|
98
|
+
"#000000",
|
|
99
|
+
"#800000",
|
|
100
|
+
"#008000",
|
|
101
|
+
"#808000",
|
|
102
|
+
"#000080",
|
|
103
|
+
"#800080",
|
|
104
|
+
"#008080",
|
|
105
|
+
"#c0c0c0",
|
|
106
|
+
"#808080",
|
|
107
|
+
"#ff0000",
|
|
108
|
+
"#00ff00",
|
|
109
|
+
"#ffff00",
|
|
110
|
+
"#0000ff",
|
|
111
|
+
"#ff00ff",
|
|
112
|
+
"#00ffff",
|
|
113
|
+
"#ffffff",
|
|
114
|
+
];
|
|
115
|
+
if (index < 16) {
|
|
116
|
+
return basicColors[index];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Color cube (16-231): 6x6x6 = 216 colors
|
|
120
|
+
if (index < 232) {
|
|
121
|
+
const cubeIndex = index - 16;
|
|
122
|
+
const r = Math.floor(cubeIndex / 36);
|
|
123
|
+
const g = Math.floor((cubeIndex % 36) / 6);
|
|
124
|
+
const b = cubeIndex % 6;
|
|
125
|
+
const toHex = (n: number) => (n === 0 ? 0 : 55 + n * 40).toString(16).padStart(2, "0");
|
|
126
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Grayscale (232-255): 24 shades
|
|
130
|
+
const gray = 8 + (index - 232) * 10;
|
|
131
|
+
const grayHex = gray.toString(16).padStart(2, "0");
|
|
132
|
+
return `#${grayHex}${grayHex}${grayHex}`;
|
|
133
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { type } from "@oh-my-pi/omptype";
|
|
4
|
+
import { adjustHsv, getCustomThemesDir, isEnoent } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { detectColorMode, resolveThemeColors } from "./color";
|
|
6
|
+
import darkThemeJson from "./dark.json" with { type: "json" };
|
|
7
|
+
import { defaultThemes } from "./defaults";
|
|
8
|
+
import lightThemeJson from "./light.json" with { type: "json" };
|
|
9
|
+
import { type ColorMode, type ThemeBg, type ThemeColor, type ThemeJson, themeJsonSchema } from "./schema";
|
|
10
|
+
import { normalizeSpinnerFramesOverride, type SymbolPreset } from "./symbols";
|
|
11
|
+
import { Theme } from "./theme-class";
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Theme Loading
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
const BUILTIN_THEMES: Record<string, ThemeJson> = {
|
|
18
|
+
dark: darkThemeJson as ThemeJson,
|
|
19
|
+
light: lightThemeJson as ThemeJson,
|
|
20
|
+
...(defaultThemes as Record<string, ThemeJson>),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function getBuiltinThemes(): Record<string, ThemeJson> {
|
|
24
|
+
return BUILTIN_THEMES;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getAvailableThemes(): Promise<string[]> {
|
|
28
|
+
const themes = new Set<string>(Object.keys(getBuiltinThemes()));
|
|
29
|
+
const customThemesDir = getCustomThemesDir();
|
|
30
|
+
try {
|
|
31
|
+
const files = await fs.promises.readdir(customThemesDir);
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
if (file.endsWith(".json")) {
|
|
34
|
+
themes.add(file.slice(0, -5));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Directory doesn't exist or isn't readable
|
|
39
|
+
}
|
|
40
|
+
return Array.from(themes).sort();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ThemeInfo {
|
|
44
|
+
name: string;
|
|
45
|
+
path: string | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function getAvailableThemesWithPaths(): Promise<ThemeInfo[]> {
|
|
49
|
+
const result: ThemeInfo[] = [];
|
|
50
|
+
|
|
51
|
+
// Built-in themes (embedded, no file path)
|
|
52
|
+
for (const name of Object.keys(getBuiltinThemes())) {
|
|
53
|
+
result.push({ name, path: undefined });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Custom themes
|
|
57
|
+
const customThemesDir = getCustomThemesDir();
|
|
58
|
+
try {
|
|
59
|
+
const files = await fs.promises.readdir(customThemesDir);
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
if (file.endsWith(".json")) {
|
|
62
|
+
const name = file.slice(0, -5);
|
|
63
|
+
if (!result.some(themeInfo => themeInfo.name === name)) {
|
|
64
|
+
result.push({ name, path: path.join(customThemesDir, file) });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// Directory doesn't exist or isn't readable
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return result.sort((a, b) => a.name.localeCompare(b.name));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function loadThemeJson(name: string): Promise<ThemeJson> {
|
|
76
|
+
const builtinThemes = getBuiltinThemes();
|
|
77
|
+
if (name in builtinThemes) {
|
|
78
|
+
return builtinThemes[name];
|
|
79
|
+
}
|
|
80
|
+
const customThemesDir = getCustomThemesDir();
|
|
81
|
+
const themePath = path.join(customThemesDir, `${name}.json`);
|
|
82
|
+
let content: string;
|
|
83
|
+
try {
|
|
84
|
+
content = await Bun.file(themePath).text();
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (isEnoent(err)) throw new Error(`Theme not found: ${name}`);
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
let json: unknown;
|
|
90
|
+
try {
|
|
91
|
+
json = JSON.parse(content);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw new Error(`Failed to parse theme ${name}: ${error}`);
|
|
94
|
+
}
|
|
95
|
+
let parsed: ThemeJson;
|
|
96
|
+
try {
|
|
97
|
+
parsed = themeJsonSchema(json) as ThemeJson;
|
|
98
|
+
if (parsed instanceof type.errors) {
|
|
99
|
+
throw new Error(parsed.summary);
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
103
|
+
// Extract color key information if available
|
|
104
|
+
const missingColorMatch = errorMessage.match(/missing keys: (.+)/i);
|
|
105
|
+
const missingColors: string[] = missingColorMatch ? missingColorMatch[1].split(",").map(s => s.trim()) : [];
|
|
106
|
+
|
|
107
|
+
let fullErrorMessage = `Invalid theme "${name}":\n`;
|
|
108
|
+
if (missingColors.length > 0) {
|
|
109
|
+
fullErrorMessage += `\nMissing required color tokens:\n`;
|
|
110
|
+
fullErrorMessage += missingColors.map(c => ` - ${c}`).join("\n");
|
|
111
|
+
fullErrorMessage += `\n\nPlease add these colors to your theme's "colors" object.`;
|
|
112
|
+
fullErrorMessage += `\nSee the built-in themes (dark.json, light.json) for reference values.`;
|
|
113
|
+
}
|
|
114
|
+
fullErrorMessage += `\n\nValidation error:\n - ${errorMessage}`;
|
|
115
|
+
|
|
116
|
+
throw new Error(fullErrorMessage);
|
|
117
|
+
}
|
|
118
|
+
return parsed;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface CreateThemeOptions {
|
|
122
|
+
mode?: ColorMode;
|
|
123
|
+
symbolPresetOverride?: SymbolPreset;
|
|
124
|
+
colorBlindMode?: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** HSV adjustment to shift green toward blue for colorblind mode (red-green colorblindness) */
|
|
128
|
+
const COLORBLIND_ADJUSTMENT = { h: 60, s: 0.71 };
|
|
129
|
+
|
|
130
|
+
export function createTheme(themeJson: ThemeJson, options: CreateThemeOptions = {}): Theme {
|
|
131
|
+
const { mode, symbolPresetOverride, colorBlindMode } = options;
|
|
132
|
+
const colorMode = mode ?? detectColorMode();
|
|
133
|
+
const resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars);
|
|
134
|
+
|
|
135
|
+
if (colorBlindMode) {
|
|
136
|
+
const added = resolvedColors.toolDiffAdded;
|
|
137
|
+
if (typeof added === "string" && added.startsWith("#")) {
|
|
138
|
+
resolvedColors.toolDiffAdded = adjustHsv(added, COLORBLIND_ADJUSTMENT);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const fgColors: Record<ThemeColor, string | number> = {} as Record<ThemeColor, string | number>;
|
|
143
|
+
const bgColors: Record<ThemeBg, string | number> = {} as Record<ThemeBg, string | number>;
|
|
144
|
+
const bgColorKeys: Set<string> = new Set([
|
|
145
|
+
"selectedBg",
|
|
146
|
+
"userMessageBg",
|
|
147
|
+
"customMessageBg",
|
|
148
|
+
"toolPendingBg",
|
|
149
|
+
"toolSuccessBg",
|
|
150
|
+
"toolErrorBg",
|
|
151
|
+
"statusLineBg",
|
|
152
|
+
]);
|
|
153
|
+
for (const [key, value] of Object.entries(resolvedColors)) {
|
|
154
|
+
if (bgColorKeys.has(key)) {
|
|
155
|
+
bgColors[key as ThemeBg] = value;
|
|
156
|
+
} else {
|
|
157
|
+
fgColors[key as ThemeColor] = value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Extract symbol configuration - settings override takes precedence over theme
|
|
161
|
+
const symbolPreset: SymbolPreset = symbolPresetOverride ?? themeJson.symbols?.preset ?? "unicode";
|
|
162
|
+
const symbolOverrides = themeJson.symbols?.overrides ?? {};
|
|
163
|
+
const spinnerFramesOverrides = normalizeSpinnerFramesOverride(themeJson.symbols?.spinnerFrames);
|
|
164
|
+
return new Theme(fgColors, bgColors, colorMode, symbolPreset, symbolOverrides, spinnerFramesOverrides);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function loadTheme(name: string, options: CreateThemeOptions = {}): Promise<Theme> {
|
|
168
|
+
const themeJson = await loadThemeJson(name);
|
|
169
|
+
return createTheme(themeJson, options);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function getThemeByName(name: string): Promise<Theme | undefined> {
|
|
173
|
+
try {
|
|
174
|
+
return await loadTheme(name);
|
|
175
|
+
} catch {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|