@oh-my-pi/pi-coding-agent 15.8.3 → 15.9.1
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 +113 -31
- package/dist/types/capability/skill.d.ts +7 -0
- package/dist/types/cli/update-cli.d.ts +15 -1
- package/dist/types/config/append-only-context-mode.d.ts +8 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +15 -0
- package/dist/types/config/settings-schema.d.ts +38 -24
- package/dist/types/debug/protocol-probe.d.ts +38 -0
- package/dist/types/debug/terminal-info.d.ts +34 -0
- package/dist/types/exa/mcp-client.d.ts +2 -1
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mcp/json-rpc.d.ts +6 -1
- package/dist/types/mcp/tool-bridge.d.ts +4 -0
- package/dist/types/mcp/transports/stdio.d.ts +16 -7
- package/dist/types/mnemopi/state.d.ts +2 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/assistant-message.d.ts +3 -2
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +11 -0
- package/dist/types/modes/components/plugin-settings.d.ts +40 -8
- package/dist/types/modes/components/session-selector.d.ts +8 -3
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/components/todo-reminder.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +36 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +20 -1
- package/dist/types/modes/utils/keybinding-matchers.d.ts +4 -0
- package/dist/types/plan-mode/plan-handoff.d.ts +20 -0
- package/dist/types/session/agent-session.d.ts +5 -2
- package/dist/types/session/history-storage.d.ts +3 -4
- package/dist/types/session/indexed-session-storage.d.ts +59 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/redis-session-storage.d.ts +12 -85
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-storage.d.ts +5 -7
- package/dist/types/session/sql-session-storage.d.ts +16 -85
- package/dist/types/slash-commands/types.d.ts +17 -4
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/telemetry-export.d.ts +19 -0
- package/dist/types/tiny/compiled-runtime.d.ts +35 -0
- package/dist/types/tiny/text.d.ts +17 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/index.d.ts +4 -2
- package/dist/types/tools/path-utils.d.ts +1 -1
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/{todo-write.d.ts → todo.d.ts} +18 -18
- package/dist/types/utils/session-color.d.ts +7 -2
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +2 -2
- package/dist/types/web/search/providers/base.d.ts +14 -0
- package/dist/types/web/search/providers/exa.d.ts +9 -0
- package/dist/types/web/search/providers/perplexity.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +66 -1
- package/package.json +15 -9
- package/scripts/build-binary.ts +12 -0
- package/src/capability/skill.ts +7 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/session-picker.ts +1 -0
- package/src/cli/update-cli.ts +54 -2
- package/src/commands/completions.ts +1 -1
- package/src/config/append-only-context-mode.ts +37 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +24 -57
- package/src/debug/index.ts +67 -1
- package/src/debug/protocol-probe.ts +267 -0
- package/src/debug/terminal-info.ts +127 -0
- package/src/exa/mcp-client.ts +11 -5
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -3
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +3 -3
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/main.ts +16 -2
- package/src/mcp/json-rpc.ts +8 -0
- package/src/mcp/render.ts +3 -0
- package/src/mcp/tool-bridge.ts +10 -2
- package/src/mcp/transports/http.ts +33 -16
- package/src/mcp/transports/stdio.ts +37 -12
- package/src/mnemopi/state.ts +4 -4
- package/src/modes/acp/acp-agent.ts +168 -3
- package/src/modes/acp/acp-event-mapper.ts +7 -7
- package/src/modes/components/agent-dashboard.ts +103 -31
- package/src/modes/components/assistant-message.ts +3 -2
- package/src/modes/components/extensions/extension-dashboard.ts +56 -10
- package/src/modes/components/history-search.ts +128 -14
- package/src/modes/components/hook-selector.ts +149 -14
- package/src/modes/components/plugin-settings.ts +270 -36
- package/src/modes/components/session-selector.ts +81 -19
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +2 -1
- package/src/modes/components/status-line.ts +1 -1
- package/src/modes/components/tips.txt +6 -2
- package/src/modes/components/todo-reminder.ts +1 -1
- package/src/modes/components/tool-execution.ts +9 -4
- package/src/modes/components/transcript-container.ts +109 -0
- package/src/modes/controllers/command-controller.ts +4 -3
- package/src/modes/controllers/event-controller.ts +12 -7
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/input-controller.ts +10 -5
- package/src/modes/controllers/selector-controller.ts +30 -19
- package/src/modes/controllers/todo-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +56 -9
- package/src/modes/print-mode.ts +5 -0
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/modes/theme/theme.ts +48 -8
- package/src/modes/utils/keybinding-matchers.ts +10 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/plan-mode/plan-handoff.ts +37 -0
- package/src/priority.json +4 -0
- package/src/prompts/goals/goal-continuation.md +1 -1
- package/src/prompts/steering/user-interjection.md +10 -0
- package/src/prompts/system/agent-creation-architect.md +1 -26
- package/src/prompts/system/eager-todo.md +3 -3
- package/src/prompts/system/orchestrate-notice.md +5 -5
- package/src/prompts/system/plan-mode-approved.md +14 -17
- package/src/prompts/system/plan-mode-reference.md +3 -6
- package/src/prompts/system/subagent-system-prompt.md +11 -0
- package/src/prompts/system/system-prompt.md +143 -145
- package/src/prompts/system/title-system.md +3 -2
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/browser.md +33 -30
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +3 -1
- package/src/prompts/tools/{todo-write.md → todo.md} +5 -5
- package/src/sdk.ts +6 -21
- package/src/session/agent-session.ts +44 -21
- package/src/session/history-storage.ts +11 -18
- package/src/session/indexed-session-storage.ts +430 -0
- package/src/session/messages.ts +80 -0
- package/src/session/redis-session-storage.ts +66 -377
- package/src/session/session-manager.ts +109 -23
- package/src/session/session-storage.ts +148 -68
- package/src/session/sql-session-storage.ts +131 -382
- package/src/slash-commands/helpers/todo.ts +2 -2
- package/src/slash-commands/types.ts +27 -10
- package/src/task/executor.ts +9 -1
- package/src/task/index.ts +51 -1
- package/src/telemetry-export.ts +126 -0
- package/src/tiny/compiled-runtime.ts +179 -0
- package/src/tiny/text.ts +112 -1
- package/src/tiny/worker.ts +24 -2
- package/src/tools/ask.ts +133 -87
- package/src/tools/fetch.ts +17 -4
- package/src/tools/find.ts +2 -2
- package/src/tools/index.ts +6 -4
- package/src/tools/memory-recall.ts +1 -1
- package/src/tools/memory-reflect.ts +1 -1
- package/src/tools/path-utils.ts +16 -7
- package/src/tools/renderers.ts +2 -2
- package/src/tools/search.ts +6 -3
- package/src/tools/ssh.ts +26 -10
- package/src/tools/{todo-write.ts → todo.ts} +32 -35
- package/src/tools/write.ts +14 -2
- package/src/tui/status-line.ts +15 -4
- package/src/utils/session-color.ts +39 -14
- package/src/utils/title-generator.ts +9 -2
- package/src/web/search/index.ts +4 -2
- package/src/web/search/provider.ts +19 -35
- package/src/web/search/providers/base.ts +17 -0
- package/src/web/search/providers/exa.ts +111 -7
- package/src/web/search/providers/perplexity.ts +8 -4
- package/src/web/search/types.ts +74 -32
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
} from "../../tools/json-tree";
|
|
33
33
|
import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
|
|
34
34
|
import { toolRenderers } from "../../tools/renderers";
|
|
35
|
-
import {
|
|
35
|
+
import { TODO_STRIKE_TOTAL_FRAMES } from "../../tools/todo";
|
|
36
36
|
import { renderStatusLine } from "../../tui";
|
|
37
37
|
import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
38
38
|
import { renderDiff } from "./diff";
|
|
@@ -149,6 +149,10 @@ const SPINNER_RENDER_INTERVAL_MS = 16;
|
|
|
149
149
|
* 60fps render cadence (mirrors `Loader`). */
|
|
150
150
|
const SPINNER_GLYPH_ADVANCE_MS = 80;
|
|
151
151
|
|
|
152
|
+
// Stable per-instance counter so each tool execution's inline images get a
|
|
153
|
+
// graphics id that survives child re-creation (the image budget keys off it).
|
|
154
|
+
let toolExecutionInstanceSeq = 0;
|
|
155
|
+
|
|
152
156
|
/**
|
|
153
157
|
* Component that renders a tool call with its result (updateable)
|
|
154
158
|
*/
|
|
@@ -158,6 +162,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
158
162
|
#multiFileBoxes: (Box | Spacer)[] = []; // Extra boxes for multi-file edit results
|
|
159
163
|
#imageComponents: Image[] = [];
|
|
160
164
|
#imageSpacers: Spacer[] = [];
|
|
165
|
+
readonly #instanceId = ++toolExecutionInstanceSeq;
|
|
161
166
|
#toolName: string;
|
|
162
167
|
#toolLabel: string;
|
|
163
168
|
#args: any;
|
|
@@ -447,7 +452,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
447
452
|
}
|
|
448
453
|
|
|
449
454
|
#updateTodoStrikeAnimation(): void {
|
|
450
|
-
if (this.#toolName !== "
|
|
455
|
+
if (this.#toolName !== "todo" || this.#isPartial || this.#result?.isError) {
|
|
451
456
|
this.#stopTodoStrikeAnimation();
|
|
452
457
|
return;
|
|
453
458
|
}
|
|
@@ -462,7 +467,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
462
467
|
this.#renderState.spinnerFrame = 0;
|
|
463
468
|
this.#todoStrikeInterval = setInterval(() => {
|
|
464
469
|
const nextFrame = (this.#spinnerFrame ?? 0) + 1;
|
|
465
|
-
if (nextFrame >
|
|
470
|
+
if (nextFrame > TODO_STRIKE_TOTAL_FRAMES) {
|
|
466
471
|
this.#stopTodoStrikeAnimation();
|
|
467
472
|
} else {
|
|
468
473
|
this.#spinnerFrame = nextFrame;
|
|
@@ -754,7 +759,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
754
759
|
imageData,
|
|
755
760
|
imageMimeType,
|
|
756
761
|
{ fallbackColor: (s: string) => theme.fg("toolOutput", s) },
|
|
757
|
-
resolveImageOptions(),
|
|
762
|
+
{ ...resolveImageOptions(), budget: this.#ui.imageBudget, imageKey: `te${this.#instanceId}:${i}` },
|
|
758
763
|
);
|
|
759
764
|
this.#imageComponents.push(imageComponent);
|
|
760
765
|
this.addChild(imageComponent);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type Component, Container, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
2
|
+
|
|
3
|
+
const kSnapshot = Symbol("transcript.frozenRender");
|
|
4
|
+
|
|
5
|
+
interface FrozenRender {
|
|
6
|
+
width: number;
|
|
7
|
+
lines: string[];
|
|
8
|
+
generation: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SnapshotCarrier {
|
|
12
|
+
[kSnapshot]?: FrozenRender;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Transcript container that freezes the rendered output of every block except
|
|
17
|
+
* the bottom-most (live) one on terminals where committed native scrollback is
|
|
18
|
+
* immutable.
|
|
19
|
+
*
|
|
20
|
+
* On ED3-risk terminals with an unobservable viewport (ghostty/kitty/iTerm2/…)
|
|
21
|
+
* the renderer cannot clear saved lines (`\x1b[3J` may yank a reader) or query
|
|
22
|
+
* whether the user has scrolled, so any block that re-lays-out *after* it has
|
|
23
|
+
* scrolled past the viewport leaves a stale duplicate above the live region
|
|
24
|
+
* (a finalized assistant message re-wrapping, a tool preview collapsing to its
|
|
25
|
+
* compact result, a late async tool completion). The renderer's only safe move
|
|
26
|
+
* for such an offscreen edit is to not repaint — which is correct only if the
|
|
27
|
+
* committed region never changes underneath it.
|
|
28
|
+
*
|
|
29
|
+
* This container provides that guarantee: a block's render is snapshotted while
|
|
30
|
+
* it is the live (bottom-most) block, and once a newer block is appended it
|
|
31
|
+
* replays the snapshot instead of recomputing. Mutations after a block leaves
|
|
32
|
+
* live are intentionally deferred until the next checkpoint {@link thaw} (prompt
|
|
33
|
+
* submit → native-scrollback rebuild), where the whole transcript is replayed
|
|
34
|
+
* and any drift reconciles safely. On terminals that can rebuild history this
|
|
35
|
+
* freezing is unnecessary, so it renders every block live for full fidelity.
|
|
36
|
+
*/
|
|
37
|
+
export class TranscriptContainer extends Container {
|
|
38
|
+
// Bumped to invalidate every block's snapshot at once; a snapshot is only
|
|
39
|
+
// honored when its stored generation still matches.
|
|
40
|
+
#generation = 0;
|
|
41
|
+
// The block that was bottom-most (live) on the previous render. When the live
|
|
42
|
+
// position moves past it, its snapshot was last refreshed mid-stream and may
|
|
43
|
+
// predate content that finalized in the same coalesced frame that appended the
|
|
44
|
+
// block now below it — so it must recompute once on the live→frozen transition.
|
|
45
|
+
#prevLiveChild: Component | undefined;
|
|
46
|
+
|
|
47
|
+
override invalidate(): void {
|
|
48
|
+
// A theme/global invalidation forces a full recompute on the rebuild that
|
|
49
|
+
// follows; retire every snapshot.
|
|
50
|
+
this.#generation++;
|
|
51
|
+
super.invalidate();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override clear(): void {
|
|
55
|
+
this.#generation++;
|
|
56
|
+
super.clear();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Retire all frozen snapshots so the next render reflects each block's current
|
|
61
|
+
* state. Call at reconciliation checkpoints (prompt submit) where the whole
|
|
62
|
+
* transcript is replayed into native scrollback and any drift a frozen block
|
|
63
|
+
* accumulated is reconciled.
|
|
64
|
+
*/
|
|
65
|
+
thaw(): void {
|
|
66
|
+
this.#generation++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override render(width: number): string[] {
|
|
70
|
+
width = Math.max(1, width);
|
|
71
|
+
if (!TERMINAL.eagerEraseScrollbackRisk) return super.render(width);
|
|
72
|
+
|
|
73
|
+
const lines: string[] = [];
|
|
74
|
+
const liveIndex = this.children.length - 1;
|
|
75
|
+
const liveChild = this.children[liveIndex];
|
|
76
|
+
const prevLiveChild = this.#prevLiveChild;
|
|
77
|
+
this.#prevLiveChild = liveChild;
|
|
78
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
79
|
+
const child = this.children[i]! as Component & SnapshotCarrier;
|
|
80
|
+
if (child !== liveChild) {
|
|
81
|
+
const snapshot = child[kSnapshot];
|
|
82
|
+
// Replay the block's last render from while it was live. A stale
|
|
83
|
+
// generation (post-thaw) or width mismatch (resize in flight, an
|
|
84
|
+
// explicit rebuild that reconciles history anyway) recomputes instead.
|
|
85
|
+
// The block that was live on the previous render is also recomputed
|
|
86
|
+
// here: TUI render coalescing can advance its content (final streamed
|
|
87
|
+
// tokens) in the very frame that appends the block now below it, so its
|
|
88
|
+
// cached snapshot predates that final content. Recomputing on the
|
|
89
|
+
// transition seals the block at its true final state, not a mid-stream one.
|
|
90
|
+
if (
|
|
91
|
+
child !== prevLiveChild &&
|
|
92
|
+
snapshot &&
|
|
93
|
+
snapshot.generation === this.#generation &&
|
|
94
|
+
snapshot.width === width
|
|
95
|
+
) {
|
|
96
|
+
lines.push(...snapshot.lines);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const rendered = child.render(width);
|
|
101
|
+
// Cache every block's latest render. While a block is live this keeps its
|
|
102
|
+
// snapshot current; on the frame it stops being live the recompute above
|
|
103
|
+
// refreshes it to the final state before it freezes.
|
|
104
|
+
child[kSnapshot] = { width, lines: rendered, generation: this.#generation };
|
|
105
|
+
lines.push(...rendered);
|
|
106
|
+
}
|
|
107
|
+
return lines;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
14
14
|
import { formatDuration, Snowflake } from "@oh-my-pi/pi-utils";
|
|
15
15
|
import { $ } from "bun";
|
|
16
|
+
import { shouldEnableAppendOnlyContext } from "../../config/append-only-context-mode";
|
|
16
17
|
import { loadCustomShare } from "../../export/custom-share";
|
|
17
18
|
import type { CompactOptions } from "../../extensibility/extensions/types";
|
|
18
19
|
import {
|
|
@@ -397,10 +398,10 @@ export class CommandController {
|
|
|
397
398
|
// Append-only context
|
|
398
399
|
{
|
|
399
400
|
const setting = this.ctx.settings.get("provider.appendOnlyContext") ?? "auto";
|
|
400
|
-
const
|
|
401
|
-
const mode = setting
|
|
401
|
+
const model = this.ctx.session.model;
|
|
402
|
+
const mode = shouldEnableAppendOnlyContext(setting, model);
|
|
402
403
|
const activeLabel = mode ? theme.fg("success", "active") : theme.fg("dim", "inactive");
|
|
403
|
-
const settingLabel = setting === "auto" ? `${setting} (${provider ?? "?"})` : setting;
|
|
404
|
+
const settingLabel = setting === "auto" ? `${setting} (${model?.provider ?? "?"})` : setting;
|
|
404
405
|
info += `${theme.fg("dim", "Append-Only:")} ${activeLabel} (setting: ${settingLabel})\n`;
|
|
405
406
|
}
|
|
406
407
|
info += `${theme.bold("Tokens")}\n`;
|
|
@@ -282,6 +282,7 @@ export class EventController {
|
|
|
282
282
|
this.ctx.hideThinkingBlock,
|
|
283
283
|
() => this.ctx.ui.requestRender(),
|
|
284
284
|
this.ctx.session.extensionRunner?.getAssistantThinkingRenderers(),
|
|
285
|
+
this.ctx.ui.imageBudget,
|
|
285
286
|
);
|
|
286
287
|
this.ctx.streamingMessage = event.message;
|
|
287
288
|
this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
|
|
@@ -586,18 +587,18 @@ export class EventController {
|
|
|
586
587
|
this.ctx.ui.requestRender();
|
|
587
588
|
}
|
|
588
589
|
}
|
|
589
|
-
// Update todo display when
|
|
590
|
-
if (event.toolName === "
|
|
590
|
+
// Update todo display when todo tool completes
|
|
591
|
+
if (event.toolName === "todo" && !event.isError) {
|
|
591
592
|
const details = event.result.details as { phases?: TodoPhase[] } | undefined;
|
|
592
593
|
if (details?.phases) {
|
|
593
594
|
this.ctx.setTodos(details.phases);
|
|
594
595
|
}
|
|
595
|
-
} else if (event.toolName === "
|
|
596
|
+
} else if (event.toolName === "todo" && event.isError) {
|
|
596
597
|
const textContent = event.result.content.find(
|
|
597
598
|
(content: { type: string; text?: string }) => content.type === "text",
|
|
598
599
|
)?.text;
|
|
599
600
|
this.ctx.showWarning(
|
|
600
|
-
`Todo update failed${textContent ? `: ${textContent}` : ". Progress may be stale until
|
|
601
|
+
`Todo update failed${textContent ? `: ${textContent}` : ". Progress may be stale until todo succeeds."}`,
|
|
601
602
|
);
|
|
602
603
|
}
|
|
603
604
|
if (event.toolName === "resolve" && !event.isError) {
|
|
@@ -847,9 +848,13 @@ export class EventController {
|
|
|
847
848
|
const last = this.ctx.session.getLastAssistantMessage?.();
|
|
848
849
|
if (last?.stopReason === "aborted" || last?.stopReason === "error") return;
|
|
849
850
|
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
851
|
+
const sessionName = this.ctx.sessionManager.getSessionName();
|
|
852
|
+
TERMINAL.sendNotification({
|
|
853
|
+
title: sessionName || "Oh My Pi",
|
|
854
|
+
body: "Complete",
|
|
855
|
+
type: "completion",
|
|
856
|
+
actions: "focus",
|
|
857
|
+
});
|
|
853
858
|
}
|
|
854
859
|
|
|
855
860
|
async handleBackgroundEvent(event: AgentSessionEvent): Promise<void> {
|
|
@@ -625,6 +625,9 @@ export class ExtensionUiController {
|
|
|
625
625
|
tui: this.ctx.ui,
|
|
626
626
|
outline: dialogOptions?.outline,
|
|
627
627
|
disabledIndices: dialogOptions?.disabledIndices,
|
|
628
|
+
selectionMarker: dialogOptions?.selectionMarker,
|
|
629
|
+
checkedIndices: dialogOptions?.checkedIndices,
|
|
630
|
+
markableCount: dialogOptions?.markableCount,
|
|
628
631
|
maxVisible,
|
|
629
632
|
slider: extra?.slider,
|
|
630
633
|
},
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
-
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
2
|
import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui";
|
|
4
3
|
import { $env, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
5
4
|
import { getRoleInfo } from "../../config/model-registry";
|
|
@@ -13,6 +12,7 @@ import type { AgentSessionEvent } from "../../session/agent-session";
|
|
|
13
12
|
import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
|
|
14
13
|
import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
|
|
15
14
|
import { isTinyTitleLocalModelKey } from "../../tiny/models";
|
|
15
|
+
import { isLowSignalTitleInput } from "../../tiny/text";
|
|
16
16
|
import { tinyTitleClient } from "../../tiny/title-client";
|
|
17
17
|
import type { TinyTitleProgressEvent } from "../../tiny/title-protocol";
|
|
18
18
|
import { copyToClipboard, readImageFromClipboard, readTextFromClipboard } from "../../utils/clipboard";
|
|
@@ -376,9 +376,12 @@ export class InputController {
|
|
|
376
376
|
// First, move any pending bash components to chat
|
|
377
377
|
this.ctx.flushPendingBashComponents();
|
|
378
378
|
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
379
|
+
// Auto-generate a session title while the session is still unnamed.
|
|
380
|
+
// Greetings / acknowledgements / empty input carry no task, so they are
|
|
381
|
+
// skipped deterministically (no model invoked, no download-progress UI)
|
|
382
|
+
// and the session stays unnamed — the next user message gets a fresh
|
|
383
|
+
// chance, so titling defers past "hi" instead of latching onto it.
|
|
384
|
+
if (!this.ctx.sessionManager.getSessionName() && !$env.PI_NO_TITLE && !isLowSignalTitleInput(text)) {
|
|
382
385
|
this.#showTinyTitleDownloadProgress(this.ctx.settings.get("providers.tinyModel"));
|
|
383
386
|
const registry = this.ctx.session.modelRegistry;
|
|
384
387
|
generateSessionTitle(
|
|
@@ -390,7 +393,9 @@ export class InputController {
|
|
|
390
393
|
provider => this.ctx.session.agent.metadataForProvider(provider),
|
|
391
394
|
)
|
|
392
395
|
.then(async title => {
|
|
393
|
-
|
|
396
|
+
// Re-check: a concurrent attempt for an earlier message may have
|
|
397
|
+
// already named the session. Don't clobber it.
|
|
398
|
+
if (title && !this.ctx.sessionManager.getSessionName()) {
|
|
394
399
|
const applied = await this.ctx.sessionManager.setSessionName(title, "auto");
|
|
395
400
|
if (applied) {
|
|
396
401
|
setSessionTerminalTitle(
|
|
@@ -133,7 +133,11 @@ export class SelectorController {
|
|
|
133
133
|
const availableWidth = this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
|
|
134
134
|
return this.ctx.statusLine.getTopBorder(availableWidth).content;
|
|
135
135
|
},
|
|
136
|
-
onPluginsChanged: () => {
|
|
136
|
+
onPluginsChanged: async () => {
|
|
137
|
+
const projectPath = await resolveActiveProjectRegistryPath(this.ctx.sessionManager.getCwd());
|
|
138
|
+
clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
|
|
139
|
+
await this.ctx.refreshSlashCommandState();
|
|
140
|
+
await this.ctx.session.refreshSshTool({ activateIfAvailable: true });
|
|
137
141
|
this.ctx.ui.requestRender();
|
|
138
142
|
},
|
|
139
143
|
onCancel: () => {
|
|
@@ -184,16 +188,19 @@ export class SelectorController {
|
|
|
184
188
|
*/
|
|
185
189
|
async showExtensionsDashboard(): Promise<void> {
|
|
186
190
|
const dashboard = await ExtensionDashboard.create(getProjectDir(), this.ctx.settings, this.ctx.ui.terminal.rows);
|
|
187
|
-
this.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
dashboard.onRequestRender = () => {
|
|
193
|
-
this.ctx.ui.requestRender();
|
|
194
|
-
};
|
|
195
|
-
return { component: dashboard, focus: dashboard };
|
|
191
|
+
const overlay = this.ctx.ui.showOverlay(dashboard, {
|
|
192
|
+
width: "100%",
|
|
193
|
+
maxHeight: "100%",
|
|
194
|
+
anchor: "top-left",
|
|
195
|
+
margin: 0,
|
|
196
196
|
});
|
|
197
|
+
dashboard.onClose = () => {
|
|
198
|
+
overlay.hide();
|
|
199
|
+
this.ctx.ui.requestRender();
|
|
200
|
+
};
|
|
201
|
+
dashboard.onRequestRender = () => {
|
|
202
|
+
this.ctx.ui.requestRender();
|
|
203
|
+
};
|
|
197
204
|
}
|
|
198
205
|
|
|
199
206
|
/**
|
|
@@ -208,16 +215,19 @@ export class SelectorController {
|
|
|
208
215
|
activeModelPattern,
|
|
209
216
|
defaultModelPattern,
|
|
210
217
|
});
|
|
211
|
-
this.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
dashboard.onRequestRender = () => {
|
|
217
|
-
this.ctx.ui.requestRender();
|
|
218
|
-
};
|
|
219
|
-
return { component: dashboard, focus: dashboard };
|
|
218
|
+
const overlay = this.ctx.ui.showOverlay(dashboard, {
|
|
219
|
+
width: "100%",
|
|
220
|
+
maxHeight: "100%",
|
|
221
|
+
anchor: "top-left",
|
|
222
|
+
margin: 0,
|
|
220
223
|
});
|
|
224
|
+
dashboard.onClose = () => {
|
|
225
|
+
overlay.hide();
|
|
226
|
+
this.ctx.ui.requestRender();
|
|
227
|
+
};
|
|
228
|
+
dashboard.onRequestRender = () => {
|
|
229
|
+
this.ctx.ui.requestRender();
|
|
230
|
+
};
|
|
221
231
|
}
|
|
222
232
|
|
|
223
233
|
/**
|
|
@@ -763,6 +773,7 @@ export class SelectorController {
|
|
|
763
773
|
loadAllSessions: () => SessionManager.listAll(),
|
|
764
774
|
allSessions,
|
|
765
775
|
startInAllScope,
|
|
776
|
+
getTerminalRows: () => this.ctx.ui.terminal.rows,
|
|
766
777
|
},
|
|
767
778
|
);
|
|
768
779
|
selector.setOnRequestRender(() => this.ctx.ui.requestRender());
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type TodoItem,
|
|
9
9
|
type TodoPhase,
|
|
10
10
|
USER_TODO_EDIT_CUSTOM_TYPE,
|
|
11
|
-
} from "../../tools/todo
|
|
11
|
+
} from "../../tools/todo";
|
|
12
12
|
import { copyToClipboard } from "../../utils/clipboard";
|
|
13
13
|
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
14
14
|
import type { InteractiveModeContext } from "../types";
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
Markdown,
|
|
29
29
|
ProcessTerminal,
|
|
30
30
|
Spacer,
|
|
31
|
+
setTerminalTextSizing,
|
|
32
|
+
TERMINAL,
|
|
31
33
|
Text,
|
|
32
34
|
TUI,
|
|
33
35
|
visibleWidth,
|
|
@@ -84,7 +86,7 @@ import type { LspStartupServerInfo } from "../tools";
|
|
|
84
86
|
import { normalizeLocalScheme } from "../tools/path-utils";
|
|
85
87
|
import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
|
|
86
88
|
import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
|
|
87
|
-
import { formatPhaseDisplayName, selectStickyTodoWindow, todoMatchesAnyDescription } from "../tools/todo
|
|
89
|
+
import { formatPhaseDisplayName, selectStickyTodoWindow, todoMatchesAnyDescription } from "../tools/todo";
|
|
88
90
|
import { ToolError } from "../tools/tool-errors";
|
|
89
91
|
import type { EventBus } from "../utils/event-bus";
|
|
90
92
|
import { getEditorCommand, openInEditor } from "../utils/external-editor";
|
|
@@ -100,6 +102,7 @@ import type { HookInputComponent } from "./components/hook-input";
|
|
|
100
102
|
import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
|
|
101
103
|
import { StatusLineComponent } from "./components/status-line";
|
|
102
104
|
import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
105
|
+
import { TranscriptContainer } from "./components/transcript-container";
|
|
103
106
|
import { WelcomeComponent, type LspServerInfo as WelcomeLspServerInfo } from "./components/welcome";
|
|
104
107
|
import { BtwController } from "./controllers/btw-controller";
|
|
105
108
|
import { CommandController } from "./controllers/command-controller";
|
|
@@ -255,7 +258,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
255
258
|
historyStorage?: HistoryStorage;
|
|
256
259
|
|
|
257
260
|
ui: TUI;
|
|
258
|
-
chatContainer:
|
|
261
|
+
chatContainer: TranscriptContainer;
|
|
259
262
|
pendingMessagesContainer: Container;
|
|
260
263
|
statusContainer: Container;
|
|
261
264
|
todoContainer: Container;
|
|
@@ -390,7 +393,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
390
393
|
|
|
391
394
|
this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
|
|
392
395
|
this.ui.setClearOnShrink(settings.get("clearOnShrink"));
|
|
393
|
-
this.
|
|
396
|
+
this.ui.setMaxInlineImages(settings.get("tui.maxInlineImages"));
|
|
397
|
+
// OSC 66 text-sizing is Kitty-only; resolve the setting against the terminal's
|
|
398
|
+
// capability (`TERMINAL.textSizing` defaults on for Kitty) so it stays off
|
|
399
|
+
// unless the user opts in, and never emits raw escapes on other terminals.
|
|
400
|
+
setTerminalTextSizing(settings.get("tui.textSizing") && TERMINAL.textSizing);
|
|
401
|
+
this.chatContainer = new TranscriptContainer();
|
|
394
402
|
this.pendingMessagesContainer = new Container();
|
|
395
403
|
this.statusContainer = new Container();
|
|
396
404
|
this.todoContainer = new Container();
|
|
@@ -599,7 +607,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
599
607
|
await this.initHooksAndCustomTools();
|
|
600
608
|
|
|
601
609
|
// Restore mode from session (e.g. plan mode on resume)
|
|
602
|
-
|
|
610
|
+
this.session.setSessionSwitchReconciler?.(() => this.#reconcileModeFromSession());
|
|
611
|
+
await this.#reconcileModeFromSession();
|
|
603
612
|
|
|
604
613
|
// Restore unsent editor draft from previous session shutdown (Ctrl+D).
|
|
605
614
|
// One-shot: consumeDraft removes the sidecar after read so the next
|
|
@@ -915,6 +924,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
915
924
|
this.#pendingSubmissionDispose = undefined;
|
|
916
925
|
}
|
|
917
926
|
this.editor.setText("");
|
|
927
|
+
// Reconciliation checkpoint: the rebuild below replays the whole transcript
|
|
928
|
+
// into native scrollback, so retire frozen block snapshots and let every
|
|
929
|
+
// block render its current state.
|
|
930
|
+
this.chatContainer.thaw();
|
|
918
931
|
this.ui.refreshNativeScrollbackIfDirty({ allowUnknownViewport: true });
|
|
919
932
|
this.ensureLoadingAnimation();
|
|
920
933
|
this.ui.requestRender();
|
|
@@ -1001,7 +1014,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1001
1014
|
} else {
|
|
1002
1015
|
const accentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
|
|
1003
1016
|
const sessionName = accentEnabled ? this.sessionManager.getSessionName() : undefined;
|
|
1004
|
-
const hex = sessionName ? getSessionAccentHex(sessionName) : undefined;
|
|
1017
|
+
const hex = sessionName ? getSessionAccentHex(sessionName, theme.accentSurfaceLuminance) : undefined;
|
|
1005
1018
|
const ansi = getSessionAccentAnsi(hex);
|
|
1006
1019
|
if (ansi) {
|
|
1007
1020
|
this.editor.borderColor = (str: string) => `${ansi}${str}\x1b[39m`;
|
|
@@ -1060,7 +1073,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1060
1073
|
* Auto-complete any pending/in_progress todo whose content matches a
|
|
1061
1074
|
* subagent that has finished successfully. Fires on every observer
|
|
1062
1075
|
* `onChange` so the visual state stays in sync with subagent lifecycle
|
|
1063
|
-
* without requiring the agent to issue a follow-up `
|
|
1076
|
+
* without requiring the agent to issue a follow-up `todo`. Failed
|
|
1064
1077
|
* and aborted subagents are intentionally NOT auto-completed — those
|
|
1065
1078
|
* stay open so the user (or the next agent turn) can decide what to do.
|
|
1066
1079
|
*
|
|
@@ -1358,8 +1371,42 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1358
1371
|
}
|
|
1359
1372
|
}
|
|
1360
1373
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1374
|
+
async #clearTransientModeState(): Promise<void> {
|
|
1375
|
+
if (this.planModeEnabled || this.planModePaused) {
|
|
1376
|
+
if (this.#planModePreviousTools !== undefined) {
|
|
1377
|
+
await this.session.setActiveToolsByName(this.#planModePreviousTools);
|
|
1378
|
+
}
|
|
1379
|
+
this.session.setStandingResolveHandler?.(null);
|
|
1380
|
+
this.session.setPlanModeState(undefined);
|
|
1381
|
+
this.planModeEnabled = false;
|
|
1382
|
+
this.planModePaused = false;
|
|
1383
|
+
this.planModePlanFilePath = undefined;
|
|
1384
|
+
this.#planModePreviousTools = undefined;
|
|
1385
|
+
this.#planModePreviousModelState = undefined;
|
|
1386
|
+
this.#pendingModelSwitch = undefined;
|
|
1387
|
+
this.#planModeHasEntered = false;
|
|
1388
|
+
this.#updatePlanModeStatus();
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
if (this.goalModeEnabled || this.goalModePaused) {
|
|
1392
|
+
if (this.#goalModePreviousTools !== undefined) {
|
|
1393
|
+
await this.session.setActiveToolsByName(this.#goalModePreviousTools);
|
|
1394
|
+
}
|
|
1395
|
+
this.session.setGoalModeState(undefined);
|
|
1396
|
+
this.goalModeEnabled = false;
|
|
1397
|
+
this.goalModePaused = false;
|
|
1398
|
+
this.#goalModePreviousTools = undefined;
|
|
1399
|
+
this.#goalTurnHadToolCalls = false;
|
|
1400
|
+
this.#goalContinuationTurnInFlight = false;
|
|
1401
|
+
this.#goalSuppressNextContinuation = false;
|
|
1402
|
+
this.#cancelGoalContinuation();
|
|
1403
|
+
this.#updateGoalModeStatus();
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/** Reconcile mode state from session entries on resume/switch. */
|
|
1408
|
+
async #reconcileModeFromSession(): Promise<void> {
|
|
1409
|
+
await this.#clearTransientModeState();
|
|
1363
1410
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1364
1411
|
const goalEnabled = this.session.settings.get("goal.enabled");
|
|
1365
1412
|
if (!goalEnabled && (sessionContext.mode === "goal" || sessionContext.mode === "goal_paused")) {
|
|
@@ -2495,7 +2542,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2495
2542
|
const accentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
|
|
2496
2543
|
const sessionName = accentEnabled ? this.sessionManager.getSessionName() : undefined;
|
|
2497
2544
|
if (!sessionName) return undefined;
|
|
2498
|
-
const hex = getSessionAccentHex(sessionName);
|
|
2545
|
+
const hex = getSessionAccentHex(sessionName, theme.accentSurfaceLuminance);
|
|
2499
2546
|
const main = getSessionAccentAnsi(hex);
|
|
2500
2547
|
const dim = getSessionAccentAnsi(adjustHsv(hex, { s: 0.55, v: 0.65 }));
|
|
2501
2548
|
return main && dim ? { main, dim } : undefined;
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
|
9
9
|
import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import type { AgentSession } from "../session/agent-session";
|
|
11
11
|
import { isSilentAbort } from "../session/messages";
|
|
12
|
+
import { flushTelemetryExport } from "../telemetry-export";
|
|
12
13
|
import { initializeExtensions } from "./runtime-init";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -83,6 +84,10 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
83
84
|
!isSilentAbort(assistantMsg.errorMessage)
|
|
84
85
|
) {
|
|
85
86
|
const errorLine = sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
|
|
87
|
+
// Flush before this hard exit — it bypasses the awaited postmortem.quit()
|
|
88
|
+
// in main(), and the postmortem `exit` handler can't await, so the error
|
|
89
|
+
// spans would otherwise stay buffered in the batch processor and drop.
|
|
90
|
+
await flushTelemetryExport();
|
|
86
91
|
const flushed = process.stderr.write(`${errorLine}\n`);
|
|
87
92
|
if (flushed) {
|
|
88
93
|
process.exit(1);
|
|
@@ -10,7 +10,7 @@ import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
|
|
|
10
10
|
import type { BashResult } from "../../exec/bash-executor";
|
|
11
11
|
import type { ContextUsage } from "../../extensibility/extensions/types";
|
|
12
12
|
import type { SessionStats } from "../../session/agent-session";
|
|
13
|
-
import type { TodoPhase } from "../../tools/todo
|
|
13
|
+
import type { TodoPhase } from "../../tools/todo";
|
|
14
14
|
|
|
15
15
|
// ============================================================================
|
|
16
16
|
// RPC Commands (stdin)
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
supportsLanguage as nativeSupportsLanguage,
|
|
11
11
|
} from "@oh-my-pi/pi-natives";
|
|
12
12
|
import type { EditorTheme, MarkdownTheme, SelectListTheme, SymbolTheme } from "@oh-my-pi/pi-tui";
|
|
13
|
-
import { adjustHsv, getCustomThemesDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
13
|
+
import { adjustHsv, colorLuma, getCustomThemesDir, isEnoent, logger, relativeLuminance } from "@oh-my-pi/pi-utils";
|
|
14
14
|
import chalk from "chalk";
|
|
15
15
|
import * as z from "zod/v4";
|
|
16
16
|
// Embed theme JSON files at build time
|
|
@@ -136,6 +136,9 @@ export type SymbolKey =
|
|
|
136
136
|
// Checkboxes
|
|
137
137
|
| "checkbox.checked"
|
|
138
138
|
| "checkbox.unchecked"
|
|
139
|
+
// Radio (single-choice)
|
|
140
|
+
| "radio.selected"
|
|
141
|
+
| "radio.unselected"
|
|
139
142
|
// Text Formatting
|
|
140
143
|
| "format.bullet"
|
|
141
144
|
| "format.dash"
|
|
@@ -302,6 +305,9 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
302
305
|
// Checkboxes
|
|
303
306
|
"checkbox.checked": "☑",
|
|
304
307
|
"checkbox.unchecked": "☐",
|
|
308
|
+
// Radio (single-choice)
|
|
309
|
+
"radio.selected": "◉",
|
|
310
|
+
"radio.unselected": "○",
|
|
305
311
|
// Formatting
|
|
306
312
|
"format.bullet": "•",
|
|
307
313
|
"format.dash": "—",
|
|
@@ -559,6 +565,11 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
559
565
|
"checkbox.checked": "\uf14a",
|
|
560
566
|
// pick: | alt:
|
|
561
567
|
"checkbox.unchecked": "\uf096",
|
|
568
|
+
// Radio (single-choice)
|
|
569
|
+
// pick: (fa-dot-circle-o) | alt: ◉
|
|
570
|
+
"radio.selected": "\uf192",
|
|
571
|
+
// pick: (fa-circle-o) | alt: ○
|
|
572
|
+
"radio.unselected": "\uf10c",
|
|
562
573
|
// pick: | alt: •
|
|
563
574
|
"format.bullet": "\uf111",
|
|
564
575
|
// pick: – | alt: — ― -
|
|
@@ -731,6 +742,8 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
731
742
|
// Checkboxes
|
|
732
743
|
"checkbox.checked": "[x]",
|
|
733
744
|
"checkbox.unchecked": "[ ]",
|
|
745
|
+
"radio.selected": "(o)",
|
|
746
|
+
"radio.unselected": "( )",
|
|
734
747
|
"format.bullet": "*",
|
|
735
748
|
"format.dash": "-",
|
|
736
749
|
"format.bracketLeft": "[",
|
|
@@ -1277,6 +1290,16 @@ export class Theme {
|
|
|
1277
1290
|
#bgColors: Record<ThemeBg, string>;
|
|
1278
1291
|
#symbols: SymbolMap;
|
|
1279
1292
|
#spinnerFramesOverrides: Partial<Record<SpinnerType, string[]>>;
|
|
1293
|
+
/**
|
|
1294
|
+
* Perceptual luma (0..1) of the status-line background — used to classify the
|
|
1295
|
+
* theme light/dark. Undefined when it can't be resolved. Classified against the
|
|
1296
|
+
* status line (the surface session accents render on) rather than the chat bubble
|
|
1297
|
+
* (`userMessageBg`), which some themes (e.g. `porcelain`) style dark on an
|
|
1298
|
+
* otherwise-light theme.
|
|
1299
|
+
*/
|
|
1300
|
+
readonly statusLineLuminance: number | undefined;
|
|
1301
|
+
/** WCAG relative luminance of the status-line background — basis for accent contrast. */
|
|
1302
|
+
readonly #statusLineContrastLuminance: number | undefined;
|
|
1280
1303
|
|
|
1281
1304
|
constructor(
|
|
1282
1305
|
fgColors: Record<ThemeColor, string | number>,
|
|
@@ -1286,6 +1309,8 @@ export class Theme {
|
|
|
1286
1309
|
symbolOverrides: Partial<Record<SymbolKey, string>>,
|
|
1287
1310
|
spinnerFramesOverrides: Partial<Record<SpinnerType, string[]>> = {},
|
|
1288
1311
|
) {
|
|
1312
|
+
this.statusLineLuminance = colorLuma(bgColors.statusLineBg);
|
|
1313
|
+
this.#statusLineContrastLuminance = relativeLuminance(bgColors.statusLineBg);
|
|
1289
1314
|
this.#fgColors = {} as Record<ThemeColor, string>;
|
|
1290
1315
|
for (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {
|
|
1291
1316
|
this.#fgColors[key] = fgAnsi(value, mode);
|
|
@@ -1307,6 +1332,19 @@ export class Theme {
|
|
|
1307
1332
|
this.#spinnerFramesOverrides = spinnerFramesOverrides;
|
|
1308
1333
|
}
|
|
1309
1334
|
|
|
1335
|
+
/** True when the active theme has a light status-line background. */
|
|
1336
|
+
get isLight(): boolean {
|
|
1337
|
+
return this.statusLineLuminance !== undefined && this.statusLineLuminance > 0.5;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* Surface luminance to size session accents against on light themes; undefined on
|
|
1342
|
+
* dark themes so accents stay vivid. Pass straight to `getSessionAccentHex`.
|
|
1343
|
+
*/
|
|
1344
|
+
get accentSurfaceLuminance(): number | undefined {
|
|
1345
|
+
return this.isLight ? this.#statusLineContrastLuminance : undefined;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1310
1348
|
fg(color: ThemeColor, text: string): string {
|
|
1311
1349
|
const ansi = this.#fgColors[color];
|
|
1312
1350
|
if (!ansi) throw new Error(`Unknown theme color: ${color}`);
|
|
@@ -1570,6 +1608,13 @@ export class Theme {
|
|
|
1570
1608
|
};
|
|
1571
1609
|
}
|
|
1572
1610
|
|
|
1611
|
+
get radio() {
|
|
1612
|
+
return {
|
|
1613
|
+
selected: this.#symbols["radio.selected"],
|
|
1614
|
+
unselected: this.#symbols["radio.unselected"],
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1573
1618
|
get format() {
|
|
1574
1619
|
return {
|
|
1575
1620
|
bullet: this.#symbols["format.bullet"],
|
|
@@ -2312,13 +2357,8 @@ export function isLightTheme(themeName?: string): boolean {
|
|
|
2312
2357
|
}
|
|
2313
2358
|
try {
|
|
2314
2359
|
const resolved = resolveVarRefs(themeJson.colors.userMessageBg, themeJson.vars ?? {});
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
const g = parseInt(resolved.slice(3, 5), 16) / 255;
|
|
2318
|
-
const b = parseInt(resolved.slice(5, 7), 16) / 255;
|
|
2319
|
-
// Relative luminance (ITU-R BT.709)
|
|
2320
|
-
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
2321
|
-
return luminance > 0.5;
|
|
2360
|
+
const luminance = colorLuma(resolved);
|
|
2361
|
+
return luminance !== undefined && luminance > 0.5;
|
|
2322
2362
|
} catch {
|
|
2323
2363
|
return false;
|
|
2324
2364
|
}
|