pi-ui-extend 0.1.15 → 0.1.18
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/apps/desktop-tauri/README.md +103 -0
- package/apps/desktop-tauri/bin/pix-desktop.mjs +89 -0
- package/dist/app/app.d.ts +2 -0
- package/dist/app/app.js +21 -6
- package/dist/app/commands/command-controller.js +1 -0
- package/dist/app/commands/command-host.d.ts +2 -0
- package/dist/app/commands/command-navigation-actions.d.ts +9 -0
- package/dist/app/commands/command-navigation-actions.js +62 -3
- package/dist/app/commands/command-registry.d.ts +1 -0
- package/dist/app/commands/command-registry.js +8 -0
- package/dist/app/constants.d.ts +0 -1
- package/dist/app/constants.js +0 -1
- package/dist/app/icons.d.ts +1 -0
- package/dist/app/icons.js +2 -0
- package/dist/app/input/input-action-controller.d.ts +1 -0
- package/dist/app/input/input-action-controller.js +5 -4
- package/dist/app/input/input-controller.d.ts +1 -0
- package/dist/app/input/input-controller.js +29 -0
- package/dist/app/input/input-paste-handler.d.ts +1 -1
- package/dist/app/input/input-paste-handler.js +6 -5
- package/dist/app/model/model-usage-status.js +4 -27
- package/dist/app/popup/menu-items-controller.d.ts +2 -0
- package/dist/app/popup/menu-items-controller.js +37 -14
- package/dist/app/popup/popup-menu-controller.js +30 -5
- package/dist/app/rendering/editor-panels.js +20 -9
- package/dist/app/rendering/popup-menu-renderer.d.ts +12 -0
- package/dist/app/rendering/popup-menu-renderer.js +151 -53
- package/dist/app/rendering/render-controller.js +29 -15
- package/dist/app/rendering/render-text.js +5 -2
- package/dist/app/rendering/status-line-renderer.d.ts +7 -0
- package/dist/app/rendering/status-line-renderer.js +191 -94
- package/dist/app/rendering/toast-controller.d.ts +1 -0
- package/dist/app/rendering/toast-controller.js +17 -0
- package/dist/app/screen/mouse-controller.d.ts +1 -0
- package/dist/app/screen/mouse-controller.js +17 -20
- package/dist/app/screen/scroll-controller.d.ts +1 -0
- package/dist/app/screen/scroll-controller.js +6 -0
- package/dist/app/screen/status-controller.js +2 -1
- package/dist/app/session/request-history.d.ts +4 -0
- package/dist/app/session/request-history.js +11 -0
- package/dist/app/session/session-search.js +10 -0
- package/dist/app/session/tabs-controller.d.ts +4 -4
- package/dist/app/session/tabs-controller.js +64 -6
- package/dist/app/todo/todo-model.d.ts +2 -2
- package/dist/app/todo/todo-model.js +15 -17
- package/dist/app/types.d.ts +12 -4
- package/dist/config.d.ts +1 -0
- package/dist/config.js +10 -1
- package/dist/default-pix-config.js +2 -0
- package/dist/fuzzy.d.ts +2 -0
- package/dist/fuzzy.js +27 -7
- package/dist/input-editor.d.ts +9 -0
- package/dist/input-editor.js +52 -0
- package/dist/schemas/pix-schema.d.ts +1 -0
- package/dist/schemas/pix-schema.js +1 -0
- package/dist/theme.js +6 -6
- package/dist/ui.d.ts +8 -0
- package/external/pi-tools-suite/README.md +2 -2
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +40 -5
- package/external/pi-tools-suite/src/antigravity-auth/commands.ts +1 -37
- package/external/pi-tools-suite/src/antigravity-auth/constants.ts +0 -2
- package/external/pi-tools-suite/src/antigravity-auth/index.ts +3 -16
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +33 -17
- package/external/pi-tools-suite/src/antigravity-auth/status.ts +1 -1
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +4 -12
- package/external/pi-tools-suite/src/antigravity-auth/types.ts +21 -0
- package/external/pi-tools-suite/src/config.ts +43 -0
- package/external/pi-tools-suite/src/dcp/commands.ts +1 -1
- package/external/pi-tools-suite/src/dcp/index.ts +21 -1
- package/external/pi-tools-suite/src/dcp/state.ts +225 -3
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +5 -0
- package/external/pi-tools-suite/src/index.ts +1 -0
- package/external/pi-tools-suite/src/telegram-mirror/README.md +168 -0
- package/external/pi-tools-suite/src/telegram-mirror/bot.ts +228 -0
- package/external/pi-tools-suite/src/telegram-mirror/events.ts +94 -0
- package/external/pi-tools-suite/src/telegram-mirror/format.ts +120 -0
- package/external/pi-tools-suite/src/telegram-mirror/index.ts +424 -0
- package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +419 -0
- package/external/pi-tools-suite/src/telegram-mirror/multiplexer.ts +408 -0
- package/external/pi-tools-suite/src/telegram-mirror/renderer.ts +214 -0
- package/external/pi-tools-suite/src/todo/index.ts +3 -64
- package/external/pi-tools-suite/src/todo/state/persistence.ts +0 -1
- package/external/pi-tools-suite/src/todo/state/state-reducer.ts +7 -37
- package/external/pi-tools-suite/src/todo/todo.ts +2 -18
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +2 -11
- package/external/pi-tools-suite/src/todo/tool/types.ts +0 -29
- package/external/pi-tools-suite/src/todo/view/format.ts +1 -3
- package/external/pi-tools-suite/src/tool-descriptions.ts +5 -4
- package/external/pi-tools-suite/src/usage/lib/google.ts +50 -30
- package/external/pi-tools-suite/src/usage/lib/types.ts +12 -2
- package/package.json +14 -3
- package/schemas/pix.json +5 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Pix Desktop (Tauri)
|
|
2
|
+
|
|
3
|
+
Tauri-based desktop UI for the Pi coding agent. It is a sibling workspace of the `pix` terminal app and uses the same `@earendil-works/pi-coding-agent` SDK through a Node sidecar.
|
|
4
|
+
|
|
5
|
+
> **Status — current prototype.** React talks to a Rust Tauri host via `rpc_call` / `rpc_subscribe`. Rust proxies line-delimited SDK-shaped JSON to a custom Node dispatcher. Implemented: workspace picker, persistent sessions, tabbed chat with per-workspace tab restore, expanded tool-call cards, history loading, SDK/pi-tools-suite slash-command discovery with extension argument completions, path/general composer autocomplete, image attachments via paste/drop/file picker, Web Speech voice dictation, captured `!shell` commands, raw `!!` PTY terminal surface, extension UI request dialogs with select search/timeouts, toasts/widgets/status plus session-scoped lifecycle, explicit degraded handling for custom/component extension UI, real pi-tools-suite interactive command smoke coverage, desktop-native `/model`/`/compact`/`/undo`, streaming/abort, and status bar. There is intentionally no sidebar.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
React (Vite, port 1420)
|
|
11
|
+
│ invoke("rpc_call", { cmd }) / invoke("rpc_subscribe", { onEvent })
|
|
12
|
+
▼
|
|
13
|
+
Tauri Rust host (src-tauri/)
|
|
14
|
+
│ JSONL over stdio, SDK flat shape { id?, type, ... }
|
|
15
|
+
▼
|
|
16
|
+
Node sidecar (sidecar/src/)
|
|
17
|
+
│ custom dispatcher + @earendil-works/pi-coding-agent SDK
|
|
18
|
+
▼
|
|
19
|
+
AgentSession / SessionManager
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The frontend never imports or calls the SDK directly. Rust owns native APIs and sidecar process management; the sidecar owns SDK runtime/session operations.
|
|
23
|
+
|
|
24
|
+
## Layout
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
apps/desktop-tauri/
|
|
28
|
+
├── src/ # React frontend
|
|
29
|
+
│ ├── App.tsx # workspace picker, topbar, tabs, chat, history transform
|
|
30
|
+
│ ├── App.css # Tokyo-Night dark theme
|
|
31
|
+
│ └── tools/ # tool-call renderer registry and renderers
|
|
32
|
+
├── sidecar/src/ # Node SDK bridge
|
|
33
|
+
│ ├── main.ts # create runtime, switch cwd, run dispatcher
|
|
34
|
+
│ ├── dispatcher.ts # command switch and event subscription rebinding
|
|
35
|
+
│ ├── pix-handlers.ts # pix:list_sessions
|
|
36
|
+
│ ├── framing.ts # strict LF JSONL framing
|
|
37
|
+
│ └── protocol.ts # wire types
|
|
38
|
+
└── src-tauri/src/ # Rust host and sidecar bridge
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Wire protocol
|
|
42
|
+
|
|
43
|
+
This is **not JSON-RPC 2.0**. The sidecar uses the SDK-style flat JSONL protocol:
|
|
44
|
+
|
|
45
|
+
- Command: `{ "id": "req-1", "type": "prompt", "message": "hi", "images": [] }`
|
|
46
|
+
- Response: `{ "id": "req-1", "type": "response", "command": "prompt", "success": true, "data": ... }`
|
|
47
|
+
- Event: `{ "type": "agent_start" | "message_update" | "tool_execution_*" | ... }`
|
|
48
|
+
|
|
49
|
+
Implemented sidecar commands include `prompt`, `abort`, `get_state`, `get_messages`, `get_session_stats`, `get_commands`, `get_command_completions`, `extension_ui_response`, `get_models`, `set_model`, `compact`, `undo_last_turn`, `new_session`, `switch_session`, `set_session_name`, `pix:list_sessions`, and `pix:set_cwd`. The sidecar emits `extension_ui_request` events for extension `ctx.ui.*` calls, and the frontend answers dialog methods with `extension_ui_response`. The Rust host also exposes native `run_shell`, `complete_path`, and `pty_*` commands for the desktop `!cmd` flow, composer path autocomplete, and raw `!!` terminal sessions.
|
|
50
|
+
|
|
51
|
+
## Setup and run
|
|
52
|
+
|
|
53
|
+
From the repo root:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install --ignore-scripts
|
|
57
|
+
npm run desktop:icons
|
|
58
|
+
npm run desktop:dev
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
On first launch, choose a project folder. Pix Desktop stores the selected workspace in localStorage under `pix-desktop.workspace`, stores open tabs per cwd under `pix-desktop.tabs:<cwd>`, and resumes the saved active tab when possible.
|
|
62
|
+
|
|
63
|
+
## Verification
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm run desktop:check
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Equivalent expanded checks:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm --prefix apps/desktop-tauri run build
|
|
73
|
+
npm --prefix apps/desktop-tauri/sidecar run check
|
|
74
|
+
cargo check --manifest-path apps/desktop-tauri/src-tauri/Cargo.toml
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Env overrides
|
|
78
|
+
|
|
79
|
+
| Variable | Purpose |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `PIX_SIDECAR_CMD` | Command to spawn, default `node`. |
|
|
82
|
+
| `PIX_SIDECAR_ARGS` | Whitespace-separated args, overrides default `--import tsx`. |
|
|
83
|
+
| `PIX_SIDECAR_PATH` | Explicit sidecar entry path. |
|
|
84
|
+
| `PIX_SIDECAR_AGENT_DIR` | Pi agent dir for auth/skills/extensions, default `~/.pi/agent`. |
|
|
85
|
+
| `PIX_SIDECAR_SESSION_MODE` | `persistent` by default; use `in-memory` for ephemeral tests. |
|
|
86
|
+
| `RUST_LOG` | Rust tracing filter. |
|
|
87
|
+
|
|
88
|
+
## Current UX notes
|
|
89
|
+
|
|
90
|
+
- Tabs are the only session navigation surface; the old sidebar was removed.
|
|
91
|
+
- Open tabs and the active tab are restored per workspace across Tauri restarts.
|
|
92
|
+
- Typing `/` opens a slash-command menu. Desktop built-ins (`/help`, `/new`, `/clear`, `/refresh`, `/abort`) are merged with SDK-discovered extension, prompt-template, and skill commands from `get_commands`; selecting a discovered command sends it through `prompt` with arguments preserved. When an extension command exposes `getArgumentCompletions`, the frontend debounces `get_command_completions` and shows argument suggestions in the same keyboard/click popup.
|
|
93
|
+
- Path/general autocomplete is handled locally through the Rust `complete_path` helper, scoped to the selected workspace. It completes `@path` mentions in normal messages, path-like `!cmd` shell tokens, and generic slash-command arguments when no richer extension/model completion is available.
|
|
94
|
+
- Images can be attached from the composer with the image button, paste, or drag/drop. The frontend previews them locally, sends SDK `ImageContent[]` through the sidecar `prompt` command, and keeps file attachments as text/path mentions for now.
|
|
95
|
+
- Voice dictation is available from the composer mic button when the current WebView exposes `SpeechRecognition`/`webkitSpeechRecognition`; final transcripts are appended to the composer. Unsupported/error states are shown inline. Offline Vosk parity remains future work.
|
|
96
|
+
- Extension commands can request simple UI through the RPC-style `extension_ui_request` surface: `select`, `confirm`, `input`, and `editor` show modal dialogs. Select dialogs include local search, long-list scrolling, option counts, and timeout countdown/self-cancel affordances when an extension passes `dialogOpts.timeout`; `notify` shows toasts; `setWidget` renders scroll-contained text widgets above/below the composer; `setStatus` and working-indicator APIs add status-bar entries; `setHeader`/`setFooter`, component widgets, `custom()`, `setEditorComponent()`, and extension autocomplete providers now have explicit degraded desktop behavior/status instead of silent no-ops; extension widgets/status/dialogs are cleared on session/workspace switch so stale UI does not leak across tabs; `set_editor_text` fills the composer.
|
|
97
|
+
- Real `pi-tools-suite` command smoke checks have exercised `/prompt-commands` (`select`, `notify`, `input` cancelled before mutation) and `/subagent-preset-config` (`select` cancelled) through the desktop sidecar path. Keep `PIX_SIDECAR_SESSION_MODE=in-memory` for these checks, but do not redirect `HOME` unless extension discovery is configured for that home; otherwise user extensions are not loaded and slash commands fall through to normal prompts.
|
|
98
|
+
- Desktop-native interactive built-ins are available for commands that are not prompt-invokable: `/model` opens a model picker and `/model <provider/id>` sets directly; `/compact [instructions]` runs SDK compaction; `/undo` navigates back to the latest user turn and restores returned editor text when available.
|
|
99
|
+
- Typing `!command` runs a short non-interactive shell command in the selected workspace and renders captured stdout/stderr as a shell tool card. Typing `!!command` opens an xterm.js-backed PTY panel in the current workspace for interactive/raw terminal programs; bare `!!` opens the user's shell.
|
|
100
|
+
- Tool-call cards include specialized renderers for shell/file/patch/todo/web/folder operations plus repo-index tools, ast-grep/apply, question prompts, subagent orchestration, context compression, and skill activation.
|
|
101
|
+
- Switching tabs/folders and closing tabs are blocked while an agent run is streaming, because the sidecar has one active SDK session subscription.
|
|
102
|
+
- On session switch or workspace restore, the UI calls `get_messages` and transforms SDK messages into sanitized chat messages, filtering reasoning/image internals and attaching tool results to tool-call cards.
|
|
103
|
+
- Sidecar logs must go to stderr only; stdout is reserved for JSONL protocol records.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pix-desktop — CLI launcher for the Tauri-based Pix Desktop UI.
|
|
4
|
+
*
|
|
5
|
+
* Behaves like the regular `pix` CLI: launch from any project folder and
|
|
6
|
+
* Pix Desktop treats that folder as the workspace cwd, scoping sessions to
|
|
7
|
+
* it just like the terminal pix does.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* cd /path/to/project
|
|
11
|
+
* pix-desktop
|
|
12
|
+
*
|
|
13
|
+
* Environment:
|
|
14
|
+
* PIX_DESKTOP_BIN Path to a specific Tauri binary (overrides discovery).
|
|
15
|
+
* PIX_DESKTOP_DEBUG Set to "1" to log resolved binary/cwd before launch.
|
|
16
|
+
*
|
|
17
|
+
* Build the binary first via one of:
|
|
18
|
+
* cd apps/desktop-tauri && npm run tauri:dev # development (with hot reload)
|
|
19
|
+
* cd apps/desktop-tauri && npm run tauri:build # production bundle
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import { existsSync } from "node:fs";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { platform } from "node:os";
|
|
27
|
+
|
|
28
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const tauriTarget = join(here, "..", "src-tauri", "target");
|
|
30
|
+
|
|
31
|
+
const exeSuffix = platform() === "win32" ? ".exe" : "";
|
|
32
|
+
const macBundle = join(
|
|
33
|
+
tauriTarget,
|
|
34
|
+
"release",
|
|
35
|
+
"bundle",
|
|
36
|
+
"macos",
|
|
37
|
+
"Pix.app",
|
|
38
|
+
"Contents",
|
|
39
|
+
"MacOS",
|
|
40
|
+
"pix-desktop",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Discovery order: env override → debug → release raw → release macOS bundle.
|
|
44
|
+
const candidates = [
|
|
45
|
+
join(tauriTarget, "debug", `pix-desktop${exeSuffix}`),
|
|
46
|
+
join(tauriTarget, "release", `pix-desktop${exeSuffix}`),
|
|
47
|
+
macBundle,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
if (process.env.PIX_DESKTOP_BIN) candidates.unshift(process.env.PIX_DESKTOP_BIN);
|
|
51
|
+
|
|
52
|
+
const bin = candidates.find((p) => existsSync(p));
|
|
53
|
+
|
|
54
|
+
if (!bin) {
|
|
55
|
+
console.error("pix-desktop: Tauri binary not found. Build it first:");
|
|
56
|
+
console.error("");
|
|
57
|
+
console.error(" cd apps/desktop-tauri && npm run tauri:dev # development");
|
|
58
|
+
console.error(" cd apps/desktop-tauri && npm run tauri:build # production");
|
|
59
|
+
console.error("");
|
|
60
|
+
console.error("Or set PIX_DESKTOP_BIN=/path/to/pix-desktop to point at a custom binary.");
|
|
61
|
+
process.exit(127);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (process.env.PIX_DESKTOP_DEBUG) {
|
|
65
|
+
console.error(`[pix-desktop] binary: ${bin}`);
|
|
66
|
+
console.error(`[pix-desktop] cwd: ${process.cwd()}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const child = spawn(bin, process.argv.slice(2), {
|
|
70
|
+
stdio: "inherit",
|
|
71
|
+
cwd: process.cwd(),
|
|
72
|
+
env: process.env,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
child.on("exit", (code, signal) => {
|
|
76
|
+
if (signal) {
|
|
77
|
+
// Re-raise the signal in the launcher so wrapping shells see it.
|
|
78
|
+
try { process.kill(process.pid, signal); } catch { /* ignore */ }
|
|
79
|
+
process.exit(128 + 15); // SIGTERM-ish
|
|
80
|
+
}
|
|
81
|
+
process.exit(code ?? 0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Forward common signals to the child so Ctrl-C cleans up the Tauri window.
|
|
85
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
86
|
+
process.on(sig, () => {
|
|
87
|
+
if (!child.killed) child.kill(sig);
|
|
88
|
+
});
|
|
89
|
+
}
|
package/dist/app/app.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export declare class PiUiExtendApp {
|
|
|
44
44
|
private readonly extensionShutdownHandler;
|
|
45
45
|
private runtime;
|
|
46
46
|
private readonly inputEditor;
|
|
47
|
+
private lastInputEditorContentVersion;
|
|
47
48
|
private readonly requestHistory;
|
|
48
49
|
/** Shortcut: get/set the editor text as a plain string. */
|
|
49
50
|
private get input();
|
|
@@ -101,6 +102,7 @@ export declare class PiUiExtendApp {
|
|
|
101
102
|
private clearToastTimers;
|
|
102
103
|
private render;
|
|
103
104
|
private scheduleRender;
|
|
105
|
+
private syncScrollAfterInputEditorChange;
|
|
104
106
|
private renderStatusLine;
|
|
105
107
|
private terminalColumns;
|
|
106
108
|
private terminalRows;
|
package/dist/app/app.js
CHANGED
|
@@ -110,6 +110,7 @@ export class PiUiExtendApp {
|
|
|
110
110
|
extensionShutdownHandler = () => { };
|
|
111
111
|
runtime;
|
|
112
112
|
inputEditor = new InputEditor();
|
|
113
|
+
lastInputEditorContentVersion = this.inputEditor.contentVersion;
|
|
113
114
|
requestHistory;
|
|
114
115
|
/** Shortcut: get/set the editor text as a plain string. */
|
|
115
116
|
get input() { return this.inputEditor.text; }
|
|
@@ -126,6 +127,8 @@ export class PiUiExtendApp {
|
|
|
126
127
|
constructor(options) {
|
|
127
128
|
this.options = options;
|
|
128
129
|
this.theme = THEMES[options.themeName];
|
|
130
|
+
this.pixConfig = loadPixConfig(this.options.cwd);
|
|
131
|
+
setAppIconTheme(this.pixConfig.iconTheme.name);
|
|
129
132
|
const app = this;
|
|
130
133
|
this.blinkController = new AppBlinkController({
|
|
131
134
|
render: () => this.render(),
|
|
@@ -157,6 +160,7 @@ export class PiUiExtendApp {
|
|
|
157
160
|
});
|
|
158
161
|
this.tabsController = new AppTabsController({
|
|
159
162
|
options: this.options,
|
|
163
|
+
maxProjectSessions: this.pixConfig.maxProjectSessions,
|
|
160
164
|
blinkController: this.blinkController,
|
|
161
165
|
runtime: () => this.runtime,
|
|
162
166
|
createRuntimeForNewSession: () => this.createRuntime(newTabRuntimeOptions(this.options)),
|
|
@@ -175,8 +179,8 @@ export class PiUiExtendApp {
|
|
|
175
179
|
loadSessionHistory: () => this.loadSessionHistory(),
|
|
176
180
|
loadSessionHistoryAsync: (options) => this.loadSessionHistoryAsync(options),
|
|
177
181
|
syncUserSessionEntryMetadata: () => this.workspaceActions.syncUserSessionEntryMetadata(),
|
|
178
|
-
captureInputState: () =>
|
|
179
|
-
restoreInputState: (state) => this.restoreTabInputState(state
|
|
182
|
+
captureInputState: () => this.inputEditor.draftState,
|
|
183
|
+
restoreInputState: (state) => this.restoreTabInputState(state),
|
|
180
184
|
closeMenusForTabSwitch: () => this.popupMenus.closeMenusForTabSwitch(),
|
|
181
185
|
captureDeferredUserMessages: () => this.queuedMessages.captureDeferredUserMessages(),
|
|
182
186
|
restoreDeferredUserMessages: (messages) => this.queuedMessages.restoreDeferredUserMessages(messages),
|
|
@@ -184,8 +188,6 @@ export class PiUiExtendApp {
|
|
|
184
188
|
showToast: (message, kind) => this.showToast(message, kind),
|
|
185
189
|
render: () => this.render(),
|
|
186
190
|
});
|
|
187
|
-
this.pixConfig = loadPixConfig(this.options.cwd);
|
|
188
|
-
setAppIconTheme(this.pixConfig.iconTheme.name);
|
|
189
191
|
this.terminalBellSoundController = new TerminalBellSoundController();
|
|
190
192
|
this.promptEnhancer = new AppPromptEnhancerController({
|
|
191
193
|
runtime: () => this.runtime,
|
|
@@ -224,10 +226,12 @@ export class PiUiExtendApp {
|
|
|
224
226
|
const popupMenuRenderer = new PopupMenuRenderer({
|
|
225
227
|
theme: this.theme,
|
|
226
228
|
screenStyler: this.screenStyler,
|
|
229
|
+
modelColors: this.pixConfig.modelColors,
|
|
227
230
|
get entries() { return app.entries; },
|
|
228
231
|
get session() { return app.runtime?.session; },
|
|
229
232
|
get resumeLoading() { return app.resumeLoading; },
|
|
230
233
|
get resumeSessionCount() { return app.resumeSessions.length; },
|
|
234
|
+
get userMessageJumpLoading() { return app.menuItems.isUserMessageJumpLoading(); },
|
|
231
235
|
});
|
|
232
236
|
this.popupMenus = new AppPopupMenuController({
|
|
233
237
|
get entries() { return app.entries; },
|
|
@@ -430,6 +434,7 @@ export class PiUiExtendApp {
|
|
|
430
434
|
this.commandController = new AppCommandController({
|
|
431
435
|
options: this.options,
|
|
432
436
|
runtime: () => this.runtime,
|
|
437
|
+
requestHistory: () => this.requestHistory,
|
|
433
438
|
getInput: () => this.input,
|
|
434
439
|
setInput: (value) => this.setInput(value),
|
|
435
440
|
promptEnhancerModelRef: () => this.pixConfig.promptEnhancer.modelRef,
|
|
@@ -614,6 +619,7 @@ export class PiUiExtendApp {
|
|
|
614
619
|
addEntry: (entry) => this.addEntry(entry),
|
|
615
620
|
addSessionAbortedEntry: () => this.sessionEvents.addSessionAbortedEntry(),
|
|
616
621
|
showToast: (message, kind) => this.showToast(message, kind),
|
|
622
|
+
dismissActiveDialog: () => this.toastController.dismissActiveDialog(),
|
|
617
623
|
stopVoiceInput: () => this.voiceController.stopRecording(),
|
|
618
624
|
isShellCommandRunning: () => this.shellController.isRunning(),
|
|
619
625
|
runChatShellCommand: (command) => this.shellController.run(command),
|
|
@@ -814,10 +820,10 @@ export class PiUiExtendApp {
|
|
|
814
820
|
this.popupMenus.resetInputMenuDismissals();
|
|
815
821
|
this.autocompleteController.dispose();
|
|
816
822
|
}
|
|
817
|
-
restoreTabInputState(
|
|
823
|
+
restoreTabInputState(state) {
|
|
818
824
|
this.requestHistory.resetNavigation();
|
|
819
825
|
this.popupMenus.resetInputMenuDismissals();
|
|
820
|
-
this.inputEditor.
|
|
826
|
+
this.inputEditor.setDraftState(state);
|
|
821
827
|
this.autocompleteController.dispose();
|
|
822
828
|
}
|
|
823
829
|
async clearPersistedInputDraft() {
|
|
@@ -1007,6 +1013,7 @@ export class PiUiExtendApp {
|
|
|
1007
1013
|
this.scheduledRenderTimer = undefined;
|
|
1008
1014
|
}
|
|
1009
1015
|
this.autocompleteController.observeInput();
|
|
1016
|
+
this.syncScrollAfterInputEditorChange();
|
|
1010
1017
|
this.renderController.render();
|
|
1011
1018
|
}
|
|
1012
1019
|
scheduleRender() {
|
|
@@ -1014,10 +1021,18 @@ export class PiUiExtendApp {
|
|
|
1014
1021
|
return;
|
|
1015
1022
|
this.scheduledRenderTimer = setTimeout(() => {
|
|
1016
1023
|
this.scheduledRenderTimer = undefined;
|
|
1024
|
+
this.syncScrollAfterInputEditorChange();
|
|
1017
1025
|
this.renderController.render();
|
|
1018
1026
|
}, COALESCED_RENDER_DELAY_MS);
|
|
1019
1027
|
this.scheduledRenderTimer.unref?.();
|
|
1020
1028
|
}
|
|
1029
|
+
syncScrollAfterInputEditorChange() {
|
|
1030
|
+
const contentVersion = this.inputEditor.contentVersion;
|
|
1031
|
+
if (contentVersion === this.lastInputEditorContentVersion)
|
|
1032
|
+
return;
|
|
1033
|
+
this.lastInputEditorContentVersion = contentVersion;
|
|
1034
|
+
this.scrollController.scrollToBottom();
|
|
1035
|
+
}
|
|
1021
1036
|
renderStatusLine() {
|
|
1022
1037
|
this.renderController.renderStatusLine();
|
|
1023
1038
|
}
|
|
@@ -54,6 +54,7 @@ export class AppCommandController {
|
|
|
54
54
|
runCloneCommand: () => this.navigationActions.runCloneCommand(),
|
|
55
55
|
runTreeCommand: (argumentsText) => this.navigationActions.runTreeCommand(argumentsText),
|
|
56
56
|
runJumpCommand: (argumentsText) => this.navigationActions.runJumpCommand(argumentsText),
|
|
57
|
+
runHistoryCommand: (argumentsText) => this.navigationActions.runHistoryCommand(argumentsText),
|
|
57
58
|
runSearchCommand: (argumentsText) => this.navigationActions.runSearchCommand(argumentsText),
|
|
58
59
|
runUnsupportedBuiltinCommand: (commandName, message) => this.navigationActions.runUnsupportedBuiltinCommand(commandName, message),
|
|
59
60
|
runResumePathCommand: (sessionPath) => this.navigationActions.runResumePathCommand(sessionPath),
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { AgentSession, AgentSessionRuntime, SessionInfo } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { SessionSearchResult } from "../session/session-search.js";
|
|
3
|
+
import type { AppRequestHistory } from "../session/request-history.js";
|
|
3
4
|
import type { ActivePopupMenu, AppOptions, Entry, ModelMenuValue, PixMenuItem, PixMenuOptions, PopupMenuPlacement, ScopedSessionModel, SessionModel, ThinkingMenuValue } from "../types.js";
|
|
4
5
|
import type { ToastNotifier } from "../../ui.js";
|
|
5
6
|
export type DirectPopupMenu = Exclude<ActivePopupMenu, "slash">;
|
|
6
7
|
export type CommandControllerHost = {
|
|
7
8
|
readonly options: AppOptions;
|
|
8
9
|
runtime(): AgentSessionRuntime | undefined;
|
|
10
|
+
requestHistory(): AppRequestHistory;
|
|
9
11
|
getInput(): string;
|
|
10
12
|
setInput(value: string): void;
|
|
11
13
|
promptEnhancerModelRef(): string;
|
|
@@ -2,6 +2,14 @@ import type { SessionInfo } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { CommandControllerHost } from "./command-host.js";
|
|
3
3
|
import { type ResumeSessionLoaderOptions } from "../session/resume-session-loader.js";
|
|
4
4
|
import type { PopupMenuPlacement } from "../types.js";
|
|
5
|
+
export declare function formatHistoryMenuLabel(text: string): string;
|
|
6
|
+
export declare function historyHighlightRanges(ranges: readonly {
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
}[], text: string): {
|
|
10
|
+
start: number;
|
|
11
|
+
end: number;
|
|
12
|
+
}[];
|
|
5
13
|
export declare class NavigationCommandActions {
|
|
6
14
|
private readonly host;
|
|
7
15
|
private readonly resumeSessionLoader;
|
|
@@ -11,6 +19,7 @@ export declare class NavigationCommandActions {
|
|
|
11
19
|
runCloneCommand(): Promise<void>;
|
|
12
20
|
runTreeCommand(argumentsText: string): Promise<void>;
|
|
13
21
|
runJumpCommand(argumentsText: string): Promise<void>;
|
|
22
|
+
runHistoryCommand(argumentsText: string): Promise<void>;
|
|
14
23
|
runSearchCommand(argumentsText: string): Promise<void>;
|
|
15
24
|
runUnsupportedBuiltinCommand(commandName: string, message: string): Promise<void>;
|
|
16
25
|
runResumePathCommand(sessionPath: string): Promise<void>;
|
|
@@ -11,6 +11,24 @@ function nextTick() {
|
|
|
11
11
|
setImmediate(resolve);
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
|
+
export function formatHistoryMenuLabel(text) {
|
|
15
|
+
return sanitizeText(text).replace(/\n/g, " ↵ ");
|
|
16
|
+
}
|
|
17
|
+
export function historyHighlightRanges(ranges, text) {
|
|
18
|
+
return ranges.map((range) => ({
|
|
19
|
+
start: historyLabelIndex(range.start, text),
|
|
20
|
+
end: historyLabelIndex(range.end, text),
|
|
21
|
+
})).filter((range) => range.end > range.start);
|
|
22
|
+
}
|
|
23
|
+
function historyLabelIndex(index, text) {
|
|
24
|
+
const before = text.slice(0, Math.max(0, Math.min(index, text.length)));
|
|
25
|
+
const newlineCount = before.split("\n").length - 1;
|
|
26
|
+
return before.length + newlineCount * 2;
|
|
27
|
+
}
|
|
28
|
+
function formatHistoryMenuDescription(text) {
|
|
29
|
+
const lines = sanitizeText(text).split("\n");
|
|
30
|
+
return lines.length > 1 ? `${lines.length} lines` : undefined;
|
|
31
|
+
}
|
|
14
32
|
export class NavigationCommandActions {
|
|
15
33
|
host;
|
|
16
34
|
resumeSessionLoader;
|
|
@@ -98,12 +116,53 @@ export class NavigationCommandActions {
|
|
|
98
116
|
const runtime = getRuntime(this.host, "jump");
|
|
99
117
|
if (!runtime)
|
|
100
118
|
return;
|
|
101
|
-
this.host.setStatus("scanning session messages…");
|
|
102
|
-
this.host.render();
|
|
103
|
-
await this.host.refreshUserMessageJumpMenuItems();
|
|
104
119
|
this.host.openDirectPopupMenu("user-message-jump", { preserveStatus: true });
|
|
105
120
|
this.host.setDirectPopupMenuQuery(argumentsText.trim());
|
|
106
121
|
this.host.render();
|
|
122
|
+
try {
|
|
123
|
+
await this.host.refreshUserMessageJumpMenuItems();
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
this.host.toast.error(`Could not load jump messages: ${error instanceof Error ? error.message : String(error)}`);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
this.host.render();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async runHistoryCommand(argumentsText) {
|
|
133
|
+
const query = argumentsText.trim();
|
|
134
|
+
const matches = this.host.requestHistory().searchMatches(query, 100);
|
|
135
|
+
if (matches.length === 0) {
|
|
136
|
+
this.host.addEntry({ id: createId("system"), kind: "system", text: query ? `No command history found for: ${query}` : "Command history is empty." });
|
|
137
|
+
this.host.toast.info(query ? "No matching command history" : "Command history is empty");
|
|
138
|
+
this.host.setSessionStatus(this.host.runtime()?.session);
|
|
139
|
+
this.host.render();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const selected = await this.host.showMenu(matches.map((match) => {
|
|
143
|
+
const description = formatHistoryMenuDescription(match.value);
|
|
144
|
+
return {
|
|
145
|
+
value: match.value,
|
|
146
|
+
label: formatHistoryMenuLabel(match.value),
|
|
147
|
+
labelHighlightRanges: match.matchedText === match.label ? historyHighlightRanges(match.matchedRanges, match.value) : [],
|
|
148
|
+
...(description === undefined ? {} : { description }),
|
|
149
|
+
};
|
|
150
|
+
}), {
|
|
151
|
+
title: query ? `Search command history: ${query}` : "Command history",
|
|
152
|
+
placeholder: "Filter history",
|
|
153
|
+
emptyText: "No matching command history",
|
|
154
|
+
searchable: true,
|
|
155
|
+
minScorePerCharacter: 8,
|
|
156
|
+
preferKeyboardLayoutMatches: true,
|
|
157
|
+
});
|
|
158
|
+
if (!selected) {
|
|
159
|
+
this.host.setSessionStatus(this.host.runtime()?.session);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.host.setInput(selected);
|
|
163
|
+
this.host.toast.info("Restored command from history");
|
|
164
|
+
this.host.setSessionStatus(this.host.runtime()?.session);
|
|
165
|
+
this.host.render();
|
|
107
166
|
}
|
|
108
167
|
async runSearchCommand(argumentsText) {
|
|
109
168
|
const runtime = getIdleRuntime(this.host, "search");
|
|
@@ -25,6 +25,7 @@ export type CommandRegistryActions = {
|
|
|
25
25
|
runCloneCommand(): Promise<void>;
|
|
26
26
|
runTreeCommand(argumentsText: string): Promise<void>;
|
|
27
27
|
runJumpCommand(argumentsText: string): Promise<void>;
|
|
28
|
+
runHistoryCommand(argumentsText: string): Promise<void>;
|
|
28
29
|
runSearchCommand(argumentsText: string): Promise<void>;
|
|
29
30
|
runUnsupportedBuiltinCommand(commandName: string, message: string): Promise<void>;
|
|
30
31
|
runReloadCommand(): Promise<void>;
|
|
@@ -185,6 +185,14 @@ export function createSlashCommands(actions, host) {
|
|
|
185
185
|
allowArguments: true,
|
|
186
186
|
run: (argumentsText) => actions.runJumpCommand(argumentsText),
|
|
187
187
|
},
|
|
188
|
+
{
|
|
189
|
+
name: "history",
|
|
190
|
+
description: "Search command history and restore a match",
|
|
191
|
+
kind: "builtin",
|
|
192
|
+
keywords: ["command", "request", "prompt", "find", "recent"],
|
|
193
|
+
allowArguments: true,
|
|
194
|
+
run: (argumentsText) => actions.runHistoryCommand(argumentsText),
|
|
195
|
+
},
|
|
188
196
|
{
|
|
189
197
|
name: "search",
|
|
190
198
|
description: "Search sessions and open a match in a new tab",
|
package/dist/app/constants.d.ts
CHANGED
|
@@ -42,7 +42,6 @@ export declare const GIT_BRANCH_CACHE_MS = 30000;
|
|
|
42
42
|
export declare const TODO_TOOL_NAME = "todo";
|
|
43
43
|
export declare const TODO_ACTIONS: readonly ["create", "update", "batch_create", "batch_update", "list", "get", "delete", "clear", "export", "import"];
|
|
44
44
|
export declare const TODO_STATUSES: readonly ["pending", "in_progress", "deferred", "completed", "deleted"];
|
|
45
|
-
export declare const TODO_PRIORITIES: readonly ["low", "medium", "high", "urgent"];
|
|
46
45
|
export declare const SUBAGENT_STATUSES: readonly ["planned", "running", "retrying", "done", "failed", "stopped"];
|
|
47
46
|
export declare const SUBAGENT_ACTIVE_STATUSES: readonly ["planned", "running", "retrying"];
|
|
48
47
|
export declare const SUBAGENT_TERMINAL_STATUSES: readonly ["done", "failed", "stopped"];
|
package/dist/app/constants.js
CHANGED
|
@@ -90,7 +90,6 @@ export const TODO_ACTIONS = [
|
|
|
90
90
|
"import",
|
|
91
91
|
];
|
|
92
92
|
export const TODO_STATUSES = ["pending", "in_progress", "deferred", "completed", "deleted"];
|
|
93
|
-
export const TODO_PRIORITIES = ["low", "medium", "high", "urgent"];
|
|
94
93
|
export const SUBAGENT_STATUSES = ["planned", "running", "retrying", "done", "failed", "stopped"];
|
|
95
94
|
export const SUBAGENT_ACTIVE_STATUSES = ["planned", "running", "retrying"];
|
|
96
95
|
export const SUBAGENT_TERMINAL_STATUSES = ["done", "failed", "stopped"];
|
package/dist/app/icons.d.ts
CHANGED
package/dist/app/icons.js
CHANGED
|
@@ -18,6 +18,7 @@ const NERD_FONT_ICONS = {
|
|
|
18
18
|
deleted: "\u{f0159}",
|
|
19
19
|
deferred: "\u{f0377}",
|
|
20
20
|
info: "\u{f02fc}",
|
|
21
|
+
lightbulb: "\u{f0335}",
|
|
21
22
|
microphone: "\u{f036c}",
|
|
22
23
|
plus: "\u{f0415}",
|
|
23
24
|
pause: "\u{f03e4}",
|
|
@@ -44,6 +45,7 @@ const FALLBACK_ICONS = {
|
|
|
44
45
|
deleted: "×",
|
|
45
46
|
deferred: "↷",
|
|
46
47
|
info: "i",
|
|
48
|
+
lightbulb: "💡",
|
|
47
49
|
microphone: "m",
|
|
48
50
|
plus: "+",
|
|
49
51
|
pause: "⏸",
|
|
@@ -19,6 +19,7 @@ export type AppInputActionControllerHost = {
|
|
|
19
19
|
addEntry(entry: Entry): void;
|
|
20
20
|
addSessionAbortedEntry(): void;
|
|
21
21
|
showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
|
|
22
|
+
dismissActiveDialog?(): boolean;
|
|
22
23
|
stopVoiceInput(): Promise<void>;
|
|
23
24
|
isShellCommandRunning(): boolean;
|
|
24
25
|
runChatShellCommand(command: string): Promise<InteractiveShellCommandResult>;
|
|
@@ -80,10 +80,11 @@ export class AppInputActionController {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
closeActiveGlobalUi() {
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
if (this.popupMenus.syncActivePopupMenu()) {
|
|
84
|
+
this.popupMenus.cancelActivePopupMenu();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return this.host.dismissActiveDialog?.() ?? false;
|
|
87
88
|
}
|
|
88
89
|
async abortStreamingSession(runtime, options) {
|
|
89
90
|
const session = runtime.session;
|
|
@@ -37,6 +37,7 @@ export declare class AppInputController {
|
|
|
37
37
|
handleChunk(chunk: Buffer): void;
|
|
38
38
|
private consumeSharedEditorShiftEnter;
|
|
39
39
|
private drainInputBuffer;
|
|
40
|
+
private consumeBracketedPastePayload;
|
|
40
41
|
private getEscapeSequences;
|
|
41
42
|
private isPendingEscapeSequence;
|
|
42
43
|
private consumeEscapeSequence;
|
|
@@ -42,6 +42,8 @@ export class AppInputController {
|
|
|
42
42
|
}
|
|
43
43
|
drainInputBuffer() {
|
|
44
44
|
while (this.inputBuffer.length > 0) {
|
|
45
|
+
if (this.consumeBracketedPastePayload())
|
|
46
|
+
continue;
|
|
45
47
|
const mouseMatch = /^\x1b\[<(\d+);(-?\d+);(-?\d+)([mM])/.exec(this.inputBuffer);
|
|
46
48
|
if (mouseMatch) {
|
|
47
49
|
this.inputBuffer = this.inputBuffer.slice(mouseMatch[0].length);
|
|
@@ -85,6 +87,23 @@ export class AppInputController {
|
|
|
85
87
|
this.handleChar(char);
|
|
86
88
|
}
|
|
87
89
|
}
|
|
90
|
+
consumeBracketedPastePayload() {
|
|
91
|
+
if (!this.host.inputEditor.isInBracketedPaste)
|
|
92
|
+
return false;
|
|
93
|
+
const endSequence = "\x1b[201~";
|
|
94
|
+
const endIndex = this.inputBuffer.indexOf(endSequence);
|
|
95
|
+
if (endIndex === 0)
|
|
96
|
+
return false;
|
|
97
|
+
const payloadEnd = endIndex === -1
|
|
98
|
+
? safeBracketedPastePayloadLength(this.inputBuffer, endSequence)
|
|
99
|
+
: endIndex;
|
|
100
|
+
if (payloadEnd === 0)
|
|
101
|
+
return false;
|
|
102
|
+
const payload = this.inputBuffer.slice(0, payloadEnd);
|
|
103
|
+
this.inputBuffer = this.inputBuffer.slice(payloadEnd);
|
|
104
|
+
this.pasteHandler.appendBracketedPasteText(normalizeBracketedPastePayload(payload));
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
88
107
|
getEscapeSequences() {
|
|
89
108
|
return [
|
|
90
109
|
["\x1b[13;2u", () => this.insertInputNewline()],
|
|
@@ -425,3 +444,13 @@ export class AppInputController {
|
|
|
425
444
|
return this.host.isShiftPressed?.() ?? isNativeShiftPressed();
|
|
426
445
|
}
|
|
427
446
|
}
|
|
447
|
+
function normalizeBracketedPastePayload(payload) {
|
|
448
|
+
return payload.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
449
|
+
}
|
|
450
|
+
function safeBracketedPastePayloadLength(buffer, endSequence) {
|
|
451
|
+
for (let length = Math.min(buffer.length, endSequence.length - 1); length > 0; length--) {
|
|
452
|
+
if (endSequence.startsWith(buffer.slice(buffer.length - length)))
|
|
453
|
+
return buffer.length - length;
|
|
454
|
+
}
|
|
455
|
+
return buffer.length;
|
|
456
|
+
}
|
|
@@ -7,7 +7,7 @@ export type InputPasteHost = {
|
|
|
7
7
|
};
|
|
8
8
|
export declare class InputPasteHandler {
|
|
9
9
|
private readonly host;
|
|
10
|
-
private
|
|
10
|
+
private pasteBufferParts;
|
|
11
11
|
private readonly recentPasteFingerprints;
|
|
12
12
|
private suppressImagePathPasteUntil;
|
|
13
13
|
constructor(host: InputPasteHost);
|
|
@@ -7,7 +7,7 @@ import { normalizePastedTextForDuplicateKey } from "../rendering/render-text.js"
|
|
|
7
7
|
const PASTE_FINGERPRINT_PREFIX_CHARS = 64 * 1024;
|
|
8
8
|
export class InputPasteHandler {
|
|
9
9
|
host;
|
|
10
|
-
|
|
10
|
+
pasteBufferParts = [];
|
|
11
11
|
recentPasteFingerprints = new Map();
|
|
12
12
|
suppressImagePathPasteUntil = 0;
|
|
13
13
|
constructor(host) {
|
|
@@ -31,15 +31,16 @@ export class InputPasteHandler {
|
|
|
31
31
|
}
|
|
32
32
|
beginBracketedPaste() {
|
|
33
33
|
this.host.inputEditor.beginBracketedPaste();
|
|
34
|
-
this.
|
|
34
|
+
this.pasteBufferParts = [];
|
|
35
35
|
}
|
|
36
36
|
appendBracketedPasteText(text) {
|
|
37
|
-
|
|
37
|
+
if (text)
|
|
38
|
+
this.pasteBufferParts.push(text);
|
|
38
39
|
}
|
|
39
40
|
endBracketedPaste() {
|
|
40
41
|
this.host.inputEditor.endBracketedPaste();
|
|
41
|
-
const text = this.
|
|
42
|
-
this.
|
|
42
|
+
const text = this.pasteBufferParts.join("");
|
|
43
|
+
this.pasteBufferParts = [];
|
|
43
44
|
this.handlePasteEnd(text);
|
|
44
45
|
}
|
|
45
46
|
async handleClipboardImagePaste() {
|