@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.2
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 +142 -1
- package/dist/types/cli/dry-balance-cli.d.ts +15 -1
- package/dist/types/cli/startup-cwd.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/commit/analysis/conventional.d.ts +2 -2
- package/dist/types/commit/analysis/summary.d.ts +2 -2
- package/dist/types/commit/changelog/generate.d.ts +2 -2
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/index.d.ts +3 -3
- package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
- package/dist/types/commit/model-selection.d.ts +10 -4
- package/dist/types/config/api-key-resolver.d.ts +34 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-provider-priority.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +17 -1
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/config/settings.d.ts +7 -2
- package/dist/types/dap/config.d.ts +14 -1
- package/dist/types/dap/types.d.ts +10 -0
- package/dist/types/debug/report-bundle.d.ts +3 -0
- package/dist/types/edit/file-snapshot-store.d.ts +18 -10
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -1
- package/dist/types/lsp/client.d.ts +10 -0
- package/dist/types/lsp/utils.d.ts +3 -2
- package/dist/types/main.d.ts +3 -9
- package/dist/types/mcp/tool-bridge.d.ts +2 -0
- package/dist/types/modes/components/chat-block.d.ts +64 -0
- package/dist/types/modes/components/custom-editor.d.ts +4 -1
- package/dist/types/modes/components/overlay-box.d.ts +17 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
- package/dist/types/modes/components/plan-toc.d.ts +41 -0
- package/dist/types/modes/components/read-tool-group.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +11 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +17 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
- package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +16 -5
- package/dist/types/modes/magic-keywords.d.ts +1 -1
- package/dist/types/modes/markdown-prose.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +21 -5
- package/dist/types/modes/utils/copy-targets.d.ts +21 -1
- package/dist/types/modes/workflow.d.ts +3 -3
- package/dist/types/plan-mode/approved-plan.d.ts +27 -8
- package/dist/types/plan-mode/plan-protection.d.ts +4 -4
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +21 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/messages.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +8 -3
- package/dist/types/slash-commands/types.d.ts +4 -6
- package/dist/types/task/executor.d.ts +17 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/render.d.ts +3 -2
- package/dist/types/tools/archive-reader.d.ts +5 -0
- package/dist/types/tools/ast-edit.d.ts +3 -0
- package/dist/types/tools/ast-grep.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/eval.d.ts +8 -0
- package/dist/types/tools/find.d.ts +8 -4
- package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
- package/dist/types/tools/github-cache.d.ts +12 -0
- package/dist/types/tools/grouped-file-output.d.ts +95 -12
- package/dist/types/tools/memory-render.d.ts +4 -1
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/plan-mode-guard.d.ts +8 -9
- package/dist/types/tools/render-utils.d.ts +5 -9
- package/dist/types/tools/search.d.ts +6 -2
- package/dist/types/tools/sqlite-reader.d.ts +1 -0
- package/dist/types/tools/todo.d.ts +3 -2
- package/dist/types/tools/write.d.ts +3 -0
- package/dist/types/tools/yield.d.ts +8 -0
- package/dist/types/tui/output-block.d.ts +16 -4
- package/dist/types/tui/status-line.d.ts +3 -0
- package/dist/types/utils/enhanced-paste.d.ts +20 -0
- package/dist/types/web/search/providers/kimi.d.ts +1 -1
- package/package.json +9 -9
- package/src/auto-thinking/classifier.ts +5 -1
- package/src/cli/args.ts +3 -1
- package/src/cli/dry-balance-cli.ts +54 -21
- package/src/cli/gallery-cli.ts +4 -1
- package/src/cli/gallery-fixtures/misc.ts +29 -0
- package/src/cli/startup-cwd.ts +68 -0
- package/src/commands/launch.ts +3 -0
- package/src/commit/analysis/conventional.ts +2 -2
- package/src/commit/analysis/summary.ts +2 -2
- package/src/commit/changelog/generate.ts +2 -2
- package/src/commit/changelog/index.ts +2 -2
- package/src/commit/map-reduce/index.ts +3 -3
- package/src/commit/map-reduce/map-phase.ts +2 -2
- package/src/commit/map-reduce/reduce-phase.ts +2 -2
- package/src/commit/model-selection.ts +36 -11
- package/src/commit/pipeline.ts +4 -4
- package/src/config/api-key-resolver.ts +58 -0
- package/src/config/model-provider-priority.ts +55 -0
- package/src/config/model-registry.ts +29 -24
- package/src/config/model-resolver.ts +39 -7
- package/src/config/settings-schema.ts +10 -0
- package/src/config/settings.ts +106 -43
- package/src/dap/config.ts +41 -2
- package/src/dap/defaults.json +1 -0
- package/src/dap/session.ts +1 -0
- package/src/dap/types.ts +10 -0
- package/src/debug/index.ts +47 -53
- package/src/debug/raw-sse-buffer.ts +7 -4
- package/src/debug/report-bundle.ts +9 -0
- package/src/edit/file-snapshot-store.ts +33 -1
- package/src/edit/hashline/filesystem.ts +2 -1
- package/src/edit/renderer.ts +82 -78
- package/src/eval/__tests__/llm-bridge.test.ts +110 -31
- package/src/eval/js/context-manager.ts +32 -15
- package/src/eval/llm-bridge.ts +22 -6
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/executor.ts +23 -11
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +10 -1
- package/src/goals/tools/goal-tool.ts +36 -26
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/lsp/client.ts +23 -11
- package/src/lsp/config.ts +11 -1
- package/src/lsp/index.ts +61 -9
- package/src/lsp/utils.ts +3 -2
- package/src/main.ts +100 -72
- package/src/mcp/tool-bridge.ts +2 -0
- package/src/memories/index.ts +14 -7
- package/src/mnemopi/backend.ts +5 -1
- package/src/modes/acp/acp-agent.ts +33 -26
- package/src/modes/components/assistant-message.ts +2 -9
- package/src/modes/components/chat-block.ts +111 -0
- package/src/modes/components/copy-selector.ts +1 -44
- package/src/modes/components/custom-editor.ts +164 -109
- package/src/modes/components/custom-message.ts +1 -3
- package/src/modes/components/execution-shared.ts +1 -2
- package/src/modes/components/hook-message.ts +1 -3
- package/src/modes/components/model-selector.ts +59 -13
- package/src/modes/components/oauth-selector.ts +33 -7
- package/src/modes/components/overlay-box.ts +108 -0
- package/src/modes/components/plan-review-overlay.ts +799 -0
- package/src/modes/components/plan-toc.ts +138 -0
- package/src/modes/components/read-tool-group.ts +20 -4
- package/src/modes/components/skill-message.ts +0 -1
- package/src/modes/components/status-line.ts +19 -4
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/todo-reminder.ts +0 -2
- package/src/modes/components/tool-execution.ts +68 -88
- package/src/modes/components/transcript-container.ts +84 -24
- package/src/modes/components/user-message.ts +2 -3
- package/src/modes/controllers/command-controller-shared.ts +7 -6
- package/src/modes/controllers/command-controller.ts +57 -55
- package/src/modes/controllers/event-controller.ts +67 -40
- package/src/modes/controllers/extension-ui-controller.ts +10 -73
- package/src/modes/controllers/input-controller.ts +170 -126
- package/src/modes/controllers/mcp-command-controller.ts +69 -60
- package/src/modes/controllers/selector-controller.ts +23 -25
- package/src/modes/controllers/streaming-reveal.ts +212 -0
- package/src/modes/controllers/tan-command-controller.ts +173 -0
- package/src/modes/interactive-mode.ts +274 -112
- package/src/modes/magic-keywords.ts +1 -1
- package/src/modes/markdown-prose.ts +1 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/theme/shimmer.ts +20 -9
- package/src/modes/theme/theme-schema.json +1 -1
- package/src/modes/theme/theme.ts +8 -4
- package/src/modes/types.ts +21 -7
- package/src/modes/utils/copy-targets.ts +133 -27
- package/src/modes/utils/ui-helpers.ts +44 -46
- package/src/modes/workflow.ts +10 -10
- package/src/plan-mode/approved-plan.ts +66 -43
- package/src/plan-mode/plan-protection.ts +4 -4
- package/src/prompts/system/background-tan-dispatch.md +8 -0
- package/src/prompts/system/plan-mode-active.md +67 -58
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/eval.md +2 -1
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +37 -46
- package/src/session/agent-session.ts +119 -18
- package/src/session/auth-storage.ts +2 -0
- package/src/session/messages.ts +26 -0
- package/src/session/session-manager.ts +109 -28
- package/src/slash-commands/builtin-registry.ts +36 -9
- package/src/slash-commands/types.ts +4 -6
- package/src/task/executor.ts +76 -38
- package/src/task/index.ts +4 -0
- package/src/task/render.ts +211 -147
- package/src/tools/archive-reader.ts +64 -0
- package/src/tools/ask.ts +119 -164
- package/src/tools/ast-edit.ts +98 -71
- package/src/tools/ast-grep.ts +37 -43
- package/src/tools/bash.ts +57 -6
- package/src/tools/browser/tab-supervisor.ts +13 -1
- package/src/tools/browser/tab-worker.ts +33 -4
- package/src/tools/debug.ts +20 -8
- package/src/tools/eval.ts +13 -2
- package/src/tools/fetch.ts +297 -7
- package/src/tools/find.ts +51 -30
- package/src/tools/gh-cache-invalidation.ts +200 -0
- package/src/tools/gh-renderer.ts +81 -42
- package/src/tools/github-cache.ts +25 -0
- package/src/tools/grouped-file-output.ts +272 -48
- package/src/tools/image-gen.ts +150 -103
- package/src/tools/inspect-image-renderer.ts +63 -41
- package/src/tools/inspect-image.ts +10 -3
- package/src/tools/job.ts +3 -4
- package/src/tools/memory-render.ts +4 -1
- package/src/tools/path-utils.ts +28 -2
- package/src/tools/plan-mode-guard.ts +66 -39
- package/src/tools/read.ts +48 -28
- package/src/tools/render-utils.ts +21 -37
- package/src/tools/resolve.ts +14 -0
- package/src/tools/search-tool-bm25.ts +36 -23
- package/src/tools/search.ts +118 -81
- package/src/tools/sqlite-reader.ts +9 -12
- package/src/tools/todo.ts +118 -52
- package/src/tools/write.ts +83 -64
- package/src/tools/yield.ts +10 -1
- package/src/tui/output-block.ts +60 -13
- package/src/tui/status-line.ts +5 -1
- package/src/utils/commit-message-generator.ts +11 -3
- package/src/utils/enhanced-paste.ts +230 -0
- package/src/utils/title-generator.ts +2 -1
- package/src/web/search/providers/anthropic.ts +25 -19
- package/src/web/search/providers/codex.ts +37 -8
- package/src/web/search/providers/exa.ts +11 -3
- package/src/web/search/providers/kimi.ts +28 -17
- package/src/web/search/providers/parallel.ts +35 -24
- package/src/web/search/providers/synthetic.ts +8 -6
- package/src/web/search/providers/tavily.ts +9 -8
- package/src/web/search/providers/zai.ts +8 -6
|
@@ -4,7 +4,7 @@ import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
|
4
4
|
import { settings } from "../../config/settings";
|
|
5
5
|
import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
|
|
6
6
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
7
|
-
import { isSilentAbort } from "../../session/messages";
|
|
7
|
+
import { isSilentAbort, resolveAbortLabel } from "../../session/messages";
|
|
8
8
|
import { resolveImageOptions } from "../../tools/render-utils";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -208,10 +208,6 @@ export class AssistantMessageComponent extends Container {
|
|
|
208
208
|
c => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()),
|
|
209
209
|
);
|
|
210
210
|
|
|
211
|
-
if (hasVisibleContent) {
|
|
212
|
-
this.#contentContainer.addChild(new Spacer(1));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
211
|
// Render content in order
|
|
216
212
|
let thinkingIndex = 0;
|
|
217
213
|
for (let i = 0; i < message.content.length; i++) {
|
|
@@ -257,10 +253,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
257
253
|
const hasToolCalls = message.content.some(c => c.type === "toolCall");
|
|
258
254
|
if (!hasToolCalls) {
|
|
259
255
|
if (message.stopReason === "aborted" && !isSilentAbort(message.errorMessage)) {
|
|
260
|
-
const abortMessage =
|
|
261
|
-
message.errorMessage && message.errorMessage !== "Request was aborted"
|
|
262
|
-
? message.errorMessage
|
|
263
|
-
: "Operation aborted";
|
|
256
|
+
const abortMessage = resolveAbortLabel(message.errorMessage);
|
|
264
257
|
if (hasVisibleContent) {
|
|
265
258
|
this.#contentContainer.addChild(new Spacer(1));
|
|
266
259
|
} else {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Container } from "@oh-my-pi/pi-tui";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Capabilities a mounted {@link ChatBlock} may use against its host transcript.
|
|
5
|
+
* Kept minimal so blocks never reach into the full TUI/InteractiveMode surface.
|
|
6
|
+
*/
|
|
7
|
+
export interface ChatBlockHost {
|
|
8
|
+
/** Schedule a repaint of the transcript. */
|
|
9
|
+
requestRender(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Lifecycle-aware transcript block — the "return a block, let the host mount it"
|
|
14
|
+
* primitive, modelled on React/Svelte component lifecycles.
|
|
15
|
+
*
|
|
16
|
+
* Producers build and return a `ChatBlock` instead of poking `chatContainer` and
|
|
17
|
+
* `ui.requestRender()` directly. The host (`ctx.present`) appends it and calls
|
|
18
|
+
* {@link mount}, which runs {@link onMount}; effects started there register
|
|
19
|
+
* teardown via {@link onCleanup}. The block repaints through {@link requestRender}
|
|
20
|
+
* — never touching the TUI — and tears down exactly once on {@link finish}
|
|
21
|
+
* (self-complete: stop the animation, keep the final frame in the transcript) or
|
|
22
|
+
* {@link dispose} (host discards it, e.g. a transcript reset).
|
|
23
|
+
*
|
|
24
|
+
* While mounted and unfinished a block reports `isTranscriptBlockFinalized() ===
|
|
25
|
+
* false` so {@link "../components/transcript-container".TranscriptContainer}
|
|
26
|
+
* keeps it in the live, repaintable region on ED3-risk terminals; after
|
|
27
|
+
* `finish()`/`dispose()` it reports `true` and freezes at its final content.
|
|
28
|
+
*/
|
|
29
|
+
export abstract class ChatBlock extends Container {
|
|
30
|
+
#host: ChatBlockHost | undefined;
|
|
31
|
+
#cleanups: Array<() => void> = [];
|
|
32
|
+
#active = false;
|
|
33
|
+
#disposed = false;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Run setup after the block is in the transcript: start timers/subscriptions
|
|
37
|
+
* and register their teardown with {@link onCleanup}. Default: no-op (a block
|
|
38
|
+
* whose content is fixed at construction needs no mount work).
|
|
39
|
+
*/
|
|
40
|
+
protected onMount(): void {}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register a teardown to run on {@link finish}/{@link dispose}, à la a
|
|
44
|
+
* `useEffect` cleanup. If the block is already disposed the cleanup runs
|
|
45
|
+
* immediately so callers never leak.
|
|
46
|
+
*/
|
|
47
|
+
protected onCleanup(cleanup: () => void): void {
|
|
48
|
+
if (this.#disposed) {
|
|
49
|
+
cleanup();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.#cleanups.push(cleanup);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Ask the host to repaint. No-op before mount or after dispose. */
|
|
56
|
+
protected requestRender(): void {
|
|
57
|
+
this.#host?.requestRender();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** True between {@link mount} and {@link finish}/{@link dispose}. */
|
|
61
|
+
protected get active(): boolean {
|
|
62
|
+
return this.#active;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Host-only: attach the host and run {@link onMount}. Idempotent — a second
|
|
67
|
+
* call (e.g. a transcript rebuild that re-presents the same instance) is a
|
|
68
|
+
* no-op.
|
|
69
|
+
*/
|
|
70
|
+
mount(host: ChatBlockHost): void {
|
|
71
|
+
if (this.#host || this.#disposed) return;
|
|
72
|
+
this.#host = host;
|
|
73
|
+
this.#active = true;
|
|
74
|
+
this.onMount();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Self-complete: stop ongoing effects and freeze the block at its current
|
|
79
|
+
* content, leaving it rendered in the transcript. Use when the operation the
|
|
80
|
+
* block represents finishes (connection resolved, download done).
|
|
81
|
+
*/
|
|
82
|
+
finish(): void {
|
|
83
|
+
if (!this.#active) return;
|
|
84
|
+
this.#active = false;
|
|
85
|
+
this.#runCleanups();
|
|
86
|
+
this.requestRender();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Host-only teardown: release everything and propagate to children. Called
|
|
91
|
+
* when the host permanently discards the block (transcript reset). Idempotent.
|
|
92
|
+
*/
|
|
93
|
+
override dispose(): void {
|
|
94
|
+
if (this.#disposed) return;
|
|
95
|
+
this.#disposed = true;
|
|
96
|
+
this.#active = false;
|
|
97
|
+
this.#runCleanups();
|
|
98
|
+
super.dispose();
|
|
99
|
+
this.#host = undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Live blocks stay repaintable; finished/disposed ones may freeze. */
|
|
103
|
+
isTranscriptBlockFinalized(): boolean {
|
|
104
|
+
return !this.#active;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#runCleanups(): void {
|
|
108
|
+
const cleanups = this.#cleanups.splice(0);
|
|
109
|
+
for (const cleanup of cleanups) cleanup();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
matchesSelectUp,
|
|
11
11
|
} from "../utils/keybinding-matchers";
|
|
12
12
|
import { keyHint, rawKeyHint } from "./keybinding-hints";
|
|
13
|
+
import { bottomBorder, divider, row, topBorder } from "./overlay-box";
|
|
13
14
|
|
|
14
15
|
/** Minimum rows reserved for the tree even on short terminals. */
|
|
15
16
|
const MIN_TREE_ROWS = 3;
|
|
@@ -32,50 +33,6 @@ interface FlatNode {
|
|
|
32
33
|
ancestorHasNext: boolean[];
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
/** Pad or truncate a (possibly ANSI-styled) string to exactly `width` columns. */
|
|
36
|
-
function fit(text: string, width: number): string {
|
|
37
|
-
if (width <= 0) return "";
|
|
38
|
-
const w = visibleWidth(text);
|
|
39
|
-
if (w === width) return text;
|
|
40
|
-
if (w < width) return text + padding(width - w);
|
|
41
|
-
const cut = truncateToWidth(text, width);
|
|
42
|
-
const cw = visibleWidth(cut);
|
|
43
|
-
return cw < width ? cut + padding(width - cw) : cut;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function paint(s: string): string {
|
|
47
|
-
return theme.fg("border", s);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function topBorder(width: number, title: string): string {
|
|
51
|
-
const box = theme.boxSharp;
|
|
52
|
-
const inner = Math.max(0, width - 2);
|
|
53
|
-
if (!title) return paint(box.topLeft + box.horizontal.repeat(inner) + box.topRight);
|
|
54
|
-
const shown = truncateToWidth(` ${title} `, Math.max(0, inner - 2));
|
|
55
|
-
const fillWidth = Math.max(0, inner - 1 - visibleWidth(shown));
|
|
56
|
-
return (
|
|
57
|
-
paint(box.topLeft + box.horizontal) +
|
|
58
|
-
theme.bold(theme.fg("accent", shown)) +
|
|
59
|
-
paint(box.horizontal.repeat(fillWidth) + box.topRight)
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function divider(width: number): string {
|
|
64
|
-
const box = theme.boxSharp;
|
|
65
|
-
return paint(box.teeRight + box.horizontal.repeat(Math.max(0, width - 2)) + box.teeLeft);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function bottomBorder(width: number): string {
|
|
69
|
-
const box = theme.boxSharp;
|
|
70
|
-
return paint(box.bottomLeft + box.horizontal.repeat(Math.max(0, width - 2)) + box.bottomRight);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Wrap pre-styled content in vertical borders with single-column insets. */
|
|
74
|
-
function row(content: string, width: number): string {
|
|
75
|
-
const box = theme.boxSharp;
|
|
76
|
-
return `${paint(box.vertical)} ${fit(content, Math.max(0, width - 4))} ${paint(box.vertical)}`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
36
|
/** Render one tree connector as exactly three cells (e.g. "├─ ", "└─ ", "|--"). */
|
|
80
37
|
function connectorCells(symbol: string): string {
|
|
81
38
|
const chars = Array.from(symbol);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Editor, type KeyId,
|
|
1
|
+
import { addKeyAliases, canonicalKeyId, Editor, type KeyId, parseKey, parseKittySequence } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import type { AppKeybinding } from "../../config/keybindings";
|
|
3
3
|
import { imageReferenceHyperlink, renderImageReferences } from "../image-references";
|
|
4
4
|
import { highlightMagicKeywords } from "../magic-keywords";
|
|
@@ -47,13 +47,36 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
|
|
|
47
47
|
"app.clipboard.copyPrompt": ["alt+shift+c"],
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
function buildMatchKeys(keys: readonly KeyId[]): Set<string> {
|
|
51
|
+
const matchKeys = new Set<string>();
|
|
52
|
+
for (const key of keys) {
|
|
53
|
+
addKeyAliases(matchKeys, key);
|
|
54
|
+
}
|
|
55
|
+
return matchKeys;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
59
|
+
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
60
|
+
const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i;
|
|
61
|
+
|
|
62
|
+
export function extractBracketedImagePastePath(data: string): string | undefined {
|
|
63
|
+
if (!data.startsWith(BRACKETED_PASTE_START)) return undefined;
|
|
64
|
+
const endIndex = data.indexOf(BRACKETED_PASTE_END, BRACKETED_PASTE_START.length);
|
|
65
|
+
if (endIndex === -1 || endIndex + BRACKETED_PASTE_END.length !== data.length) return undefined;
|
|
66
|
+
|
|
67
|
+
const pasted = data.slice(BRACKETED_PASTE_START.length, endIndex).trim();
|
|
68
|
+
if (!pasted || /[\r\n]/.test(pasted)) return undefined;
|
|
69
|
+
if (!BRACKETED_IMAGE_PATH_REGEX.test(pasted)) return undefined;
|
|
70
|
+
return pasted;
|
|
71
|
+
}
|
|
72
|
+
|
|
50
73
|
/**
|
|
51
74
|
* Custom editor that handles configurable app-level shortcuts for coding-agent.
|
|
52
75
|
*/
|
|
53
76
|
export class CustomEditor extends Editor {
|
|
54
77
|
imageLinks?: readonly (string | undefined)[];
|
|
55
78
|
|
|
56
|
-
/** Gradient-highlight the "ultrathink" / "orchestrate" / "
|
|
79
|
+
/** Gradient-highlight the "ultrathink" / "orchestrate" / "workflowz" keywords as the user types
|
|
57
80
|
* them, skipping any occurrence inside code spans, fenced blocks, or XML sections. Also make
|
|
58
81
|
* pasted image placeholders visually distinct and hyperlink them once their blob file exists. */
|
|
59
82
|
decorateText = (text: string): string =>
|
|
@@ -82,6 +105,8 @@ export class CustomEditor extends Editor {
|
|
|
82
105
|
onCopyPrompt?: () => void;
|
|
83
106
|
/** Called when the configured image-paste shortcut is pressed. */
|
|
84
107
|
onPasteImage?: () => Promise<boolean>;
|
|
108
|
+
/** Called when a bracketed paste contains exactly one image-file path. */
|
|
109
|
+
onPasteImagePath?: (path: string) => void;
|
|
85
110
|
/** Called when the configured raw text-paste shortcut is pressed. */
|
|
86
111
|
onPasteTextRaw?: () => void;
|
|
87
112
|
/** Called when the configured dequeue shortcut is pressed. */
|
|
@@ -91,21 +116,38 @@ export class CustomEditor extends Editor {
|
|
|
91
116
|
|
|
92
117
|
/** Custom key handlers from extensions and non-built-in app actions. */
|
|
93
118
|
#customKeyHandlers = new Map<KeyId, () => void>();
|
|
119
|
+
#customMatchKeys = new Map<string, () => void>();
|
|
94
120
|
#actionKeys = new Map<ConfigurableEditorAction, KeyId[]>(
|
|
95
121
|
Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [action as ConfigurableEditorAction, [...keys]]),
|
|
96
122
|
);
|
|
123
|
+
#actionMatchKeys = new Map<ConfigurableEditorAction, Set<string>>(
|
|
124
|
+
Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [
|
|
125
|
+
action as ConfigurableEditorAction,
|
|
126
|
+
buildMatchKeys(keys),
|
|
127
|
+
]),
|
|
128
|
+
);
|
|
97
129
|
|
|
98
130
|
setActionKeys(action: ConfigurableEditorAction, keys: KeyId[]): void {
|
|
99
131
|
this.#actionKeys.set(action, [...keys]);
|
|
132
|
+
this.#rebuildActionMatchKeys(action);
|
|
100
133
|
}
|
|
101
134
|
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
135
|
+
#rebuildActionMatchKeys(action: ConfigurableEditorAction): void {
|
|
136
|
+
this.#actionMatchKeys.set(action, buildMatchKeys(this.#actionKeys.get(action) ?? []));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#rebuildCustomMatchKeys(): void {
|
|
140
|
+
this.#customMatchKeys.clear();
|
|
141
|
+
for (const [keyId, handler] of this.#customKeyHandlers) {
|
|
142
|
+
for (const alias of buildMatchKeys([keyId])) {
|
|
143
|
+
// Preserve current iteration behavior: the first registered handler for colliding aliases wins.
|
|
144
|
+
if (!this.#customMatchKeys.has(alias)) this.#customMatchKeys.set(alias, handler);
|
|
145
|
+
}
|
|
107
146
|
}
|
|
108
|
-
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#matchesAction(canonical: string | undefined, action: ConfigurableEditorAction): boolean {
|
|
150
|
+
return canonical !== undefined && (this.#actionMatchKeys.get(action)?.has(canonical) ?? false);
|
|
109
151
|
}
|
|
110
152
|
|
|
111
153
|
/**
|
|
@@ -113,6 +155,7 @@ export class CustomEditor extends Editor {
|
|
|
113
155
|
*/
|
|
114
156
|
setCustomKeyHandler(key: KeyId, handler: () => void): void {
|
|
115
157
|
this.#customKeyHandlers.set(key, handler);
|
|
158
|
+
this.#rebuildCustomMatchKeys();
|
|
116
159
|
}
|
|
117
160
|
|
|
118
161
|
/**
|
|
@@ -120,6 +163,7 @@ export class CustomEditor extends Editor {
|
|
|
120
163
|
*/
|
|
121
164
|
removeCustomKeyHandler(key: KeyId): void {
|
|
122
165
|
this.#customKeyHandlers.delete(key);
|
|
166
|
+
this.#rebuildCustomMatchKeys();
|
|
123
167
|
}
|
|
124
168
|
|
|
125
169
|
/**
|
|
@@ -127,135 +171,146 @@ export class CustomEditor extends Editor {
|
|
|
127
171
|
*/
|
|
128
172
|
clearCustomKeyHandlers(): void {
|
|
129
173
|
this.#customKeyHandlers.clear();
|
|
174
|
+
this.#rebuildCustomMatchKeys();
|
|
130
175
|
}
|
|
131
176
|
|
|
132
177
|
handleInput(data: string): void {
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
178
|
+
const kittyParsed = parseKittySequence(data);
|
|
179
|
+
if (kittyParsed && (kittyParsed.modifier & 64) !== 0 && this.onCapsLock) {
|
|
135
180
|
// Caps Lock is modifier bit 64
|
|
136
181
|
this.onCapsLock();
|
|
137
182
|
return;
|
|
138
183
|
}
|
|
139
184
|
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
185
|
+
const pastedImagePath = extractBracketedImagePastePath(data);
|
|
186
|
+
if (pastedImagePath && this.onPasteImagePath) {
|
|
187
|
+
this.onPasteImagePath(pastedImagePath);
|
|
143
188
|
return;
|
|
144
189
|
}
|
|
145
190
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
this.onPasteTextRaw();
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
191
|
+
const parsedKey = parseKey(data);
|
|
192
|
+
const canonical = parsedKey !== undefined ? canonicalKeyId(parsedKey) : undefined;
|
|
151
193
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this.
|
|
155
|
-
|
|
156
|
-
|
|
194
|
+
if (canonical !== undefined) {
|
|
195
|
+
// Intercept configured image paste (async - fires and handles result)
|
|
196
|
+
if (this.#matchesAction(canonical, "app.clipboard.pasteImage") && this.onPasteImage) {
|
|
197
|
+
void this.onPasteImage();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
157
200
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
201
|
+
// Intercept configured raw text paste (fires and handles result)
|
|
202
|
+
if (this.#matchesAction(canonical, "app.clipboard.pasteTextRaw") && this.onPasteTextRaw) {
|
|
203
|
+
this.onPasteTextRaw();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
163
206
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
207
|
+
// Intercept configured external editor shortcut
|
|
208
|
+
if (this.#matchesAction(canonical, "app.editor.external") && this.onExternalEditor) {
|
|
209
|
+
this.onExternalEditor();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
169
212
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
213
|
+
// Intercept configured temporary model selector shortcut
|
|
214
|
+
if (this.#matchesAction(canonical, "app.model.selectTemporary") && this.onSelectModelTemporary) {
|
|
215
|
+
this.onSelectModelTemporary();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
175
218
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
219
|
+
// Intercept configured display reset shortcut
|
|
220
|
+
if (this.#matchesAction(canonical, "app.display.reset") && this.onDisplayReset) {
|
|
221
|
+
this.onDisplayReset();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
181
224
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
225
|
+
// Intercept configured suspend shortcut
|
|
226
|
+
if (this.#matchesAction(canonical, "app.suspend") && this.onSuspend) {
|
|
227
|
+
this.onSuspend();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
187
230
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
231
|
+
// Intercept configured thinking block visibility toggle
|
|
232
|
+
if (this.#matchesAction(canonical, "app.thinking.toggle") && this.onToggleThinking) {
|
|
233
|
+
this.onToggleThinking();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
193
236
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
237
|
+
// Intercept configured model selector shortcut
|
|
238
|
+
if (this.#matchesAction(canonical, "app.model.select") && this.onSelectModel) {
|
|
239
|
+
this.onSelectModel();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
199
242
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
243
|
+
// Intercept configured history search shortcut
|
|
244
|
+
if (this.#matchesAction(canonical, "app.history.search") && this.onHistorySearch) {
|
|
245
|
+
this.onHistorySearch();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
205
248
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
249
|
+
// Intercept configured tool output expansion shortcut
|
|
250
|
+
if (this.#matchesAction(canonical, "app.tools.expand") && this.onExpandTools) {
|
|
251
|
+
this.onExpandTools();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
211
254
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
255
|
+
// Intercept configured backward model cycling (check before forward cycling)
|
|
256
|
+
if (this.#matchesAction(canonical, "app.model.cycleBackward") && this.onCycleModelBackward) {
|
|
257
|
+
this.onCycleModelBackward();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
217
260
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
// single ESC from both closing an @ completion and aborting an active
|
|
224
|
-
// agent run (#1655).
|
|
225
|
-
if (this.#matchesAction(data, "app.interrupt") && this.onEscape && !this.isShowingAutocomplete()) {
|
|
226
|
-
this.onEscape();
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
261
|
+
// Intercept configured forward model cycling
|
|
262
|
+
if (this.#matchesAction(canonical, "app.model.cycleForward") && this.onCycleModelForward) {
|
|
263
|
+
this.onCycleModelForward();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
229
266
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
267
|
+
// Intercept configured thinking level cycling
|
|
268
|
+
if (this.#matchesAction(canonical, "app.thinking.cycle") && this.onCycleThinkingLevel) {
|
|
269
|
+
this.onCycleThinkingLevel();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
235
272
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
273
|
+
// Intercept configured interrupt shortcut.
|
|
274
|
+
// When the autocomplete popup is visible, ESC's first job is to dismiss
|
|
275
|
+
// the popup — let super.handleInput() route it to #cancelAutocomplete().
|
|
276
|
+
// The user can press ESC again afterward to fire the global interrupt
|
|
277
|
+
// handler. This matches the standard TUI/IDE pattern and prevents a
|
|
278
|
+
// single ESC from both closing an @ completion and aborting an active
|
|
279
|
+
// agent run (#1655).
|
|
280
|
+
if (this.#matchesAction(canonical, "app.interrupt") && this.onEscape && !this.isShowingAutocomplete()) {
|
|
281
|
+
this.onEscape();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
243
284
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
285
|
+
// Intercept configured clear shortcut
|
|
286
|
+
if (this.#matchesAction(canonical, "app.clear") && this.onClear) {
|
|
287
|
+
this.onClear();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
249
290
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
291
|
+
// Intercept configured exit shortcut. Always consume the shortcut so it
|
|
292
|
+
// never reaches the parent handler; firing onExit is the controller's
|
|
293
|
+
// chance to snapshot the current text as a draft before shutting down.
|
|
294
|
+
if (this.#matchesAction(canonical, "app.exit")) {
|
|
295
|
+
this.onExit?.();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
255
298
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
299
|
+
// Intercept configured dequeue shortcut (restore queued message to editor)
|
|
300
|
+
if (this.#matchesAction(canonical, "app.message.dequeue") && this.onDequeue) {
|
|
301
|
+
this.onDequeue();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Intercept configured copy-prompt shortcut
|
|
306
|
+
if (this.#matchesAction(canonical, "app.clipboard.copyPrompt") && this.onCopyPrompt) {
|
|
307
|
+
this.onCopyPrompt();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check custom key handlers (extensions)
|
|
312
|
+
const handler = this.#customMatchKeys.get(canonical);
|
|
313
|
+
if (handler) {
|
|
259
314
|
handler();
|
|
260
315
|
return;
|
|
261
316
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
2
|
-
import { Box, Container
|
|
2
|
+
import { Box, Container } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import type { MessageRenderer } from "../../extensibility/extensions/types";
|
|
4
4
|
import { theme } from "../../modes/theme/theme";
|
|
5
5
|
import type { CustomMessage } from "../../session/messages";
|
|
@@ -20,8 +20,6 @@ export class CustomMessageComponent extends Container {
|
|
|
20
20
|
) {
|
|
21
21
|
super();
|
|
22
22
|
|
|
23
|
-
this.addChild(new Spacer(1));
|
|
24
|
-
|
|
25
23
|
// Create box with custom background (used for default rendering)
|
|
26
24
|
this.#box = new Box(1, 1, t => theme.bg("customMessageBg", t));
|
|
27
25
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* stay in their respective files.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { type Component, Container, Loader,
|
|
10
|
+
import { type Component, Container, Loader, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
11
11
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
12
12
|
import { formatTruncationMetaNotice, type TruncationMeta } from "../../tools/output-meta";
|
|
13
13
|
import { DynamicBorder } from "./dynamic-border";
|
|
@@ -31,7 +31,6 @@ export function buildExecutionFrame(
|
|
|
31
31
|
): { contentContainer: Container; loader: Loader } {
|
|
32
32
|
const borderColor = (str: string) => theme.fg(colorKey, str);
|
|
33
33
|
|
|
34
|
-
parent.addChild(new Spacer(1));
|
|
35
34
|
parent.addChild(new DynamicBorder(borderColor));
|
|
36
35
|
|
|
37
36
|
const contentContainer = new Container();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
2
|
-
import { Box, Container
|
|
2
|
+
import { Box, Container } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import type { HookMessageRenderer } from "../../extensibility/hooks/types";
|
|
4
4
|
import { theme } from "../../modes/theme/theme";
|
|
5
5
|
import type { HookMessage } from "../../session/messages";
|
|
@@ -23,8 +23,6 @@ export class HookMessageComponent extends Container {
|
|
|
23
23
|
) {
|
|
24
24
|
super();
|
|
25
25
|
|
|
26
|
-
this.addChild(new Spacer(1));
|
|
27
|
-
|
|
28
26
|
// Create box with purple background (used for default rendering)
|
|
29
27
|
this.#box = new Box(1, 1, t => theme.bg("customMessageBg", t));
|
|
30
28
|
|