@selesai/code 0.8.2 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +4 -2
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.js +18 -0
- package/dist/cli/config-selector.js +2 -2
- package/dist/cli/startup-ui.d.ts +1 -1
- package/dist/cli/startup-ui.js +2 -2
- package/dist/core/keybindings.d.ts +44 -4
- package/dist/core/llama/provider.js +22 -7
- package/dist/core/model-resolver.js +2 -0
- package/dist/core/model-runtime.d.ts +4 -2
- package/dist/core/model-runtime.js +23 -2
- package/dist/core/remote-catalog-provider.js +72 -67
- package/dist/core/settings-manager.d.ts +8 -0
- package/dist/core/settings-manager.js +18 -0
- package/dist/extensions/pi-powerline-footer/CHANGELOG.md +2 -1
- package/dist/extensions/pi-powerline-footer/index.ts +3 -272
- package/dist/extensions/pi-powerline-footer/package.json +4 -5
- package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -6
- package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +2 -2
- package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +1 -1
- package/dist/extensions/pi-powerline-footer/tsconfig.json +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/main.js +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts +6 -2
- package/dist/modes/interactive/components/settings-selector.js +20 -0
- package/dist/modes/interactive/interactive-mode.d.ts +28 -0
- package/dist/modes/interactive/interactive-mode.js +214 -28
- package/dist/modes/interactive/theme/dark.json +1 -0
- package/dist/modes/interactive/theme/light.json +1 -0
- package/dist/modes/interactive/theme/theme-controller.d.ts +3 -0
- package/dist/modes/interactive/theme/theme-controller.js +10 -1
- package/dist/modes/interactive/theme/theme.d.ts +2 -2
- package/dist/modes/interactive/theme/theme.js +24 -6
- package/docs/keybindings.md +35 -7
- package/docs/settings.md +16 -14
- package/docs/usage.md +5 -0
- package/package.json +5 -5
- package/dist/extensions/pi-powerline-footer/fixed-editor/cluster.ts +0 -113
- package/dist/extensions/pi-powerline-footer/fixed-editor/terminal-split.ts +0 -1080
- package/dist/extensions/pi-powerline-footer/tests/fixed-editor.test.ts +0 -1452
- package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -49
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
-
import
|
|
8
|
+
import * as TuiLayouts from "@earendil-works/pi-tui";
|
|
9
|
+
import { CombinedAutocompleteProvider, Container, fuzzyFilter, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TuiAltScreen, TuiMainScreen, visibleWidth, } from "@earendil-works/pi-tui";
|
|
9
10
|
import chalk from "chalk";
|
|
10
11
|
import { spawn, spawnSync } from "child_process";
|
|
11
12
|
import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, getUpdateInstruction, PACKAGE_NAME, VERSION, } from "../../config.js";
|
|
@@ -31,6 +32,7 @@ import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipb
|
|
|
31
32
|
import { parseGitUrl } from "../../utils/git.js";
|
|
32
33
|
import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
|
|
33
34
|
import { getCwdRelativePath } from "../../utils/paths.js";
|
|
35
|
+
import { openBrowser } from "../../utils/open-browser.js";
|
|
34
36
|
import { getPiUserAgent } from "../../utils/pi-user-agent.js";
|
|
35
37
|
import { killTrackedDetachedChildren } from "../../utils/shell.js";
|
|
36
38
|
import { ensureTool } from "../../utils/tools-manager.js";
|
|
@@ -162,13 +164,60 @@ function formatLoginProviderCompletionDescription(provider) {
|
|
|
162
164
|
const authTypes = provider.authTypes.map(formatAuthSelectorProviderType).join("/");
|
|
163
165
|
return provider.name === provider.id ? authTypes : `${provider.name} · ${authTypes}`;
|
|
164
166
|
}
|
|
167
|
+
/** Composition root for selecting the interactive terminal renderer. */
|
|
168
|
+
export function createInteractiveTui(options) {
|
|
169
|
+
const terminal = options.terminal ?? new ProcessTerminal();
|
|
170
|
+
if (options.tuiMode === "fullscreen") {
|
|
171
|
+
return new TuiAltScreen(terminal, options.showHardwareCursor, options.logDirectory, {
|
|
172
|
+
openUrl: openBrowser,
|
|
173
|
+
onRightClickPaste: options.onRightClickPaste,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return new TuiMainScreen(terminal, options.showHardwareCursor, options.logDirectory);
|
|
177
|
+
}
|
|
178
|
+
/** Stable reference for components while InteractiveMode replaces the active renderer. */
|
|
179
|
+
export function createInteractiveTuiReference(getTui) {
|
|
180
|
+
return new Proxy({}, {
|
|
181
|
+
get: (_target, property) => {
|
|
182
|
+
const tui = getTui();
|
|
183
|
+
const value = Reflect.get(tui, property, tui);
|
|
184
|
+
if (typeof value !== "function")
|
|
185
|
+
return value;
|
|
186
|
+
let methodTui = tui;
|
|
187
|
+
let method = value;
|
|
188
|
+
return (...args) => {
|
|
189
|
+
const currentTui = getTui();
|
|
190
|
+
if (currentTui !== methodTui) {
|
|
191
|
+
const currentMethod = Reflect.get(currentTui, property, currentTui);
|
|
192
|
+
if (typeof currentMethod !== "function") {
|
|
193
|
+
throw new TypeError(`TUI property ${String(property)} is not callable`);
|
|
194
|
+
}
|
|
195
|
+
methodTui = currentTui;
|
|
196
|
+
method = currentMethod;
|
|
197
|
+
}
|
|
198
|
+
return Reflect.apply(method, methodTui, args);
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
set: (_target, property, value) => {
|
|
202
|
+
const tui = getTui();
|
|
203
|
+
return Reflect.set(tui, property, value, tui);
|
|
204
|
+
},
|
|
205
|
+
has: (_target, property) => Reflect.has(getTui(), property),
|
|
206
|
+
getPrototypeOf: () => Reflect.getPrototypeOf(getTui()),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
165
209
|
export class InteractiveMode {
|
|
166
210
|
runtimeHost;
|
|
211
|
+
renderer;
|
|
167
212
|
ui;
|
|
213
|
+
mainScreenRenderState;
|
|
214
|
+
documentContainer;
|
|
168
215
|
loadedResourcesContainer;
|
|
169
216
|
chatContainer;
|
|
170
217
|
pendingMessagesContainer;
|
|
171
218
|
statusContainer;
|
|
219
|
+
transcriptScrollView;
|
|
220
|
+
fullscreenLayoutRoot;
|
|
172
221
|
defaultEditor;
|
|
173
222
|
editor;
|
|
174
223
|
editorComponentFactory;
|
|
@@ -177,6 +226,7 @@ export class InteractiveMode {
|
|
|
177
226
|
fdPath;
|
|
178
227
|
editorContainer;
|
|
179
228
|
footer;
|
|
229
|
+
footerContainer;
|
|
180
230
|
footerDataProvider;
|
|
181
231
|
// Stored so the same manager can be injected into custom editors, selectors, and extension UI.
|
|
182
232
|
keybindings;
|
|
@@ -232,7 +282,9 @@ export class InteractiveMode {
|
|
|
232
282
|
extensionSelector = undefined;
|
|
233
283
|
extensionInput = undefined;
|
|
234
284
|
extensionEditor = undefined;
|
|
235
|
-
|
|
285
|
+
// Extension terminal input listeners, keyed by handler so they can be
|
|
286
|
+
// re-registered on the active renderer after a TUI mode switch.
|
|
287
|
+
extensionTerminalInputUnsubscribers = new Map();
|
|
236
288
|
// Extension widgets (components rendered above/below the editor)
|
|
237
289
|
extensionWidgetsAbove = new Map();
|
|
238
290
|
extensionWidgetsBelow = new Map();
|
|
@@ -247,6 +299,9 @@ export class InteractiveMode {
|
|
|
247
299
|
// Custom header from extension (undefined = use built-in header)
|
|
248
300
|
customHeader = undefined;
|
|
249
301
|
options;
|
|
302
|
+
onRightClickPaste = () => {
|
|
303
|
+
void this.handleRightClickPaste();
|
|
304
|
+
};
|
|
250
305
|
autoTrustOnReloadCwd;
|
|
251
306
|
themeController;
|
|
252
307
|
// Convenience accessors
|
|
@@ -264,7 +319,8 @@ export class InteractiveMode {
|
|
|
264
319
|
}
|
|
265
320
|
constructor(runtimeHost, options = {}) {
|
|
266
321
|
this.runtimeHost = runtimeHost;
|
|
267
|
-
|
|
322
|
+
const tuiMode = options.tuiMode ?? this.settingsManager.getTuiMode();
|
|
323
|
+
this.options = { ...options, tuiMode };
|
|
268
324
|
this.autoTrustOnReloadCwd = options.autoTrustOnReloadCwd;
|
|
269
325
|
this.runtimeHost.setBeforeSessionInvalidate(() => {
|
|
270
326
|
this.resetExtensionUI();
|
|
@@ -273,11 +329,21 @@ export class InteractiveMode {
|
|
|
273
329
|
await this.rebindCurrentSession({ renderBeforeBind: true });
|
|
274
330
|
});
|
|
275
331
|
this.version = VERSION;
|
|
276
|
-
this.
|
|
332
|
+
this.renderer = createInteractiveTui({
|
|
333
|
+
tuiMode,
|
|
334
|
+
showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
|
|
335
|
+
logDirectory: getAgentDir(),
|
|
336
|
+
onRightClickPaste: this.onRightClickPaste,
|
|
337
|
+
});
|
|
338
|
+
this.ui = createInteractiveTuiReference(() => this.renderer);
|
|
277
339
|
this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
|
|
340
|
+
this.documentContainer = new Container();
|
|
278
341
|
this.headerContainer = new Container();
|
|
279
342
|
this.loadedResourcesContainer = new Container();
|
|
280
343
|
this.chatContainer = new Container();
|
|
344
|
+
this.documentContainer.addChild(this.headerContainer);
|
|
345
|
+
this.documentContainer.addChild(this.loadedResourcesContainer);
|
|
346
|
+
this.documentContainer.addChild(this.chatContainer);
|
|
281
347
|
this.pendingMessagesContainer = new Container();
|
|
282
348
|
this.statusContainer = new Container();
|
|
283
349
|
this.widgetContainerAbove = new Container();
|
|
@@ -296,6 +362,8 @@ export class InteractiveMode {
|
|
|
296
362
|
this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
|
|
297
363
|
this.footer = new FooterComponent(this.session, this.footerDataProvider);
|
|
298
364
|
this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
|
|
365
|
+
this.footerContainer = new Container();
|
|
366
|
+
this.footerContainer.addChild(this.footer);
|
|
299
367
|
// Load hide thinking block setting
|
|
300
368
|
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
301
369
|
this.outputPad = this.settingsManager.getOutputPad();
|
|
@@ -453,6 +521,73 @@ export class InteractiveMode {
|
|
|
453
521
|
}
|
|
454
522
|
this.chatContainer.addChild(new DynamicBorder());
|
|
455
523
|
}
|
|
524
|
+
mountInteractiveTui(tui, components) {
|
|
525
|
+
for (const component of components)
|
|
526
|
+
tui.addChild(component);
|
|
527
|
+
if (TuiLayouts.isViewportTUI(tui)) {
|
|
528
|
+
if (!this.fullscreenLayoutRoot)
|
|
529
|
+
throw new Error("Fullscreen layout is not initialized");
|
|
530
|
+
tui.setLayoutRoot(this.fullscreenLayoutRoot);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
stopInteractiveTui() {
|
|
534
|
+
if (this.renderer.mode === "fullscreen") {
|
|
535
|
+
while (this.renderer.hasOverlayEntries)
|
|
536
|
+
this.renderer.hideOverlay();
|
|
537
|
+
this.switchTuiMode("regular", false, false);
|
|
538
|
+
this.renderer.renderNow();
|
|
539
|
+
}
|
|
540
|
+
this.ui.stop();
|
|
541
|
+
}
|
|
542
|
+
switchTuiMode(mode, restoreProgress = true, startRenderer = true) {
|
|
543
|
+
const previousUi = this.renderer;
|
|
544
|
+
if (mode === previousUi.mode)
|
|
545
|
+
return true;
|
|
546
|
+
if (previousUi.hasOverlayEntries)
|
|
547
|
+
return false;
|
|
548
|
+
const components = [...previousUi.children];
|
|
549
|
+
const focus = previousUi.getFocusedComponent();
|
|
550
|
+
const terminal = previousUi.terminal;
|
|
551
|
+
const showHardwareCursor = previousUi.getShowHardwareCursor();
|
|
552
|
+
const clearOnShrink = previousUi.getClearOnShrink();
|
|
553
|
+
const onDebug = previousUi.onDebug;
|
|
554
|
+
if (previousUi instanceof TuiMainScreen) {
|
|
555
|
+
this.mainScreenRenderState = previousUi.captureRenderState();
|
|
556
|
+
}
|
|
557
|
+
previousUi.stop({ preserveScreen: true });
|
|
558
|
+
previousUi.setFocus(null);
|
|
559
|
+
previousUi.clear();
|
|
560
|
+
if (TuiLayouts.isViewportTUI(previousUi))
|
|
561
|
+
previousUi.setLayoutRoot(undefined);
|
|
562
|
+
const nextUi = createInteractiveTui({
|
|
563
|
+
tuiMode: mode,
|
|
564
|
+
showHardwareCursor,
|
|
565
|
+
logDirectory: getAgentDir(),
|
|
566
|
+
terminal,
|
|
567
|
+
onRightClickPaste: this.onRightClickPaste,
|
|
568
|
+
});
|
|
569
|
+
nextUi.setClearOnShrink(clearOnShrink);
|
|
570
|
+
nextUi.onDebug = onDebug;
|
|
571
|
+
if (nextUi instanceof TuiMainScreen && this.mainScreenRenderState) {
|
|
572
|
+
nextUi.restoreRenderState(this.mainScreenRenderState);
|
|
573
|
+
}
|
|
574
|
+
this.renderer = nextUi;
|
|
575
|
+
this.options.tuiMode = mode;
|
|
576
|
+
this.mountInteractiveTui(nextUi, components);
|
|
577
|
+
nextUi.invalidate();
|
|
578
|
+
nextUi.setFocus(focus);
|
|
579
|
+
if (!startRenderer)
|
|
580
|
+
return true;
|
|
581
|
+
nextUi.start();
|
|
582
|
+
this.themeController.rebindTui();
|
|
583
|
+
this.rebindExtensionTerminalInputListeners();
|
|
584
|
+
if (restoreProgress &&
|
|
585
|
+
this.settingsManager.getShowTerminalProgress() &&
|
|
586
|
+
(this.session.isStreaming || this.session.isCompacting)) {
|
|
587
|
+
terminal.setProgress(true);
|
|
588
|
+
}
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
456
591
|
async init() {
|
|
457
592
|
if (this.isInitialized)
|
|
458
593
|
return;
|
|
@@ -476,18 +611,36 @@ export class InteractiveMode {
|
|
|
476
611
|
: "";
|
|
477
612
|
console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
|
|
478
613
|
}
|
|
479
|
-
//
|
|
480
|
-
// Keep loaded resources before chat so restored session messages never precede them.
|
|
481
|
-
this.ui.addChild(this.headerContainer);
|
|
482
|
-
this.ui.addChild(this.loadedResourcesContainer);
|
|
483
|
-
this.ui.addChild(this.chatContainer);
|
|
484
|
-
this.ui.addChild(this.pendingMessagesContainer);
|
|
485
|
-
this.ui.addChild(this.statusContainer);
|
|
614
|
+
// Keep one component tree and remount it when changing renderers.
|
|
486
615
|
this.renderWidgets(); // Initialize with default spacer
|
|
487
|
-
this.
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
616
|
+
this.transcriptScrollView = new TuiLayouts.ScrollView(this.documentContainer, {
|
|
617
|
+
follow: "end",
|
|
618
|
+
primary: true,
|
|
619
|
+
overscroll: "chain",
|
|
620
|
+
scrollbar: this.settingsManager.getFullscreenScrollbar(),
|
|
621
|
+
scrollbarStyle: (text) => theme.bg("scrollbarThumb", text),
|
|
622
|
+
});
|
|
623
|
+
const dock = new TuiLayouts.VStack([
|
|
624
|
+
{ component: this.pendingMessagesContainer, shrink: 1, minSize: 0 },
|
|
625
|
+
{ component: this.statusContainer, shrink: 1, minSize: 0 },
|
|
626
|
+
{ component: this.widgetContainerAbove, shrink: 1, minSize: 0 },
|
|
627
|
+
{ component: this.editorContainer, shrink: 1, minSize: 3 },
|
|
628
|
+
{ component: this.widgetContainerBelow, shrink: 1, minSize: 0 },
|
|
629
|
+
{ component: this.footerContainer, shrink: 1, minSize: 1 },
|
|
630
|
+
]);
|
|
631
|
+
this.fullscreenLayoutRoot = new TuiLayouts.VStack([
|
|
632
|
+
{ component: this.transcriptScrollView, basis: 0, grow: 1, shrink: 1, minSize: 1 },
|
|
633
|
+
{ component: dock, basis: "auto", grow: 0, shrink: 1, minSize: 1 },
|
|
634
|
+
]);
|
|
635
|
+
this.mountInteractiveTui(this.renderer, [
|
|
636
|
+
this.documentContainer,
|
|
637
|
+
this.pendingMessagesContainer,
|
|
638
|
+
this.statusContainer,
|
|
639
|
+
this.widgetContainerAbove,
|
|
640
|
+
this.editorContainer,
|
|
641
|
+
this.widgetContainerBelow,
|
|
642
|
+
this.footerContainer,
|
|
643
|
+
]);
|
|
491
644
|
this.ui.setFocus(this.editor);
|
|
492
645
|
this.setupKeyHandlers();
|
|
493
646
|
this.setupEditorSubmitHandler();
|
|
@@ -1577,22 +1730,16 @@ export class InteractiveMode {
|
|
|
1577
1730
|
if (this.customFooter?.dispose) {
|
|
1578
1731
|
this.customFooter.dispose();
|
|
1579
1732
|
}
|
|
1580
|
-
|
|
1581
|
-
if (this.customFooter) {
|
|
1582
|
-
this.ui.removeChild(this.customFooter);
|
|
1583
|
-
}
|
|
1584
|
-
else {
|
|
1585
|
-
this.ui.removeChild(this.footer);
|
|
1586
|
-
}
|
|
1733
|
+
this.footerContainer.clear();
|
|
1587
1734
|
if (factory) {
|
|
1588
1735
|
// Create and add custom footer, passing the data provider
|
|
1589
1736
|
this.customFooter = factory(this.ui, theme, this.footerDataProvider);
|
|
1590
|
-
this.
|
|
1737
|
+
this.footerContainer.addChild(this.customFooter);
|
|
1591
1738
|
}
|
|
1592
1739
|
else {
|
|
1593
1740
|
// Restore built-in footer
|
|
1594
1741
|
this.customFooter = undefined;
|
|
1595
|
-
this.
|
|
1742
|
+
this.footerContainer.addChild(this.footer);
|
|
1596
1743
|
}
|
|
1597
1744
|
this.ui.requestRender();
|
|
1598
1745
|
}
|
|
@@ -1639,14 +1786,19 @@ export class InteractiveMode {
|
|
|
1639
1786
|
}
|
|
1640
1787
|
addExtensionTerminalInputListener(handler) {
|
|
1641
1788
|
const unsubscribe = this.ui.addInputListener(handler);
|
|
1642
|
-
this.extensionTerminalInputUnsubscribers.
|
|
1789
|
+
this.extensionTerminalInputUnsubscribers.set(handler, unsubscribe);
|
|
1643
1790
|
return () => {
|
|
1644
1791
|
unsubscribe();
|
|
1645
|
-
this.extensionTerminalInputUnsubscribers.delete(
|
|
1792
|
+
this.extensionTerminalInputUnsubscribers.delete(handler);
|
|
1646
1793
|
};
|
|
1647
1794
|
}
|
|
1795
|
+
rebindExtensionTerminalInputListeners() {
|
|
1796
|
+
for (const handler of this.extensionTerminalInputUnsubscribers.keys()) {
|
|
1797
|
+
this.extensionTerminalInputUnsubscribers.set(handler, this.ui.addInputListener(handler));
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1648
1800
|
clearExtensionTerminalInputListeners() {
|
|
1649
|
-
for (const unsubscribe of this.extensionTerminalInputUnsubscribers) {
|
|
1801
|
+
for (const unsubscribe of this.extensionTerminalInputUnsubscribers.values()) {
|
|
1650
1802
|
unsubscribe();
|
|
1651
1803
|
}
|
|
1652
1804
|
this.extensionTerminalInputUnsubscribers.clear();
|
|
@@ -2074,6 +2226,22 @@ export class InteractiveMode {
|
|
|
2074
2226
|
void this.handleClipboardPaste();
|
|
2075
2227
|
};
|
|
2076
2228
|
}
|
|
2229
|
+
async handleRightClickPaste() {
|
|
2230
|
+
const target = this.renderer.getFocusedComponent();
|
|
2231
|
+
const handleInput = target?.handleInput;
|
|
2232
|
+
if (!target || !handleInput)
|
|
2233
|
+
return;
|
|
2234
|
+
try {
|
|
2235
|
+
const text = await readClipboardText();
|
|
2236
|
+
if (!text || this.renderer.getFocusedComponent() !== target)
|
|
2237
|
+
return;
|
|
2238
|
+
handleInput.call(target, `\x1b[200~${text}\x1b[201~`);
|
|
2239
|
+
this.ui.requestRender();
|
|
2240
|
+
}
|
|
2241
|
+
catch {
|
|
2242
|
+
// Silently ignore clipboard errors (may not have permission, etc.)
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2077
2245
|
async handleClipboardPaste() {
|
|
2078
2246
|
try {
|
|
2079
2247
|
const image = await readClipboardImage();
|
|
@@ -3470,6 +3638,8 @@ export class InteractiveMode {
|
|
|
3470
3638
|
quietStartup: this.settingsManager.getQuietStartup(),
|
|
3471
3639
|
clearOnShrink: this.settingsManager.getClearOnShrink(),
|
|
3472
3640
|
showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
|
|
3641
|
+
tuiMode: this.settingsManager.getTuiMode(),
|
|
3642
|
+
fullscreenScrollbar: this.settingsManager.getFullscreenScrollbar(),
|
|
3473
3643
|
warnings: this.settingsManager.getWarnings(),
|
|
3474
3644
|
}, {
|
|
3475
3645
|
onAutoCompactChange: (enabled) => {
|
|
@@ -3609,6 +3779,22 @@ export class InteractiveMode {
|
|
|
3609
3779
|
onShowTerminalProgressChange: (enabled) => {
|
|
3610
3780
|
this.settingsManager.setShowTerminalProgress(enabled);
|
|
3611
3781
|
},
|
|
3782
|
+
onTuiModeChange: (mode) => {
|
|
3783
|
+
if (!this.switchTuiMode(mode)) {
|
|
3784
|
+
this.settingsManager.setTuiMode(this.ui.mode);
|
|
3785
|
+
this.showStatus("Close active overlays before changing TUI mode");
|
|
3786
|
+
return;
|
|
3787
|
+
}
|
|
3788
|
+
this.settingsManager.setTuiMode(mode);
|
|
3789
|
+
if (!this.activeStatusIndicator)
|
|
3790
|
+
this.statusContainer.clear();
|
|
3791
|
+
this.showStatus(`TUI mode: ${mode}`);
|
|
3792
|
+
},
|
|
3793
|
+
onFullscreenScrollbarChange: (mode) => {
|
|
3794
|
+
this.settingsManager.setFullscreenScrollbar(mode);
|
|
3795
|
+
this.transcriptScrollView?.setScrollbar(mode);
|
|
3796
|
+
this.ui.requestRender();
|
|
3797
|
+
},
|
|
3612
3798
|
onWarningsChange: (warnings) => {
|
|
3613
3799
|
this.settingsManager.setWarnings(warnings);
|
|
3614
3800
|
},
|
|
@@ -5106,7 +5292,7 @@ export class InteractiveMode {
|
|
|
5106
5292
|
this.unsubscribe();
|
|
5107
5293
|
}
|
|
5108
5294
|
if (this.isInitialized) {
|
|
5109
|
-
this.
|
|
5295
|
+
this.stopInteractiveTui();
|
|
5110
5296
|
this.isInitialized = false;
|
|
5111
5297
|
}
|
|
5112
5298
|
this.unregisterSignalHandlers();
|
|
@@ -13,7 +13,9 @@ export declare class InteractiveThemeController {
|
|
|
13
13
|
private terminalTheme;
|
|
14
14
|
private activeThemeName;
|
|
15
15
|
private autoSyncEnabled;
|
|
16
|
+
private terminalColorSchemeUnsubscribe;
|
|
16
17
|
constructor(ui: TUI, settingsManager: SettingsManager, showError: (message: string) => void, onChanged: () => void);
|
|
18
|
+
rebindTui(): void;
|
|
17
19
|
applyFromSettings(): Promise<void>;
|
|
18
20
|
setThemeName(themeName: string, showError?: boolean): ThemeResult;
|
|
19
21
|
setThemeInstance(themeInstance: Theme): ThemeResult;
|
|
@@ -23,6 +25,7 @@ export declare class InteractiveThemeController {
|
|
|
23
25
|
private applyThemeName;
|
|
24
26
|
private notifyChanged;
|
|
25
27
|
private setAutoSync;
|
|
28
|
+
private bindTerminalColorSchemeListener;
|
|
26
29
|
private applyTerminalTheme;
|
|
27
30
|
}
|
|
28
31
|
export {};
|
|
@@ -7,6 +7,7 @@ export class InteractiveThemeController {
|
|
|
7
7
|
terminalTheme = detectTerminalBackgroundFromEnv().theme;
|
|
8
8
|
activeThemeName;
|
|
9
9
|
autoSyncEnabled = false;
|
|
10
|
+
terminalColorSchemeUnsubscribe;
|
|
10
11
|
constructor(ui, settingsManager, showError, onChanged) {
|
|
11
12
|
this.ui = ui;
|
|
12
13
|
this.settingsManager = settingsManager;
|
|
@@ -14,7 +15,12 @@ export class InteractiveThemeController {
|
|
|
14
15
|
this.onChanged = onChanged;
|
|
15
16
|
this.activeThemeName = resolveThemeSetting(this.settingsManager.getThemeSetting(), this.terminalTheme);
|
|
16
17
|
initTheme(this.activeThemeName, true);
|
|
17
|
-
this.
|
|
18
|
+
this.bindTerminalColorSchemeListener();
|
|
19
|
+
}
|
|
20
|
+
rebindTui() {
|
|
21
|
+
this.terminalColorSchemeUnsubscribe?.();
|
|
22
|
+
this.bindTerminalColorSchemeListener();
|
|
23
|
+
this.ui.setTerminalColorSchemeNotifications(this.autoSyncEnabled);
|
|
18
24
|
}
|
|
19
25
|
async applyFromSettings() {
|
|
20
26
|
const themeSetting = this.settingsManager.getThemeSetting();
|
|
@@ -84,6 +90,9 @@ export class InteractiveThemeController {
|
|
|
84
90
|
this.autoSyncEnabled = enabled;
|
|
85
91
|
this.ui.setTerminalColorSchemeNotifications(enabled);
|
|
86
92
|
}
|
|
93
|
+
bindTerminalColorSchemeListener() {
|
|
94
|
+
this.terminalColorSchemeUnsubscribe = this.ui.onTerminalColorSchemeChange((terminalTheme) => this.applyTerminalTheme(terminalTheme));
|
|
95
|
+
}
|
|
87
96
|
applyTerminalTheme(terminalTheme) {
|
|
88
97
|
if (!this.autoSyncEnabled)
|
|
89
98
|
return;
|
|
@@ -2,7 +2,7 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
|
2
2
|
import { type EditorTheme, type MarkdownTheme, type RgbColor, type SelectListTheme, type SettingsListTheme } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { SourceInfo } from "../../../core/source-info.ts";
|
|
4
4
|
export type ThemeColor = "accent" | "border" | "borderAccent" | "borderMuted" | "success" | "error" | "warning" | "muted" | "dim" | "text" | "thinkingText" | "userMessageText" | "customMessageText" | "customMessageLabel" | "toolTitle" | "toolOutput" | "mdHeading" | "mdLink" | "mdLinkUrl" | "mdCode" | "mdCodeBlock" | "mdCodeBlockBorder" | "mdQuote" | "mdQuoteBorder" | "mdHr" | "mdListBullet" | "toolDiffAdded" | "toolDiffRemoved" | "toolDiffContext" | "syntaxComment" | "syntaxKeyword" | "syntaxFunction" | "syntaxVariable" | "syntaxString" | "syntaxNumber" | "syntaxType" | "syntaxOperator" | "syntaxPunctuation" | "thinkingOff" | "thinkingMinimal" | "thinkingLow" | "thinkingMedium" | "thinkingHigh" | "thinkingXhigh" | "thinkingMax" | "bashMode";
|
|
5
|
-
export type ThemeBg = "selectedBg" | "userMessageBg" | "customMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
|
5
|
+
export type ThemeBg = "selectedBg" | "scrollbarThumb" | "userMessageBg" | "customMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
|
6
6
|
type ColorMode = "truecolor" | "256color";
|
|
7
7
|
export declare class Theme {
|
|
8
8
|
readonly name?: string;
|
|
@@ -11,7 +11,7 @@ export declare class Theme {
|
|
|
11
11
|
private fgColors;
|
|
12
12
|
private bgColors;
|
|
13
13
|
private mode;
|
|
14
|
-
constructor(fgColors: Record<ThemeColor, string | number>, bgColors: Record<ThemeBg, string | number
|
|
14
|
+
constructor(fgColors: Record<ThemeColor, string | number>, bgColors: Record<Exclude<ThemeBg, "scrollbarThumb">, string | number> & Partial<Record<"scrollbarThumb", string | number>>, mode: ColorMode, options?: {
|
|
15
15
|
name?: string;
|
|
16
16
|
sourcePath?: string;
|
|
17
17
|
sourceInfo?: SourceInfo;
|
|
@@ -31,8 +31,9 @@ const ThemeJsonSchema = Type.Object({
|
|
|
31
31
|
dim: ColorValueSchema,
|
|
32
32
|
text: ColorValueSchema,
|
|
33
33
|
thinkingText: ColorValueSchema,
|
|
34
|
-
// Backgrounds & Content Text (11
|
|
34
|
+
// Backgrounds & Content Text (11 required, 1 optional)
|
|
35
35
|
selectedBg: ColorValueSchema,
|
|
36
|
+
scrollbarThumb: Type.Optional(ColorValueSchema),
|
|
36
37
|
userMessageBg: ColorValueSchema,
|
|
37
38
|
userMessageText: ColorValueSchema,
|
|
38
39
|
customMessageBg: ColorValueSchema,
|
|
@@ -224,7 +225,11 @@ function resolveThemeColors(colors, vars = {}) {
|
|
|
224
225
|
return resolved;
|
|
225
226
|
}
|
|
226
227
|
function withThemeColorFallbacks(colors) {
|
|
227
|
-
return {
|
|
228
|
+
return {
|
|
229
|
+
...colors,
|
|
230
|
+
thinkingMax: colors.thinkingMax ?? colors.thinkingXhigh,
|
|
231
|
+
scrollbarThumb: colors.scrollbarThumb ?? colors.selectedBg,
|
|
232
|
+
};
|
|
228
233
|
}
|
|
229
234
|
// ============================================================================
|
|
230
235
|
// Theme Class
|
|
@@ -247,7 +252,11 @@ export class Theme {
|
|
|
247
252
|
this.fgColors.set(key, fgAnsi(value, mode));
|
|
248
253
|
}
|
|
249
254
|
this.bgColors = new Map();
|
|
250
|
-
|
|
255
|
+
const backgrounds = {
|
|
256
|
+
...bgColors,
|
|
257
|
+
scrollbarThumb: bgColors.scrollbarThumb ?? bgColors.selectedBg,
|
|
258
|
+
};
|
|
259
|
+
for (const [key, value] of Object.entries(backgrounds)) {
|
|
251
260
|
this.bgColors.set(key, bgAnsi(value, mode));
|
|
252
261
|
}
|
|
253
262
|
}
|
|
@@ -463,6 +472,7 @@ function createTheme(themeJson, mode, sourcePath) {
|
|
|
463
472
|
const bgColors = {};
|
|
464
473
|
const bgColorKeys = new Set([
|
|
465
474
|
"selectedBg",
|
|
475
|
+
"scrollbarThumb",
|
|
466
476
|
"userMessageBg",
|
|
467
477
|
"customMessageBg",
|
|
468
478
|
"toolPendingBg",
|
|
@@ -588,15 +598,23 @@ export async function detectTerminalBackgroundTheme({ ui, timeoutMs, env, }) {
|
|
|
588
598
|
return detectTerminalBackgroundFromEnv({ env });
|
|
589
599
|
}
|
|
590
600
|
export async function detectTerminalThemeForAuto({ ui, timeoutMs, env, }) {
|
|
601
|
+
let colorSchemePromise;
|
|
602
|
+
try {
|
|
603
|
+
colorSchemePromise = ui.queryTerminalColorScheme?.({ timeoutMs });
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
// Fall back to OSC 11 / COLORFGBG detection when starting the color-scheme query fails.
|
|
607
|
+
}
|
|
608
|
+
const backgroundThemePromise = detectTerminalBackgroundTheme({ ui, timeoutMs, env });
|
|
591
609
|
try {
|
|
592
|
-
const colorScheme = await
|
|
610
|
+
const colorScheme = await colorSchemePromise;
|
|
593
611
|
if (colorScheme)
|
|
594
612
|
return colorScheme;
|
|
595
613
|
}
|
|
596
614
|
catch {
|
|
597
|
-
// Fall back to OSC 11 / COLORFGBG detection
|
|
615
|
+
// Fall back to the concurrently queried OSC 11 / COLORFGBG detection.
|
|
598
616
|
}
|
|
599
|
-
return (await
|
|
617
|
+
return (await backgroundThemePromise).theme;
|
|
600
618
|
}
|
|
601
619
|
export function getDefaultTheme() {
|
|
602
620
|
return detectTerminalBackgroundFromEnv().theme;
|
package/docs/keybindings.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Keybindings
|
|
2
2
|
|
|
3
|
-
All keyboard shortcuts can be customized via `~/.
|
|
3
|
+
All keyboard shortcuts can be customized via `~/.selesai/agent/keybindings.json`. Each action can be bound to one or more keys.
|
|
4
4
|
|
|
5
5
|
The config file uses the same namespaced keybinding ids that pi uses internally and that extension authors use in `keyHint()` and injected `keybindings` managers.
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ After editing `keybindings.json`, run `/reload` in pi to apply the changes witho
|
|
|
10
10
|
|
|
11
11
|
## Key Format
|
|
12
12
|
|
|
13
|
-
`modifier+key` where modifiers are `ctrl`, `shift`, `alt` (combinable) and keys are:
|
|
13
|
+
`modifier+key` where modifiers are `ctrl`, `shift`, `alt`, `super` (combinable) and keys are:
|
|
14
14
|
|
|
15
15
|
- **Letters:** `a-z`
|
|
16
16
|
- **Digits:** `0-9`
|
|
@@ -18,7 +18,9 @@ After editing `keybindings.json`, run `/reload` in pi to apply the changes witho
|
|
|
18
18
|
- **Function:** `f1`-`f12`
|
|
19
19
|
- **Symbols:** `` ` ``, `-`, `=`, `[`, `]`, `\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?`
|
|
20
20
|
|
|
21
|
-
Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `ctrl+1`, etc.
|
|
21
|
+
Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `super+k`, `ctrl+super+k`, `ctrl+1`, etc.
|
|
22
|
+
|
|
23
|
+
`super` bindings require a terminal that reports the modifier separately, typically through the Kitty keyboard protocol. They may not work in terminals without that support.
|
|
22
24
|
|
|
23
25
|
## All Actions
|
|
24
26
|
|
|
@@ -78,16 +80,42 @@ Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `ctrl+1
|
|
|
78
80
|
| `tui.select.confirm` | `enter` | Confirm selection |
|
|
79
81
|
| `tui.select.cancel` | `escape`, `ctrl+c` | Cancel selection |
|
|
80
82
|
|
|
83
|
+
### TUI Fullscreen Viewport
|
|
84
|
+
|
|
85
|
+
These actions apply when interactive mode uses fullscreen TUI mode (the Selesai default; `tuiMode: "fullscreen"` in settings or `--tui-mode fullscreen`) and target the primary transcript scroll region. Two-finger trackpad and mouse-wheel input scroll the region under the pointer, falling back to the transcript over the fixed editor/status/footer dock. Clicking an OSC 8 hyperlink opens it in the default handler. Dragging with the primary mouse button selects text and copies it to the clipboard; holding at the transcript's top or bottom edge auto-scrolls into off-screen content.
|
|
86
|
+
|
|
87
|
+
Fullscreen transcript bindings take precedence over editor bindings. The default unmodified navigation keys therefore control the transcript in fullscreen mode, while their `ctrl` variants continue to control the editor. Outside fullscreen mode, both variants control the editor.
|
|
88
|
+
|
|
89
|
+
| Key | Default mode | Fullscreen mode |
|
|
90
|
+
|-----|--------------|-----------------|
|
|
91
|
+
| `home`, `end` | Editor | Transcript |
|
|
92
|
+
| `ctrl+home`, `ctrl+end` | Editor | Editor |
|
|
93
|
+
| `pageUp`, `pageDown` | Editor | Transcript |
|
|
94
|
+
| `ctrl+pageUp`, `ctrl+pageDown` | Editor | Editor |
|
|
95
|
+
|
|
96
|
+
This routing remains configurable through the ordinary action bindings. For example, `"tui.altScreen.pageUp": "ctrl+pageUp"` makes `pageUp` control the editor and `ctrl+pageUp` control the transcript in fullscreen mode. Bind `tui.altScreen.halfPageUp` and `tui.altScreen.halfPageDown` for smaller transcript steps while keeping the full-page bindings. Setting `"tui.altScreen.pageUp": []` disables that transcript shortcut entirely. User bindings replace the defaults for that action.
|
|
97
|
+
|
|
98
|
+
| Keybinding id | Default | Description |
|
|
99
|
+
|--------|---------|-------------|
|
|
100
|
+
| `tui.altScreen.pageUp` | `pageUp` | Scroll the transcript up by one page |
|
|
101
|
+
| `tui.altScreen.pageDown` | `pageDown` | Scroll the transcript down by one page |
|
|
102
|
+
| `tui.altScreen.halfPageUp` | *(none)* | Scroll the transcript up by half a page |
|
|
103
|
+
| `tui.altScreen.halfPageDown` | *(none)* | Scroll the transcript down by half a page |
|
|
104
|
+
| `tui.altScreen.previousPrompt` | `ctrl+shift+up` | Jump to the previous marked message |
|
|
105
|
+
| `tui.altScreen.nextPrompt` | `ctrl+shift+down` | Jump to the next marked message |
|
|
106
|
+
| `tui.altScreen.top` | `home` | Scroll to the beginning of the transcript |
|
|
107
|
+
| `tui.altScreen.bottom` | `end` | Scroll to the transcript end and follow new output |
|
|
108
|
+
|
|
81
109
|
### Application
|
|
82
110
|
|
|
83
111
|
| Keybinding id | Default | Description |
|
|
84
112
|
|--------|---------|-------------|
|
|
85
113
|
| `app.interrupt` | `escape` | Cancel / abort |
|
|
86
|
-
| `app.clear` | `ctrl+c` | Clear editor |
|
|
114
|
+
| `app.clear` | `ctrl+c` | Clear editor (first) / exit (second) |
|
|
87
115
|
| `app.exit` | `ctrl+d` | Exit (when editor empty) |
|
|
88
116
|
| `app.suspend` | `ctrl+z` (none on Windows) | Suspend to background |
|
|
89
117
|
| `app.editor.external` | `ctrl+g` | Open in external editor (`$VISUAL` or `$EDITOR`) |
|
|
90
|
-
| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows) | Paste image from clipboard |
|
|
118
|
+
| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows) | Paste image or text from clipboard |
|
|
91
119
|
|
|
92
120
|
### Sessions
|
|
93
121
|
|
|
@@ -153,7 +181,7 @@ Used inside the scoped models selector (opened via `/scoped-models`).
|
|
|
153
181
|
|
|
154
182
|
## Custom Configuration
|
|
155
183
|
|
|
156
|
-
Create `~/.
|
|
184
|
+
Create `~/.selesai/agent/keybindings.json`:
|
|
157
185
|
|
|
158
186
|
```json
|
|
159
187
|
{
|
|
@@ -165,7 +193,7 @@ Create `~/.pi/agent/keybindings.json`:
|
|
|
165
193
|
|
|
166
194
|
Each action can have a single key or an array of keys. User config overrides defaults.
|
|
167
195
|
|
|
168
|
-
On native Windows, `app.suspend` has no default binding because Windows terminals do not support Unix job control. If you bind it manually,
|
|
196
|
+
On native Windows, `app.suspend` has no default binding because Windows terminals do not support Unix job control. If you bind it manually, Selesai shows a status message instead of suspending. In WSL, the normal Linux `ctrl+z`/`fg` behavior still applies.
|
|
169
197
|
|
|
170
198
|
### Emacs Example
|
|
171
199
|
|