pi-ui-extend 0.1.18 → 0.1.19
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/dist/app/app.js +8 -6
- package/dist/app/constants.d.ts +1 -0
- package/dist/app/constants.js +1 -0
- package/dist/app/input/voice-controller.js +16 -12
- package/dist/app/popup/popup-menu-controller.d.ts +1 -5
- package/dist/app/popup/popup-menu-controller.js +7 -8
- package/dist/app/process.js +7 -0
- package/dist/app/rendering/conversation-entry-renderer.js +17 -16
- package/dist/app/rendering/conversation-viewport.js +4 -35
- package/dist/app/rendering/editor-layout-renderer.d.ts +5 -1
- package/dist/app/rendering/editor-layout-renderer.js +25 -16
- package/dist/app/rendering/popup-menu-renderer.d.ts +1 -5
- package/dist/app/rendering/popup-menu-renderer.js +24 -34
- package/dist/app/rendering/render-controller.d.ts +2 -0
- package/dist/app/rendering/render-controller.js +26 -25
- package/dist/app/rendering/render-text.js +2 -2
- package/dist/app/rendering/status-line-renderer.js +1 -1
- package/dist/app/rendering/tab-line-renderer.js +3 -3
- package/dist/app/runtime.js +29 -3
- package/dist/app/screen/file-link-opener.d.ts +2 -0
- package/dist/app/screen/file-link-opener.js +84 -17
- package/dist/app/screen/mouse-controller.d.ts +0 -2
- package/dist/app/screen/mouse-controller.js +6 -12
- package/dist/app/screen/screen-styler.js +1 -1
- package/dist/app/session/lazy-session-manager.d.ts +1 -1
- package/dist/app/session/lazy-session-manager.js +64 -52
- package/dist/app/session/queued-message-controller.d.ts +6 -0
- package/dist/app/session/queued-message-controller.js +9 -1
- package/dist/app/session/queued-message-entries.d.ts +8 -0
- package/dist/app/session/queued-message-entries.js +41 -0
- package/dist/app/session/session-lifecycle-controller.d.ts +9 -1
- package/dist/app/session/session-lifecycle-controller.js +45 -11
- package/dist/app/session/tabs-controller.d.ts +11 -1
- package/dist/app/session/tabs-controller.js +197 -30
- package/dist/app/terminal/terminal-controller.d.ts +2 -0
- package/dist/app/terminal/terminal-controller.js +7 -5
- package/dist/schemas/pi-tools-suite-schema.d.ts +3 -0
- package/dist/schemas/pi-tools-suite-schema.js +3 -0
- package/dist/theme.d.ts +3 -0
- package/dist/theme.js +8 -2
- package/extensions/session-title/config.ts +3 -3
- package/extensions/session-title/index.ts +60 -5
- package/external/pi-tools-suite/README.md +3 -2
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +1 -0
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +0 -3
- package/external/pi-tools-suite/src/async-subagents/core/notifications.ts +64 -0
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +1 -0
- package/external/pi-tools-suite/src/async-subagents/index.ts +54 -8
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +4 -4
- package/external/pi-tools-suite/src/config.ts +13 -0
- package/external/pi-tools-suite/src/dcp/state.ts +9 -4
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +5 -1
- package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +580 -0
- package/external/pi-tools-suite/src/index.ts +1 -0
- package/external/pi-tools-suite/src/lib/lsp.ts +2 -5
- package/external/pi-tools-suite/src/lsp/_shared/config.ts +2 -0
- package/external/pi-tools-suite/src/lsp/_shared/types.ts +2 -0
- package/external/pi-tools-suite/src/lsp/manager.ts +15 -9
- package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +1 -0
- package/external/pi-tools-suite/src/todo/index.ts +81 -4
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +5 -0
- package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
- package/package.json +3 -14
- package/schemas/pi-tools-suite.json +19 -0
- package/apps/desktop-tauri/README.md +0 -103
- package/apps/desktop-tauri/bin/pix-desktop.mjs +0 -89
package/dist/app/app.js
CHANGED
|
@@ -169,7 +169,7 @@ export class PiUiExtendApp {
|
|
|
169
169
|
noSession: false,
|
|
170
170
|
sessionPath,
|
|
171
171
|
}),
|
|
172
|
-
activateRuntime: (runtime) => this.activateRuntime(runtime),
|
|
172
|
+
activateRuntime: (runtime, options) => this.activateRuntime(runtime, options),
|
|
173
173
|
disposeRuntime: (runtime) => this.terminalController.disposeRuntime(runtime),
|
|
174
174
|
isRunning: () => this.running,
|
|
175
175
|
setStatus: (status) => this.setStatus(status),
|
|
@@ -398,6 +398,7 @@ export class PiUiExtendApp {
|
|
|
398
398
|
get subagentsWidgetState() { return app.subagentsWidgetController.widgetState; },
|
|
399
399
|
get voicePartialText() { return app.voicePartialText; },
|
|
400
400
|
get autocompleteSuggestion() { return app.autocompleteController.suggestionText(); },
|
|
401
|
+
get queuedMessageWidgetEntries() { return app.queuedMessages.deferredQueuedEntries(); },
|
|
401
402
|
renderExtensionInputComponent: (width) => this.extensionUiController.renderActiveCustomUi(width),
|
|
402
403
|
extensionInputUsesEditor: () => this.extensionUiController.activeCustomUiUsesEditor(),
|
|
403
404
|
widgetTuiHandle: () => this.extensionUiController.widgetTuiHandle(),
|
|
@@ -586,6 +587,7 @@ export class PiUiExtendApp {
|
|
|
586
587
|
statusLineRenderer: this.statusLineRenderer,
|
|
587
588
|
tabLineRenderer: this.tabLineRenderer,
|
|
588
589
|
toastController: this.toastController,
|
|
590
|
+
loadingConversationOverlayText: () => this.tabsController.isSwitching() ? "Loading…" : undefined,
|
|
589
591
|
voiceProgressOverlayText: () => this.voiceController.progressOverlayText(),
|
|
590
592
|
});
|
|
591
593
|
this.requestHistory = new AppRequestHistory({
|
|
@@ -767,15 +769,15 @@ export class PiUiExtendApp {
|
|
|
767
769
|
// Startup update checks should never interrupt the TUI.
|
|
768
770
|
}
|
|
769
771
|
}
|
|
770
|
-
async bindCurrentSession() {
|
|
771
|
-
await this.sessionLifecycle.bindCurrentSession();
|
|
772
|
+
async bindCurrentSession(options) {
|
|
773
|
+
await this.sessionLifecycle.bindCurrentSession(options);
|
|
772
774
|
}
|
|
773
|
-
async activateRuntime(runtime) {
|
|
775
|
+
async activateRuntime(runtime, options) {
|
|
774
776
|
this.runtime = runtime;
|
|
775
777
|
runtime.setRebindSession(async () => {
|
|
776
|
-
await this.bindCurrentSession();
|
|
778
|
+
await this.bindCurrentSession({ awaitExtensions: false });
|
|
777
779
|
});
|
|
778
|
-
await this.bindCurrentSession();
|
|
780
|
+
await this.bindCurrentSession(options);
|
|
779
781
|
}
|
|
780
782
|
createExtensionEventBus() {
|
|
781
783
|
return createIsolatedExtensionEventBus((channel, data) => {
|
package/dist/app/constants.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare const DISABLE_TERMINAL_WRAP = "\u001B[?7l";
|
|
|
27
27
|
export declare const ENABLE_TERMINAL_WRAP = "\u001B[?7h";
|
|
28
28
|
export declare const HIDE_CURSOR = "\u001B[?25l";
|
|
29
29
|
export declare const SHOW_CURSOR = "\u001B[?25h";
|
|
30
|
+
export declare const RESET_TERMINAL_VIEWPORT_STATE = "\u001B[?6l\u001B[?69l\u001B[r";
|
|
30
31
|
export declare const CLEAR_TERMINAL = "\u001B[2J\u001B[3J\u001B[H";
|
|
31
32
|
export declare const THINKING_TOOL_NAME = "thinking";
|
|
32
33
|
export declare const SUBAGENTS_TOOL_NAME = "subagents";
|
package/dist/app/constants.js
CHANGED
|
@@ -60,6 +60,7 @@ export const DISABLE_TERMINAL_WRAP = "\x1b[?7l";
|
|
|
60
60
|
export const ENABLE_TERMINAL_WRAP = "\x1b[?7h";
|
|
61
61
|
export const HIDE_CURSOR = "\x1b[?25l";
|
|
62
62
|
export const SHOW_CURSOR = "\x1b[?25h";
|
|
63
|
+
export const RESET_TERMINAL_VIEWPORT_STATE = "\x1b[?6l\x1b[?69l\x1b[r";
|
|
63
64
|
export const CLEAR_TERMINAL = "\x1b[2J\x1b[3J\x1b[H";
|
|
64
65
|
export const THINKING_TOOL_NAME = "thinking";
|
|
65
66
|
export const SUBAGENTS_TOOL_NAME = "subagents";
|
|
@@ -5,6 +5,7 @@ import http from "node:http";
|
|
|
5
5
|
import https from "node:https";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
+
import { pipeline } from "node:stream/promises";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
10
|
import { savePixDictationLanguage } from "../../config.js";
|
|
10
11
|
import { APP_ICONS } from "../icons.js";
|
|
@@ -364,33 +365,36 @@ async function pathExists(path) {
|
|
|
364
365
|
async function downloadFile(url, destination, redirects = 3) {
|
|
365
366
|
await new Promise((resolve, reject) => {
|
|
366
367
|
const client = url.startsWith("https:") ? https : http;
|
|
368
|
+
let settled = false;
|
|
369
|
+
const finish = (callback) => {
|
|
370
|
+
if (settled)
|
|
371
|
+
return;
|
|
372
|
+
settled = true;
|
|
373
|
+
callback();
|
|
374
|
+
};
|
|
367
375
|
const request = client.get(url, (response) => {
|
|
368
376
|
const statusCode = response.statusCode ?? 0;
|
|
369
377
|
const location = response.headers.location;
|
|
370
378
|
if ([301, 302, 303, 307, 308].includes(statusCode) && location && redirects > 0) {
|
|
371
379
|
response.resume();
|
|
372
380
|
const redirectedUrl = new URL(location, url).toString();
|
|
373
|
-
downloadFile(redirectedUrl, destination, redirects - 1).then(resolve, reject);
|
|
381
|
+
downloadFile(redirectedUrl, destination, redirects - 1).then(() => finish(resolve), (error) => finish(() => reject(error)));
|
|
374
382
|
return;
|
|
375
383
|
}
|
|
376
384
|
if (statusCode !== 200) {
|
|
377
385
|
response.resume();
|
|
378
|
-
reject(new Error(`download failed with HTTP ${statusCode}`));
|
|
386
|
+
finish(() => reject(new Error(`download failed with HTTP ${statusCode}`)));
|
|
379
387
|
return;
|
|
380
388
|
}
|
|
381
389
|
const file = createWriteStream(destination);
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (error)
|
|
385
|
-
reject(error);
|
|
386
|
-
else
|
|
387
|
-
resolve();
|
|
388
|
-
});
|
|
390
|
+
response.on("error", (error) => {
|
|
391
|
+
finish(() => reject(error));
|
|
389
392
|
});
|
|
390
|
-
file.
|
|
391
|
-
|
|
393
|
+
pipeline(response, file).then(() => finish(resolve), (error) => finish(() => reject(error)));
|
|
394
|
+
});
|
|
395
|
+
request.on("error", (error) => {
|
|
396
|
+
finish(() => reject(error));
|
|
392
397
|
});
|
|
393
|
-
request.on("error", reject);
|
|
394
398
|
});
|
|
395
399
|
}
|
|
396
400
|
async function extractZip(zipPath, destination) {
|
|
@@ -15,11 +15,7 @@ type PopupMenuRendererPort = {
|
|
|
15
15
|
effectivePopupMenuWidth(columns: number): number;
|
|
16
16
|
styleOverlayLine(row: number, line: RenderedLine, width: number, activeMenu: PopupMenu<unknown>): string;
|
|
17
17
|
overlayPlainText(line: RenderedLine, width: number): string;
|
|
18
|
-
|
|
19
|
-
userContentWidth: number;
|
|
20
|
-
userContentLeft: number;
|
|
21
|
-
userLine: (text: string, entryId?: string, syntaxHighlight?: RenderedLine["syntaxHighlight"]) => RenderedLine;
|
|
22
|
-
}, menu: PopupMenu<UserMessagePopupMenuValue>): RenderedLine[];
|
|
18
|
+
renderUserMessageMenu(width: number, menu: PopupMenu<UserMessagePopupMenuValue>): RenderedLine[];
|
|
23
19
|
renderSlashCommandMenu(width: number, menu: PopupMenu<SlashCommandMenuValue>): RenderedLine[];
|
|
24
20
|
renderModelMenu(width: number, menu: PopupMenu<ModelPopupMenuValue>): RenderedLine[];
|
|
25
21
|
renderThinkingMenu(width: number, menu: PopupMenu<ThinkingPopupMenuValue>): RenderedLine[];
|
|
@@ -435,10 +435,8 @@ export class AppPopupMenuController {
|
|
|
435
435
|
renderActivePopupMenu(width) {
|
|
436
436
|
if (this.syncQueueMessageMenu())
|
|
437
437
|
return this.renderer.renderQueueMessageMenu(width, this.queueMessageMenu);
|
|
438
|
-
// User-message actions are rendered inline inside the selected message block.
|
|
439
|
-
// They must never also appear as the global popup above the input editor.
|
|
440
438
|
if (this.syncUserMessageMenu())
|
|
441
|
-
return
|
|
439
|
+
return this.renderer.renderUserMessageMenu(width, this.userMessageMenu);
|
|
442
440
|
if (this.syncUserMessageJumpMenu())
|
|
443
441
|
return this.renderer.renderUserMessageJumpMenu(width, this.userMessageJumpMenu, this.directPopupMenuQuery);
|
|
444
442
|
if (this.syncResumeMenu()) {
|
|
@@ -474,15 +472,16 @@ export class AppPopupMenuController {
|
|
|
474
472
|
return this.renderer.overlayPlainText(line, width);
|
|
475
473
|
}
|
|
476
474
|
isDynamicConversationBlock(entry) {
|
|
477
|
-
|
|
475
|
+
void entry;
|
|
476
|
+
return false;
|
|
478
477
|
}
|
|
479
478
|
hasDynamicConversationBlock() {
|
|
480
|
-
return
|
|
479
|
+
return false;
|
|
481
480
|
}
|
|
482
481
|
renderInlineUserMessageMenu(entry, options) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return
|
|
482
|
+
void entry;
|
|
483
|
+
void options;
|
|
484
|
+
return [];
|
|
486
485
|
}
|
|
487
486
|
withoutCloseMenuItems(items) {
|
|
488
487
|
return items.filter((item) => item.label.trim().toLowerCase() !== "cancel");
|
package/dist/app/process.js
CHANGED
|
@@ -7,6 +7,7 @@ export async function runProcess(command, args = [], options = {}) {
|
|
|
7
7
|
let stderr = "";
|
|
8
8
|
let error;
|
|
9
9
|
let timedOut = false;
|
|
10
|
+
let forceKillTimer;
|
|
10
11
|
const child = spawn(command, [...args], {
|
|
11
12
|
cwd: options.cwd,
|
|
12
13
|
env: options.env,
|
|
@@ -21,6 +22,10 @@ export async function runProcess(command, args = [], options = {}) {
|
|
|
21
22
|
: setTimeout(() => {
|
|
22
23
|
timedOut = true;
|
|
23
24
|
child.kill("SIGTERM");
|
|
25
|
+
forceKillTimer = setTimeout(() => {
|
|
26
|
+
child.kill("SIGKILL");
|
|
27
|
+
}, 3_000);
|
|
28
|
+
forceKillTimer.unref?.();
|
|
24
29
|
}, options.timeoutMs);
|
|
25
30
|
timer?.unref?.();
|
|
26
31
|
child.stdout.on("data", (chunk) => {
|
|
@@ -35,6 +40,8 @@ export async function runProcess(command, args = [], options = {}) {
|
|
|
35
40
|
child.once("close", (status, signal) => {
|
|
36
41
|
if (timer)
|
|
37
42
|
clearTimeout(timer);
|
|
43
|
+
if (forceKillTimer)
|
|
44
|
+
clearTimeout(forceKillTimer);
|
|
38
45
|
resolve({
|
|
39
46
|
status,
|
|
40
47
|
signal,
|
|
@@ -9,26 +9,19 @@ export function renderConversationEntry(entry, width, options) {
|
|
|
9
9
|
const { left: userContentLeft, contentWidth: userContentWidth } = horizontalPaddingLayout(width);
|
|
10
10
|
const userLine = (text, entryId, syntaxHighlight, segments) => ({
|
|
11
11
|
text: padHorizontalText(text, width),
|
|
12
|
-
colorOverride: options.colors.
|
|
13
|
-
backgroundOverride: options.colors.userMessageBackground,
|
|
12
|
+
colorOverride: options.colors.warning,
|
|
14
13
|
...(segments && segments.length > 0 ? { segments: segments.map((segment) => ({ ...segment, start: segment.start + userContentLeft, end: segment.end + userContentLeft })) } : {}),
|
|
15
14
|
...(syntaxHighlight === undefined ? {} : { syntaxHighlight }),
|
|
16
15
|
...(entryId === undefined ? {} : { target: { kind: "user-message", id: entryId } }),
|
|
17
16
|
});
|
|
18
17
|
const queuedLine = (text, entryId, segments) => ({
|
|
19
18
|
text,
|
|
20
|
-
|
|
21
|
-
backgroundOverride: options.colors.userMessageBackground,
|
|
19
|
+
colorOverride: options.colors.warning,
|
|
22
20
|
...(segments && segments.length > 0 ? { segments } : {}),
|
|
23
21
|
target: { kind: "queue-message", id: entryId },
|
|
24
22
|
});
|
|
25
23
|
const userMessageLines = (userEntry) => {
|
|
26
|
-
const lines =
|
|
27
|
-
userLine("", userEntry.id),
|
|
28
|
-
...renderMarkdownTextLines(userEntry.text, userContentWidth, userContentLeft).map((line) => userLine(line.text, userEntry.id, line.syntaxHighlight, line.segments)),
|
|
29
|
-
];
|
|
30
|
-
lines.push(...options.renderInlineUserMessageMenu(userEntry, { userContentWidth, userContentLeft, userLine }));
|
|
31
|
-
lines.push(userLine("", userEntry.id));
|
|
24
|
+
const lines = renderMarkdownTextLines(userEntry.text, userContentWidth, userContentLeft).map((line) => userLine(line.text, userEntry.id, line.syntaxHighlight, line.segments));
|
|
32
25
|
return attachImageClickTargets(lines, userEntry.id, userEntry.images, { foreground: options.colors.info, underline: true });
|
|
33
26
|
};
|
|
34
27
|
const queuedMessageLines = (queuedEntry) => {
|
|
@@ -70,10 +63,18 @@ function renderAssistantLines(text, width, options) {
|
|
|
70
63
|
const displayText = applyOutputFilters(text, options.outputFilters).trimEnd();
|
|
71
64
|
if (!displayText)
|
|
72
65
|
return [];
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
const { left: contentLeft, contentWidth } = horizontalPaddingLayout(width);
|
|
67
|
+
const contentLines = renderMarkdownTextLines(displayText, contentWidth, contentLeft);
|
|
68
|
+
if (contentLines.length === 0)
|
|
69
|
+
return [];
|
|
70
|
+
const lines = [];
|
|
71
|
+
for (const line of contentLines) {
|
|
72
|
+
lines.push({
|
|
73
|
+
text: padHorizontalText(line.text, width),
|
|
74
|
+
colorOverride: options.colors.assistantForeground,
|
|
75
|
+
...(line.segments && line.segments.length > 0 ? { segments: line.segments.map((segment) => ({ ...segment, start: segment.start + contentLeft, end: segment.end + contentLeft })) } : {}),
|
|
76
|
+
...(line.syntaxHighlight ? { syntaxHighlight: line.syntaxHighlight } : {}),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return lines;
|
|
79
80
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { resolveToolRule } from "../../config.js";
|
|
2
2
|
import { stringDisplayWidth } from "../../terminal-width.js";
|
|
3
3
|
import { renderConversationEntry as renderConversationEntryLines } from "./conversation-entry-renderer.js";
|
|
4
|
-
import { horizontalPaddingLayout
|
|
4
|
+
import { horizontalPaddingLayout } from "./render-text.js";
|
|
5
|
+
import { sdkQueuedMessageEntries } from "../session/queued-message-entries.js";
|
|
5
6
|
export class ConversationViewport {
|
|
6
7
|
host;
|
|
7
8
|
blockCachesByWidth = new Map();
|
|
@@ -119,39 +120,7 @@ export class ConversationViewport {
|
|
|
119
120
|
return lineCount;
|
|
120
121
|
}
|
|
121
122
|
queuedEntries() {
|
|
122
|
-
|
|
123
|
-
const entries = [];
|
|
124
|
-
for (const [index, text] of (session?.getSteeringMessages() ?? []).entries()) {
|
|
125
|
-
entries.push({
|
|
126
|
-
id: `queued-sdk-steering-${index}-${shortHash(text)}`,
|
|
127
|
-
kind: "queued",
|
|
128
|
-
mode: "steering",
|
|
129
|
-
text,
|
|
130
|
-
queueSource: "sdk-steering",
|
|
131
|
-
queueIndex: index,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
for (const [index, text] of (session?.getFollowUpMessages() ?? []).entries()) {
|
|
135
|
-
entries.push({
|
|
136
|
-
id: `queued-sdk-follow-up-${index}-${shortHash(text)}`,
|
|
137
|
-
kind: "queued",
|
|
138
|
-
mode: "follow-up",
|
|
139
|
-
text,
|
|
140
|
-
queueSource: "sdk-follow-up",
|
|
141
|
-
queueIndex: index,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
for (const [index, message] of this.host.deferredUserMessages.entries()) {
|
|
145
|
-
entries.push({
|
|
146
|
-
id: `${message.id}-${index}`,
|
|
147
|
-
kind: "queued",
|
|
148
|
-
mode: "steering",
|
|
149
|
-
text: message.displayText,
|
|
150
|
-
queueSource: "deferred",
|
|
151
|
-
queueIndex: index,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
return entries;
|
|
123
|
+
return sdkQueuedMessageEntries(this.host.session);
|
|
155
124
|
}
|
|
156
125
|
layoutForWidth(width) {
|
|
157
126
|
const queued = this.queuedEntries();
|
|
@@ -301,7 +270,7 @@ export class ConversationViewport {
|
|
|
301
270
|
return estimateWrappedLineCount(entry.text, width);
|
|
302
271
|
case "user": {
|
|
303
272
|
const { contentWidth } = horizontalPaddingLayout(width);
|
|
304
|
-
return
|
|
273
|
+
return estimateWrappedLineCount(entry.text, contentWidth);
|
|
305
274
|
}
|
|
306
275
|
case "queued": {
|
|
307
276
|
const { contentWidth } = horizontalPaddingLayout(width);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { InputEditor } from "../../input-editor.js";
|
|
2
2
|
import type { Theme } from "../../theme.js";
|
|
3
|
-
import type { EditorLayout, ExtensionWidgetRegistration, ExtensionWidgetTheme, SubagentsWidgetState, TodoDetails, WidgetTuiHandle } from "../types.js";
|
|
3
|
+
import type { EditorLayout, Entry, ExtensionWidgetRegistration, ExtensionWidgetTheme, SubagentsWidgetState, TodoDetails, WidgetTuiHandle } from "../types.js";
|
|
4
4
|
export type EditorLayoutRendererHost = {
|
|
5
5
|
readonly theme: Theme;
|
|
6
6
|
readonly inputEditor: InputEditor;
|
|
@@ -11,6 +11,9 @@ export type EditorLayoutRendererHost = {
|
|
|
11
11
|
readonly subagentsWidgetState: SubagentsWidgetState | undefined;
|
|
12
12
|
readonly voicePartialText: string | undefined;
|
|
13
13
|
readonly autocompleteSuggestion: string | undefined;
|
|
14
|
+
readonly queuedMessageWidgetEntries: readonly Extract<Entry, {
|
|
15
|
+
kind: "queued";
|
|
16
|
+
}>[];
|
|
14
17
|
renderExtensionInputComponent(width: number): string[] | undefined;
|
|
15
18
|
extensionInputUsesEditor(): boolean;
|
|
16
19
|
widgetTuiHandle(): WidgetTuiHandle;
|
|
@@ -24,6 +27,7 @@ export declare class EditorLayoutRenderer {
|
|
|
24
27
|
private renderWidgetRegistration;
|
|
25
28
|
private renderWidgetComponent;
|
|
26
29
|
private renderAboveEditorEntities;
|
|
30
|
+
private renderQueuedMessageWidgets;
|
|
27
31
|
private renderVoicePartial;
|
|
28
32
|
private renderExtensionWidgets;
|
|
29
33
|
private limitEntityLines;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { ABOVE_EDITOR_WIDGET_KEY_GROUPS, BUILT_IN_SUBAGENTS_WIDGET_KEYS, INPUT_MAX_ROWS, LEGACY_TODO_WIDGET_KEYS, } from "../constants.js";
|
|
2
2
|
import { renderSubagentsPanel, renderTodoPanel } from "./editor-panels.js";
|
|
3
|
-
import { ellipsizeDisplay, horizontalPaddingLayout, padHorizontalText, sanitizeText } from "./render-text.js";
|
|
3
|
+
import { ellipsizeDisplay, horizontalPaddingLayout, padHorizontalText, sanitizeText, wrapText } from "./render-text.js";
|
|
4
4
|
import { APP_ICONS } from "../icons.js";
|
|
5
|
-
const INPUT_FRAME_VERTICAL = "│";
|
|
6
5
|
export class EditorLayoutRenderer {
|
|
7
6
|
host;
|
|
8
7
|
constructor(host) {
|
|
@@ -12,13 +11,13 @@ export class EditorLayoutRenderer {
|
|
|
12
11
|
const maxAvailableInputRows = Math.max(1, rows - 5);
|
|
13
12
|
const renderedInput = this.renderInput(width, Math.min(INPUT_MAX_ROWS, maxAvailableInputRows), maxAvailableInputRows);
|
|
14
13
|
const maxEntityRows = Math.max(0, rows - renderedInput.lines.length - 4);
|
|
15
|
-
const
|
|
16
|
-
const aboveEditorEntities = this.renderAboveEditorEntities(
|
|
14
|
+
const editorEntityWidth = inputFrameContentWidth(width);
|
|
15
|
+
const aboveEditorEntities = this.renderAboveEditorEntities(editorEntityWidth);
|
|
17
16
|
let aboveEditorLines = this.limitEntityLines(aboveEditorEntities.lines, maxEntityRows);
|
|
18
17
|
if (aboveEditorEntities.hasWidgets && aboveEditorLines.length < maxEntityRows) {
|
|
19
18
|
aboveEditorLines = [...aboveEditorLines, { text: "", variant: "normal" }];
|
|
20
19
|
}
|
|
21
|
-
const belowEditorLines = this.limitEntityLines(this.renderExtensionWidgets("belowEditor",
|
|
20
|
+
const belowEditorLines = this.limitEntityLines(this.renderExtensionWidgets("belowEditor", editorEntityWidth), maxEntityRows - aboveEditorLines.length);
|
|
22
21
|
const inputBottomSeparatorRow = rows - 1;
|
|
23
22
|
const belowEditorStartRow = inputBottomSeparatorRow - belowEditorLines.length;
|
|
24
23
|
const inputStartRow = belowEditorStartRow - renderedInput.lines.length;
|
|
@@ -57,7 +56,8 @@ export class EditorLayoutRenderer {
|
|
|
57
56
|
const hasBuiltInTodoPanel = todoPanelLines.length > 0;
|
|
58
57
|
const subagentsPanelLines = renderSubagentsPanel(this.host.subagentsWidgetState, this.host.subagentsPanelExpanded, width, this.host.theme.colors);
|
|
59
58
|
const hasBuiltInSubagentsPanel = subagentsPanelLines.length > 0;
|
|
60
|
-
const
|
|
59
|
+
const queuedMessageWidgetLines = this.renderQueuedMessageWidgets(width);
|
|
60
|
+
const lines = [...todoPanelLines, ...subagentsPanelLines, ...queuedMessageWidgetLines];
|
|
61
61
|
let hasWidgets = lines.length > 0;
|
|
62
62
|
const consumedWidgetKeys = new Set();
|
|
63
63
|
for (const widgetKeys of ABOVE_EDITOR_WIDGET_KEY_GROUPS) {
|
|
@@ -103,6 +103,22 @@ export class EditorLayoutRenderer {
|
|
|
103
103
|
lines.push(...this.renderVoicePartial(width));
|
|
104
104
|
return { lines, hasWidgets };
|
|
105
105
|
}
|
|
106
|
+
renderQueuedMessageWidgets(width) {
|
|
107
|
+
const lines = [];
|
|
108
|
+
for (const entry of this.host.queuedMessageWidgetEntries) {
|
|
109
|
+
const icon = entry.queueSource === "deferred" ? APP_ICONS.pause : APP_ICONS.timerSand;
|
|
110
|
+
const wrapped = wrapText(`${icon} ${sanitizeText(entry.text)}`, width);
|
|
111
|
+
for (const [index, text] of wrapped.entries()) {
|
|
112
|
+
lines.push({
|
|
113
|
+
text: padHorizontalText(text, width),
|
|
114
|
+
colorOverride: this.host.theme.colors.warning,
|
|
115
|
+
target: { kind: "queue-message", id: entry.id },
|
|
116
|
+
...(index === 0 ? { segments: [{ start: 0, end: icon.length, foreground: this.host.theme.colors.info }] } : {}),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return lines;
|
|
121
|
+
}
|
|
106
122
|
renderVoicePartial(width) {
|
|
107
123
|
const partial = this.host.voicePartialText?.trim();
|
|
108
124
|
if (!partial)
|
|
@@ -150,7 +166,7 @@ export class EditorLayoutRenderer {
|
|
|
150
166
|
const scrollBar = usesEditor
|
|
151
167
|
? inputScrollBarMetrics(rendered.visualLines.length, visibleLines.length, rendered.scrollOffset)
|
|
152
168
|
: undefined;
|
|
153
|
-
const editorLines = usesEditor ? visibleLines.map((vl) =>
|
|
169
|
+
const editorLines = usesEditor ? visibleLines.map((vl) => padHorizontalText(vl.text, width)) : [];
|
|
154
170
|
const editorTagSpans = usesEditor
|
|
155
171
|
? visibleLines.map((vl) => vl.tagSpans.map((span) => ({
|
|
156
172
|
start: span.start + left,
|
|
@@ -163,7 +179,7 @@ export class EditorLayoutRenderer {
|
|
|
163
179
|
end: span.end + left,
|
|
164
180
|
})))
|
|
165
181
|
: [];
|
|
166
|
-
const paddedCustomLines = customLines.map((line) =>
|
|
182
|
+
const paddedCustomLines = customLines.map((line) => padHorizontalText(line, width));
|
|
167
183
|
return {
|
|
168
184
|
lines: [...paddedCustomLines, ...editorLines],
|
|
169
185
|
cursorRowOffset: customLines.length + rendered.cursorVisualRow - rendered.scrollOffset,
|
|
@@ -196,15 +212,8 @@ export class EditorLayoutRenderer {
|
|
|
196
212
|
];
|
|
197
213
|
}
|
|
198
214
|
}
|
|
199
|
-
function frameInputLine(line) {
|
|
200
|
-
if (line.length <= 0)
|
|
201
|
-
return line;
|
|
202
|
-
if (line.length === 1)
|
|
203
|
-
return INPUT_FRAME_VERTICAL;
|
|
204
|
-
return `${INPUT_FRAME_VERTICAL}${line.slice(1, -1)}${INPUT_FRAME_VERTICAL}`;
|
|
205
|
-
}
|
|
206
215
|
function inputFrameContentWidth(width) {
|
|
207
|
-
return Math.max(1, width
|
|
216
|
+
return Math.max(1, width);
|
|
208
217
|
}
|
|
209
218
|
function inputScrollBarMetrics(totalLineCount, visibleRowCount, scrollOffset) {
|
|
210
219
|
if (visibleRowCount <= 0 || totalLineCount <= visibleRowCount)
|
|
@@ -22,11 +22,7 @@ export declare class PopupMenuRenderer {
|
|
|
22
22
|
effectivePopupMenuWidth(columns: number): number;
|
|
23
23
|
styleOverlayLine(row: number, line: RenderedLine, width: number, activeMenu: PopupMenu<unknown>): string;
|
|
24
24
|
overlayPlainText(line: RenderedLine, width: number): string;
|
|
25
|
-
|
|
26
|
-
userContentWidth: number;
|
|
27
|
-
userContentLeft: number;
|
|
28
|
-
userLine: (text: string, entryId?: string, syntaxHighlight?: RenderedLine["syntaxHighlight"]) => RenderedLine;
|
|
29
|
-
}, menu: PopupMenu<UserMessageMenuValue>): RenderedLine[];
|
|
25
|
+
renderUserMessageMenu(width: number, menu: PopupMenu<UserMessageMenuValue>): RenderedLine[];
|
|
30
26
|
renderSlashCommandMenu(width: number, menu: PopupMenu<SlashCommand>): RenderedLine[];
|
|
31
27
|
renderModelMenu(width: number, menu: PopupMenu<ModelMenuValue>): RenderedLine[];
|
|
32
28
|
renderThinkingMenu(width: number, menu: PopupMenu<ThinkingMenuValue>): RenderedLine[];
|
|
@@ -49,41 +49,31 @@ export class PopupMenuRenderer {
|
|
|
49
49
|
const rightMargin = Math.max(0, width - margin - menuWidth);
|
|
50
50
|
return `${" ".repeat(margin)}${padOrTrimPlain(line.text, menuWidth)}${" ".repeat(rightMargin)}`;
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
headerLine.target = { kind: "popup-menu-close" };
|
|
55
|
-
headerLine.segments = [{
|
|
56
|
-
start: options.userContentLeft,
|
|
57
|
-
end: options.userContentLeft + options.userContentWidth,
|
|
58
|
-
foreground: this.host.theme.colors.accent,
|
|
59
|
-
background: this.host.theme.colors.popupHeaderBackground,
|
|
60
|
-
bold: true,
|
|
61
|
-
}];
|
|
62
|
-
const lines = [headerLine];
|
|
52
|
+
renderUserMessageMenu(width, menu) {
|
|
53
|
+
const lines = [this.popupMenuHeader("Message actions", width)];
|
|
63
54
|
for (const item of menu.visibleItems()) {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
line
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
];
|
|
55
|
+
const marker = item.selected ? "▶ " : " ";
|
|
56
|
+
const text = `${marker}${this.labelDescriptionText(item.label, item.description, width - 2, 18)}`;
|
|
57
|
+
const labelStart = 2;
|
|
58
|
+
const labelEnd = Math.min(text.length, labelStart + item.label.length);
|
|
59
|
+
const description = item.description ? sanitizeText(item.description).replace(/\s+/gu, " ") : "";
|
|
60
|
+
const descriptionStart = description ? text.indexOf(description, labelEnd) : -1;
|
|
61
|
+
const line = {
|
|
62
|
+
text,
|
|
63
|
+
target: { kind: "popup-menu", index: item.index },
|
|
64
|
+
segments: [
|
|
65
|
+
...(item.selected ? [{ start: 0, end: 1, foreground: this.host.theme.colors.accent, bold: true }] : []),
|
|
66
|
+
{
|
|
67
|
+
start: labelStart,
|
|
68
|
+
end: labelEnd,
|
|
69
|
+
foreground: this.userMessageActionForeground(item.value),
|
|
70
|
+
bold: item.selected,
|
|
71
|
+
},
|
|
72
|
+
...(descriptionStart >= 0
|
|
73
|
+
? [{ start: descriptionStart, end: text.length, foreground: this.host.theme.colors.muted }]
|
|
74
|
+
: []),
|
|
75
|
+
],
|
|
76
|
+
};
|
|
87
77
|
lines.push(line);
|
|
88
78
|
}
|
|
89
79
|
return lines;
|
|
@@ -24,6 +24,7 @@ export type AppRenderControllerDeps = {
|
|
|
24
24
|
tabLineRenderer: TabLineRenderer;
|
|
25
25
|
toastController: AppToastController;
|
|
26
26
|
outputBuffer?: TerminalOutputBuffer;
|
|
27
|
+
loadingConversationOverlayText?: () => string | undefined;
|
|
27
28
|
voiceProgressOverlayText(): string | undefined;
|
|
28
29
|
};
|
|
29
30
|
export declare class AppRenderController {
|
|
@@ -38,4 +39,5 @@ export declare class AppRenderController {
|
|
|
38
39
|
private renderClickFlashOverlay;
|
|
39
40
|
private updateStatusMouseState;
|
|
40
41
|
private renderVoiceProgressOverlay;
|
|
42
|
+
private renderConversationLoadingOverlay;
|
|
41
43
|
}
|
|
@@ -6,10 +6,6 @@ import { ANSI_RESET, colorLine, colorize } from "../../theme.js";
|
|
|
6
6
|
import { stringDisplayWidth } from "../../terminal-width.js";
|
|
7
7
|
import { padOrTrimPlain } from "./render-text.js";
|
|
8
8
|
const INPUT_FRAME = {
|
|
9
|
-
topLeft: "╭",
|
|
10
|
-
topRight: "╮",
|
|
11
|
-
bottomLeft: "╰",
|
|
12
|
-
bottomRight: "╯",
|
|
13
9
|
horizontal: "─",
|
|
14
10
|
};
|
|
15
11
|
export class AppRenderController {
|
|
@@ -129,6 +125,11 @@ export class AppRenderController {
|
|
|
129
125
|
setRenderedBackground(row, rendered?.backgroundOverride);
|
|
130
126
|
appendFrameOutput("conversation", row, this.renderFrameRow(row, this.deps.screenStyler.styleBaseLine(row, rendered, conversationColumns)));
|
|
131
127
|
}
|
|
128
|
+
const loadingConversationOverlay = this.renderConversationLoadingOverlay(this.deps.loadingConversationOverlayText?.(), conversationColumns, topReservedRows, bodyHeight);
|
|
129
|
+
if (loadingConversationOverlay) {
|
|
130
|
+
this.deps.mouseController.renderedRowTexts.set(loadingConversationOverlay.row, loadingConversationOverlay.text);
|
|
131
|
+
appendFrameOutput("conversation", loadingConversationOverlay.row, this.renderFrameRow(loadingConversationOverlay.row, loadingConversationOverlay.output));
|
|
132
|
+
}
|
|
132
133
|
const aboveEditorStartRow = inputSeparatorRow + 1;
|
|
133
134
|
for (let index = 0; index < aboveEditorLines.length; index += 1) {
|
|
134
135
|
const rendered = frameRenderedLine(aboveEditorLines[index], columns, this.deps.theme, this.deps.screenStyler);
|
|
@@ -160,7 +161,7 @@ export class AppRenderController {
|
|
|
160
161
|
const row = toScreenRow(inputStartRow + index);
|
|
161
162
|
this.deps.mouseController.renderedRowTexts.set(row, inputLine);
|
|
162
163
|
const tagColor = this.deps.theme.colors.accent;
|
|
163
|
-
const styledLine = this.deps.screenStyler.styleInputLine(row, inputLine, tagSpans, suggestionSpans, columns, tagColor, this.deps.theme.colors.muted
|
|
164
|
+
const styledLine = this.deps.screenStyler.styleInputLine(row, inputLine, tagSpans, suggestionSpans, columns, tagColor, this.deps.theme.colors.muted);
|
|
164
165
|
appendFrameOutput("inputStatus", row, this.renderFrameRow(row, styledLine));
|
|
165
166
|
}
|
|
166
167
|
if (renderedInput.scrollBar && columns > 0) {
|
|
@@ -327,40 +328,40 @@ export class AppRenderController {
|
|
|
327
328
|
].join("");
|
|
328
329
|
return { row: Math.min(2, rows - 1), text, output };
|
|
329
330
|
}
|
|
331
|
+
renderConversationLoadingOverlay(message, width, topReservedRows, bodyHeight) {
|
|
332
|
+
if (!message || width <= 0 || bodyHeight <= 0)
|
|
333
|
+
return undefined;
|
|
334
|
+
const overlayWidth = Math.min(stringDisplayWidth(message), width);
|
|
335
|
+
const leftWidth = Math.max(0, Math.floor((width - overlayWidth) / 2));
|
|
336
|
+
const rightWidth = Math.max(0, width - leftWidth - overlayWidth);
|
|
337
|
+
const text = `${" ".repeat(leftWidth)}${padOrTrimPlain(message, overlayWidth)}${" ".repeat(rightWidth)}`;
|
|
338
|
+
const row = topReservedRows + Math.floor((bodyHeight + 1) / 2);
|
|
339
|
+
const output = this.deps.screenStyler.styleLine(row, text, width, {
|
|
340
|
+
foreground: this.deps.theme.colors.muted,
|
|
341
|
+
});
|
|
342
|
+
return { row, text, output };
|
|
343
|
+
}
|
|
330
344
|
}
|
|
331
345
|
function visibleToastStates(toastController) {
|
|
332
346
|
const candidate = toastController;
|
|
333
347
|
return typeof candidate.visibleStates === "function" ? candidate.visibleStates() : candidate.toast?.visibleStates ?? [];
|
|
334
348
|
}
|
|
335
349
|
function inputFrameLine(width, edge) {
|
|
350
|
+
void edge;
|
|
336
351
|
if (width <= 0)
|
|
337
352
|
return "";
|
|
338
|
-
|
|
339
|
-
return edge === "top" ? INPUT_FRAME.topLeft : INPUT_FRAME.bottomLeft;
|
|
340
|
-
const left = edge === "top" ? INPUT_FRAME.topLeft : INPUT_FRAME.bottomLeft;
|
|
341
|
-
const right = edge === "top" ? INPUT_FRAME.topRight : INPUT_FRAME.bottomRight;
|
|
342
|
-
return `${left}${INPUT_FRAME.horizontal.repeat(Math.max(0, width - 2))}${right}`;
|
|
353
|
+
return INPUT_FRAME.horizontal.repeat(width);
|
|
343
354
|
}
|
|
344
355
|
function frameRenderedLine(line, width, theme, screenStyler) {
|
|
345
356
|
if (width <= 0)
|
|
346
357
|
return { line, text: "", output: () => "" };
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
});
|
|
351
|
-
return { line, text: "│", output: () => border };
|
|
352
|
-
}
|
|
353
|
-
const innerWidth = Math.max(0, width - 2);
|
|
354
|
-
const innerText = padOrTrimPlain(line?.text ?? "", innerWidth);
|
|
355
|
-
const innerLine = line ? frameInnerRenderedLine(line, innerText, innerWidth) : undefined;
|
|
356
|
-
const leftBorder = colorize("│", {
|
|
357
|
-
foreground: theme.colors.inputBorder,
|
|
358
|
-
});
|
|
359
|
-
const rightBorder = leftBorder;
|
|
358
|
+
void theme;
|
|
359
|
+
const text = padOrTrimPlain(line?.text ?? "", width);
|
|
360
|
+
const outputLine = line ? frameInnerRenderedLine(line, text, width) : undefined;
|
|
360
361
|
return {
|
|
361
362
|
line,
|
|
362
|
-
text
|
|
363
|
-
output: (row) =>
|
|
363
|
+
text,
|
|
364
|
+
output: (row) => screenStyler.styleBaseLine(row, outputLine, width),
|
|
364
365
|
};
|
|
365
366
|
}
|
|
366
367
|
function frameInnerRenderedLine(line, text, width) {
|
|
@@ -98,8 +98,8 @@ export function padOrTrimPlain(text, width) {
|
|
|
98
98
|
}
|
|
99
99
|
export function horizontalPaddingLayout(width) {
|
|
100
100
|
const safeWidth = Math.max(1, width);
|
|
101
|
-
const left =
|
|
102
|
-
const right =
|
|
101
|
+
const left = 0;
|
|
102
|
+
const right = 0;
|
|
103
103
|
return { left, right, contentWidth: Math.max(1, safeWidth - left - right) };
|
|
104
104
|
}
|
|
105
105
|
export function padHorizontalText(text, width) {
|
|
@@ -110,7 +110,7 @@ export class StatusLineRenderer {
|
|
|
110
110
|
}
|
|
111
111
|
inputBorderWidgetSegments(layout, text) {
|
|
112
112
|
const colors = this.host.theme.colors;
|
|
113
|
-
const background = colors.
|
|
113
|
+
const background = colors.inputBorderWidgetBackground;
|
|
114
114
|
const segments = [];
|
|
115
115
|
const pushWidgetSegment = (widget, foreground) => {
|
|
116
116
|
if (!widget)
|