pi-ui-extend 0.1.34 → 0.1.35
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/README.md +20 -0
- package/dist/app/app.js +4 -2
- package/dist/app/constants.d.ts +2 -1
- package/dist/app/constants.js +6 -1
- package/dist/app/input/input-controller.d.ts +4 -1
- package/dist/app/input/input-controller.js +95 -16
- package/dist/app/input/input-paste-handler.js +3 -1
- package/dist/app/input/terminal-edit-shortcuts.d.ts +20 -0
- package/dist/app/input/terminal-edit-shortcuts.js +50 -16
- package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
- package/dist/app/rendering/conversation-entry-renderer.js +1 -1
- package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
- package/dist/app/rendering/conversation-tool-renderer.js +21 -0
- package/dist/app/rendering/conversation-viewport.d.ts +3 -0
- package/dist/app/rendering/conversation-viewport.js +41 -5
- package/dist/app/rendering/editor-layout-renderer.js +3 -2
- package/dist/app/rendering/editor-panels.js +27 -10
- package/dist/app/runtime.d.ts +1 -0
- package/dist/app/runtime.js +33 -14
- package/dist/app/session/session-event-controller.d.ts +7 -0
- package/dist/app/session/session-event-controller.js +78 -0
- package/dist/app/session/tabs-controller.js +3 -1
- package/dist/app/subagents/subagents-widget-controller.d.ts +10 -2
- package/dist/app/subagents/subagents-widget-controller.js +141 -70
- package/dist/app/terminal/terminal-controller.d.ts +10 -0
- package/dist/app/terminal/terminal-controller.js +91 -2
- package/dist/app/todo/todo-model.js +2 -0
- package/dist/app/todo/todo-widget-controller.d.ts +2 -0
- package/dist/app/todo/todo-widget-controller.js +17 -7
- package/dist/app/types.d.ts +4 -0
- package/dist/bundled-extensions/question/tui.js +8 -1
- package/dist/bundled-extensions/session-title/index.js +65 -14
- package/dist/input-editor-files.js +23 -4
- package/dist/markdown-format.d.ts +4 -1
- package/dist/markdown-format.js +76 -9
- package/external/pi-tools-suite/README.md +71 -1
- package/external/pi-tools-suite/package.json +3 -3
- package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -6
- package/external/pi-tools-suite/src/async-subagents/index.ts +133 -37
- package/external/pi-tools-suite/src/context-usage.ts +6 -1
- package/external/pi-tools-suite/src/dcp/commands.ts +3 -2
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +9 -4
- package/external/pi-tools-suite/src/dcp/config.ts +142 -6
- package/external/pi-tools-suite/src/dcp/index.ts +20 -8
- package/external/pi-tools-suite/src/dcp/prompts.ts +17 -9
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +59 -15
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +6 -8
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +51 -1
- package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +16 -11
- package/external/pi-tools-suite/src/model-tools/index.ts +24 -12
- package/external/pi-tools-suite/src/prompt-commands/index.ts +11 -2
- package/external/pi-tools-suite/src/telegram-mirror/index.ts +66 -27
- package/external/pi-tools-suite/src/todo/index.ts +87 -16
- package/external/pi-tools-suite/src/todo/state/store.ts +41 -10
- package/external/pi-tools-suite/src/todo/todo.ts +49 -6
- package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -160,6 +160,26 @@ Before committing code changes, run:
|
|
|
160
160
|
npm run check
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
+
### Keeping the bundled extension's SDK pin in sync
|
|
164
|
+
|
|
165
|
+
Pix bundles the `pi-tools-suite` extension (in `external/pi-tools-suite`), which
|
|
166
|
+
runs inside the Pi host process. Its `@earendil-works/*` peerDependencies must
|
|
167
|
+
match the host Pi SDK version exactly, or npm can resolve a stale copy in the
|
|
168
|
+
suite's own `node_modules` and cause a double-load (e.g. `0.75.4` in the suite
|
|
169
|
+
vs `0.79.4` in the host).
|
|
170
|
+
|
|
171
|
+
When you bump the Pi SDK in the root `package.json`, re-pin the suite and
|
|
172
|
+
reinstall it:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npm run sync:sdk-pin # rewrite suite peerDeps to host version
|
|
176
|
+
cd external/pi-tools-suite && npm install --ignore-scripts # update suite lockfile/node_modules
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`npm run check` runs `npm run sync:sdk-pin:check` first, so a stale pin fails
|
|
180
|
+
the check fast. Use `npm run sync:sdk-pin:check` on its own for a drift-only
|
|
181
|
+
report (non-zero exit on drift).
|
|
182
|
+
|
|
163
183
|
## Configuration
|
|
164
184
|
|
|
165
185
|
Useful environment variables:
|
package/dist/app/app.js
CHANGED
|
@@ -166,7 +166,7 @@ export class PiUiExtendApp {
|
|
|
166
166
|
maxProjectSessions: () => this.pixConfig.maxProjectSessions,
|
|
167
167
|
blinkController: this.blinkController,
|
|
168
168
|
runtime: () => this.runtime,
|
|
169
|
-
createRuntimeForNewSession: () => this.createRuntime(newTabRuntimeOptions(this.options)),
|
|
169
|
+
createRuntimeForNewSession: () => this.createRuntime(newTabRuntimeOptions(this.options), this.runtime === undefined ? {} : { reuseServicesFrom: this.runtime }),
|
|
170
170
|
createRuntimeForSession: (sessionPath) => this.createRuntime({
|
|
171
171
|
...this.options,
|
|
172
172
|
noSession: false,
|
|
@@ -334,6 +334,7 @@ export class PiUiExtendApp {
|
|
|
334
334
|
});
|
|
335
335
|
this.todoWidgetController = new AppTodoWidgetController({
|
|
336
336
|
sessionFile: () => this.runtime?.session.sessionFile,
|
|
337
|
+
sessionId: () => this.runtime?.session.sessionId,
|
|
337
338
|
isRunning: () => this.running,
|
|
338
339
|
render: () => this.scheduleRender(),
|
|
339
340
|
});
|
|
@@ -761,10 +762,11 @@ export class PiUiExtendApp {
|
|
|
761
762
|
});
|
|
762
763
|
this.slashCommands = this.commandController.slashCommands;
|
|
763
764
|
}
|
|
764
|
-
createRuntime(options) {
|
|
765
|
+
createRuntime(options, runtimeOptions = {}) {
|
|
765
766
|
return createPixRuntime(options, {
|
|
766
767
|
eventBus: this.createExtensionEventBus(),
|
|
767
768
|
config: this.pixConfig,
|
|
769
|
+
...runtimeOptions,
|
|
768
770
|
});
|
|
769
771
|
}
|
|
770
772
|
async loadStartupConfig() {
|
package/dist/app/constants.d.ts
CHANGED
|
@@ -19,7 +19,8 @@ export declare const REQUEST_HISTORY_VERSION = 1;
|
|
|
19
19
|
export declare const REQUEST_HISTORY_MAX_ENTRIES = 200;
|
|
20
20
|
export declare const REQUEST_HISTORY_MAX_BYTES: number;
|
|
21
21
|
export declare const REQUEST_HISTORY_MAX_ENTRY_BYTES: number;
|
|
22
|
-
export declare const ENABLE_TERMINAL_KEY_REPORTING = "\u001B[>7u\u001B[
|
|
22
|
+
export declare const ENABLE_TERMINAL_KEY_REPORTING = "\u001B[>7u\u001B[?u\u001B[c";
|
|
23
|
+
export declare const ENABLE_TERMINAL_MODIFY_OTHER_KEYS = "\u001B[>4;2m";
|
|
23
24
|
export declare const DISABLE_TERMINAL_KEY_REPORTING = "\u001B[<u\u001B[>4;0m";
|
|
24
25
|
export declare const ENABLE_BRACKETED_PASTE = "\u001B[?2004h";
|
|
25
26
|
export declare const DISABLE_BRACKETED_PASTE = "\u001B[?2004l";
|
package/dist/app/constants.js
CHANGED
|
@@ -52,7 +52,12 @@ export const REQUEST_HISTORY_VERSION = 1;
|
|
|
52
52
|
export const REQUEST_HISTORY_MAX_ENTRIES = 200;
|
|
53
53
|
export const REQUEST_HISTORY_MAX_BYTES = 128 * 1024;
|
|
54
54
|
export const REQUEST_HISTORY_MAX_ENTRY_BYTES = 16 * 1024;
|
|
55
|
-
|
|
55
|
+
// Match pi/@earendil-works/pi-tui keyboard setup: request Kitty keyboard
|
|
56
|
+
// protocol flags, query the terminal response, and use xterm modifyOtherKeys
|
|
57
|
+
// only as a response-driven fallback. Enabling both protocols blindly can make
|
|
58
|
+
// terminals disagree about modified Enter reporting.
|
|
59
|
+
export const ENABLE_TERMINAL_KEY_REPORTING = "\x1b[>7u\x1b[?u\x1b[c";
|
|
60
|
+
export const ENABLE_TERMINAL_MODIFY_OTHER_KEYS = "\x1b[>4;2m";
|
|
56
61
|
export const DISABLE_TERMINAL_KEY_REPORTING = "\x1b[<u\x1b[>4;0m";
|
|
57
62
|
export const ENABLE_BRACKETED_PASTE = "\x1b[?2004h";
|
|
58
63
|
export const DISABLE_BRACKETED_PASTE = "\x1b[?2004l";
|
|
@@ -35,7 +35,7 @@ export declare class AppInputController {
|
|
|
35
35
|
private readonly pasteHandler;
|
|
36
36
|
constructor(host: InputControllerHost);
|
|
37
37
|
handleChunk(chunk: Buffer): void;
|
|
38
|
-
private
|
|
38
|
+
private consumeSharedEditorInput;
|
|
39
39
|
private drainInputBuffer;
|
|
40
40
|
private consumeBracketedPastePayload;
|
|
41
41
|
private getEscapeSequences;
|
|
@@ -45,6 +45,9 @@ export declare class AppInputController {
|
|
|
45
45
|
private consumeCommandArrowPageSequence;
|
|
46
46
|
private consumeCommandArrowPageMatch;
|
|
47
47
|
private consumeTerminalEditShortcutSequence;
|
|
48
|
+
private consumeIgnoredModifiedKeySequence;
|
|
49
|
+
private consumeClipboardImagePasteSequence;
|
|
50
|
+
private consumeShiftEnterSequence;
|
|
48
51
|
private consumeTerminalInterruptSequence;
|
|
49
52
|
private handleArrowUp;
|
|
50
53
|
private handleArrowDown;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { InputPasteHandler } from "./input-paste-handler.js";
|
|
2
2
|
import { hasTerminalCommandModifier, isNativeCommandPressed, isNativeShiftPressed } from "./native-modifiers.js";
|
|
3
|
-
import { parseTerminalEditShortcutSequence, parseTerminalInterruptSequence, terminalEditShortcutForControlChar } from "./terminal-edit-shortcuts.js";
|
|
3
|
+
import { parseTerminalEditShortcutSequence, parseTerminalInterruptSequence, parseTerminalModifiedKeySequence, terminalEditShortcutForControlChar, terminalKeyIsClipboardImagePaste, terminalKeyIsShiftEnter, terminalKeyShouldIgnore, } from "./terminal-edit-shortcuts.js";
|
|
4
|
+
const SHIFT_ENTER_ESCAPE_SEQUENCES = ["\x1b\r", "\x1b\n"];
|
|
4
5
|
export class AppInputController {
|
|
5
6
|
host;
|
|
6
7
|
inputBuffer = "";
|
|
@@ -16,7 +17,7 @@ export class AppInputController {
|
|
|
16
17
|
this.drainInputBuffer();
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
|
-
if (this.
|
|
20
|
+
if (this.consumeSharedEditorInput(data))
|
|
20
21
|
return;
|
|
21
22
|
const extensionInput = this.host.handleExtensionTerminalInput(data);
|
|
22
23
|
if (extensionInput.consume)
|
|
@@ -28,17 +29,41 @@ export class AppInputController {
|
|
|
28
29
|
this.inputBuffer += data;
|
|
29
30
|
this.drainInputBuffer();
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
+
consumeSharedEditorInput(data) {
|
|
32
33
|
if (this.host.extensionInputUsesEditor?.() !== true)
|
|
33
34
|
return false;
|
|
34
35
|
if (this.host.inputEditor.isInBracketedPaste)
|
|
35
36
|
return false;
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
if (data === "\n") {
|
|
38
|
+
this.insertInputNewline();
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (data === "\r" && this.isShiftPressed()) {
|
|
42
|
+
this.insertInputNewline();
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (SHIFT_ENTER_ESCAPE_SEQUENCES.includes(data)) {
|
|
46
|
+
this.insertInputNewline();
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (data === "\x16") {
|
|
50
|
+
void this.pasteHandler.handleClipboardImagePaste();
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
const modifiedKey = parseTerminalModifiedKeySequence(data);
|
|
54
|
+
if (modifiedKey.kind !== "key")
|
|
39
55
|
return false;
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
if (terminalKeyShouldIgnore(modifiedKey.key))
|
|
57
|
+
return true;
|
|
58
|
+
if (terminalKeyIsShiftEnter(modifiedKey.key)) {
|
|
59
|
+
this.insertInputNewline();
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (terminalKeyIsClipboardImagePaste(modifiedKey.key)) {
|
|
63
|
+
void this.pasteHandler.handleClipboardImagePaste();
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
42
67
|
}
|
|
43
68
|
drainInputBuffer() {
|
|
44
69
|
while (this.inputBuffer.length > 0) {
|
|
@@ -72,11 +97,26 @@ export class AppInputController {
|
|
|
72
97
|
continue;
|
|
73
98
|
if (terminalInterruptSequence === "pending")
|
|
74
99
|
return;
|
|
100
|
+
const shiftEnterSequence = this.consumeShiftEnterSequence();
|
|
101
|
+
if (shiftEnterSequence === "consumed")
|
|
102
|
+
continue;
|
|
103
|
+
if (shiftEnterSequence === "pending")
|
|
104
|
+
return;
|
|
105
|
+
const clipboardImagePasteSequence = this.consumeClipboardImagePasteSequence();
|
|
106
|
+
if (clipboardImagePasteSequence === "consumed")
|
|
107
|
+
continue;
|
|
108
|
+
if (clipboardImagePasteSequence === "pending")
|
|
109
|
+
return;
|
|
75
110
|
const terminalEditShortcutSequence = this.consumeTerminalEditShortcutSequence();
|
|
76
111
|
if (terminalEditShortcutSequence === "consumed")
|
|
77
112
|
continue;
|
|
78
113
|
if (terminalEditShortcutSequence === "pending")
|
|
79
114
|
return;
|
|
115
|
+
const ignoredModifiedKeySequence = this.consumeIgnoredModifiedKeySequence();
|
|
116
|
+
if (ignoredModifiedKeySequence === "consumed")
|
|
117
|
+
continue;
|
|
118
|
+
if (ignoredModifiedKeySequence === "pending")
|
|
119
|
+
return;
|
|
80
120
|
if (this.consumeEscapeSequence())
|
|
81
121
|
continue;
|
|
82
122
|
if (this.isPendingEscapeSequence())
|
|
@@ -106,13 +146,9 @@ export class AppInputController {
|
|
|
106
146
|
}
|
|
107
147
|
getEscapeSequences() {
|
|
108
148
|
return [
|
|
109
|
-
[
|
|
110
|
-
["\x1b[13;2~", () => this.insertInputNewline()],
|
|
111
|
-
["\x1b[27;2;13~", () => this.insertInputNewline()],
|
|
149
|
+
...SHIFT_ENTER_ESCAPE_SEQUENCES.map((sequence) => [sequence, () => this.insertInputNewline()]),
|
|
112
150
|
["\x1b[13u", () => this.host.handleEnter()],
|
|
113
151
|
["\x1b[13;1u", () => this.host.handleEnter()],
|
|
114
|
-
["\x1b\r", () => this.insertInputNewline()],
|
|
115
|
-
["\x1b\n", () => this.insertInputNewline()],
|
|
116
152
|
["\x1b[5~", () => this.host.scrollByPage(-1)],
|
|
117
153
|
["\x1b[6~", () => this.host.scrollByPage(1)],
|
|
118
154
|
["\x1b[A", () => this.handleArrowUp()],
|
|
@@ -139,8 +175,6 @@ export class AppInputController {
|
|
|
139
175
|
["\x1b[201~", () => this.pasteHandler.endBracketedPaste()],
|
|
140
176
|
["\x1b[1;2H", () => { this.host.inputEditor.moveToLineStartExtend(); this.host.render(); }],
|
|
141
177
|
["\x1b[1;2F", () => { this.host.inputEditor.moveToLineEndExtend(); this.host.render(); }],
|
|
142
|
-
["\x1b[118;5u", () => { void this.pasteHandler.handleClipboardImagePaste(); }],
|
|
143
|
-
["\x1b[27;5;118~", () => { void this.pasteHandler.handleClipboardImagePaste(); }],
|
|
144
178
|
["\x1b[122;9u", () => this.undoInput()],
|
|
145
179
|
["\x1b[27;9;122~", () => this.undoInput()],
|
|
146
180
|
["\x1b[90;10u", () => this.redoInput()],
|
|
@@ -232,6 +266,43 @@ export class AppInputController {
|
|
|
232
266
|
}
|
|
233
267
|
return "consumed";
|
|
234
268
|
}
|
|
269
|
+
consumeIgnoredModifiedKeySequence() {
|
|
270
|
+
const result = parseTerminalModifiedKeySequence(this.inputBuffer);
|
|
271
|
+
if (result.kind === "pending")
|
|
272
|
+
return "pending";
|
|
273
|
+
if (result.kind === "none")
|
|
274
|
+
return "none";
|
|
275
|
+
if (!terminalKeyShouldIgnore(result.key))
|
|
276
|
+
return "none";
|
|
277
|
+
this.inputBuffer = this.inputBuffer.slice(result.key.length);
|
|
278
|
+
return "consumed";
|
|
279
|
+
}
|
|
280
|
+
consumeClipboardImagePasteSequence() {
|
|
281
|
+
const result = parseTerminalModifiedKeySequence(this.inputBuffer);
|
|
282
|
+
if (result.kind === "pending")
|
|
283
|
+
return "pending";
|
|
284
|
+
if (result.kind === "none")
|
|
285
|
+
return "none";
|
|
286
|
+
if (!terminalKeyIsClipboardImagePaste(result.key))
|
|
287
|
+
return "none";
|
|
288
|
+
this.inputBuffer = this.inputBuffer.slice(result.key.length);
|
|
289
|
+
if (!terminalKeyShouldIgnore(result.key))
|
|
290
|
+
void this.pasteHandler.handleClipboardImagePaste();
|
|
291
|
+
return "consumed";
|
|
292
|
+
}
|
|
293
|
+
consumeShiftEnterSequence() {
|
|
294
|
+
const result = parseTerminalModifiedKeySequence(this.inputBuffer);
|
|
295
|
+
if (result.kind === "pending")
|
|
296
|
+
return "pending";
|
|
297
|
+
if (result.kind === "none")
|
|
298
|
+
return "none";
|
|
299
|
+
if (!terminalKeyIsShiftEnter(result.key))
|
|
300
|
+
return "none";
|
|
301
|
+
this.inputBuffer = this.inputBuffer.slice(result.key.length);
|
|
302
|
+
if (!terminalKeyShouldIgnore(result.key))
|
|
303
|
+
this.insertInputNewline();
|
|
304
|
+
return "consumed";
|
|
305
|
+
}
|
|
235
306
|
consumeTerminalInterruptSequence() {
|
|
236
307
|
const result = parseTerminalInterruptSequence(this.inputBuffer);
|
|
237
308
|
if (result.kind === "pending")
|
|
@@ -401,7 +472,15 @@ export class AppInputController {
|
|
|
401
472
|
this.host.toggleVoiceRecording();
|
|
402
473
|
return;
|
|
403
474
|
}
|
|
404
|
-
if (char === "\
|
|
475
|
+
if (char === "\n") {
|
|
476
|
+
if (this.host.inputEditor.isInBracketedPaste) {
|
|
477
|
+
this.pasteHandler.appendBracketedPasteText("\n");
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
this.insertInputNewline();
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (char === "\r") {
|
|
405
484
|
if (this.host.inputEditor.isInBracketedPaste) {
|
|
406
485
|
this.pasteHandler.appendBracketedPasteText("\n");
|
|
407
486
|
return;
|
|
@@ -92,8 +92,10 @@ export class InputPasteHandler {
|
|
|
92
92
|
return false;
|
|
93
93
|
}
|
|
94
94
|
handlePasteEnd(text) {
|
|
95
|
-
if (!text)
|
|
95
|
+
if (!text) {
|
|
96
|
+
void this.handleClipboardImagePaste();
|
|
96
97
|
return;
|
|
98
|
+
}
|
|
97
99
|
const filePath = this.plainPasteFilePath(text);
|
|
98
100
|
if (filePath) {
|
|
99
101
|
if (isImagePath(filePath) && Date.now() < this.suppressImagePathPasteUntil) {
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export type TerminalEditShortcut = "undo" | "redo";
|
|
2
|
+
export type ParsedTerminalModifiedKeyResult = {
|
|
3
|
+
readonly kind: "key";
|
|
4
|
+
readonly key: ParsedModifiedKey;
|
|
5
|
+
} | {
|
|
6
|
+
readonly kind: "pending";
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: "none";
|
|
9
|
+
};
|
|
2
10
|
export type TerminalEditShortcutSequenceResult = {
|
|
3
11
|
readonly kind: "shortcut";
|
|
4
12
|
readonly shortcut: TerminalEditShortcut;
|
|
@@ -11,6 +19,14 @@ export type TerminalEditShortcutSequenceResult = {
|
|
|
11
19
|
} | {
|
|
12
20
|
readonly kind: "none";
|
|
13
21
|
};
|
|
22
|
+
interface ParsedModifiedKey {
|
|
23
|
+
readonly codepoint: number;
|
|
24
|
+
readonly baseLayoutKey: number | undefined;
|
|
25
|
+
readonly modifier: number;
|
|
26
|
+
readonly eventType: number | undefined;
|
|
27
|
+
readonly length: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function parseTerminalModifiedKeySequence(input: string): ParsedTerminalModifiedKeyResult;
|
|
14
30
|
export declare function parseTerminalEditShortcutSequence(input: string): TerminalEditShortcutSequenceResult;
|
|
15
31
|
export declare function parseTerminalInterruptSequence(input: string): {
|
|
16
32
|
readonly kind: "interrupt";
|
|
@@ -20,4 +36,8 @@ export declare function parseTerminalInterruptSequence(input: string): {
|
|
|
20
36
|
} | {
|
|
21
37
|
readonly kind: "none";
|
|
22
38
|
};
|
|
39
|
+
export declare function terminalKeyIsShiftEnter(key: ParsedModifiedKey): boolean;
|
|
40
|
+
export declare function terminalKeyIsClipboardImagePaste(key: ParsedModifiedKey): boolean;
|
|
41
|
+
export declare function terminalKeyShouldIgnore(key: ParsedModifiedKey): boolean;
|
|
23
42
|
export declare function terminalEditShortcutForControlChar(char: string, shiftPressed: boolean): TerminalEditShortcut | undefined;
|
|
43
|
+
export {};
|
|
@@ -3,34 +3,60 @@ const CONTROL_MODIFIER_FLAG = 4;
|
|
|
3
3
|
const COMMAND_MODIFIER_FLAG = 8;
|
|
4
4
|
const LOCK_MODIFIER_MASK = 64 + 128;
|
|
5
5
|
const KEY_CODE_C = 99;
|
|
6
|
+
const KEY_CODE_ENTER = 13;
|
|
7
|
+
const KEY_CODE_V = 118;
|
|
6
8
|
const KEY_CODE_Y = 121;
|
|
7
9
|
const KEY_CODE_Z = 122;
|
|
8
10
|
const CYRILLIC_SMALL_ES_CODE = 1089;
|
|
9
11
|
const CYRILLIC_CAPITAL_ES_CODE = 1057;
|
|
12
|
+
const CYRILLIC_SMALL_EM_CODE = 1084;
|
|
13
|
+
const CYRILLIC_CAPITAL_EM_CODE = 1052;
|
|
10
14
|
const KITTY_CSI_U_SEQUENCE = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u/;
|
|
11
15
|
const XTERM_MODIFY_OTHER_KEYS_SEQUENCE = /^\x1b\[27;(\d+);(\d+)~/;
|
|
12
|
-
export function
|
|
16
|
+
export function parseTerminalModifiedKeySequence(input) {
|
|
13
17
|
const kitty = parseKittyCsiUSequence(input);
|
|
14
18
|
if (kitty)
|
|
15
|
-
return
|
|
19
|
+
return { kind: "key", key: kitty };
|
|
16
20
|
const xterm = parseXtermModifyOtherKeysSequence(input);
|
|
17
21
|
if (xterm)
|
|
18
|
-
return
|
|
19
|
-
if (
|
|
22
|
+
return { kind: "key", key: xterm };
|
|
23
|
+
if (isPotentialModifiedKeyPrefix(input))
|
|
20
24
|
return { kind: "pending" };
|
|
21
25
|
return { kind: "none" };
|
|
22
26
|
}
|
|
27
|
+
export function parseTerminalEditShortcutSequence(input) {
|
|
28
|
+
const result = parseTerminalModifiedKeySequence(input);
|
|
29
|
+
if (result.kind === "pending")
|
|
30
|
+
return { kind: "pending" };
|
|
31
|
+
if (result.kind === "none")
|
|
32
|
+
return { kind: "none" };
|
|
33
|
+
return terminalEditShortcutResult(result.key);
|
|
34
|
+
}
|
|
23
35
|
export function parseTerminalInterruptSequence(input) {
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
26
|
-
return { kind: "interrupt", length: kitty.length };
|
|
27
|
-
const xterm = parseXtermModifyOtherKeysSequence(input);
|
|
28
|
-
if (xterm && terminalKeyIsControlC(xterm))
|
|
29
|
-
return { kind: "interrupt", length: xterm.length };
|
|
30
|
-
if (isPotentialInterruptPrefix(input))
|
|
36
|
+
const result = parseTerminalModifiedKeySequence(input);
|
|
37
|
+
if (result.kind === "pending")
|
|
31
38
|
return { kind: "pending" };
|
|
39
|
+
if (result.kind === "key" && terminalKeyIsControlC(result.key))
|
|
40
|
+
return { kind: "interrupt", length: result.key.length };
|
|
41
|
+
if (result.kind === "key" || isPotentialInterruptPrefix(input))
|
|
42
|
+
return { kind: "none" };
|
|
32
43
|
return { kind: "none" };
|
|
33
44
|
}
|
|
45
|
+
export function terminalKeyIsShiftEnter(key) {
|
|
46
|
+
const effectiveModifier = key.modifier & ~LOCK_MODIFIER_MASK;
|
|
47
|
+
if ((effectiveModifier & SHIFT_MODIFIER_FLAG) === 0)
|
|
48
|
+
return false;
|
|
49
|
+
return terminalKeyMatchesCodepoint(key, KEY_CODE_ENTER);
|
|
50
|
+
}
|
|
51
|
+
export function terminalKeyIsClipboardImagePaste(key) {
|
|
52
|
+
const effectiveModifier = key.modifier & ~LOCK_MODIFIER_MASK;
|
|
53
|
+
if ((effectiveModifier & (CONTROL_MODIFIER_FLAG | COMMAND_MODIFIER_FLAG)) === 0)
|
|
54
|
+
return false;
|
|
55
|
+
return terminalKeyMatchesCodepoint(key, KEY_CODE_V, CYRILLIC_SMALL_EM_CODE, CYRILLIC_CAPITAL_EM_CODE);
|
|
56
|
+
}
|
|
57
|
+
export function terminalKeyShouldIgnore(key) {
|
|
58
|
+
return key.eventType === 3;
|
|
59
|
+
}
|
|
34
60
|
export function terminalEditShortcutForControlChar(char, shiftPressed) {
|
|
35
61
|
if (char === "\u001a")
|
|
36
62
|
return shiftPressed ? "redo" : "undo";
|
|
@@ -95,6 +121,17 @@ function interruptCodepointIsC(codepoint) {
|
|
|
95
121
|
const normalized = normalizeLetterCodepoint(codepoint);
|
|
96
122
|
return normalized === KEY_CODE_C || normalized === CYRILLIC_SMALL_ES_CODE || normalized === CYRILLIC_CAPITAL_ES_CODE;
|
|
97
123
|
}
|
|
124
|
+
function terminalKeyMatchesCodepoint(key, ...expectedCodepoints) {
|
|
125
|
+
return expectedCodepoints.some((codepoint) => keyMatchesCodepoint(key, codepoint));
|
|
126
|
+
}
|
|
127
|
+
function keyMatchesCodepoint(key, expectedCodepoint) {
|
|
128
|
+
const normalizedExpected = normalizeLetterCodepoint(expectedCodepoint);
|
|
129
|
+
const primary = normalizeLetterCodepoint(key.codepoint);
|
|
130
|
+
if (primary === normalizedExpected)
|
|
131
|
+
return true;
|
|
132
|
+
const baseLayout = key.baseLayoutKey;
|
|
133
|
+
return baseLayout !== undefined && normalizeLetterCodepoint(baseLayout) === normalizedExpected;
|
|
134
|
+
}
|
|
98
135
|
function terminalEditShortcutForKey(key, effectiveModifier) {
|
|
99
136
|
const codepoint = editShortcutCodepoint(key);
|
|
100
137
|
if (codepoint === KEY_CODE_Y)
|
|
@@ -114,16 +151,13 @@ function normalizeLetterCodepoint(codepoint) {
|
|
|
114
151
|
return codepoint + 32;
|
|
115
152
|
return codepoint;
|
|
116
153
|
}
|
|
117
|
-
function
|
|
154
|
+
function isPotentialModifiedKeyPrefix(input) {
|
|
118
155
|
if (!input.startsWith("\x1b["))
|
|
119
156
|
return false;
|
|
120
157
|
if (input.includes("u") || input.includes("~"))
|
|
121
158
|
return false;
|
|
122
159
|
const body = input.slice(2);
|
|
123
|
-
|
|
124
|
-
return false;
|
|
125
|
-
const possibleStarts = ["122", "121", "90", "27;"];
|
|
126
|
-
return possibleStarts.some((start) => start.startsWith(body) || body.startsWith(start));
|
|
160
|
+
return /^[\d:;]*$/.test(body);
|
|
127
161
|
}
|
|
128
162
|
function isPotentialInterruptPrefix(input) {
|
|
129
163
|
if (!input.startsWith("\x1b["))
|
|
@@ -14,6 +14,7 @@ export type ConversationEntryRenderOptions = {
|
|
|
14
14
|
availableThinkingLevels?: readonly string[];
|
|
15
15
|
superCompactTools?: boolean;
|
|
16
16
|
allThinkingExpanded?: boolean;
|
|
17
|
+
currentTimeMs?: number;
|
|
17
18
|
renderInlineUserMessageMenu: (entry: Extract<Entry, {
|
|
18
19
|
kind: "user";
|
|
19
20
|
}>, context: InlineUserMessageMenuContext) => RenderedLine[];
|
|
@@ -75,7 +75,7 @@ function renderAssistantLines(text, width, options) {
|
|
|
75
75
|
if (!displayText)
|
|
76
76
|
return [];
|
|
77
77
|
const { left: contentLeft, contentWidth } = horizontalPaddingLayout(width);
|
|
78
|
-
const contentLines = renderMarkdownTextLines(displayText, contentWidth, contentLeft);
|
|
78
|
+
const contentLines = renderMarkdownTextLines(displayText, contentWidth, contentLeft, { preserveWrappedWordSeparator: true });
|
|
79
79
|
if (contentLines.length === 0)
|
|
80
80
|
return [];
|
|
81
81
|
const lines = [];
|
|
@@ -8,6 +8,7 @@ export type ConversationToolRenderOptions = {
|
|
|
8
8
|
availableThinkingLevels?: readonly string[];
|
|
9
9
|
superCompactTools?: boolean;
|
|
10
10
|
allThinkingExpanded?: boolean;
|
|
11
|
+
currentTimeMs?: number;
|
|
11
12
|
};
|
|
12
13
|
export declare function renderConversationToolEntry(entry: Extract<Entry, {
|
|
13
14
|
kind: "tool";
|
|
@@ -56,9 +56,11 @@ export function renderThinkingEntry(entry, width, options) {
|
|
|
56
56
|
const headerColorOverride = entry.level
|
|
57
57
|
? thinkingLevelThemeColor(entry.level, options.colors, options.availableThinkingLevels)
|
|
58
58
|
: undefined;
|
|
59
|
+
const elapsed = thinkingElapsedText(entry, options.currentTimeMs ?? Date.now());
|
|
59
60
|
return renderToolBlock({
|
|
60
61
|
id: entry.id,
|
|
61
62
|
toolName: THINKING_TOOL_NAME,
|
|
63
|
+
...(elapsed === undefined ? {} : { headerArgs: elapsed }),
|
|
62
64
|
expanded,
|
|
63
65
|
status: entry.status,
|
|
64
66
|
isError: false,
|
|
@@ -74,6 +76,25 @@ export function renderThinkingEntry(entry, width, options) {
|
|
|
74
76
|
...(headerColorOverride === undefined ? {} : { headerColorOverride }),
|
|
75
77
|
});
|
|
76
78
|
}
|
|
79
|
+
function thinkingElapsedText(entry, currentTimeMs) {
|
|
80
|
+
if (entry.startedAt === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
const endTimeMs = entry.finishedAt ?? currentTimeMs;
|
|
83
|
+
const elapsedMs = Math.max(0, endTimeMs - entry.startedAt);
|
|
84
|
+
return formatThinkingElapsed(elapsedMs);
|
|
85
|
+
}
|
|
86
|
+
function formatThinkingElapsed(elapsedMs) {
|
|
87
|
+
const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000));
|
|
88
|
+
const seconds = totalSeconds % 60;
|
|
89
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
90
|
+
if (totalMinutes < 1)
|
|
91
|
+
return `${seconds}s`;
|
|
92
|
+
const minutes = totalMinutes % 60;
|
|
93
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
94
|
+
if (hours < 1)
|
|
95
|
+
return `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
|
|
96
|
+
return `${hours}h ${minutes.toString().padStart(2, "0")}m`;
|
|
97
|
+
}
|
|
77
98
|
function trimTrailingBlankLines(text) {
|
|
78
99
|
return text.replace(/(?:\r?\n[ \t]*)+$/u, "");
|
|
79
100
|
}
|
|
@@ -49,11 +49,14 @@ export declare class ConversationViewport {
|
|
|
49
49
|
private blockCacheForWidth;
|
|
50
50
|
private refreshDynamicLayoutEntries;
|
|
51
51
|
private ensureEntryMeasured;
|
|
52
|
+
private hasDynamicConversationBlock;
|
|
53
|
+
private isDynamicConversationBlock;
|
|
52
54
|
private refreshLayoutEntry;
|
|
53
55
|
private measuredLineCountForEntry;
|
|
54
56
|
private estimatedLineCountForEntry;
|
|
55
57
|
private lineCountWithGap;
|
|
56
58
|
private estimatedBlockLineCountForEntry;
|
|
59
|
+
private estimatedToolEntryLineCount;
|
|
57
60
|
private nextVisibleEntry;
|
|
58
61
|
private nextEstimatedVisibleEntry;
|
|
59
62
|
private gapAfterEntry;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveToolRule } from "../../config.js";
|
|
2
|
+
import { renderToolDisplay } from "../../tool-renderers/index.js";
|
|
2
3
|
import { stringDisplayWidth } from "../../terminal-width.js";
|
|
3
4
|
import { renderConversationEntry as renderConversationEntryLines } from "./conversation-entry-renderer.js";
|
|
4
5
|
import { horizontalPaddingLayout } from "./render-text.js";
|
|
@@ -70,7 +71,7 @@ export class ConversationViewport {
|
|
|
70
71
|
+ (this.host.superCompactTools ? 1_000_000_000 : 0)
|
|
71
72
|
+ (this.host.allThinkingExpanded ? 2_000_000_000 : 0);
|
|
72
73
|
const cached = blockCache.get(entry.id);
|
|
73
|
-
const dynamic = this.
|
|
74
|
+
const dynamic = this.isDynamicConversationBlock(entry);
|
|
74
75
|
if (!dynamic && cached?.version === version)
|
|
75
76
|
return cached;
|
|
76
77
|
const availableThinkingLevels = this.host.availableThinkingLevels?.();
|
|
@@ -82,6 +83,7 @@ export class ConversationViewport {
|
|
|
82
83
|
...(availableThinkingLevels ? { availableThinkingLevels } : {}),
|
|
83
84
|
superCompactTools: Boolean(this.host.superCompactTools),
|
|
84
85
|
allThinkingExpanded: Boolean(this.host.allThinkingExpanded),
|
|
86
|
+
currentTimeMs: Date.now(),
|
|
85
87
|
renderInlineUserMessageMenu: (userEntry, context) => this.host.renderInlineUserMessageMenu(userEntry, context),
|
|
86
88
|
});
|
|
87
89
|
const block = {
|
|
@@ -156,7 +158,7 @@ export class ConversationViewport {
|
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
160
|
this.refreshDirtyLayoutEntries(layout, width);
|
|
159
|
-
if (this.
|
|
161
|
+
if (this.hasDynamicConversationBlock(layout.entries)) {
|
|
160
162
|
this.refreshDynamicLayoutEntries(layout, width);
|
|
161
163
|
}
|
|
162
164
|
return layout;
|
|
@@ -278,7 +280,7 @@ export class ConversationViewport {
|
|
|
278
280
|
}
|
|
279
281
|
refreshDynamicLayoutEntries(layout, width) {
|
|
280
282
|
for (let index = 0; index < layout.entries.length; index += 1) {
|
|
281
|
-
if (this.
|
|
283
|
+
if (this.isDynamicConversationBlock(layout.entries[index]))
|
|
282
284
|
this.refreshLayoutEntry(layout, width, index, true);
|
|
283
285
|
}
|
|
284
286
|
}
|
|
@@ -286,10 +288,17 @@ export class ConversationViewport {
|
|
|
286
288
|
const entry = layout.entries[index];
|
|
287
289
|
if (!entry)
|
|
288
290
|
return false;
|
|
289
|
-
if (layout.measuredLineCounts[index] === true && !this.
|
|
291
|
+
if (layout.measuredLineCounts[index] === true && !this.isDynamicConversationBlock(entry))
|
|
290
292
|
return false;
|
|
291
293
|
return this.refreshLayoutEntry(layout, width, index, true);
|
|
292
294
|
}
|
|
295
|
+
hasDynamicConversationBlock(entries) {
|
|
296
|
+
return this.host.hasDynamicConversationBlock?.() === true || entries.some((entry) => this.isDynamicConversationBlock(entry));
|
|
297
|
+
}
|
|
298
|
+
isDynamicConversationBlock(entry) {
|
|
299
|
+
return (entry.kind === "thinking" && entry.status === "running" && entry.startedAt !== undefined)
|
|
300
|
+
|| this.host.isDynamicConversationBlock(entry);
|
|
301
|
+
}
|
|
293
302
|
refreshLayoutEntry(layout, width, index, measure) {
|
|
294
303
|
const entry = layout.entries[index];
|
|
295
304
|
if (!entry)
|
|
@@ -348,11 +357,38 @@ export class ConversationViewport {
|
|
|
348
357
|
case "shell":
|
|
349
358
|
return estimateToolLikeLineCount("shell", entry.expanded, `${entry.output}\n${entry.status}`, width, this.host.pixConfig, this.host.superCompactTools === true, true);
|
|
350
359
|
case "tool":
|
|
351
|
-
return
|
|
360
|
+
return this.estimatedToolEntryLineCount(entry, width);
|
|
352
361
|
default:
|
|
353
362
|
return 1;
|
|
354
363
|
}
|
|
355
364
|
}
|
|
365
|
+
estimatedToolEntryLineCount(entry, width) {
|
|
366
|
+
const display = renderToolDisplay({
|
|
367
|
+
toolName: entry.toolName,
|
|
368
|
+
argsText: entry.argsText,
|
|
369
|
+
output: entry.output,
|
|
370
|
+
details: entry.details,
|
|
371
|
+
isError: entry.isError,
|
|
372
|
+
status: entry.status,
|
|
373
|
+
cwd: this.host.cwd,
|
|
374
|
+
colors: this.host.colors,
|
|
375
|
+
});
|
|
376
|
+
const toolName = display.toolName ?? entry.toolName;
|
|
377
|
+
const rule = resolveToolRule(toolName, this.host.pixConfig.toolRenderer);
|
|
378
|
+
if (rule.hidden)
|
|
379
|
+
return 0;
|
|
380
|
+
const bodyWidth = Math.max(1, width - 2);
|
|
381
|
+
if (entry.expanded)
|
|
382
|
+
return 1 + estimateWrappedLineCount(display.expandedText, bodyWidth);
|
|
383
|
+
if (rule.compactHidden || (rule.defaultExpanded === true && this.host.superCompactTools !== true))
|
|
384
|
+
return 1;
|
|
385
|
+
const body = display.collapsedBody.trimEnd();
|
|
386
|
+
if (!body || rule.previewLines === 0)
|
|
387
|
+
return 1;
|
|
388
|
+
const bodyLineCount = estimateWrappedLineCount(body, bodyWidth);
|
|
389
|
+
const previewLineCount = Math.min(rule.previewLines, bodyLineCount);
|
|
390
|
+
return this.host.superCompactTools === true ? 1 : 1 + previewLineCount;
|
|
391
|
+
}
|
|
356
392
|
nextVisibleEntry(entries, index, width) {
|
|
357
393
|
for (let nextIndex = index + 1; nextIndex < entries.length; nextIndex += 1) {
|
|
358
394
|
const nextEntry = entries[nextIndex];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ABOVE_EDITOR_WIDGET_KEY_GROUPS, BUILT_IN_SUBAGENTS_WIDGET_KEYS,
|
|
1
|
+
import { ABOVE_EDITOR_WIDGET_KEY_GROUPS, BUILT_IN_SUBAGENTS_WIDGET_KEYS, LEGACY_TODO_WIDGET_KEYS, } from "../constants.js";
|
|
2
2
|
import { renderSubagentsPanel, renderTodoPanel } from "./editor-panels.js";
|
|
3
3
|
import { ellipsizeDisplay, horizontalPaddingLayout, padHorizontalText, sanitizeText, wrapText } from "./render-text.js";
|
|
4
4
|
import { APP_ICONS } from "../icons.js";
|
|
@@ -9,7 +9,8 @@ export class EditorLayoutRenderer {
|
|
|
9
9
|
}
|
|
10
10
|
computeLayout(width, rows) {
|
|
11
11
|
const maxAvailableInputRows = Math.max(1, rows - 4);
|
|
12
|
-
const
|
|
12
|
+
const maxComposerRows = Math.max(1, Math.min(maxAvailableInputRows, Math.floor(rows * 0.7)));
|
|
13
|
+
const renderedInput = this.renderInput(width, maxComposerRows, maxComposerRows);
|
|
13
14
|
const maxEntityRows = Math.max(0, rows - renderedInput.lines.length - 4);
|
|
14
15
|
const editorEntityWidth = inputFrameContentWidth(width);
|
|
15
16
|
const aboveEditorEntities = this.renderAboveEditorEntities(editorEntityWidth);
|