@oh-my-pi/pi-coding-agent 15.12.4 → 15.13.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 +304 -6
- package/dist/cli.js +1015 -881
- package/dist/types/async/job-manager.d.ts +15 -0
- package/dist/types/autolearn/controller.d.ts +25 -0
- package/dist/types/autolearn/managed-skills.d.ts +45 -0
- package/dist/types/autoresearch/state.d.ts +1 -1
- package/dist/types/autoresearch/types.d.ts +1 -1
- package/dist/types/cli/args.d.ts +19 -1
- package/dist/types/cli/session-picker.d.ts +1 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/cli/setup-model-picker.d.ts +14 -0
- package/dist/types/collab/protocol.d.ts +1 -1
- package/dist/types/commands/say.d.ts +24 -0
- package/dist/types/config/keybindings.d.ts +3 -3
- package/dist/types/config/model-registry.d.ts +10 -0
- package/dist/types/config/models-config-schema.d.ts +12 -0
- package/dist/types/config/models-config.d.ts +8 -2
- package/dist/types/config/settings-schema.d.ts +261 -58
- package/dist/types/export/html/index.d.ts +2 -1
- package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +47 -1
- package/dist/types/extensibility/hooks/index.d.ts +2 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
- package/dist/types/extensibility/plugins/loader.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/extensibility/skills.d.ts +10 -0
- package/dist/types/goals/guided-setup.d.ts +18 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/hindsight/transcript.d.ts +1 -1
- package/dist/types/index.d.ts +5 -0
- package/dist/types/internal-urls/local-protocol.d.ts +4 -2
- package/dist/types/main.d.ts +4 -3
- package/dist/types/mcp/startup-events.d.ts +11 -0
- package/dist/types/memories/index.d.ts +7 -0
- package/dist/types/memory-backend/local-backend.d.ts +4 -3
- package/dist/types/mnemopi/config.d.ts +4 -4
- package/dist/types/modes/components/agent-hub.d.ts +6 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
- package/dist/types/modes/components/custom-editor.d.ts +39 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/tool-execution.d.ts +26 -16
- package/dist/types/modes/components/transcript-container.d.ts +23 -2
- package/dist/types/modes/components/tree-selector.d.ts +1 -1
- package/dist/types/modes/components/usage-row.d.ts +3 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -2
- package/dist/types/modes/controllers/input-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/gradient-highlight.d.ts +9 -4
- package/dist/types/modes/image-references.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +27 -3
- package/dist/types/modes/magic-keywords.d.ts +13 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
- package/dist/types/modes/runtime-init.d.ts +4 -0
- package/dist/types/modes/theme/theme.d.ts +13 -2
- package/dist/types/modes/types.d.ts +8 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-registry.d.ts +17 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +14 -2
- package/dist/types/session/indexed-session-storage.d.ts +3 -4
- package/dist/types/session/session-context.d.ts +39 -0
- package/dist/types/session/session-entries.d.ts +159 -0
- package/dist/types/session/session-listing.d.ts +69 -0
- package/dist/types/session/session-loader.d.ts +16 -0
- package/dist/types/session/session-manager.d.ts +82 -474
- package/dist/types/session/session-migrations.d.ts +12 -0
- package/dist/types/session/session-paths.d.ts +25 -0
- package/dist/types/session/session-persistence.d.ts +8 -0
- package/dist/types/session/session-storage.d.ts +11 -12
- package/dist/types/session/snapcompact-inline.d.ts +12 -1
- package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
- package/dist/types/session/tool-choice-queue.d.ts +6 -6
- package/dist/types/stt/asr-client.d.ts +90 -0
- package/dist/types/stt/asr-protocol.d.ts +97 -0
- package/dist/types/stt/asr-worker.d.ts +2 -0
- package/dist/types/stt/downloader.d.ts +38 -0
- package/dist/types/stt/endpointer.d.ts +59 -0
- package/dist/types/stt/index.d.ts +5 -1
- package/dist/types/stt/models.d.ts +120 -0
- package/dist/types/stt/recorder.d.ts +17 -0
- package/dist/types/stt/stt-controller.d.ts +6 -0
- package/dist/types/stt/transcriber.d.ts +5 -7
- package/dist/types/stt/wav.d.ts +29 -0
- package/dist/types/system-prompt.d.ts +4 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/index.d.ts +9 -1
- package/dist/types/task/types.d.ts +36 -0
- package/dist/types/tools/bash.d.ts +2 -2
- package/dist/types/tools/eval-render.d.ts +1 -1
- package/dist/types/tools/index.d.ts +11 -1
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/learn.d.ts +51 -0
- package/dist/types/tools/manage-skill.d.ts +40 -0
- package/dist/types/tools/plan-mode-guard.d.ts +10 -0
- package/dist/types/tools/renderers.d.ts +7 -11
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +1 -1
- package/dist/types/tools/tts.d.ts +25 -0
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/tts/downloader.d.ts +20 -0
- package/dist/types/tts/index.d.ts +8 -0
- package/dist/types/tts/models.d.ts +82 -0
- package/dist/types/tts/player.d.ts +32 -0
- package/dist/types/tts/runtime.d.ts +6 -0
- package/dist/types/tts/streaming-player.d.ts +41 -0
- package/dist/types/tts/tts-client.d.ts +93 -0
- package/dist/types/tts/tts-protocol.d.ts +95 -0
- package/dist/types/tts/tts-worker.d.ts +2 -0
- package/dist/types/tts/vocalizer.d.ts +41 -0
- package/dist/types/tts/wav.d.ts +8 -0
- package/dist/types/utils/tool-choice.d.ts +8 -0
- package/dist/types/utils/tools-manager.d.ts +2 -1
- package/dist/types/utils/tools-manager.test.d.ts +1 -0
- package/dist/types/web/scrapers/github.d.ts +1 -1
- package/package.json +15 -14
- package/src/async/job-manager.ts +49 -0
- package/src/autolearn/controller.ts +139 -0
- package/src/autolearn/managed-skills.ts +257 -0
- package/src/autoresearch/state.ts +1 -1
- package/src/autoresearch/types.ts +1 -1
- package/src/cli/args.ts +56 -2
- package/src/cli/session-picker.ts +2 -1
- package/src/cli/setup-cli.ts +148 -47
- package/src/cli/setup-model-picker.ts +43 -0
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +45 -13
- package/src/collab/host.ts +1 -1
- package/src/collab/protocol.ts +1 -1
- package/src/commands/say.ts +102 -0
- package/src/commands/setup.ts +1 -1
- package/src/commit/agentic/tools/analyze-file.ts +3 -0
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-discovery.ts +11 -5
- package/src/config/model-registry.ts +64 -9
- package/src/config/models-config-schema.ts +4 -1
- package/src/config/models-config.ts +2 -1
- package/src/config/settings-schema.ts +248 -32
- package/src/config/settings.ts +10 -0
- package/src/discovery/builtin.ts +23 -1
- package/src/discovery/claude-plugins.ts +44 -5
- package/src/discovery/helpers.ts +41 -1
- package/src/eval/__tests__/budget-bridge.test.ts +1 -1
- package/src/eval/js/shared/prelude.txt +69 -17
- package/src/export/html/index.ts +3 -6
- package/src/extensibility/extensions/model-api.ts +41 -0
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +52 -1
- package/src/extensibility/extensions/wrapper.ts +41 -5
- package/src/extensibility/hooks/index.ts +2 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
- package/src/extensibility/plugins/loader.ts +30 -19
- package/src/extensibility/plugins/manager.ts +221 -90
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +96 -15
- package/src/goals/guided-setup.ts +133 -0
- package/src/goals/state.ts +1 -1
- package/src/hindsight/transcript.ts +1 -1
- package/src/index.ts +5 -0
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/internal-urls/history-protocol.ts +1 -1
- package/src/internal-urls/local-protocol.ts +29 -7
- package/src/main.ts +27 -7
- package/src/mcp/startup-events.ts +21 -0
- package/src/mcp/transports/stdio.ts +2 -1
- package/src/memories/index.ts +146 -11
- package/src/memory-backend/local-backend.ts +11 -5
- package/src/mnemopi/backend.ts +1 -0
- package/src/mnemopi/config.ts +26 -10
- package/src/modes/acp/acp-agent.ts +3 -5
- package/src/modes/components/agent-hub.ts +49 -4
- package/src/modes/components/assistant-message.ts +4 -37
- package/src/modes/components/compaction-summary-message.ts +125 -26
- package/src/modes/components/custom-editor.test.ts +96 -0
- package/src/modes/components/custom-editor.ts +164 -8
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/tool-execution.ts +82 -43
- package/src/modes/components/transcript-container.ts +70 -1
- package/src/modes/components/tree-selector.ts +1 -1
- package/src/modes/components/usage-row.ts +18 -0
- package/src/modes/components/user-message.ts +4 -2
- package/src/modes/controllers/command-controller.ts +14 -4
- package/src/modes/controllers/event-controller.ts +78 -11
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +258 -27
- package/src/modes/controllers/selector-controller.ts +12 -2
- package/src/modes/gradient-highlight.ts +21 -9
- package/src/modes/image-references.ts +20 -0
- package/src/modes/interactive-mode.ts +286 -40
- package/src/modes/magic-keywords.ts +27 -5
- package/src/modes/rpc/rpc-mode.ts +146 -14
- package/src/modes/rpc/rpc-subagents.ts +2 -2
- package/src/modes/rpc/rpc-types.ts +8 -2
- package/src/modes/runtime-init.ts +28 -3
- package/src/modes/theme/theme.ts +98 -50
- package/src/modes/types.ts +6 -2
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +34 -6
- package/src/priority.json +5 -1
- package/src/prompts/agents/task.md +1 -0
- package/src/prompts/goals/guided-goal-interview.md +8 -0
- package/src/prompts/goals/guided-goal-system.md +12 -0
- package/src/prompts/memories/read-path.md +6 -0
- package/src/prompts/system/autolearn-guidance-learn.md +1 -0
- package/src/prompts/system/autolearn-guidance.md +7 -0
- package/src/prompts/system/autolearn-nudge.md +3 -0
- package/src/prompts/system/eager-task.md +7 -0
- package/src/prompts/system/eager-todo.md +11 -6
- package/src/prompts/system/subagent-system-prompt.md +4 -0
- package/src/prompts/system/system-prompt.md +10 -5
- package/src/prompts/system/title-marker-instruction.md +1 -0
- package/src/prompts/system/title-system-marker.md +16 -0
- package/src/prompts/tools/job.md +1 -0
- package/src/prompts/tools/learn.md +7 -0
- package/src/prompts/tools/manage-skill.md +9 -0
- package/src/prompts/tools/task.md +3 -0
- package/src/registry/agent-registry.ts +30 -0
- package/src/sdk.ts +88 -24
- package/src/secrets/obfuscator.ts +1 -1
- package/src/session/agent-session.ts +209 -87
- package/src/session/history-storage.ts +2 -2
- package/src/session/indexed-session-storage.ts +7 -17
- package/src/session/session-context.ts +352 -0
- package/src/session/session-entries.ts +194 -0
- package/src/session/session-listing.ts +588 -0
- package/src/session/session-loader.ts +106 -0
- package/src/session/session-manager.ts +933 -3145
- package/src/session/session-migrations.ts +78 -0
- package/src/session/session-paths.ts +193 -0
- package/src/session/session-persistence.ts +131 -0
- package/src/session/session-storage.ts +91 -50
- package/src/session/snapcompact-inline.ts +21 -1
- package/src/session/snapcompact-savings-journal.ts +113 -0
- package/src/session/tool-choice-queue.ts +23 -11
- package/src/slash-commands/builtin-registry.ts +25 -3
- package/src/stt/asr-client.ts +520 -0
- package/src/stt/asr-protocol.ts +65 -0
- package/src/stt/asr-worker.ts +790 -0
- package/src/stt/downloader.ts +107 -47
- package/src/stt/endpointer.ts +259 -0
- package/src/stt/index.ts +5 -1
- package/src/stt/models.ts +150 -0
- package/src/stt/recorder.ts +247 -60
- package/src/stt/stt-controller.ts +201 -22
- package/src/stt/transcriber.ts +37 -68
- package/src/stt/wav.ts +173 -0
- package/src/system-prompt.ts +8 -0
- package/src/task/agents.ts +1 -2
- package/src/task/executor.ts +49 -15
- package/src/task/index.ts +60 -6
- package/src/task/render.ts +83 -8
- package/src/task/types.ts +53 -0
- package/src/tools/ask.ts +8 -0
- package/src/tools/bash.ts +4 -3
- package/src/tools/eval-render.ts +4 -3
- package/src/tools/index.ts +40 -4
- package/src/tools/irc.ts +10 -2
- package/src/tools/job.ts +14 -2
- package/src/tools/learn.ts +144 -0
- package/src/tools/manage-skill.ts +104 -0
- package/src/tools/plan-mode-guard.ts +53 -19
- package/src/tools/renderers.ts +7 -11
- package/src/tools/ssh.ts +4 -3
- package/src/tools/todo.ts +1 -1
- package/src/tools/tts.ts +203 -92
- package/src/tools/write.ts +18 -2
- package/src/tts/downloader.ts +64 -0
- package/src/tts/index.ts +8 -0
- package/src/tts/models.ts +137 -0
- package/src/tts/player.ts +137 -0
- package/src/tts/runtime.ts +21 -0
- package/src/tts/streaming-player.ts +266 -0
- package/src/tts/tts-client.ts +647 -0
- package/src/tts/tts-protocol.ts +60 -0
- package/src/tts/tts-worker.ts +497 -0
- package/src/tts/vocalizer.ts +162 -0
- package/src/tts/wav.ts +58 -0
- package/src/utils/title-generator.ts +48 -5
- package/src/utils/tool-choice.ts +16 -0
- package/src/utils/tools-manager.test.ts +25 -0
- package/src/utils/tools-manager.ts +19 -1
- package/src/web/scrapers/github.ts +96 -0
- package/src/web/search/index.ts +13 -0
- package/src/web/search/providers/searxng.ts +13 -1
- package/dist/types/stt/setup.d.ts +0 -18
- package/src/stt/setup.ts +0 -52
- package/src/stt/transcribe.py +0 -70
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import type { AssistantMessage, ImageContent
|
|
1
|
+
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { Container, Image, type ImageBudget, ImageProtocol, Markdown, Spacer, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
3
|
-
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
4
|
-
import { settings } from "../../config/settings";
|
|
5
3
|
import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
|
|
6
4
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
7
5
|
import { resolveAbortLabel, shouldRenderAbortReason } from "../../session/messages";
|
|
@@ -24,7 +22,6 @@ export class AssistantMessageComponent extends Container {
|
|
|
24
22
|
#contentContainer: Container;
|
|
25
23
|
#lastMessage?: AssistantMessage;
|
|
26
24
|
#toolImagesByCallId = new Map<string, ImageContent[]>();
|
|
27
|
-
#usageInfo?: Usage;
|
|
28
25
|
#convertedKittyImages = new Map<string, ImageContent>();
|
|
29
26
|
#kittyConversionsInFlight = new Set<string>();
|
|
30
27
|
#transcriptBlockFinalized: boolean;
|
|
@@ -40,11 +37,9 @@ export class AssistantMessageComponent extends Container {
|
|
|
40
37
|
/**
|
|
41
38
|
* Monotonic content version reported to the transcript container via
|
|
42
39
|
* {@link getTranscriptBlockVersion}. Bumped by {@link updateContent} — the
|
|
43
|
-
* choke point every mutator funnels through, including
|
|
44
|
-
*
|
|
45
|
-
* turn's `agent_start`, late tool-result images, async Kitty conversions
|
|
46
|
-
* and `setUsageInfo`. Without it, the container's committed-scrollback
|
|
47
|
-
* bypass would replay this block's pre-mutation bytes forever.
|
|
40
|
+
* choke point every mutator funnels through, including post-finalize changes
|
|
41
|
+
* such as `setErrorPinned(false)` restoring the inline error at the next
|
|
42
|
+
* turn's `agent_start`, late tool-result images, and async Kitty conversions.
|
|
48
43
|
*/
|
|
49
44
|
#blockVersion = 0;
|
|
50
45
|
/** Whether the last updateContent carried an in-flight streaming partial; such
|
|
@@ -185,13 +180,6 @@ export class AssistantMessageComponent extends Container {
|
|
|
185
180
|
}
|
|
186
181
|
}
|
|
187
182
|
|
|
188
|
-
setUsageInfo(usage: Usage): void {
|
|
189
|
-
this.#usageInfo = usage;
|
|
190
|
-
if (this.#lastMessage) {
|
|
191
|
-
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
183
|
#renderToolImages(): void {
|
|
196
184
|
const imageEntries = Array.from(this.#toolImagesByCallId.entries()).flatMap(([toolCallId, images]) =>
|
|
197
185
|
images.map((image, index) => ({ image, key: `${toolCallId}:${index}` })),
|
|
@@ -256,12 +244,6 @@ export class AssistantMessageComponent extends Container {
|
|
|
256
244
|
parts.push(`O:${content.type}`);
|
|
257
245
|
}
|
|
258
246
|
}
|
|
259
|
-
if (settings.get("display.showTokenUsage") && this.#usageInfo) {
|
|
260
|
-
const u = this.#usageInfo;
|
|
261
|
-
parts.push(`u:${u.input + u.cacheWrite}:${u.output}:${u.cacheRead}`);
|
|
262
|
-
} else {
|
|
263
|
-
parts.push("u:");
|
|
264
|
-
}
|
|
265
247
|
return parts.join("|");
|
|
266
248
|
}
|
|
267
249
|
|
|
@@ -416,21 +398,6 @@ export class AssistantMessageComponent extends Container {
|
|
|
416
398
|
) {
|
|
417
399
|
this.#appendErrorBlock(message.errorMessage);
|
|
418
400
|
}
|
|
419
|
-
|
|
420
|
-
// Token usage metadata
|
|
421
|
-
if (settings.get("display.showTokenUsage") && this.#usageInfo) {
|
|
422
|
-
const usage = this.#usageInfo;
|
|
423
|
-
const totalInput = usage.input + usage.cacheWrite;
|
|
424
|
-
const parts: string[] = [];
|
|
425
|
-
parts.push(`${theme.icon.input} ${formatNumber(totalInput)}`);
|
|
426
|
-
parts.push(`${theme.icon.output} ${formatNumber(usage.output)}`);
|
|
427
|
-
if (usage.cacheRead > 0) {
|
|
428
|
-
parts.push(`cache: ${formatNumber(usage.cacheRead)}`);
|
|
429
|
-
}
|
|
430
|
-
this.#contentContainer.addChild(new Spacer(1));
|
|
431
|
-
this.#contentContainer.addChild(new Text(theme.fg("dim", parts.join(" ")), 1, 0));
|
|
432
|
-
}
|
|
433
|
-
|
|
434
401
|
// Store fast-path state for next call
|
|
435
402
|
if (shouldCapture) {
|
|
436
403
|
this.#fastPathItems = captureItems;
|
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import { Box, type Component, Markdown } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
3
|
-
import type { CompactionSummaryMessage } from "../../session/messages";
|
|
3
|
+
import type { CompactionSummaryMessage, CustomMessage } from "../../session/messages";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* full history); only the LLM context was reset. Expanding (ctrl+o) reveals
|
|
12
|
-
* the compaction summary below the divider.
|
|
13
|
-
*/
|
|
14
|
-
export class CompactionSummaryMessageComponent implements Component {
|
|
5
|
+
interface SummaryDividerOptions {
|
|
6
|
+
label: () => string;
|
|
7
|
+
detailMarkdown: () => string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class SummaryDividerComponent implements Component {
|
|
15
11
|
#expanded = false;
|
|
16
12
|
#cache?: { width: number; lines: string[] };
|
|
17
13
|
#detail?: Box;
|
|
18
14
|
|
|
19
|
-
constructor(private readonly
|
|
15
|
+
constructor(private readonly options: SummaryDividerOptions) {}
|
|
20
16
|
|
|
21
17
|
setExpanded(expanded: boolean): void {
|
|
22
18
|
if (this.#expanded === expanded) return;
|
|
@@ -44,7 +40,7 @@ export class CompactionSummaryMessageComponent implements Component {
|
|
|
44
40
|
|
|
45
41
|
#divider(width: number): string {
|
|
46
42
|
const rule = theme.tree.horizontal;
|
|
47
|
-
const label =
|
|
43
|
+
const label = this.options.label();
|
|
48
44
|
// sep.dot ships pre-padded (" · "); trim so the hint joins with single spaces.
|
|
49
45
|
const hint = `${theme.sep.dot.trim()} ctrl+o`;
|
|
50
46
|
const plainWidth = Bun.stringWidth(`${label} ${hint}`, { countAnsiEscapeCodes: false });
|
|
@@ -66,22 +62,125 @@ export class CompactionSummaryMessageComponent implements Component {
|
|
|
66
62
|
#detailBox(): Box {
|
|
67
63
|
if (this.#detail) return this.#detail;
|
|
68
64
|
const box = new Box(1, 1, t => theme.bg("customMessageBg", t));
|
|
69
|
-
const tokenStr = this.message.tokensBefore.toLocaleString();
|
|
70
|
-
const frameCount = this.message.images?.length ?? 0;
|
|
71
|
-
const frameNote =
|
|
72
|
-
frameCount > 0 ? `\n\n_${frameCount} snapcompact frame${frameCount === 1 ? "" : "s"} attached_` : "";
|
|
73
65
|
box.addChild(
|
|
74
|
-
new Markdown(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
0,
|
|
78
|
-
getMarkdownTheme(),
|
|
79
|
-
{
|
|
80
|
-
color: (text: string) => theme.fg("customMessageText", text),
|
|
81
|
-
},
|
|
82
|
-
),
|
|
66
|
+
new Markdown(this.options.detailMarkdown(), 0, 0, getMarkdownTheme(), {
|
|
67
|
+
color: (text: string) => theme.fg("customMessageText", text),
|
|
68
|
+
}),
|
|
83
69
|
);
|
|
84
70
|
this.#detail = box;
|
|
85
71
|
return box;
|
|
86
72
|
}
|
|
87
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Compaction point in the transcript, rendered as a slim horizontal divider:
|
|
77
|
+
*
|
|
78
|
+
* ──────── 📷 compacted · ctrl+o ────────
|
|
79
|
+
*
|
|
80
|
+
* The conversation above the divider stays visible (display transcript keeps
|
|
81
|
+
* full history); only the LLM context was reset. Expanding (ctrl+o) reveals
|
|
82
|
+
* the compaction summary below the divider.
|
|
83
|
+
*/
|
|
84
|
+
export class CompactionSummaryMessageComponent implements Component {
|
|
85
|
+
#divider: SummaryDividerComponent;
|
|
86
|
+
|
|
87
|
+
constructor(private readonly message: CompactionSummaryMessage) {
|
|
88
|
+
this.#divider = new SummaryDividerComponent({
|
|
89
|
+
label: () => `${theme.icon.camera} compacted`,
|
|
90
|
+
detailMarkdown: () => this.#detailMarkdown(),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setExpanded(expanded: boolean): void {
|
|
95
|
+
this.#divider.setExpanded(expanded);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
invalidate(): void {
|
|
99
|
+
this.#divider.invalidate();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
render(width: number): readonly string[] {
|
|
103
|
+
return this.#divider.render(width);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#detailMarkdown(): string {
|
|
107
|
+
const tokenStr = this.message.tokensBefore.toLocaleString();
|
|
108
|
+
const frameCount = this.message.images?.length ?? 0;
|
|
109
|
+
const frameNote =
|
|
110
|
+
frameCount > 0 ? `\n\n_${frameCount} snapcompact frame${frameCount === 1 ? "" : "s"} attached_` : "";
|
|
111
|
+
return `**Compacted from ${tokenStr} tokens**\n\n${this.message.summary}${frameNote}`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Handoff is a compaction strategy too, but it is persisted as a custom message
|
|
117
|
+
* so the LLM sees the handoff-specific developer context. Render it with the
|
|
118
|
+
* same divider affordance as `/compact` instead of the generic `[handoff]` box.
|
|
119
|
+
*/
|
|
120
|
+
export class HandoffSummaryMessageComponent implements Component {
|
|
121
|
+
#divider: SummaryDividerComponent;
|
|
122
|
+
|
|
123
|
+
constructor(private readonly message: CustomMessage<unknown>) {
|
|
124
|
+
this.#divider = new SummaryDividerComponent({
|
|
125
|
+
label: () => `${theme.icon.context} handoff`,
|
|
126
|
+
detailMarkdown: () => this.#detailMarkdown(),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
setExpanded(expanded: boolean): void {
|
|
131
|
+
this.#divider.setExpanded(expanded);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
invalidate(): void {
|
|
135
|
+
this.#divider.invalidate();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
render(width: number): readonly string[] {
|
|
139
|
+
return this.#divider.render(width);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#detailMarkdown(): string {
|
|
143
|
+
const document = extractHandoffDocument(getCustomMessageText(this.message));
|
|
144
|
+
return `**Handoff context**\n\n${document || "_No handoff content._"}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createHandoffSummaryMessageComponent(
|
|
149
|
+
message: CustomMessage<unknown>,
|
|
150
|
+
expanded: boolean,
|
|
151
|
+
): HandoffSummaryMessageComponent | undefined {
|
|
152
|
+
if (message.customType !== "handoff" || !message.display) return undefined;
|
|
153
|
+
const component = new HandoffSummaryMessageComponent(message);
|
|
154
|
+
component.setExpanded(expanded);
|
|
155
|
+
return component;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getCustomMessageText(message: CustomMessage<unknown>): string {
|
|
159
|
+
if (typeof message.content === "string") return message.content;
|
|
160
|
+
let firstText: string | undefined;
|
|
161
|
+
let parts: string[] | undefined;
|
|
162
|
+
for (const content of message.content) {
|
|
163
|
+
if (content.type !== "text") continue;
|
|
164
|
+
if (firstText === undefined) {
|
|
165
|
+
firstText = content.text;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (parts === undefined) {
|
|
169
|
+
parts = [firstText];
|
|
170
|
+
}
|
|
171
|
+
parts.push(content.text);
|
|
172
|
+
}
|
|
173
|
+
return parts === undefined ? (firstText ?? "") : parts.join("\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function extractHandoffDocument(text: string): string {
|
|
177
|
+
const openTag = "<handoff-context>";
|
|
178
|
+
const closeTag = "</handoff-context>";
|
|
179
|
+
const openIndex = text.indexOf(openTag);
|
|
180
|
+
if (openIndex === -1) return text.trim();
|
|
181
|
+
|
|
182
|
+
const contentStart = openIndex + openTag.length;
|
|
183
|
+
const closeIndex = text.indexOf(closeTag, contentStart);
|
|
184
|
+
const document = closeIndex === -1 ? text.slice(contentStart) : text.slice(contentStart, closeIndex);
|
|
185
|
+
return document.trim();
|
|
186
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from "bun:test";
|
|
2
|
+
import { $ } from "bun";
|
|
3
|
+
import { getEditorTheme, initTheme } from "../theme/theme";
|
|
4
|
+
import { CustomEditor, SPACE_HOLD_RELEASE_MS, SPACE_HOLD_THRESHOLD } from "./custom-editor";
|
|
5
|
+
|
|
6
|
+
function makeEditor() {
|
|
7
|
+
const editor = new CustomEditor(getEditorTheme());
|
|
8
|
+
const events: string[] = [];
|
|
9
|
+
editor.sttHoldEnabled = () => true;
|
|
10
|
+
editor.onSpaceHoldStart = () => events.push("start");
|
|
11
|
+
editor.onSpaceHoldEnd = () => events.push("end");
|
|
12
|
+
return { editor, events };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function holdSpace(editor: CustomEditor, count: number): void {
|
|
16
|
+
for (let i = 0; i < count; i++) editor.handleInput(" ");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function decorateInFreshProcess(text: string): Promise<string> {
|
|
20
|
+
const customEditorUrl = new URL("./custom-editor.ts", import.meta.url).href;
|
|
21
|
+
const script = `
|
|
22
|
+
import { CustomEditor } from ${JSON.stringify(customEditorUrl)};
|
|
23
|
+
const editor = new CustomEditor({});
|
|
24
|
+
process.stdout.write(editor.decorateText(${JSON.stringify(text)}));
|
|
25
|
+
`;
|
|
26
|
+
const child = await $`bun -e ${script}`.quiet().nothrow();
|
|
27
|
+
const stdout = child.stdout.toString();
|
|
28
|
+
const stderr = child.stderr.toString();
|
|
29
|
+
if (child.exitCode !== 0) throw new Error(stderr || stdout || `decorate subprocess exited with ${child.exitCode}`);
|
|
30
|
+
return stdout;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("CustomEditor placeholder decoration", () => {
|
|
34
|
+
it("renders paste placeholders before theme initialization", async () => {
|
|
35
|
+
const output = await decorateInFreshProcess("[Paste #1, +30 lines]");
|
|
36
|
+
expect(output).toBe("[Paste #1, +30 lines]");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("renders image placeholders before theme initialization", async () => {
|
|
40
|
+
const output = await decorateInFreshProcess("[Image #1]");
|
|
41
|
+
expect(output).toBe("[Image #1]");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("CustomEditor space-hold push-to-talk", () => {
|
|
46
|
+
beforeAll(async () => {
|
|
47
|
+
await initTheme();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
vi.useRealTimers();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("inserts spaces normally below the hold threshold", () => {
|
|
55
|
+
const { editor, events } = makeEditor();
|
|
56
|
+
holdSpace(editor, SPACE_HOLD_THRESHOLD);
|
|
57
|
+
expect(editor.getText()).toBe(" ".repeat(SPACE_HOLD_THRESHOLD));
|
|
58
|
+
expect(events).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("tracks back the space burst and drives the hold lifecycle", () => {
|
|
62
|
+
vi.useFakeTimers();
|
|
63
|
+
const { editor, events } = makeEditor();
|
|
64
|
+
editor.handleInput("h");
|
|
65
|
+
editor.handleInput("i");
|
|
66
|
+
// Crossing the threshold deletes the optimistically-inserted spaces and starts recording,
|
|
67
|
+
// leaving only the pre-burst text behind.
|
|
68
|
+
holdSpace(editor, SPACE_HOLD_THRESHOLD + 1);
|
|
69
|
+
expect(editor.getText()).toBe("hi");
|
|
70
|
+
expect(events).toEqual(["start"]);
|
|
71
|
+
// Continued auto-repeat while the bar is held is swallowed: no spam, no re-trigger.
|
|
72
|
+
holdSpace(editor, 5);
|
|
73
|
+
expect(editor.getText()).toBe("hi");
|
|
74
|
+
expect(events).toEqual(["start"]);
|
|
75
|
+
// An idle gap with no further repeats means the bar was released -> stop + transcribe.
|
|
76
|
+
vi.advanceTimersByTime(SPACE_HOLD_RELEASE_MS + 1);
|
|
77
|
+
expect(events).toEqual(["start", "end"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("does not trigger when a non-space breaks the run", () => {
|
|
81
|
+
const { editor, events } = makeEditor();
|
|
82
|
+
holdSpace(editor, SPACE_HOLD_THRESHOLD);
|
|
83
|
+
editor.handleInput("x");
|
|
84
|
+
holdSpace(editor, SPACE_HOLD_THRESHOLD);
|
|
85
|
+
expect(events).toEqual([]);
|
|
86
|
+
expect(editor.getText()).toBe(`${" ".repeat(SPACE_HOLD_THRESHOLD)}x${" ".repeat(SPACE_HOLD_THRESHOLD)}`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("leaves the space bar typing normally when the gesture is disabled", () => {
|
|
90
|
+
const { editor, events } = makeEditor();
|
|
91
|
+
editor.sttHoldEnabled = () => false;
|
|
92
|
+
holdSpace(editor, SPACE_HOLD_THRESHOLD + 5);
|
|
93
|
+
expect(editor.getText()).toBe(" ".repeat(SPACE_HOLD_THRESHOLD + 5));
|
|
94
|
+
expect(events).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { addKeyAliases, canonicalKeyId, Editor, type KeyId, parseKey, parseKittySequence } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import type { AppKeybinding } from "../../config/keybindings";
|
|
3
|
+
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
3
4
|
import { imageReferenceHyperlink, PLACEHOLDER_REGEX, renderPlaceholders } from "../image-references";
|
|
4
|
-
import { highlightMagicKeywords } from "../magic-keywords";
|
|
5
|
-
import {
|
|
5
|
+
import { hasMagicKeyword, highlightMagicKeywords } from "../magic-keywords";
|
|
6
|
+
import { fgOrPlain } from "../theme/theme";
|
|
6
7
|
|
|
7
8
|
type ConfigurableEditorAction = Extract<
|
|
8
9
|
AppKeybinding,
|
|
@@ -61,6 +62,14 @@ const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i;
|
|
|
61
62
|
const BRACKETED_IMAGE_PATH_BOUNDARY_REGEX = /\.(?:png|jpe?g|gif|webp)(?=$|["']?\s)/gi;
|
|
62
63
|
const SHELL_ESCAPED_PATH_CHAR_REGEX = /\\([\\\s'"()[\]{}&;<>|?*!$`])/g;
|
|
63
64
|
|
|
65
|
+
/** Plain spaces from one auto-repeat run that trigger the space-hold push-to-talk STT gesture.
|
|
66
|
+
* Holding the space bar makes the terminal emit a burst of spaces; once more than this many land
|
|
67
|
+
* in the editor we treat it as "space held", track them back out, and start recording. */
|
|
68
|
+
export const SPACE_HOLD_THRESHOLD = 5;
|
|
69
|
+
/** Idle gap (ms) after the last repeated space that counts as the space bar being released, ending
|
|
70
|
+
* the push-to-talk recording. Must comfortably exceed the OS key-repeat interval. */
|
|
71
|
+
export const SPACE_HOLD_RELEASE_MS = 250;
|
|
72
|
+
|
|
64
73
|
function isPastedPathSeparator(char: string | undefined): boolean {
|
|
65
74
|
return char === undefined || char === " " || char === "\t" || char === "\r" || char === "\n";
|
|
66
75
|
}
|
|
@@ -136,19 +145,85 @@ export class CustomEditor extends Editor {
|
|
|
136
145
|
* instead of corrupting `[Paste #1, +30 lines]` into plain text. */
|
|
137
146
|
override atomicTokenPattern = PLACEHOLDER_REGEX;
|
|
138
147
|
|
|
148
|
+
/** Magic-keyword shimmer cadence — drives one editor repaint every 70 ms while
|
|
149
|
+
* a keyword is on screen and the prompt is focused. ~14 frames/s is smooth
|
|
150
|
+
* without flooding the renderer. */
|
|
151
|
+
static readonly SHIMMER_FRAME_MS = 70;
|
|
152
|
+
/** Time for the gradient to sweep one full cycle across each keyword. */
|
|
153
|
+
static readonly SHIMMER_PERIOD_MS = 1800;
|
|
154
|
+
|
|
155
|
+
/** Per-render scratch flag: did any layout line in this render contain a magic
|
|
156
|
+
* keyword that should shimmer? Reset by {@link #scheduleShimmerIfNeeded} each
|
|
157
|
+
* time a frame is queued. */
|
|
158
|
+
#shimmerTimer: ReturnType<typeof setTimeout> | undefined;
|
|
159
|
+
/** Repaint hook the host wires once at construction. Called from the shimmer
|
|
160
|
+
* timer to request the next animation frame. Undefined when nobody is
|
|
161
|
+
* listening (tests, headless callers); the timer chain still self-cleans. */
|
|
162
|
+
#requestShimmerRepaint: (() => void) | undefined;
|
|
163
|
+
|
|
139
164
|
/** Gradient-highlight the "ultrathink" / "orchestrate" / "workflowz" keywords as the user types
|
|
140
165
|
* them, skipping any occurrence inside code spans, fenced blocks, or XML sections. Also make
|
|
141
|
-
* pasted image placeholders visually distinct and hyperlink them once their blob file exists.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
* pasted image placeholders visually distinct and hyperlink them once their blob file exists.
|
|
167
|
+
* When the editor is focused, the buffer contains a magic keyword, and `magicKeywords.enabled`
|
|
168
|
+
* is on, the gradient shifts every frame to produce a Claude-Code-style shimmer; each render
|
|
169
|
+
* schedules the next frame, so losing focus, deleting the keyword, or flipping the setting
|
|
170
|
+
* stops the animation on its own. The static glow itself runs even when shimmering is gated
|
|
171
|
+
* off, matching existing behavior for the editor and sent bubbles. */
|
|
172
|
+
decorateText = (text: string): string => {
|
|
173
|
+
const animated = this.focused && this.#shimmerEnabled() && hasMagicKeyword(this.getText());
|
|
174
|
+
const phase = animated ? (Date.now() % CustomEditor.SHIMMER_PERIOD_MS) / CustomEditor.SHIMMER_PERIOD_MS : 0;
|
|
175
|
+
if (animated) this.#scheduleShimmerFrame();
|
|
176
|
+
return renderPlaceholders(text, {
|
|
177
|
+
renderText: value => highlightMagicKeywords(value, undefined, phase),
|
|
145
178
|
renderReference: (value, kind, index) =>
|
|
146
179
|
kind === "image"
|
|
147
180
|
? imageReferenceHyperlink(value, index, this.imageLinks, label =>
|
|
148
|
-
|
|
181
|
+
fgOrPlain("accent", label, `\x1b[1m\x1b[4m${label}\x1b[24m\x1b[22m`),
|
|
149
182
|
)
|
|
150
|
-
:
|
|
183
|
+
: fgOrPlain("accent", value, `\x1b[1m${value}\x1b[22m`),
|
|
151
184
|
});
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** Optional test/host override for the magic-keyword shimmer gate. When
|
|
188
|
+
* defined, takes precedence over the global `magicKeywords.enabled` setting,
|
|
189
|
+
* letting tests assert the gating behaviour without mutating the
|
|
190
|
+
* process-wide Settings singleton (which races with parallel test files —
|
|
191
|
+
* see issue #2582). Production wires this through the host's Settings
|
|
192
|
+
* reader and updates it on the relevant setting change. */
|
|
193
|
+
magicKeywordsEnabledOverride: boolean | undefined;
|
|
194
|
+
|
|
195
|
+
/** Whether the shimmer should advance this frame. Defaults to "on" before
|
|
196
|
+
* settings have initialised (tests, early boot) so the animation does not
|
|
197
|
+
* silently disappear during a race; settings disabling the feature wins
|
|
198
|
+
* once they are loaded. An explicit `magicKeywordsEnabledOverride` overrides
|
|
199
|
+
* both paths. */
|
|
200
|
+
#shimmerEnabled(): boolean {
|
|
201
|
+
if (this.magicKeywordsEnabledOverride !== undefined) return this.magicKeywordsEnabledOverride;
|
|
202
|
+
return isSettingsInitialized() ? settings.get("magicKeywords.enabled") : true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Bind the host's render request callback. Idempotent — the host wires this
|
|
206
|
+
* once after construction (and again after `setEditorComponent` swaps the
|
|
207
|
+
* editor). Passing `undefined` clears any pending frame. */
|
|
208
|
+
setShimmerRepaintHandler(handler: (() => void) | undefined): void {
|
|
209
|
+
this.#requestShimmerRepaint = handler;
|
|
210
|
+
if (!handler && this.#shimmerTimer) {
|
|
211
|
+
clearTimeout(this.#shimmerTimer);
|
|
212
|
+
this.#shimmerTimer = undefined;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Schedule one shimmer frame if none is already pending. The next render
|
|
217
|
+
* decides whether to schedule another, so the chain stops by itself when
|
|
218
|
+
* `focused` flips off or the keyword leaves the buffer. */
|
|
219
|
+
#scheduleShimmerFrame(): void {
|
|
220
|
+
if (this.#shimmerTimer || !this.#requestShimmerRepaint) return;
|
|
221
|
+
this.#shimmerTimer = setTimeout(() => {
|
|
222
|
+
this.#shimmerTimer = undefined;
|
|
223
|
+
this.#requestShimmerRepaint?.();
|
|
224
|
+
}, CustomEditor.SHIMMER_FRAME_MS);
|
|
225
|
+
this.#shimmerTimer.unref?.();
|
|
226
|
+
}
|
|
152
227
|
onEscape?: () => void;
|
|
153
228
|
onClear?: () => void;
|
|
154
229
|
onExit?: () => void;
|
|
@@ -178,9 +253,25 @@ export class CustomEditor extends Editor {
|
|
|
178
253
|
/** Called when left-arrow is pressed while the editor is empty (cursor necessarily at start). */
|
|
179
254
|
onLeftAtStart?: () => void;
|
|
180
255
|
|
|
256
|
+
/** Fired when a sustained space-bar hold is recognized — the push-to-talk STT start. The
|
|
257
|
+
* optimistically-typed spaces have already been deleted by the time this runs. */
|
|
258
|
+
onSpaceHoldStart?: () => void;
|
|
259
|
+
/** Fired when the held space bar is released (detected as an idle gap with no further repeated
|
|
260
|
+
* spaces) — the push-to-talk STT stop. */
|
|
261
|
+
onSpaceHoldEnd?: () => void;
|
|
262
|
+
/** Gate for the space-hold gesture. Returns false to keep the space bar inserting spaces
|
|
263
|
+
* normally; wired to `stt.enabled` so disabling STT restores plain space behavior. */
|
|
264
|
+
sttHoldEnabled?: () => boolean;
|
|
265
|
+
|
|
181
266
|
/** Custom key handlers from extensions and non-built-in app actions. */
|
|
182
267
|
#customKeyHandlers = new Map<KeyId, () => void>();
|
|
183
268
|
#customMatchKeys = new Map<string, () => void>();
|
|
269
|
+
/** Consecutive plain spaces inserted in the current run; any other key resets it. */
|
|
270
|
+
#spaceRunInserted = 0;
|
|
271
|
+
/** True while a recognized space-hold push-to-talk recording is in progress. */
|
|
272
|
+
#spaceHoldActive = false;
|
|
273
|
+
/** Idle timer that fires `onSpaceHoldEnd` once repeated spaces stop arriving. */
|
|
274
|
+
#spaceHoldTimer: NodeJS.Timeout | undefined;
|
|
184
275
|
#actionKeys = new Map<ConfigurableEditorAction, KeyId[]>(
|
|
185
276
|
Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [action as ConfigurableEditorAction, [...keys]]),
|
|
186
277
|
);
|
|
@@ -238,6 +329,68 @@ export class CustomEditor extends Editor {
|
|
|
238
329
|
this.#rebuildCustomMatchKeys();
|
|
239
330
|
}
|
|
240
331
|
|
|
332
|
+
#spaceHoldGestureEnabled(): boolean {
|
|
333
|
+
return this.onSpaceHoldStart !== undefined && (this.sttHoldEnabled?.() ?? false) && !this.isShowingAutocomplete();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Drive the space-hold push-to-talk state machine. Returns true when the gesture consumed the
|
|
337
|
+
* input so it must not reach normal editing. Holding the space bar makes the terminal emit a
|
|
338
|
+
* burst of auto-repeat spaces; once more than {@link SPACE_HOLD_THRESHOLD} of them land we treat
|
|
339
|
+
* it as a hold, delete the spam, and start recording until the repeats stop. */
|
|
340
|
+
#handleSpaceHold(data: string, canonical: string | undefined): boolean {
|
|
341
|
+
const isSpace = canonical === "space";
|
|
342
|
+
if (this.#spaceHoldActive) {
|
|
343
|
+
if (isSpace) {
|
|
344
|
+
// Auto-repeat while held: swallow it and keep the release timer alive.
|
|
345
|
+
this.#armSpaceHoldReleaseTimer();
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
// Any non-space means the bar was released — stop recording, then let the key through.
|
|
349
|
+
this.#endSpaceHold();
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
if (!isSpace) {
|
|
353
|
+
this.#spaceRunInserted = 0;
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
if (!this.#spaceHoldGestureEnabled()) return false;
|
|
357
|
+
// A short tap should still type a normal space, so insert optimistically and count the run.
|
|
358
|
+
super.handleInput(data);
|
|
359
|
+
this.#spaceRunInserted++;
|
|
360
|
+
if (this.#spaceRunInserted > SPACE_HOLD_THRESHOLD) {
|
|
361
|
+
this.deleteBeforeCursor(this.#spaceRunInserted);
|
|
362
|
+
this.#spaceRunInserted = 0;
|
|
363
|
+
this.#beginSpaceHold();
|
|
364
|
+
}
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
#beginSpaceHold(): void {
|
|
369
|
+
this.#spaceHoldActive = true;
|
|
370
|
+
this.#armSpaceHoldReleaseTimer();
|
|
371
|
+
this.onSpaceHoldStart?.();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#armSpaceHoldReleaseTimer(): void {
|
|
375
|
+
if (this.#spaceHoldTimer) clearTimeout(this.#spaceHoldTimer);
|
|
376
|
+
this.#spaceHoldTimer = setTimeout(() => {
|
|
377
|
+
this.#spaceHoldTimer = undefined;
|
|
378
|
+
this.#endSpaceHold();
|
|
379
|
+
}, SPACE_HOLD_RELEASE_MS);
|
|
380
|
+
this.#spaceHoldTimer.unref?.();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
#endSpaceHold(): void {
|
|
384
|
+
if (!this.#spaceHoldActive) return;
|
|
385
|
+
this.#spaceHoldActive = false;
|
|
386
|
+
this.#spaceRunInserted = 0;
|
|
387
|
+
if (this.#spaceHoldTimer) {
|
|
388
|
+
clearTimeout(this.#spaceHoldTimer);
|
|
389
|
+
this.#spaceHoldTimer = undefined;
|
|
390
|
+
}
|
|
391
|
+
this.onSpaceHoldEnd?.();
|
|
392
|
+
}
|
|
393
|
+
|
|
241
394
|
handleInput(data: string): void {
|
|
242
395
|
const kittyParsed = parseKittySequence(data);
|
|
243
396
|
if (kittyParsed && (kittyParsed.modifier & 64) !== 0 && this.onCapsLock) {
|
|
@@ -267,6 +420,9 @@ export class CustomEditor extends Editor {
|
|
|
267
420
|
return;
|
|
268
421
|
}
|
|
269
422
|
|
|
423
|
+
// Space-hold push-to-talk: a sustained space bar starts/stops STT instead of typing spaces.
|
|
424
|
+
if (this.#handleSpaceHold(data, canonical)) return;
|
|
425
|
+
|
|
270
426
|
if (canonical !== undefined) {
|
|
271
427
|
// Intercept configured image paste (async - fires and handles result)
|
|
272
428
|
if (this.#matchesAction(canonical, "app.clipboard.pasteImage") && this.onPasteImage) {
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import { formatBytes } from "@oh-my-pi/pi-utils";
|
|
16
16
|
import { theme } from "../../modes/theme/theme";
|
|
17
17
|
import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
18
|
-
import type { SessionInfo, SessionStatus } from "../../session/session-
|
|
18
|
+
import type { SessionInfo, SessionStatus } from "../../session/session-listing";
|
|
19
19
|
import { shortenPath } from "../../tools/render-utils";
|
|
20
20
|
import { DynamicBorder } from "./dynamic-border";
|
|
21
21
|
import { HookSelectorComponent } from "./hook-selector";
|
|
@@ -90,6 +90,13 @@ const CONDITIONS: Record<string, () => boolean> = {
|
|
|
90
90
|
return false;
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
|
+
autolearnActive: () => {
|
|
94
|
+
try {
|
|
95
|
+
return Settings.instance.get("autolearn.enabled") === true;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
93
100
|
autoThinkingActive: () => {
|
|
94
101
|
try {
|
|
95
102
|
return Settings.instance.get("defaultThinkingLevel") === "auto";
|