@gajae-code/coding-agent 0.9.6 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/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 +4 -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/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 +1 -1
- 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/config/keybindings.d.ts +5 -0
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +47 -7
- package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
- package/dist/types/coordinator-mcp/server.d.ts +67 -6
- package/dist/types/daemon/control-types.d.ts +10 -0
- package/dist/types/daemon/operator-contract.d.ts +47 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
- package/dist/types/gjc-runtime/launch-tmux.d.ts +26 -7
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +34 -3
- package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +309 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +49 -17
- package/dist/types/internal-urls/local-protocol.d.ts +13 -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/gajae-pet-widget.d.ts +59 -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/session-selector.d.ts +22 -9
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- 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/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/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 +26 -2
- package/dist/types/session/cache-economics.d.ts +79 -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/task/executor.d.ts +3 -0
- package/dist/types/task/receipt.d.ts +3 -1
- package/dist/types/task/types.d.ts +8 -0
- package/dist/types/tools/ask-answer-registry.d.ts +5 -0
- package/dist/types/tools/ask.d.ts +25 -1
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
- package/dist/types/tools/index.d.ts +58 -6
- package/dist/types/tools/telegram-send.d.ts +7 -0
- package/dist/types/utils/external-editor.d.ts +2 -0
- package/package.json +9 -9
- 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 +50 -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/coordinator-mcp/model-preset.ts +100 -0
- package/src/coordinator-mcp/server.ts +2653 -425
- package/src/daemon/control-types.ts +11 -0
- package/src/daemon/operator-contract.ts +121 -0
- package/src/deep-interview/render-middleware.ts +67 -2
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
- package/src/gjc-runtime/launch-tmux.ts +702 -135
- package/src/gjc-runtime/session-state-sidecar.ts +756 -135
- package/src/gjc-runtime/team-runtime.ts +6 -0
- package/src/gjc-runtime/tmux-common.ts +26 -7
- package/src/gjc-runtime/tmux-gc.ts +52 -9
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +185 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +2060 -0
- package/src/gjc-runtime/tmux-sessions.ts +871 -68
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/internal-urls/local-protocol.ts +29 -10
- package/src/main.ts +298 -75
- package/src/modes/acp/acp-agent.ts +728 -98
- 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/gajae-pet-widget.ts +360 -0
- 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/session-selector.ts +222 -221
- package/src/modes/components/settings-selector.ts +10 -0
- 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 +36 -17
- package/src/modes/controllers/input-controller.ts +8 -2
- package/src/modes/controllers/selector-controller.ts +62 -15
- package/src/modes/interactive-mode.ts +183 -7
- package/src/modes/irc-observation-ledger.ts +127 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
- 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/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-cli.ts +2 -0
- 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 +396 -145
- package/src/session/cache-economics.ts +275 -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/slash-commands/builtin-registry.ts +143 -9
- 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/ask.ts +355 -33
- 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 "@gajae-code/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 "@gajae-code/tui";
|
|
3
14
|
import { formatNumber } from "@gajae-code/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 "@gajae-code/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;
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnimationRegistration,
|
|
3
|
+
buildGajaePixelFrames,
|
|
4
|
+
type Component,
|
|
5
|
+
type Container,
|
|
6
|
+
type GajaePixelFrameName,
|
|
7
|
+
type GajaePixelFrames,
|
|
8
|
+
getCellDimensions,
|
|
9
|
+
ImageProtocol,
|
|
10
|
+
PARA_PARA_STEPS,
|
|
11
|
+
PET_SKINS,
|
|
12
|
+
type PetMode,
|
|
13
|
+
type PetSkinId,
|
|
14
|
+
petBurstDurationMs,
|
|
15
|
+
petBurstFrame,
|
|
16
|
+
registerAnimationCallback,
|
|
17
|
+
TERMINAL,
|
|
18
|
+
type TUI,
|
|
19
|
+
} from "@gajae-code/tui";
|
|
20
|
+
import type { CustomEditor } from "./custom-editor";
|
|
21
|
+
|
|
22
|
+
/** Re-exported from the tui skin registry so widget-relative imports stay valid. */
|
|
23
|
+
export type { PetMode, PetSkinId };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Empty columns on each side of the pet: an explicit inset from the right edge,
|
|
27
|
+
* with the composer's own right gutter (setRightGutterWidth(1)) as the left gap.
|
|
28
|
+
*/
|
|
29
|
+
const PET_SIDE_MARGIN = 1;
|
|
30
|
+
/** Sub-cell drop after the one-row safety lift, preserving a small bottom gap. */
|
|
31
|
+
const PET_SIXEL_DROP_PX = 9;
|
|
32
|
+
/**
|
|
33
|
+
* Kitty sub-cell drop below the one-row safety lift, as a fraction of the live cell
|
|
34
|
+
* height so it scales with the font. `floor` keeps it inside the cell; the value is
|
|
35
|
+
* clamped to the cell height.
|
|
36
|
+
*/
|
|
37
|
+
const KITTY_DROP_FRACTION = 0.45;
|
|
38
|
+
const petKittyDropPx = (cellHeightPx: number): number =>
|
|
39
|
+
Math.min(Math.max(0, cellHeightPx - 1), Math.floor(cellHeightPx * KITTY_DROP_FRACTION));
|
|
40
|
+
const PET_RAISE_ROWS = 1;
|
|
41
|
+
/** Working animation: the shared para-para beats looped end to end. */
|
|
42
|
+
const WORK_LOOP_TOTAL = PARA_PARA_STEPS.reduce((sum, [, ms]) => sum + ms, 0);
|
|
43
|
+
/** Random gap between automatic claw flexes (fires while idle AND working). */
|
|
44
|
+
const AUTO_FLEX_MIN_GAP_MS = 12_000;
|
|
45
|
+
const AUTO_FLEX_MAX_GAP_MS = 40_000;
|
|
46
|
+
// Deterministic idle loop: gaze around with a rare visor flicker.
|
|
47
|
+
const IDLE_LOOP: Array<[GajaePixelFrameName, number]> = [
|
|
48
|
+
["base", 1100],
|
|
49
|
+
["gazeL", 350],
|
|
50
|
+
["base", 500],
|
|
51
|
+
["gazeR", 350],
|
|
52
|
+
["base", 800],
|
|
53
|
+
["flicker", 150],
|
|
54
|
+
];
|
|
55
|
+
const IDLE_LOOP_TOTAL = IDLE_LOOP.reduce((sum, [, ms]) => sum + ms, 0);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Selector preview: fire the first burst this soon after a skin is previewed, so the
|
|
59
|
+
* pet shows one idle eye-roll (base -> gazeL -> base -> gazeR) and then its signature
|
|
60
|
+
* flex/cry. Live use keeps the random AUTO_FLEX gap; only preview forces this demo.
|
|
61
|
+
*/
|
|
62
|
+
const PREVIEW_INTRO_MS = 2300;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Wraps the composer editor, reserving a right-side area beside it where the
|
|
66
|
+
* real-pixel pet is drawn. The editor just renders narrower; the pet pixels
|
|
67
|
+
* are emitted separately as an absolute-positioned overlay.
|
|
68
|
+
*/
|
|
69
|
+
export class PetFramedEditor implements Component {
|
|
70
|
+
#editor: CustomEditor;
|
|
71
|
+
#reserve = 0;
|
|
72
|
+
|
|
73
|
+
constructor(editor: CustomEditor) {
|
|
74
|
+
this.#editor = editor;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setReserve(columns: number): void {
|
|
78
|
+
this.#reserve = columns;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
canFit(width: number): boolean {
|
|
82
|
+
return this.#reserve > 0 && width > this.#reserve + 8;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
invalidate(): void {
|
|
86
|
+
this.#editor.invalidate?.();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
render(width: number): string[] {
|
|
90
|
+
if (!this.canFit(width)) {
|
|
91
|
+
return this.#editor.render(width);
|
|
92
|
+
}
|
|
93
|
+
return this.#editor.render(width - this.#reserve);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The gajae pet: a 16x16 real-pixel sprite living in a reserved area beside
|
|
99
|
+
* the composer. It is nearest-neighbor scaled to the two terminal rows occupied
|
|
100
|
+
* by an empty one-line composer and lifted one row so its feet meet the input
|
|
101
|
+
* box's bottom edge.
|
|
102
|
+
*
|
|
103
|
+
* Rendering has two paths that share one payload builder:
|
|
104
|
+
* - a post-render emitter re-draws the sprite after every TUI write (line
|
|
105
|
+
* renders clear the pet cells, so the overlay must be re-applied), and
|
|
106
|
+
* - frame advances write the payload directly to the terminal, because the
|
|
107
|
+
* TUI skips writes entirely when no component line changed.
|
|
108
|
+
*
|
|
109
|
+
* Requires a sixel- or kitty-graphics terminal (`pixelProtocol()`).
|
|
110
|
+
*/
|
|
111
|
+
export class GajaePetWidget {
|
|
112
|
+
#ui: TUI;
|
|
113
|
+
#editor: CustomEditor;
|
|
114
|
+
#editorContainer: Container;
|
|
115
|
+
#floorContainer: Container;
|
|
116
|
+
#framedEditor: PetFramedEditor;
|
|
117
|
+
#isWorking: () => boolean;
|
|
118
|
+
#getComposerBottomOffset: () => number;
|
|
119
|
+
#mode: PetMode = "off";
|
|
120
|
+
#pixel: GajaePixelFrames | undefined;
|
|
121
|
+
#frame: GajaePixelFrameName = "base";
|
|
122
|
+
#animation: AnimationRegistration | undefined;
|
|
123
|
+
#flexUntil = 0;
|
|
124
|
+
#nextAutoFlexAt = 0;
|
|
125
|
+
#autoFlexGapMs: [number, number] | null;
|
|
126
|
+
#forcedProtocol: "sixel" | "kitty" | undefined;
|
|
127
|
+
/** Cell metrics the current frames were built for; a change triggers a rebuild. */
|
|
128
|
+
#builtCellW = 0;
|
|
129
|
+
#builtCellH = 0;
|
|
130
|
+
|
|
131
|
+
constructor(options: {
|
|
132
|
+
ui: TUI;
|
|
133
|
+
editor: CustomEditor;
|
|
134
|
+
editorContainer: Container;
|
|
135
|
+
floorContainer: Container;
|
|
136
|
+
isWorking: () => boolean;
|
|
137
|
+
/** Rows rendered below the composer box (pet floor + hook widgets). */
|
|
138
|
+
getComposerBottomOffset: () => number;
|
|
139
|
+
forcePixelProtocol?: "sixel" | "kitty";
|
|
140
|
+
/** Random [min, max] ms between auto-flexes; null disables. */
|
|
141
|
+
autoFlexGapMs?: [number, number] | null;
|
|
142
|
+
}) {
|
|
143
|
+
this.#ui = options.ui;
|
|
144
|
+
this.#editor = options.editor;
|
|
145
|
+
this.#editorContainer = options.editorContainer;
|
|
146
|
+
this.#floorContainer = options.floorContainer;
|
|
147
|
+
this.#framedEditor = new PetFramedEditor(options.editor);
|
|
148
|
+
this.#isWorking = options.isWorking;
|
|
149
|
+
this.#getComposerBottomOffset = options.getComposerBottomOffset;
|
|
150
|
+
this.#forcedProtocol = options.forcePixelProtocol;
|
|
151
|
+
this.#autoFlexGapMs =
|
|
152
|
+
options.autoFlexGapMs === undefined ? [AUTO_FLEX_MIN_GAP_MS, AUTO_FLEX_MAX_GAP_MS] : options.autoFlexGapMs;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Protocol available for the real-pixel pet, if any. */
|
|
156
|
+
static pixelProtocol(): "sixel" | "kitty" | null {
|
|
157
|
+
if (TERMINAL.imageProtocol === ImageProtocol.Kitty) return "kitty";
|
|
158
|
+
if (TERMINAL.imageProtocol === ImageProtocol.Sixel) return "sixel";
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get mode(): PetMode {
|
|
163
|
+
return this.#mode;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get isFlexing(): boolean {
|
|
167
|
+
return performance.now() < this.#flexUntil;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
setMode(mode: PetMode): void {
|
|
171
|
+
this.#applyMode(mode, true);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Live preview during a selector: change the sprite without re-mounting the
|
|
175
|
+
* composer editor (that would tear down the open overlay). After a short idle
|
|
176
|
+
* eye-roll it fires the signature burst once (RedGajae flex, BlueGajae para-para
|
|
177
|
+
* then sob) so the selector demos the animation instead of waiting the random gap. */
|
|
178
|
+
previewMode(mode: PetMode): void {
|
|
179
|
+
this.#applyMode(mode, false);
|
|
180
|
+
if (mode !== "off" && this.#autoFlexGapMs) {
|
|
181
|
+
this.#nextAutoFlexAt = performance.now() + PREVIEW_INTRO_MS;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#applyMode(mode: PetMode, mountComposer: boolean): void {
|
|
186
|
+
if (mode === this.#mode) return;
|
|
187
|
+
|
|
188
|
+
if (mode === "off") {
|
|
189
|
+
this.#mode = "off";
|
|
190
|
+
this.#animation?.unregister();
|
|
191
|
+
this.#animation = undefined;
|
|
192
|
+
this.#ui.setPostRenderEmitter(undefined);
|
|
193
|
+
this.#floorContainer.clear();
|
|
194
|
+
this.#pixel = undefined;
|
|
195
|
+
this.#framedEditor.setReserve(0);
|
|
196
|
+
if (mountComposer) this.#mountEditor(false);
|
|
197
|
+
this.#ui.requestRender(true);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const protocol = this.#forcedProtocol ?? GajaePetWidget.pixelProtocol();
|
|
202
|
+
if (!protocol) return;
|
|
203
|
+
this.#mode = mode;
|
|
204
|
+
this.#frame = "base";
|
|
205
|
+
this.#flexUntil = 0;
|
|
206
|
+
this.#nextAutoFlexAt = 0;
|
|
207
|
+
this.#buildPixel(protocol);
|
|
208
|
+
if (mountComposer) this.#mountEditor(true);
|
|
209
|
+
// The pet overlays the composer's bottom rows; no floor row is reserved, so
|
|
210
|
+
// the composer stays pinned to the terminal bottom.
|
|
211
|
+
this.#floorContainer.clear();
|
|
212
|
+
this.#ui.setPostRenderEmitter(() => this.#overlayPayload());
|
|
213
|
+
this.#animation ??= registerAnimationCallback(now => this.#tick(now), 80);
|
|
214
|
+
this.#ui.requestRender(true);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** (Re)build the encoded frames for the current terminal cell metrics. */
|
|
218
|
+
#buildPixel(protocol: "sixel" | "kitty"): void {
|
|
219
|
+
const cell = getCellDimensions();
|
|
220
|
+
this.#builtCellW = cell.widthPx;
|
|
221
|
+
this.#builtCellH = cell.heightPx;
|
|
222
|
+
const skin: PetSkinId = this.#mode === "off" ? "red" : this.#mode;
|
|
223
|
+
this.#pixel = buildGajaePixelFrames({
|
|
224
|
+
protocol,
|
|
225
|
+
skin,
|
|
226
|
+
cellWidthPx: cell.widthPx,
|
|
227
|
+
cellHeightPx: cell.heightPx,
|
|
228
|
+
targetRows: 2,
|
|
229
|
+
sixelTopPaddingPx: protocol === "sixel" ? PET_SIXEL_DROP_PX : 0,
|
|
230
|
+
kittyCellYOffsetPx: protocol === "kitty" ? petKittyDropPx(cell.heightPx) : 0,
|
|
231
|
+
});
|
|
232
|
+
this.#framedEditor.setReserve(this.#pixel.columns + PET_SIDE_MARGIN);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
dispose(): void {
|
|
236
|
+
this.#animation?.unregister();
|
|
237
|
+
this.#animation = undefined;
|
|
238
|
+
this.#ui.setPostRenderEmitter(undefined);
|
|
239
|
+
this.#floorContainer.clear();
|
|
240
|
+
this.#framedEditor.setReserve(0);
|
|
241
|
+
this.#mountEditor(false);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#mountEditor(framed: boolean): void {
|
|
245
|
+
this.#editorContainer.clear();
|
|
246
|
+
this.#editorContainer.addChild(framed ? this.#framedEditor : this.#editor);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Re-mount the composer editor (framed when a skin is active) after an overlay. */
|
|
250
|
+
remountComposer(): void {
|
|
251
|
+
this.#mountEditor(this.#mode !== "off");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#pickFrame(now: number): GajaePixelFrameName {
|
|
255
|
+
const mode = this.#mode;
|
|
256
|
+
if (mode === "off") return "base";
|
|
257
|
+
// Random idle burst → the skin's own animation, driven by its burst descriptor
|
|
258
|
+
// (RedGajae holds a flex; BlueGajae dances the para-para then sobs).
|
|
259
|
+
if (now < this.#flexUntil) {
|
|
260
|
+
const burst = PET_SKINS[mode].burst;
|
|
261
|
+
const elapsed = now - (this.#flexUntil - petBurstDurationMs(burst));
|
|
262
|
+
return petBurstFrame(burst, elapsed, now);
|
|
263
|
+
}
|
|
264
|
+
// Working → loop the shared para-para beats.
|
|
265
|
+
if (this.#isWorking()) {
|
|
266
|
+
let d = now % WORK_LOOP_TOTAL;
|
|
267
|
+
for (const [frame, ms] of PARA_PARA_STEPS) {
|
|
268
|
+
if (d < ms) return frame;
|
|
269
|
+
d -= ms;
|
|
270
|
+
}
|
|
271
|
+
return "base";
|
|
272
|
+
}
|
|
273
|
+
let t = now % IDLE_LOOP_TOTAL;
|
|
274
|
+
for (const [frame, ms] of IDLE_LOOP) {
|
|
275
|
+
if (t < ms) return frame;
|
|
276
|
+
t -= ms;
|
|
277
|
+
}
|
|
278
|
+
return "base";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#scheduleAutoFlex(now: number): void {
|
|
282
|
+
if (!this.#autoFlexGapMs) return;
|
|
283
|
+
const [min, max] = this.#autoFlexGapMs;
|
|
284
|
+
this.#nextAutoFlexAt = now + min + Math.random() * Math.max(0, max - min);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#tick(now: number): void {
|
|
288
|
+
if (this.#mode === "off" || !this.#pixel) return;
|
|
289
|
+
// A font/zoom change resizes the terminal cells; rebuild the frames so the
|
|
290
|
+
// kitty image and its sub-cell drop match the new cell metrics.
|
|
291
|
+
const cell = getCellDimensions();
|
|
292
|
+
if (cell.widthPx !== this.#builtCellW || cell.heightPx !== this.#builtCellH) {
|
|
293
|
+
const protocol = this.#forcedProtocol ?? GajaePetWidget.pixelProtocol();
|
|
294
|
+
if (protocol) {
|
|
295
|
+
this.#buildPixel(protocol);
|
|
296
|
+
this.#mountEditor(true);
|
|
297
|
+
this.#ui.requestRender(true);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Random show-off, both while idle and while working. Each skin's burst runs for
|
|
301
|
+
// its own length (RedGajae a brief flex; BlueGajae a para-para cycle plus sob).
|
|
302
|
+
if (this.#autoFlexGapMs && now >= this.#flexUntil) {
|
|
303
|
+
if (this.#nextAutoFlexAt === 0) {
|
|
304
|
+
this.#scheduleAutoFlex(now);
|
|
305
|
+
} else if (now >= this.#nextAutoFlexAt) {
|
|
306
|
+
const burstMs = petBurstDurationMs(PET_SKINS[this.#mode].burst);
|
|
307
|
+
this.#flexUntil = now + burstMs;
|
|
308
|
+
this.#scheduleAutoFlex(now + burstMs);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const frame = this.#pickFrame(now);
|
|
312
|
+
if (frame === this.#frame) return;
|
|
313
|
+
this.#frame = frame;
|
|
314
|
+
// Write directly: a frame swap changes no component line, so the TUI
|
|
315
|
+
// would skip the render write (and with it the post-render emitter).
|
|
316
|
+
const payload = this.#overlayPayload(true) ?? "";
|
|
317
|
+
if (payload && this.#ui.terminalAvailable) {
|
|
318
|
+
this.#ui.terminal.write(`\x1b[?2026h\x1b7${payload}\x1b8\x1b[?2026l`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
#petPosition(): { x: number; y: number } | null {
|
|
323
|
+
const pixel = this.#pixel;
|
|
324
|
+
if (!pixel) return null;
|
|
325
|
+
const columns = this.#ui.terminal.columns;
|
|
326
|
+
if (!this.#framedEditor.canFit(columns)) return null;
|
|
327
|
+
const rows = this.#ui.terminal.rows;
|
|
328
|
+
// The sprite is lifted one safety row above the scrolling edge, then dropped
|
|
329
|
+
// back onto the composer's bottom border per protocol (sixel via transparent
|
|
330
|
+
// top padding, kitty via a sub-cell Y offset baked into the frames).
|
|
331
|
+
const composerBottom = rows - this.#getComposerBottomOffset();
|
|
332
|
+
const y = composerBottom - pixel.rows - PET_RAISE_ROWS;
|
|
333
|
+
const x = columns - pixel.columns - PET_SIDE_MARGIN;
|
|
334
|
+
if (y < 0 || x < 0) return null;
|
|
335
|
+
return { x, y };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
#clearPetPayload(x: number, y: number): string {
|
|
339
|
+
const pixel = this.#pixel;
|
|
340
|
+
if (pixel?.protocol !== "sixel") return "";
|
|
341
|
+
let out = "\x1b[0m";
|
|
342
|
+
for (let row = 0; row < pixel.rasterRows; row++) {
|
|
343
|
+
out += `\x1b[${y + row + 1};${x + 1}H\x1b[${pixel.columns}X`;
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Draw escape payload at the pet's absolute position. */
|
|
349
|
+
#overlayPayload(clearPet = false): string | null {
|
|
350
|
+
const pixel = this.#pixel;
|
|
351
|
+
if (!pixel) return null;
|
|
352
|
+
const pos = this.#petPosition();
|
|
353
|
+
if (!pos) return null;
|
|
354
|
+
const { x, y } = pos;
|
|
355
|
+
|
|
356
|
+
let out = clearPet ? this.#clearPetPayload(x, y) : "";
|
|
357
|
+
out += `\x1b[${y + 1};${x + 1}H${pixel.frames[this.#frame]}`;
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -69,6 +69,11 @@ export interface HookSelectorOptions {
|
|
|
69
69
|
* completion behavior as the main prompt editor.
|
|
70
70
|
*/
|
|
71
71
|
autocompleteProvider?: AutocompleteProvider;
|
|
72
|
+
/**
|
|
73
|
+
* Maps raw single-key accelerators to option labels. Unset maps preserve
|
|
74
|
+
* the legacy selector key handling exactly.
|
|
75
|
+
*/
|
|
76
|
+
acceleratorMap?: Readonly<Record<string, string>>;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
class OutlinedList extends Container {
|
|
@@ -351,6 +356,7 @@ export class HookSelectorComponent extends Container {
|
|
|
351
356
|
#baseHelpText: string;
|
|
352
357
|
#tui: TUI | undefined;
|
|
353
358
|
#autocompleteProvider: AutocompleteProvider | undefined;
|
|
359
|
+
#acceleratorMap: Readonly<Record<string, string>> | undefined;
|
|
354
360
|
constructor(
|
|
355
361
|
title: string,
|
|
356
362
|
options: string[],
|
|
@@ -375,6 +381,7 @@ export class HookSelectorComponent extends Container {
|
|
|
375
381
|
this.#clarificationInput = opts?.clarificationInput;
|
|
376
382
|
this.#tui = opts?.tui;
|
|
377
383
|
this.#autocompleteProvider = opts?.autocompleteProvider;
|
|
384
|
+
this.#acceleratorMap = opts?.acceleratorMap;
|
|
378
385
|
|
|
379
386
|
this.addChild(new DynamicBorder());
|
|
380
387
|
this.addChild(new Spacer(1));
|
|
@@ -502,6 +509,11 @@ export class HookSelectorComponent extends Container {
|
|
|
502
509
|
this.#handleInputModeKey(keyData, this.#inlineEditor);
|
|
503
510
|
return;
|
|
504
511
|
}
|
|
512
|
+
const acceleratedOption = this.#acceleratorMap?.[keyData.toLowerCase()];
|
|
513
|
+
if (acceleratedOption && this.#options.includes(acceleratedOption)) {
|
|
514
|
+
this.#onSelectCallback(acceleratedOption);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
505
517
|
if (matchesKey(keyData, "up") || keyData === "k") {
|
|
506
518
|
this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
|
|
507
519
|
this.#updateList();
|