@sayknow-cli/coding-agent 0.3.12 → 0.3.15
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/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/log-experiment.d.ts +2 -2
- package/dist/types/cli/classify-install-target.d.ts +0 -10
- package/dist/types/cli/daemon-cli.d.ts +2 -0
- package/dist/types/cli/initial-message.d.ts +1 -1
- package/dist/types/cli/malloc-env-guard.d.ts +65 -0
- package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +3 -1
- package/dist/types/cli/session-picker.d.ts +4 -3
- package/dist/types/cli/update-cli.d.ts +22 -3
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/commands/daemon.d.ts +6 -2
- package/dist/types/commands/launch.d.ts +1 -0
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/commands/web-search.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +4 -4
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -9
- package/dist/types/commit/agentic/tools/schemas.d.ts +9 -9
- package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -9
- package/dist/types/commit/analysis/conventional.d.ts +1 -1
- package/dist/types/commit/analysis/summary.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +3 -3
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
- package/dist/types/commit/shared-llm.d.ts +9 -9
- package/dist/types/config/keybindings.d.ts +134 -129
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +35 -35
- package/dist/types/config/models-config-schema.d.ts +111 -111
- package/dist/types/config/settings-schema.d.ts +56 -16
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
- package/dist/types/coordinator-mcp/policy.d.ts +3 -0
- package/dist/types/coordinator-mcp/server.d.ts +79 -6
- package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
- package/dist/types/daemon/control-types.d.ts +10 -0
- package/dist/types/daemon/operator-contract.d.ts +47 -0
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +12 -12
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
- package/dist/types/edit/modes/patch.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -1
- package/dist/types/goals/tools/goal-tool.d.ts +6 -6
- package/dist/types/i18n/messages/en.d.ts +8 -8
- package/dist/types/internal-urls/local-protocol.d.ts +13 -9
- package/dist/types/lsp/index.d.ts +9 -9
- package/dist/types/lsp/types.d.ts +9 -9
- package/dist/types/main.d.ts +66 -2
- package/dist/types/modes/acp/acp-agent.d.ts +19 -2
- package/dist/types/modes/acp/acp-mode.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
- package/dist/types/modes/components/pet-selector.d.ts +11 -0
- package/dist/types/modes/components/sayknow-pet-widget.d.ts +59 -0
- package/dist/types/modes/components/session-selector.d.ts +22 -9
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +0 -10
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/controllers/event-controller.d.ts +4 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +24 -2
- package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
- package/dist/types/modes/print-mode.d.ts +3 -4
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
- package/dist/types/modes/theme/defaults/index.d.ts +568 -568
- package/dist/types/modes/types.d.ts +28 -2
- package/dist/types/modes/utils/abort-message.d.ts +1 -1
- package/dist/types/modes/utils/context-usage.d.ts +4 -19
- package/dist/types/modes/utils/irc-message.d.ts +38 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
- package/dist/types/notifications/config.d.ts +34 -0
- package/dist/types/notifications/daemon-paths.d.ts +7 -0
- package/dist/types/notifications/index.d.ts +27 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
- package/dist/types/notifications/notification-service.d.ts +144 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
- package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
- package/dist/types/notifications/telegram-daemon.d.ts +130 -3
- package/dist/types/notifications/telegram-reference.d.ts +19 -2
- package/dist/types/notifications/topic-registry.d.ts +24 -4
- package/dist/types/registry/agent-registry.d.ts +2 -0
- package/dist/types/rlm/index.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +40 -3
- package/dist/types/session/cache-economics.d.ts +79 -0
- package/dist/types/session/context-estimation.d.ts +31 -0
- package/dist/types/session/provider-safety-stop.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +124 -4
- package/dist/types/session/session-storage.d.ts +155 -13
- package/dist/types/setup/credential-auto-import.d.ts +2 -2
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +34 -3
- package/dist/types/skc-runtime/state-schema.d.ts +19 -19
- package/dist/types/skc-runtime/tmux-common.d.ts +6 -0
- package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +309 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +49 -17
- package/dist/types/task/agents.d.ts +2 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/receipt.d.ts +3 -1
- package/dist/types/task/types.d.ts +35 -27
- package/dist/types/tools/ask-answer-registry.d.ts +5 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
- package/dist/types/tools/browser.d.ts +28 -28
- package/dist/types/tools/computer.d.ts +20 -20
- package/dist/types/tools/cron.d.ts +2 -2
- package/dist/types/tools/debug.d.ts +40 -40
- package/dist/types/tools/gh.d.ts +10 -10
- package/dist/types/tools/image-gen.d.ts +2 -2
- package/dist/types/tools/index.d.ts +58 -6
- package/dist/types/tools/monitor.d.ts +6 -6
- package/dist/types/tools/skill-discovery.d.ts +4 -4
- package/dist/types/tools/subagent.d.ts +12 -12
- package/dist/types/tools/telegram-send.d.ts +7 -0
- package/dist/types/tools/todo-write.d.ts +8 -8
- package/dist/types/utils/external-editor.d.ts +2 -0
- package/dist/types/web/insane/bridge.d.ts +9 -9
- package/dist/types/web/search/index.d.ts +4 -4
- package/package.json +8 -8
- package/scripts/g004-tmux-smoke.ts +196 -36
- package/src/cli/daemon-cli.ts +19 -29
- package/src/cli/malloc-env-guard.test.ts +104 -0
- package/src/cli/malloc-env-guard.ts +149 -0
- package/src/cli/notify-cli.ts +78 -14
- package/src/cli/session-picker.ts +23 -40
- package/src/cli/update-cli.ts +56 -37
- package/src/cli.ts +51 -10
- package/src/commands/daemon.ts +19 -5
- package/src/commands/harness.ts +515 -47
- package/src/commands/launch.ts +11 -3
- package/src/commands/notify.ts +5 -1
- package/src/commands/session.ts +6 -1
- package/src/config/keybindings.ts +5 -0
- package/src/config/model-profile-activation.ts +1 -1
- package/src/config/model-profiles.ts +14 -14
- package/src/config/settings-schema.ts +41 -1
- package/src/config/settings.ts +96 -45
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/model-preset.ts +100 -0
- package/src/coordinator-mcp/policy.ts +30 -0
- package/src/coordinator-mcp/server.ts +2958 -430
- package/src/coordinator-mcp/session-reaper.ts +129 -0
- package/src/daemon/control-types.ts +11 -0
- package/src/daemon/operator-contract.ts +121 -0
- package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
- package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
- package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/internal-urls/local-protocol.ts +29 -10
- package/src/main.ts +301 -80
- package/src/modes/acp/acp-agent.ts +730 -100
- package/src/modes/acp/acp-mode.ts +29 -2
- package/src/modes/components/assistant-message.ts +32 -3
- package/src/modes/components/bash-execution.ts +43 -13
- package/src/modes/components/custom-editor.ts +5 -0
- package/src/modes/components/footer.ts +3 -3
- package/src/modes/components/hook-selector.ts +12 -0
- package/src/modes/components/irc-sidebar.ts +206 -0
- package/src/modes/components/pet-selector.ts +55 -0
- package/src/modes/components/sayknow-pet-widget.ts +360 -0
- package/src/modes/components/session-selector.ts +222 -221
- package/src/modes/components/settings-selector.ts +10 -0
- package/src/modes/components/status-line/segments.ts +14 -7
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +4 -171
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/components/user-message.ts +26 -9
- package/src/modes/controllers/command-controller.ts +28 -2
- package/src/modes/controllers/event-controller.ts +159 -36
- package/src/modes/controllers/extension-ui-controller.ts +37 -18
- package/src/modes/controllers/input-controller.ts +8 -2
- package/src/modes/controllers/selector-controller.ts +62 -15
- package/src/modes/interactive-mode.ts +199 -7
- package/src/modes/irc-observation-ledger.ts +127 -0
- package/src/modes/print-mode.ts +228 -92
- package/src/modes/rpc/rpc-client.ts +58 -1
- package/src/modes/rpc/rpc-mode.ts +286 -85
- package/src/modes/rpc/rpc-types.ts +15 -1
- package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
- package/src/modes/shared/agent-wire/command-validation.ts +10 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
- package/src/modes/types.ts +34 -2
- package/src/modes/utils/context-usage.ts +56 -92
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/irc-message.ts +138 -0
- package/src/modes/utils/ui-helpers.ts +216 -50
- package/src/notifications/config.ts +44 -0
- package/src/notifications/daemon-paths.ts +8 -0
- package/src/notifications/index.ts +535 -75
- package/src/notifications/lifecycle-control-runtime.ts +930 -77
- package/src/notifications/notification-service.ts +710 -0
- package/src/notifications/rate-limit-pool.ts +46 -12
- package/src/notifications/telegram-daemon-control.ts +27 -2
- package/src/notifications/telegram-daemon.ts +635 -94
- package/src/notifications/telegram-reference.ts +73 -13
- package/src/notifications/topic-registry.ts +86 -9
- package/src/prompts/system/irc-peer-roster.md +1 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/irc.md +1 -1
- package/src/registry/agent-registry.ts +3 -0
- package/src/sdk.ts +52 -1
- package/src/secrets/obfuscator.ts +4 -2
- package/src/session/agent-session.ts +670 -217
- package/src/session/cache-economics.ts +275 -0
- package/src/session/context-estimation.ts +91 -0
- package/src/session/provider-safety-stop.ts +9 -0
- package/src/session/session-dump-format.ts +6 -0
- package/src/session/session-manager.ts +809 -39
- package/src/session/session-storage.ts +582 -38
- package/src/skc-runtime/session-state-sidecar.ts +756 -135
- package/src/skc-runtime/tmux-common.ts +26 -7
- package/src/skc-runtime/tmux-gc.ts +52 -9
- package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
- package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
- package/src/skc-runtime/tmux-sessions.ts +871 -68
- package/src/slash-commands/builtin-registry.ts +153 -9
- package/src/slash-commands/helpers/context-report.ts +26 -4
- package/src/task/executor.ts +12 -3
- package/src/task/index.ts +8 -0
- package/src/task/receipt.ts +7 -2
- package/src/task/types.ts +26 -0
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/browser/tab-supervisor.ts +63 -6
- package/src/tools/browser.ts +8 -3
- package/src/tools/index.ts +81 -6
- package/src/tools/sqlite-reader.ts +8 -4
- package/src/tui/output-block.ts +18 -6
- package/src/utils/external-editor.ts +6 -1
|
@@ -5,19 +5,46 @@ import { AcpAgent } from "./acp-agent";
|
|
|
5
5
|
|
|
6
6
|
export type AcpSessionFactory = (cwd: string) => Promise<AgentSession>;
|
|
7
7
|
|
|
8
|
+
export interface AcpConnectionHandle {
|
|
9
|
+
connection: AgentSideConnection;
|
|
10
|
+
agent: AcpAgent;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
export function createAcpConnection(
|
|
9
14
|
transport: Stream,
|
|
10
15
|
createSession: AcpSessionFactory,
|
|
11
16
|
initialSession?: AgentSession,
|
|
12
17
|
): AgentSideConnection {
|
|
13
|
-
return
|
|
18
|
+
return createAcpConnectionWithAgent(transport, createSession, initialSession).connection;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create an ACP connection and return both the {@link AgentSideConnection} and the
|
|
23
|
+
* underlying {@link AcpAgent}. Callers that need to await agent shutdown (terminal
|
|
24
|
+
* delete/dispose work) after transport closure should use this instead of
|
|
25
|
+
* {@link createAcpConnection}.
|
|
26
|
+
*/
|
|
27
|
+
export function createAcpConnectionWithAgent(
|
|
28
|
+
transport: Stream,
|
|
29
|
+
createSession: AcpSessionFactory,
|
|
30
|
+
initialSession?: AgentSession,
|
|
31
|
+
): AcpConnectionHandle {
|
|
32
|
+
let agent: AcpAgent | undefined;
|
|
33
|
+
const connection = new AgentSideConnection(conn => {
|
|
34
|
+
agent = new AcpAgent(conn, createSession, initialSession);
|
|
35
|
+
return agent;
|
|
36
|
+
}, transport);
|
|
37
|
+
return { connection, agent: agent! };
|
|
14
38
|
}
|
|
15
39
|
|
|
16
40
|
export async function runAcpMode(createSession: AcpSessionFactory, initialSession?: AgentSession): Promise<never> {
|
|
17
41
|
const input = stream.Writable.toWeb(process.stdout);
|
|
18
42
|
const output = stream.Readable.toWeb(process.stdin);
|
|
19
43
|
const transport = ndJsonStream(input, output);
|
|
20
|
-
const connection =
|
|
44
|
+
const { connection, agent } = createAcpConnectionWithAgent(transport, createSession, initialSession);
|
|
21
45
|
await connection.closed;
|
|
46
|
+
// Await connection-local terminal work (active deletes, disposal) so
|
|
47
|
+
// `process.exit` never kills in-flight deletion/cleanup.
|
|
48
|
+
await agent.shutdownPromise;
|
|
22
49
|
process.exit(0);
|
|
23
50
|
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { AssistantMessage, ImageContent, Usage } from "@sayknow-cli/ai";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type Component,
|
|
4
|
+
Container,
|
|
5
|
+
Image,
|
|
6
|
+
ImageProtocol,
|
|
7
|
+
isViewportAnchorSourceRenderer,
|
|
8
|
+
Markdown,
|
|
9
|
+
Spacer,
|
|
10
|
+
TERMINAL,
|
|
11
|
+
Text,
|
|
12
|
+
type ViewportAnchorSource,
|
|
13
|
+
} from "@sayknow-cli/tui";
|
|
3
14
|
import { formatNumber } from "@sayknow-cli/utils";
|
|
4
15
|
import { settings } from "../../config/settings";
|
|
5
16
|
import { renderDeepInterviewAssistantText } from "../../deep-interview/render-middleware";
|
|
@@ -62,6 +73,7 @@ interface AssistantMessageUpdateOptions {
|
|
|
62
73
|
type AssistantChildDescriptor = {
|
|
63
74
|
key: string;
|
|
64
75
|
component: Component;
|
|
76
|
+
anchorSource?: ViewportAnchorSource;
|
|
65
77
|
};
|
|
66
78
|
|
|
67
79
|
/**
|
|
@@ -81,15 +93,16 @@ export class AssistantMessageComponent extends Container {
|
|
|
81
93
|
#contentBlockKeys = new WeakMap<object, string>();
|
|
82
94
|
#nextContentBlockKey = 0;
|
|
83
95
|
#reusableChildren = new WeakSet<Component>();
|
|
84
|
-
|
|
96
|
+
#viewportAnchorId?: string;
|
|
85
97
|
constructor(
|
|
86
98
|
message?: AssistantMessage,
|
|
87
99
|
private hideThinkingBlock = false,
|
|
88
100
|
private readonly onImageUpdate?: () => void,
|
|
101
|
+
viewportAnchorId?: string,
|
|
89
102
|
) {
|
|
90
103
|
super();
|
|
104
|
+
this.#viewportAnchorId = viewportAnchorId;
|
|
91
105
|
|
|
92
|
-
// Container for text/thinking content
|
|
93
106
|
this.#contentContainer = new Container();
|
|
94
107
|
this.addChild(this.#contentContainer);
|
|
95
108
|
|
|
@@ -98,6 +111,13 @@ export class AssistantMessageComponent extends Container {
|
|
|
98
111
|
}
|
|
99
112
|
}
|
|
100
113
|
|
|
114
|
+
#contentAnchorSource(
|
|
115
|
+
index: number,
|
|
116
|
+
kind: "text" | "thinking" | "thinking-hidden",
|
|
117
|
+
): ViewportAnchorSource | undefined {
|
|
118
|
+
return this.#viewportAnchorId ? { id: `${this.#viewportAnchorId}:content:${index}:${kind}` } : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
101
121
|
override invalidate(): void {
|
|
102
122
|
super.invalidate();
|
|
103
123
|
if (this.#lastMessage) {
|
|
@@ -288,6 +308,12 @@ export class AssistantMessageComponent extends Container {
|
|
|
288
308
|
}
|
|
289
309
|
|
|
290
310
|
#reconcileChildren(descriptors: AssistantChildDescriptor[]): void {
|
|
311
|
+
for (const child of this.#contentContainer.children) this.#contentContainer.setViewportAnchorSource(child, null);
|
|
312
|
+
for (const descriptor of descriptors) {
|
|
313
|
+
if (descriptor.anchorSource && isViewportAnchorSourceRenderer(descriptor.component)) {
|
|
314
|
+
this.#contentContainer.setViewportAnchorSource(descriptor.component, descriptor.anchorSource);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
291
317
|
const nextChildren = descriptors.map(descriptor => descriptor.component);
|
|
292
318
|
if (
|
|
293
319
|
nextChildren.length === this.#contentContainer.children.length &&
|
|
@@ -346,6 +372,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
346
372
|
descriptors.push({
|
|
347
373
|
key: `${blockKey}:text`,
|
|
348
374
|
component: this.#renderTextBlock(content, streaming && i === activeContentIndex),
|
|
375
|
+
anchorSource: this.#contentAnchorSource(i, "text"),
|
|
349
376
|
});
|
|
350
377
|
} else if (content.type === "thinking" && content.thinking.trim()) {
|
|
351
378
|
// Add spacing only when another visible assistant content block follows.
|
|
@@ -358,11 +385,13 @@ export class AssistantMessageComponent extends Container {
|
|
|
358
385
|
`${blockKey}:thinking-hidden`,
|
|
359
386
|
() => new Text(theme.italic(theme.fg("thinkingText", "Thinking...")), 1, 0),
|
|
360
387
|
),
|
|
388
|
+
anchorSource: this.#contentAnchorSource(i, "thinking-hidden"),
|
|
361
389
|
});
|
|
362
390
|
} else {
|
|
363
391
|
descriptors.push({
|
|
364
392
|
key: `${blockKey}:thinking`,
|
|
365
393
|
component: this.#renderThinkingBlock(content, streaming && i === activeContentIndex),
|
|
394
|
+
anchorSource: this.#contentAnchorSource(i, "thinking"),
|
|
366
395
|
});
|
|
367
396
|
}
|
|
368
397
|
if (visibleContentAfter[i]) {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Container,
|
|
7
7
|
Ellipsis,
|
|
8
8
|
ImageProtocol,
|
|
9
|
+
isTerminalGraphicsFallbackActive,
|
|
9
10
|
type Loader,
|
|
10
11
|
TERMINAL,
|
|
11
12
|
Text,
|
|
@@ -16,7 +17,12 @@ import {
|
|
|
16
17
|
import { sanitizeText } from "@sayknow-cli/utils";
|
|
17
18
|
import { theme } from "../../modes/theme/theme";
|
|
18
19
|
import type { TruncationMeta } from "../../tools/output-meta";
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
containsSixelSequence,
|
|
22
|
+
getSixelLineMask,
|
|
23
|
+
isSixelPassthroughEnabled,
|
|
24
|
+
sanitizeWithOptionalSixelPassthrough,
|
|
25
|
+
} from "../../utils/sixel";
|
|
20
26
|
import {
|
|
21
27
|
buildExecutionFrame,
|
|
22
28
|
buildStatusFooter,
|
|
@@ -41,6 +47,7 @@ export class BashExecutionComponent extends Container {
|
|
|
41
47
|
#truncation?: TruncationMeta;
|
|
42
48
|
#expanded = false;
|
|
43
49
|
#displayDirty = false;
|
|
50
|
+
#displayBuiltWithGraphicsFallback: boolean | undefined;
|
|
44
51
|
#chunkGate = false;
|
|
45
52
|
#contentContainer: Container;
|
|
46
53
|
#headerText: Text;
|
|
@@ -135,7 +142,8 @@ export class BashExecutionComponent extends Container {
|
|
|
135
142
|
}
|
|
136
143
|
|
|
137
144
|
override render(width: number): string[] {
|
|
138
|
-
|
|
145
|
+
const fallbackActive = isTerminalGraphicsFallbackActive();
|
|
146
|
+
if (this.#displayDirty || this.#displayBuiltWithGraphicsFallback !== fallbackActive) {
|
|
139
147
|
this.#displayDirty = false;
|
|
140
148
|
this.#updateDisplay();
|
|
141
149
|
}
|
|
@@ -143,16 +151,16 @@ export class BashExecutionComponent extends Container {
|
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
#updateDisplay(): void {
|
|
154
|
+
const fallbackActive = isTerminalGraphicsFallbackActive();
|
|
146
155
|
const availableLines = this.#outputLines;
|
|
147
|
-
|
|
148
|
-
// Apply preview truncation based on expanded state
|
|
149
|
-
const previewLogicalLines = availableLines.slice(-PREVIEW_LINES);
|
|
150
|
-
const hiddenLineCount = availableLines.length - previewLogicalLines.length;
|
|
151
156
|
const sixelLineMask =
|
|
152
|
-
TERMINAL.imageProtocol === ImageProtocol.Sixel && isSixelPassthroughEnabled()
|
|
157
|
+
fallbackActive || (TERMINAL.imageProtocol === ImageProtocol.Sixel && isSixelPassthroughEnabled())
|
|
153
158
|
? getSixelLineMask(availableLines)
|
|
154
159
|
: undefined;
|
|
155
160
|
const hasSixelOutput = sixelLineMask?.some(Boolean) ?? false;
|
|
161
|
+
const selectedLines =
|
|
162
|
+
this.#expanded || (hasSixelOutput && !fallbackActive) ? availableLines : availableLines.slice(-PREVIEW_LINES);
|
|
163
|
+
const hiddenLineCount = availableLines.length - selectedLines.length;
|
|
156
164
|
|
|
157
165
|
// Rebuild content container
|
|
158
166
|
// Detach (not dispose): #headerText and the running #loader are persistent,
|
|
@@ -165,15 +173,36 @@ export class BashExecutionComponent extends Container {
|
|
|
165
173
|
this.#contentContainer.addChild(this.#headerText);
|
|
166
174
|
|
|
167
175
|
// Output
|
|
168
|
-
if (
|
|
169
|
-
if (
|
|
170
|
-
const
|
|
171
|
-
|
|
176
|
+
if (selectedLines.length > 0) {
|
|
177
|
+
if (fallbackActive && hasSixelOutput) {
|
|
178
|
+
const displayLines: string[] = [];
|
|
179
|
+
const selectedStart = availableLines.length - selectedLines.length;
|
|
180
|
+
for (const [selectedIndex, line] of selectedLines.entries()) {
|
|
181
|
+
const index = selectedStart + selectedIndex;
|
|
182
|
+
if (sixelLineMask?.[index]) {
|
|
183
|
+
// Emit one placeholder per visible SIXEL block: at a true sequence
|
|
184
|
+
// start, at the top of a collapsed slice that begins mid-sequence,
|
|
185
|
+
// or when the line itself opens a new sequence.
|
|
186
|
+
const isBlockStart = selectedIndex === 0 || !sixelLineMask[index - 1] || containsSixelSequence(line);
|
|
187
|
+
if (isBlockStart) {
|
|
188
|
+
displayLines.push(theme.fg("muted", "[SIXEL image hidden while IRC sidebar is visible]"));
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
displayLines.push(theme.fg("muted", line));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
this.#contentContainer.addChild(new Text(`\n${displayLines.join("\n")}`, 1, 0));
|
|
195
|
+
} else if (this.#expanded || hasSixelOutput) {
|
|
196
|
+
const displayText = selectedLines
|
|
197
|
+
.map((line, selectedIndex) => {
|
|
198
|
+
const index = availableLines.length - selectedLines.length + selectedIndex;
|
|
199
|
+
return sixelLineMask?.[index] ? line : theme.fg("muted", line);
|
|
200
|
+
})
|
|
172
201
|
.join("\n");
|
|
173
202
|
this.#contentContainer.addChild(new Text(`\n${displayText}`, 1, 0));
|
|
174
203
|
} else {
|
|
175
204
|
// Use shared visual truncation utility, recomputed per render width
|
|
176
|
-
const styledOutput =
|
|
205
|
+
const styledOutput = selectedLines.map(line => theme.fg("muted", line)).join("\n");
|
|
177
206
|
this.#contentContainer.addChild(createCollapsedPreview(`\n${styledOutput}`, PREVIEW_LINES));
|
|
178
207
|
}
|
|
179
208
|
}
|
|
@@ -187,10 +216,11 @@ export class BashExecutionComponent extends Container {
|
|
|
187
216
|
exitCode: this.#exitCode,
|
|
188
217
|
truncation: this.#truncation,
|
|
189
218
|
hiddenLineCount,
|
|
190
|
-
suppressHiddenCount: hasSixelOutput,
|
|
219
|
+
suppressHiddenCount: hasSixelOutput && !fallbackActive,
|
|
191
220
|
});
|
|
192
221
|
if (footer) this.#contentContainer.addChild(footer);
|
|
193
222
|
}
|
|
223
|
+
this.#displayBuiltWithGraphicsFallback = fallbackActive;
|
|
194
224
|
}
|
|
195
225
|
|
|
196
226
|
#clampDisplayLine(line: string): string {
|
|
@@ -123,6 +123,11 @@ export class CustomEditor extends Editor {
|
|
|
123
123
|
this.#actionKeys.set(action, [...keys]);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/** Returns whether a built-in editor action consumes this configured key before custom handlers. */
|
|
127
|
+
hasActionKey(key: KeyId): boolean {
|
|
128
|
+
return [...this.#actionKeys.values()].some(keys => keys.includes(key));
|
|
129
|
+
}
|
|
130
|
+
|
|
126
131
|
#matchesAction(data: string, action: ConfigurableEditorAction): boolean {
|
|
127
132
|
const keys = this.#actionKeys.get(action);
|
|
128
133
|
if (!keys) return false;
|
|
@@ -139,8 +139,8 @@ export class FooterComponent implements Component {
|
|
|
139
139
|
// After compaction, tokens are unknown until the next LLM response.
|
|
140
140
|
const contextUsage = this.session.getContextUsage();
|
|
141
141
|
const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
|
|
142
|
-
const contextPercentValue = contextUsage?.percent
|
|
143
|
-
const contextPercent =
|
|
142
|
+
const contextPercentValue = contextUsage?.percent;
|
|
143
|
+
const contextPercent = typeof contextPercentValue === "number" ? contextPercentValue.toFixed(1) : "?";
|
|
144
144
|
|
|
145
145
|
// Replace home directory with ~
|
|
146
146
|
let pwd = shortenPath(getProjectDir());
|
|
@@ -188,7 +188,7 @@ export class FooterComponent implements Component {
|
|
|
188
188
|
contextPercent === "?"
|
|
189
189
|
? `?/${formatNumber(contextWindow)}${autoIndicator}`
|
|
190
190
|
: `${contextPercent}%/${formatNumber(contextWindow)}${autoIndicator}`;
|
|
191
|
-
if (
|
|
191
|
+
if (typeof contextPercentValue === "number") {
|
|
192
192
|
const color = getContextUsageThemeColor(getContextUsageLevel(contextPercentValue, contextWindow));
|
|
193
193
|
contextPercentStr =
|
|
194
194
|
color === "statusLineContext" ? contextPercentDisplay : theme.fg(color, contextPercentDisplay);
|
|
@@ -70,6 +70,11 @@ export interface HookSelectorOptions {
|
|
|
70
70
|
* completion behavior as the main prompt editor.
|
|
71
71
|
*/
|
|
72
72
|
autocompleteProvider?: AutocompleteProvider;
|
|
73
|
+
/**
|
|
74
|
+
* Maps raw single-key accelerators to option labels. Unset maps preserve
|
|
75
|
+
* the legacy selector key handling exactly.
|
|
76
|
+
*/
|
|
77
|
+
acceleratorMap?: Readonly<Record<string, string>>;
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
class OutlinedList extends Container {
|
|
@@ -352,6 +357,7 @@ export class HookSelectorComponent extends Container {
|
|
|
352
357
|
#baseHelpText: string;
|
|
353
358
|
#tui: TUI | undefined;
|
|
354
359
|
#autocompleteProvider: AutocompleteProvider | undefined;
|
|
360
|
+
#acceleratorMap: Readonly<Record<string, string>> | undefined;
|
|
355
361
|
constructor(
|
|
356
362
|
title: string,
|
|
357
363
|
options: string[],
|
|
@@ -376,6 +382,7 @@ export class HookSelectorComponent extends Container {
|
|
|
376
382
|
this.#clarificationInput = opts?.clarificationInput;
|
|
377
383
|
this.#tui = opts?.tui;
|
|
378
384
|
this.#autocompleteProvider = opts?.autocompleteProvider;
|
|
385
|
+
this.#acceleratorMap = opts?.acceleratorMap;
|
|
379
386
|
|
|
380
387
|
this.addChild(new DynamicBorder());
|
|
381
388
|
this.addChild(new Spacer(1));
|
|
@@ -503,6 +510,11 @@ export class HookSelectorComponent extends Container {
|
|
|
503
510
|
this.#handleInputModeKey(keyData, this.#inlineEditor);
|
|
504
511
|
return;
|
|
505
512
|
}
|
|
513
|
+
const acceleratedOption = this.#acceleratorMap?.[keyData.toLowerCase()];
|
|
514
|
+
if (acceleratedOption && this.#options.includes(acceleratedOption)) {
|
|
515
|
+
this.#onSelectCallback(acceleratedOption);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
506
518
|
if (matchesKey(keyData, "up") || keyData === "k") {
|
|
507
519
|
this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
|
|
508
520
|
this.#updateList();
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
padding,
|
|
4
|
+
renderComponentWithViewportAnchors,
|
|
5
|
+
TERMINAL,
|
|
6
|
+
truncateToWidth,
|
|
7
|
+
type ViewportAnchorProvider,
|
|
8
|
+
type ViewportAnchorRender,
|
|
9
|
+
visibleWidth,
|
|
10
|
+
withTerminalGraphicsFallback,
|
|
11
|
+
wrapTextWithAnsi,
|
|
12
|
+
} from "@sayknow-cli/tui";
|
|
13
|
+
import type { IrcObservationLedger, IrcObservationRecord } from "../irc-observation-ledger";
|
|
14
|
+
import { formatIrcMessageBlock, projectIrcText } from "../utils/irc-message";
|
|
15
|
+
|
|
16
|
+
export const IRC_SIDEBAR_WIDTH_RATIO = 0.3;
|
|
17
|
+
const IRC_SIDEBAR_MIN_WIDTH = 30;
|
|
18
|
+
const IRC_SEPARATOR_WIDTH = 3;
|
|
19
|
+
export const IRC_SIDEBAR_MAX_RENDER_ROWS = 2_048;
|
|
20
|
+
const IRC_SIDEBAR_MAX_RENDER_SOURCE_UTF8_BYTES = 64 * 1_024;
|
|
21
|
+
const IRC_SIDEBAR_OLDER_MESSAGES_ELISION = "… older IRC messages elided …";
|
|
22
|
+
const IRC_SIDEBAR_MESSAGE_ELISION = " … message elided …";
|
|
23
|
+
|
|
24
|
+
/** Computes transcript/sidebar widths while preserving at least half the terminal for the transcript. */
|
|
25
|
+
export function computeIrcSplitWidths(width: number): {
|
|
26
|
+
leftWidth: number;
|
|
27
|
+
separatorWidth: number;
|
|
28
|
+
rightWidth: number;
|
|
29
|
+
} {
|
|
30
|
+
const normalizedWidth = Math.max(0, Math.floor(width));
|
|
31
|
+
const transcriptFloor = Math.floor(normalizedWidth * 0.5);
|
|
32
|
+
const preferredSidebar = Math.max(IRC_SIDEBAR_MIN_WIDTH, Math.floor(normalizedWidth * IRC_SIDEBAR_WIDTH_RATIO));
|
|
33
|
+
const sidebarWidth = Math.max(
|
|
34
|
+
0,
|
|
35
|
+
Math.min(preferredSidebar, normalizedWidth - IRC_SEPARATOR_WIDTH - transcriptFloor),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// The sidebar yields entirely rather than rendering below its readable minimum.
|
|
39
|
+
if (sidebarWidth < IRC_SIDEBAR_MIN_WIDTH) {
|
|
40
|
+
return { leftWidth: normalizedWidth, separatorWidth: 0, rightWidth: 0 };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
leftWidth: normalizedWidth - IRC_SEPARATOR_WIDTH - sidebarWidth,
|
|
45
|
+
separatorWidth: IRC_SEPARATOR_WIDTH,
|
|
46
|
+
rightWidth: sidebarWidth,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function styleSender(componentTheme: IrcSidebarTheme, sender: string): string {
|
|
51
|
+
return componentTheme.fg("accent", componentTheme.bold(sender));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderSidebarRecord(
|
|
55
|
+
record: IrcObservationRecord,
|
|
56
|
+
width: number,
|
|
57
|
+
componentTheme: IrcSidebarTheme,
|
|
58
|
+
maxRows: number,
|
|
59
|
+
maxSourceUtf8Bytes: number,
|
|
60
|
+
): { lines: string[]; sourceUtf8Bytes: number; truncated: boolean } {
|
|
61
|
+
const bodyWidth = Math.max(1, width - 2);
|
|
62
|
+
const sourceLimit = Math.min(maxSourceUtf8Bytes, Math.max(0, maxRows - 1) * bodyWidth);
|
|
63
|
+
const clipped = projectIrcText(record.text, sourceLimit);
|
|
64
|
+
const block = formatIrcMessageBlock({ ...record, text: clipped.text });
|
|
65
|
+
const sender = styleSender(componentTheme, block.sender);
|
|
66
|
+
const time = componentTheme.fg("dim", block.time);
|
|
67
|
+
const lines = [truncateToWidth(`${sender} → ${block.recipient} · ${time}`, width)];
|
|
68
|
+
let truncated = clipped.truncated;
|
|
69
|
+
|
|
70
|
+
body: for (const bodyLine of block.bodyLines) {
|
|
71
|
+
for (const wrappedLine of wrapTextWithAnsi(bodyLine, bodyWidth)) {
|
|
72
|
+
if (lines.length >= maxRows) {
|
|
73
|
+
truncated = true;
|
|
74
|
+
break body;
|
|
75
|
+
}
|
|
76
|
+
lines.push(truncateToWidth(` ${wrappedLine}`, width));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (truncated && maxRows > 1) {
|
|
80
|
+
const marker = componentTheme.fg("dim", IRC_SIDEBAR_MESSAGE_ELISION);
|
|
81
|
+
if (lines.length >= maxRows) lines[maxRows - 1] = truncateToWidth(marker, width);
|
|
82
|
+
else lines.push(truncateToWidth(marker, width));
|
|
83
|
+
}
|
|
84
|
+
return { lines, sourceUtf8Bytes: clipped.utf8Bytes, truncated };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderSidebarRecords(ledger: IrcObservationLedger, width: number, componentTheme: IrcSidebarTheme): string[] {
|
|
88
|
+
if (width <= 0) return [];
|
|
89
|
+
|
|
90
|
+
const records = ledger.getSidebarRecords();
|
|
91
|
+
const newestFirstBlocks: string[][] = [];
|
|
92
|
+
let renderedRows = 0;
|
|
93
|
+
let renderedSourceUtf8Bytes = 0;
|
|
94
|
+
let omittedOlderRecords = false;
|
|
95
|
+
for (let index = records.length - 1; index >= 0; index--) {
|
|
96
|
+
const separatorRows = newestFirstBlocks.length > 0 ? 1 : 0;
|
|
97
|
+
const availableRows = IRC_SIDEBAR_MAX_RENDER_ROWS - renderedRows - separatorRows - 2;
|
|
98
|
+
const availableSourceUtf8Bytes = IRC_SIDEBAR_MAX_RENDER_SOURCE_UTF8_BYTES - renderedSourceUtf8Bytes;
|
|
99
|
+
const minimumRecordRows = records[index].text.length > 0 ? 2 : 1;
|
|
100
|
+
if (availableRows < minimumRecordRows || (availableSourceUtf8Bytes <= 0 && records[index].text.length > 0)) {
|
|
101
|
+
omittedOlderRecords = true;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const rendered = renderSidebarRecord(
|
|
106
|
+
records[index],
|
|
107
|
+
width,
|
|
108
|
+
componentTheme,
|
|
109
|
+
availableRows,
|
|
110
|
+
availableSourceUtf8Bytes,
|
|
111
|
+
);
|
|
112
|
+
newestFirstBlocks.push(rendered.lines);
|
|
113
|
+
renderedRows += separatorRows + rendered.lines.length;
|
|
114
|
+
renderedSourceUtf8Bytes += rendered.sourceUtf8Bytes;
|
|
115
|
+
if (rendered.truncated) {
|
|
116
|
+
omittedOlderRecords = index > 0;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const lines: string[] = [];
|
|
122
|
+
if (omittedOlderRecords) {
|
|
123
|
+
lines.push(truncateToWidth(componentTheme.fg("dim", IRC_SIDEBAR_OLDER_MESSAGES_ELISION), width));
|
|
124
|
+
}
|
|
125
|
+
for (const block of newestFirstBlocks.reverse()) {
|
|
126
|
+
if (lines.length > 0) lines.push("");
|
|
127
|
+
lines.push(...block);
|
|
128
|
+
}
|
|
129
|
+
return lines.slice(0, IRC_SIDEBAR_MAX_RENDER_ROWS);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface IrcSidebarTheme {
|
|
133
|
+
fg(color: "dim" | "accent", text: string): string;
|
|
134
|
+
bold(text: string): string;
|
|
135
|
+
readonly boxSharp: { readonly vertical: string };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type IrcSidebarThemeSource = IrcSidebarTheme | (() => IrcSidebarTheme);
|
|
139
|
+
|
|
140
|
+
/** Read-only IRC history alongside the active transcript. */
|
|
141
|
+
export class IrcSplitViewComponent implements ViewportAnchorProvider {
|
|
142
|
+
#visible = false;
|
|
143
|
+
|
|
144
|
+
constructor(
|
|
145
|
+
private readonly leftPane: Component,
|
|
146
|
+
private readonly ledger: IrcObservationLedger,
|
|
147
|
+
private readonly componentTheme: IrcSidebarThemeSource,
|
|
148
|
+
) {}
|
|
149
|
+
|
|
150
|
+
get visible(): boolean {
|
|
151
|
+
return this.#visible;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
effectiveSidebarVisible(width = process.stdout.columns ?? 0): boolean {
|
|
155
|
+
return this.#visible && computeIrcSplitWidths(width).rightWidth > 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
setVisible(visible: boolean): void {
|
|
159
|
+
if (this.#visible === visible) return;
|
|
160
|
+
this.#visible = visible;
|
|
161
|
+
this.invalidate();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
render(width: number): string[] {
|
|
165
|
+
return this.renderWithViewportAnchors(width).lines;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
renderWithViewportAnchors(width: number): ViewportAnchorRender {
|
|
169
|
+
if (!this.#visible) return renderComponentWithViewportAnchors(this.leftPane, width);
|
|
170
|
+
|
|
171
|
+
const componentTheme = typeof this.componentTheme === "function" ? this.componentTheme() : this.componentTheme;
|
|
172
|
+
const { leftWidth, separatorWidth, rightWidth } = computeIrcSplitWidths(width);
|
|
173
|
+
if (rightWidth === 0) return renderComponentWithViewportAnchors(this.leftPane, width);
|
|
174
|
+
|
|
175
|
+
const separatorText = componentTheme.fg("dim", ` ${componentTheme.boxSharp.vertical} `);
|
|
176
|
+
const separator = separatorWidth > 0 ? separatorText : "";
|
|
177
|
+
const leftRender = withTerminalGraphicsFallback(
|
|
178
|
+
() => renderComponentWithViewportAnchors(this.leftPane, leftWidth),
|
|
179
|
+
{ allowCursorNeutralImages: true },
|
|
180
|
+
);
|
|
181
|
+
const rightLines = renderSidebarRecords(this.ledger, rightWidth, componentTheme);
|
|
182
|
+
const lineCount = Math.max(leftRender.lines.length, rightLines.length);
|
|
183
|
+
const lines: string[] = [];
|
|
184
|
+
const anchors: ViewportAnchorRender["anchors"] = [];
|
|
185
|
+
|
|
186
|
+
const leftOffset = lineCount - leftRender.lines.length;
|
|
187
|
+
const rightOffset = lineCount - rightLines.length;
|
|
188
|
+
for (let index = 0; index < lineCount; index++) {
|
|
189
|
+
const leftIndex = index - leftOffset;
|
|
190
|
+
const leftRaw = leftRender.lines[leftIndex] ?? "";
|
|
191
|
+
const right = truncateToWidth(rightLines[index - rightOffset] ?? "", rightWidth);
|
|
192
|
+
if (TERMINAL.isImageLine(leftRaw)) {
|
|
193
|
+
lines.push(leftRaw + padding(leftWidth) + separator + right);
|
|
194
|
+
} else {
|
|
195
|
+
const left = truncateToWidth(leftRaw, leftWidth);
|
|
196
|
+
lines.push(left + padding(Math.max(0, leftWidth - visibleWidth(left))) + separator + right);
|
|
197
|
+
}
|
|
198
|
+
anchors.push(leftRender.anchors[leftIndex] ?? null);
|
|
199
|
+
}
|
|
200
|
+
return { lines, anchors };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
invalidate(): void {
|
|
204
|
+
this.leftPane.invalidate?.();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Container, PET_SKIN_IDS, PET_SKINS, type SelectItem, SelectList } from "@sayknow-cli/tui";
|
|
2
|
+
import { getSelectListTheme } from "../theme/theme";
|
|
3
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
4
|
+
import type { PetMode } from "./sayknow-pet-widget";
|
|
5
|
+
|
|
6
|
+
const PET_OPTIONS: { value: PetMode; label: string; description: string }[] = [
|
|
7
|
+
{ value: "off", label: "Off", description: "No pet" },
|
|
8
|
+
...PET_SKIN_IDS.map(id => ({
|
|
9
|
+
value: id,
|
|
10
|
+
label: PET_SKINS[id].label,
|
|
11
|
+
description: PET_SKINS[id].description,
|
|
12
|
+
})),
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Theme-style picker for the sayknow pet skin (Off / RedSayknow / BlueSayknow). Preview
|
|
17
|
+
* fires as the selection moves; select commits, cancel restores.
|
|
18
|
+
*/
|
|
19
|
+
export class PetSelectorComponent extends Container {
|
|
20
|
+
#selectList: SelectList;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
current: PetMode,
|
|
24
|
+
onSelect: (mode: PetMode) => void,
|
|
25
|
+
onCancel: () => void,
|
|
26
|
+
onPreview: (mode: PetMode) => void,
|
|
27
|
+
) {
|
|
28
|
+
super();
|
|
29
|
+
|
|
30
|
+
const items: SelectItem[] = PET_OPTIONS.map(option => ({
|
|
31
|
+
value: option.value,
|
|
32
|
+
label: option.label,
|
|
33
|
+
description: option.value === current ? `${option.description} (current)` : option.description,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
this.addChild(new DynamicBorder());
|
|
37
|
+
|
|
38
|
+
this.#selectList = new SelectList(items, 10, getSelectListTheme());
|
|
39
|
+
const currentIndex = PET_OPTIONS.findIndex(option => option.value === current);
|
|
40
|
+
if (currentIndex !== -1) {
|
|
41
|
+
this.#selectList.setSelectedIndex(currentIndex);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.#selectList.onSelect = item => onSelect(item.value as PetMode);
|
|
45
|
+
this.#selectList.onCancel = () => onCancel();
|
|
46
|
+
this.#selectList.onSelectionChange = item => onPreview(item.value as PetMode);
|
|
47
|
+
|
|
48
|
+
this.addChild(this.#selectList);
|
|
49
|
+
this.addChild(new DynamicBorder());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getSelectList(): SelectList {
|
|
53
|
+
return this.#selectList;
|
|
54
|
+
}
|
|
55
|
+
}
|