@selesai/code 0.8.7 → 0.8.8
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 +15 -1
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.js +11 -0
- package/dist/cli/args.test.d.ts +1 -0
- package/dist/cli/args.test.js +24 -0
- package/dist/core/agent-session.d.ts +4 -1
- package/dist/core/agent-session.js +5 -6
- package/dist/core/experimental.d.ts +4 -0
- package/dist/core/experimental.js +4 -0
- package/dist/core/extensions/types.d.ts +4 -0
- package/dist/core/keybindings.d.ts +24 -0
- package/dist/core/model-config.d.ts +5 -0
- package/dist/core/model-config.js +1 -0
- package/dist/core/project-trust.js +2 -2
- package/dist/core/sdk.d.ts +5 -3
- package/dist/core/sdk.js +2 -1
- package/dist/core/session-manager.js +2 -2
- package/dist/core/settings-manager.d.ts +6 -0
- package/dist/core/settings-manager.js +12 -0
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/tools/bash.js +2 -0
- package/dist/core/tools/edit.js +2 -0
- package/dist/core/tools/find.js +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/read.js +2 -0
- package/dist/core/tools/write.js +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +0 -18
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +26 -0
- package/dist/extensions/tokenin-onboarding.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/main.js +4 -0
- package/dist/modes/interactive/components/model-selector.js +2 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/tool-execution.js +10 -1
- package/dist/modes/interactive/interactive-mode.d.ts +8 -2
- package/dist/modes/interactive/interactive-mode.js +81 -25
- package/dist/modes/interactive/model-catalog-refresh.d.ts +6 -0
- package/dist/modes/interactive/model-catalog-refresh.js +33 -0
- package/dist/modes/interactive/theme/dark.json +2 -0
- package/dist/modes/interactive/theme/light.json +2 -0
- package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
- package/dist/modes/interactive/theme/theme-controller.js +25 -11
- package/dist/modes/interactive/theme/theme-schema.json +9 -1
- package/dist/modes/interactive/theme/theme.d.ts +5 -3
- package/dist/modes/interactive/theme/theme.js +12 -2
- package/dist/utils/abort.d.ts +4 -0
- package/dist/utils/abort.js +47 -0
- package/dist/utils/tools-manager.d.ts +5 -1
- package/dist/utils/tools-manager.js +12 -21
- package/package.json +6 -6
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { modelsAreEqual } from "@earendil-works/pi-ai";
|
|
2
2
|
import { Container, fuzzyFilter, getKeybindings, Input, Spacer, Text, } from "@earendil-works/pi-tui";
|
|
3
3
|
import { getModelSelectorSearchText } from "../model-search.js";
|
|
4
|
+
import { refreshModelCatalogs } from "../model-catalog-refresh.js";
|
|
4
5
|
import { theme } from "../theme/theme.js";
|
|
5
6
|
import { DynamicBorder } from "./dynamic-border.js";
|
|
6
7
|
import { keyHint } from "./keybinding-hints.js";
|
|
@@ -123,7 +124,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
123
124
|
this.refreshAbortController.abort();
|
|
124
125
|
}, timeoutMs);
|
|
125
126
|
try {
|
|
126
|
-
const result = await this.modelRuntime
|
|
127
|
+
const result = await refreshModelCatalogs(this.modelRuntime, this.refreshAbortController.signal);
|
|
127
128
|
if (this.closed)
|
|
128
129
|
return;
|
|
129
130
|
this.refreshStatusMessage = "";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { Transport } from "@earendil-works/pi-ai";
|
|
3
3
|
import { Container, type ScrollViewScrollbar, SettingsList } from "@earendil-works/pi-tui";
|
|
4
|
-
import type { DefaultProjectTrust, TuiMode, WarningSettings } from "../../../core/settings-manager.ts";
|
|
4
|
+
import type { DefaultProjectTrust, FullscreenExitOutput, TuiMode, WarningSettings } from "../../../core/settings-manager.ts";
|
|
5
5
|
import { type TerminalTheme } from "../theme/theme.ts";
|
|
6
6
|
export interface SettingsConfig {
|
|
7
7
|
autoCompact: boolean;
|
|
@@ -35,6 +35,7 @@ export interface SettingsConfig {
|
|
|
35
35
|
clearOnShrink: boolean;
|
|
36
36
|
showTerminalProgress: boolean;
|
|
37
37
|
tuiMode: TuiMode;
|
|
38
|
+
fullscreenExitOutput: FullscreenExitOutput;
|
|
38
39
|
fullscreenScrollbar: ScrollViewScrollbar;
|
|
39
40
|
warnings: WarningSettings;
|
|
40
41
|
}
|
|
@@ -68,6 +69,7 @@ export interface SettingsCallbacks {
|
|
|
68
69
|
onClearOnShrinkChange: (enabled: boolean) => void;
|
|
69
70
|
onShowTerminalProgressChange: (enabled: boolean) => void;
|
|
70
71
|
onTuiModeChange: (mode: TuiMode) => void;
|
|
72
|
+
onFullscreenExitOutputChange: (output: FullscreenExitOutput) => void;
|
|
71
73
|
onFullscreenScrollbarChange: (mode: ScrollViewScrollbar) => void;
|
|
72
74
|
onWarningsChange: (warnings: WarningSettings) => void;
|
|
73
75
|
onCancel: () => void;
|
|
@@ -409,6 +409,13 @@ export class SettingsSelectorComponent extends Container {
|
|
|
409
409
|
currentValue: config.tuiMode,
|
|
410
410
|
values: ["fullscreen", "regular"],
|
|
411
411
|
},
|
|
412
|
+
{
|
|
413
|
+
id: "fullscreen-exit-output",
|
|
414
|
+
label: "Fullscreen exit output",
|
|
415
|
+
description: "Print the transcript or only a session resume hint when exiting fullscreen mode",
|
|
416
|
+
currentValue: config.fullscreenExitOutput,
|
|
417
|
+
values: ["transcript", "resume-hint"],
|
|
418
|
+
},
|
|
412
419
|
{
|
|
413
420
|
id: "fullscreen-scrollbar",
|
|
414
421
|
label: "Fullscreen scrollbar",
|
|
@@ -603,6 +610,9 @@ export class SettingsSelectorComponent extends Container {
|
|
|
603
610
|
case "tui-mode":
|
|
604
611
|
callbacks.onTuiModeChange(newValue);
|
|
605
612
|
break;
|
|
613
|
+
case "fullscreen-exit-output":
|
|
614
|
+
callbacks.onFullscreenExitOutputChange(newValue);
|
|
615
|
+
break;
|
|
606
616
|
case "fullscreen-scrollbar":
|
|
607
617
|
callbacks.onFullscreenScrollbarChange(newValue);
|
|
608
618
|
break;
|
|
@@ -2,7 +2,9 @@ import { Box, Container, getCapabilities, Image, Spacer, Text } from "@earendil-
|
|
|
2
2
|
import { createAllToolDefinitions } from "../../../core/tools/index.js";
|
|
3
3
|
import { getTextOutput as getRenderedTextOutput } from "../../../core/tools/render-utils.js";
|
|
4
4
|
import { convertToPng } from "../../../utils/image-convert.js";
|
|
5
|
+
import { keyHint } from "./keybinding-hints.js";
|
|
5
6
|
import { theme } from "../theme/theme.js";
|
|
7
|
+
const FALLBACK_PREVIEW_LINES = 10;
|
|
6
8
|
export class ToolExecutionComponent extends Container {
|
|
7
9
|
contentBox;
|
|
8
10
|
contentText;
|
|
@@ -111,7 +113,14 @@ export class ToolExecutionComponent extends Container {
|
|
|
111
113
|
if (!output) {
|
|
112
114
|
return undefined;
|
|
113
115
|
}
|
|
114
|
-
|
|
116
|
+
const lines = output.split("\n");
|
|
117
|
+
const displayLines = this.expanded ? lines : lines.slice(0, FALLBACK_PREVIEW_LINES);
|
|
118
|
+
const remaining = lines.length - displayLines.length;
|
|
119
|
+
let text = displayLines.map((line) => theme.fg("toolOutput", line)).join("\n");
|
|
120
|
+
if (remaining > 0) {
|
|
121
|
+
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
|
|
122
|
+
}
|
|
123
|
+
return new Text(text, 0, 0);
|
|
115
124
|
}
|
|
116
125
|
updateArgs(args) {
|
|
117
126
|
this.args = args;
|
|
@@ -7,7 +7,7 @@ import type { Terminal } from "@earendil-works/pi-tui";
|
|
|
7
7
|
import { type TUI, TuiAltScreen, TuiMainScreen } from "@earendil-works/pi-tui";
|
|
8
8
|
import { type AgentSessionRuntime } from "../../core/agent-session-runtime.ts";
|
|
9
9
|
import { SessionManager } from "../../core/session-manager.ts";
|
|
10
|
-
import type { TuiMode } from "../../core/settings-manager.ts";
|
|
10
|
+
import type { FullscreenExitOutput, TuiMode } from "../../core/settings-manager.ts";
|
|
11
11
|
import { type LatestPiRelease } from "../../utils/version-check.ts";
|
|
12
12
|
type PendingUserInput = {
|
|
13
13
|
text: string;
|
|
@@ -34,6 +34,8 @@ export interface InteractiveModeOptions {
|
|
|
34
34
|
verbose?: boolean;
|
|
35
35
|
/** TUI layout mode. */
|
|
36
36
|
tuiMode?: TuiMode;
|
|
37
|
+
/** Initial interactive theme setting for this invocation. */
|
|
38
|
+
initialThemeSetting?: string;
|
|
37
39
|
}
|
|
38
40
|
interface InteractiveTuiOptions {
|
|
39
41
|
tuiMode: TuiMode;
|
|
@@ -93,6 +95,7 @@ export declare class InteractiveMode {
|
|
|
93
95
|
private streamingMessage;
|
|
94
96
|
private pendingTools;
|
|
95
97
|
private toolOutputExpanded;
|
|
98
|
+
private managedToolStatusStarted;
|
|
96
99
|
private hideThinkingBlock;
|
|
97
100
|
private outputPad;
|
|
98
101
|
private unsubscribe?;
|
|
@@ -271,11 +274,14 @@ export declare class InteractiveMode {
|
|
|
271
274
|
private handleClipboardPaste;
|
|
272
275
|
private consumePendingInputImages;
|
|
273
276
|
private handleGitCommand;
|
|
277
|
+
private handleStartupSubmit;
|
|
274
278
|
private setupEditorSubmitHandler;
|
|
275
279
|
private subscribeToAgent;
|
|
276
280
|
private handleEvent;
|
|
277
281
|
/** Extract text content from a user message */
|
|
278
282
|
private getUserMessageText;
|
|
283
|
+
/** Show a managed-tool status update in the chat. */
|
|
284
|
+
private showManagedToolStatus;
|
|
279
285
|
/**
|
|
280
286
|
* Show a status message in the chat.
|
|
281
287
|
*
|
|
@@ -425,6 +431,6 @@ export declare class InteractiveMode {
|
|
|
425
431
|
private checkDaxnutsEasterEgg;
|
|
426
432
|
private handleBashCommand;
|
|
427
433
|
private handleCompactCommand;
|
|
428
|
-
stop(): void;
|
|
434
|
+
stop(fullscreenExitOutput?: FullscreenExitOutput): void;
|
|
429
435
|
}
|
|
430
436
|
export {};
|
|
@@ -69,6 +69,7 @@ import { UserMessageComponent } from "./components/user-message.js";
|
|
|
69
69
|
import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
|
|
70
70
|
import { editInExternalEditor } from "./external-editor.js";
|
|
71
71
|
import { getModelSearchText } from "./model-search.js";
|
|
72
|
+
import { refreshModelCatalogs } from "./model-catalog-refresh.js";
|
|
72
73
|
import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, onThemeChange, setRegisteredThemes, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
|
|
73
74
|
import { InteractiveThemeController } from "./theme/theme-controller.js";
|
|
74
75
|
function isExpandable(obj) {
|
|
@@ -168,9 +169,21 @@ function formatLoginProviderCompletionDescription(provider) {
|
|
|
168
169
|
export function createInteractiveTui(options) {
|
|
169
170
|
const terminal = options.terminal ?? new ProcessTerminal();
|
|
170
171
|
if (options.tuiMode === "fullscreen") {
|
|
172
|
+
const styleSearchMatch = (text) => theme.bg("searchMatchBg", theme.fg("searchMatchText", text));
|
|
171
173
|
return new TuiAltScreen(terminal, options.showHardwareCursor, options.logDirectory, {
|
|
174
|
+
searchMatchStyle: (text) => theme.underline(styleSearchMatch(text)),
|
|
175
|
+
searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))),
|
|
172
176
|
openUrl: openBrowser,
|
|
173
177
|
onRightClickPaste: options.onRightClickPaste,
|
|
178
|
+
copySelection: async (text) => {
|
|
179
|
+
try {
|
|
180
|
+
await copyToClipboard(text);
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
},
|
|
174
187
|
});
|
|
175
188
|
}
|
|
176
189
|
return new TuiMainScreen(terminal, options.showHardwareCursor, options.logDirectory);
|
|
@@ -258,6 +271,7 @@ export class InteractiveMode {
|
|
|
258
271
|
pendingTools = new Map();
|
|
259
272
|
// Tool output expansion state
|
|
260
273
|
toolOutputExpanded = false;
|
|
274
|
+
managedToolStatusStarted = false;
|
|
261
275
|
// Thinking block visibility state
|
|
262
276
|
hideThinkingBlock = false;
|
|
263
277
|
outputPad = 1;
|
|
@@ -369,7 +383,12 @@ export class InteractiveMode {
|
|
|
369
383
|
this.outputPad = this.settingsManager.getOutputPad();
|
|
370
384
|
// Register themes from resource loader and initialize
|
|
371
385
|
setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
|
|
372
|
-
this.themeController = new InteractiveThemeController(this.ui,
|
|
386
|
+
this.themeController = new InteractiveThemeController(this.ui, {
|
|
387
|
+
getSettingsManager: () => this.settingsManager,
|
|
388
|
+
showError: (message) => this.showError(message),
|
|
389
|
+
onChanged: () => this.updateEditorBorderColor(),
|
|
390
|
+
initialThemeSetting: options.initialThemeSetting,
|
|
391
|
+
});
|
|
373
392
|
}
|
|
374
393
|
getAutocompleteSourceTag(sourceInfo) {
|
|
375
394
|
if (!sourceInfo) {
|
|
@@ -530,14 +549,14 @@ export class InteractiveMode {
|
|
|
530
549
|
tui.setLayoutRoot(this.fullscreenLayoutRoot);
|
|
531
550
|
}
|
|
532
551
|
}
|
|
533
|
-
stopInteractiveTui() {
|
|
534
|
-
if (this.renderer.mode === "fullscreen") {
|
|
552
|
+
stopInteractiveTui(fullscreenExitOutput) {
|
|
553
|
+
if (this.renderer.mode === "fullscreen" && fullscreenExitOutput === "transcript") {
|
|
535
554
|
while (this.renderer.hasOverlayEntries)
|
|
536
555
|
this.renderer.hideOverlay();
|
|
537
556
|
this.switchTuiMode("regular", false, false);
|
|
538
557
|
this.renderer.renderNow();
|
|
539
558
|
}
|
|
540
|
-
this.ui.stop();
|
|
559
|
+
this.ui.stop({ preserveScreen: this.renderer.mode === "fullscreen" });
|
|
541
560
|
}
|
|
542
561
|
switchTuiMode(mode, restoreProgress = true, startRenderer = true) {
|
|
543
562
|
const previousUi = this.renderer;
|
|
@@ -594,10 +613,6 @@ export class InteractiveMode {
|
|
|
594
613
|
this.registerSignalHandlers();
|
|
595
614
|
// Load changelog (only show new entries, skip for resumed sessions)
|
|
596
615
|
this.changelogMarkdown = this.getChangelogForDisplay();
|
|
597
|
-
// Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
|
|
598
|
-
// Both are needed: fd for autocomplete, rg for grep tool and bash commands
|
|
599
|
-
const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]);
|
|
600
|
-
this.fdPath = fdPath;
|
|
601
616
|
if (this.session.scopedModels.length > 0 && (this.options.verbose || !this.settingsManager.getQuietStartup())) {
|
|
602
617
|
const modelList = this.session.scopedModels
|
|
603
618
|
.map((sm) => {
|
|
@@ -641,9 +656,11 @@ export class InteractiveMode {
|
|
|
641
656
|
this.widgetContainerBelow,
|
|
642
657
|
this.footerContainer,
|
|
643
658
|
]);
|
|
659
|
+
// Accept text while startup completes, but only enable interrupt, exit, and submission feedback.
|
|
660
|
+
this.defaultEditor.onAction("app.clear", () => this.handleCtrlC());
|
|
661
|
+
this.defaultEditor.onCtrlD = () => this.handleCtrlD();
|
|
662
|
+
this.defaultEditor.onSubmit = (text) => this.handleStartupSubmit(text);
|
|
644
663
|
this.ui.setFocus(this.editor);
|
|
645
|
-
this.setupKeyHandlers();
|
|
646
|
-
this.setupEditorSubmitHandler();
|
|
647
664
|
// Start the UI before initializing extensions so session_start handlers can use interactive dialogs
|
|
648
665
|
this.ui.start();
|
|
649
666
|
this.isInitialized = true;
|
|
@@ -695,6 +712,18 @@ export class InteractiveMode {
|
|
|
695
712
|
this.headerContainer.addChild(this.builtInHeader);
|
|
696
713
|
}
|
|
697
714
|
this.ui.requestRender();
|
|
715
|
+
// Ensure fd and rg are available after mounting the TUI (downloads if missing, adds to PATH via getBinDir)
|
|
716
|
+
// so slow downloads do not make startup appear frozen.
|
|
717
|
+
// Both are needed: fd for autocomplete, rg for grep tool and bash commands.
|
|
718
|
+
const [fdPath] = await Promise.all([
|
|
719
|
+
ensureTool("fd", (status) => this.showManagedToolStatus(status)),
|
|
720
|
+
ensureTool("rg", (status) => this.showManagedToolStatus(status)),
|
|
721
|
+
]);
|
|
722
|
+
this.fdPath = fdPath;
|
|
723
|
+
// Enable the remaining input handlers only after managed-tool setup completes.
|
|
724
|
+
this.setupKeyHandlers();
|
|
725
|
+
this.setupEditorSubmitHandler();
|
|
726
|
+
this.ui.requestRender();
|
|
698
727
|
// Initialize extensions first so resources are shown before messages
|
|
699
728
|
await this.rebindCurrentSession();
|
|
700
729
|
// Render initial messages AFTER showing loaded resources
|
|
@@ -732,10 +761,12 @@ export class InteractiveMode {
|
|
|
732
761
|
async run() {
|
|
733
762
|
await this.init();
|
|
734
763
|
if (!process.env.PI_OFFLINE) {
|
|
735
|
-
|
|
736
|
-
|
|
764
|
+
const controller = new AbortController();
|
|
765
|
+
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
766
|
+
void refreshModelCatalogs(this.session.modelRuntime, controller.signal)
|
|
737
767
|
.then(() => this.updateAvailableProviderCount())
|
|
738
|
-
.catch(() => { })
|
|
768
|
+
.catch(() => { })
|
|
769
|
+
.finally(() => clearTimeout(timeout));
|
|
739
770
|
}
|
|
740
771
|
// Start version check asynchronously
|
|
741
772
|
checkForNewPiVersion(this.version, PACKAGE_NAME).then((newRelease) => {
|
|
@@ -1484,7 +1515,7 @@ export class InteractiveMode {
|
|
|
1484
1515
|
const message = error instanceof Error ? error.message : String(error);
|
|
1485
1516
|
this.showError(`${prefix}: ${message}`);
|
|
1486
1517
|
stopThemeWatcher();
|
|
1487
|
-
this.stop();
|
|
1518
|
+
this.stop("transcript");
|
|
1488
1519
|
process.exit(1);
|
|
1489
1520
|
}
|
|
1490
1521
|
renderCurrentSessionState() {
|
|
@@ -2307,6 +2338,10 @@ export class InteractiveMode {
|
|
|
2307
2338
|
}
|
|
2308
2339
|
await this.session.prompt(prompt, { images });
|
|
2309
2340
|
}
|
|
2341
|
+
handleStartupSubmit(text) {
|
|
2342
|
+
this.editor.setText(text);
|
|
2343
|
+
this.showStatus("Startup is still in progress");
|
|
2344
|
+
}
|
|
2310
2345
|
setupEditorSubmitHandler() {
|
|
2311
2346
|
this.defaultEditor.onSubmit = async (text) => {
|
|
2312
2347
|
text = text.trim();
|
|
@@ -2794,6 +2829,19 @@ export class InteractiveMode {
|
|
|
2794
2829
|
: message.content.filter((c) => c.type === "text");
|
|
2795
2830
|
return textBlocks.map((c) => c.text).join("");
|
|
2796
2831
|
}
|
|
2832
|
+
/** Show a managed-tool status update in the chat. */
|
|
2833
|
+
showManagedToolStatus(status) {
|
|
2834
|
+
if (!this.managedToolStatusStarted) {
|
|
2835
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
2836
|
+
this.managedToolStatusStarted = true;
|
|
2837
|
+
}
|
|
2838
|
+
const message = status.type === "warning" ? `Warning: ${status.message}` : status.message;
|
|
2839
|
+
const color = status.type === "warning" ? "warning" : "dim";
|
|
2840
|
+
this.chatContainer.addChild(new Text(theme.fg(color, message), 1, 0));
|
|
2841
|
+
this.lastStatusSpacer = undefined;
|
|
2842
|
+
this.lastStatusText = undefined;
|
|
2843
|
+
this.ui.requestRender();
|
|
2844
|
+
}
|
|
2797
2845
|
/**
|
|
2798
2846
|
* Show a status message in the chat.
|
|
2799
2847
|
*
|
|
@@ -3165,7 +3213,7 @@ export class InteractiveMode {
|
|
|
3165
3213
|
this.ui.stop();
|
|
3166
3214
|
}
|
|
3167
3215
|
catch { }
|
|
3168
|
-
console.error(
|
|
3216
|
+
console.error(`${APP_NAME} exiting due to uncaughtException:`);
|
|
3169
3217
|
console.error(error);
|
|
3170
3218
|
process.exit(1);
|
|
3171
3219
|
}
|
|
@@ -3626,7 +3674,7 @@ export class InteractiveMode {
|
|
|
3626
3674
|
httpIdleTimeoutMs: this.settingsManager.getHttpIdleTimeoutMs(),
|
|
3627
3675
|
thinkingLevel: this.session.thinkingLevel,
|
|
3628
3676
|
availableThinkingLevels: this.session.getAvailableThinkingLevels(),
|
|
3629
|
-
currentTheme: this.
|
|
3677
|
+
currentTheme: this.themeController.getThemeSelection() || "dark",
|
|
3630
3678
|
terminalTheme: this.themeController.getTerminalTheme(),
|
|
3631
3679
|
availableThemes: getAvailableThemes(),
|
|
3632
3680
|
hideThinkingBlock: this.hideThinkingBlock,
|
|
@@ -3644,6 +3692,7 @@ export class InteractiveMode {
|
|
|
3644
3692
|
clearOnShrink: this.settingsManager.getClearOnShrink(),
|
|
3645
3693
|
showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
|
|
3646
3694
|
tuiMode: this.settingsManager.getTuiMode(),
|
|
3695
|
+
fullscreenExitOutput: this.settingsManager.getFullscreenExitOutput(),
|
|
3647
3696
|
fullscreenScrollbar: this.settingsManager.getFullscreenScrollbar(),
|
|
3648
3697
|
warnings: this.settingsManager.getWarnings(),
|
|
3649
3698
|
}, {
|
|
@@ -3701,7 +3750,7 @@ export class InteractiveMode {
|
|
|
3701
3750
|
},
|
|
3702
3751
|
onThemeChange: (themeSetting) => {
|
|
3703
3752
|
this.settingsManager.setTheme(themeSetting);
|
|
3704
|
-
void this.themeController.
|
|
3753
|
+
void this.themeController.setThemeSetting(themeSetting);
|
|
3705
3754
|
},
|
|
3706
3755
|
onThemePreview: (themeName) => this.themeController.preview(themeName),
|
|
3707
3756
|
onHideThinkingBlockChange: (hidden) => {
|
|
@@ -3795,6 +3844,9 @@ export class InteractiveMode {
|
|
|
3795
3844
|
this.statusContainer.clear();
|
|
3796
3845
|
this.showStatus(`TUI mode: ${mode}`);
|
|
3797
3846
|
},
|
|
3847
|
+
onFullscreenExitOutputChange: (output) => {
|
|
3848
|
+
this.settingsManager.setFullscreenExitOutput(output);
|
|
3849
|
+
},
|
|
3798
3850
|
onFullscreenScrollbarChange: (mode) => {
|
|
3799
3851
|
this.settingsManager.setFullscreenScrollbar(mode);
|
|
3800
3852
|
this.transcriptScrollView?.setScrollbar(mode);
|
|
@@ -3842,7 +3894,8 @@ export class InteractiveMode {
|
|
|
3842
3894
|
return this.session.scopedModels.map((scoped) => scoped.model);
|
|
3843
3895
|
}
|
|
3844
3896
|
try {
|
|
3845
|
-
|
|
3897
|
+
const controller = new AbortController();
|
|
3898
|
+
await refreshModelCatalogs(this.session.modelRuntime, controller.signal);
|
|
3846
3899
|
return [...(await this.session.modelRuntime.getAvailable())];
|
|
3847
3900
|
}
|
|
3848
3901
|
catch {
|
|
@@ -3919,7 +3972,7 @@ export class InteractiveMode {
|
|
|
3919
3972
|
onSelect: (selection) => {
|
|
3920
3973
|
trustStore.setMany(selection.updates);
|
|
3921
3974
|
done();
|
|
3922
|
-
this.showStatus(`Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart
|
|
3975
|
+
this.showStatus(`Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart ${APP_NAME} for this to take effect.`);
|
|
3923
3976
|
},
|
|
3924
3977
|
onCancel: () => {
|
|
3925
3978
|
done();
|
|
@@ -3954,7 +4007,8 @@ export class InteractiveMode {
|
|
|
3954
4007
|
}
|
|
3955
4008
|
async showModelsSelector() {
|
|
3956
4009
|
// Get all available models
|
|
3957
|
-
|
|
4010
|
+
const controller = new AbortController();
|
|
4011
|
+
await refreshModelCatalogs(this.session.modelRuntime, controller.signal);
|
|
3958
4012
|
const allModels = [...(await this.session.modelRuntime.getAvailable())];
|
|
3959
4013
|
const allModelIds = new Set(allModels.map((model) => `${model.provider}/${model.id}`));
|
|
3960
4014
|
const configuredPatterns = this.settingsManager.getEnabledModels();
|
|
@@ -4507,7 +4561,7 @@ export class InteractiveMode {
|
|
|
4507
4561
|
this.ui.requestRender();
|
|
4508
4562
|
};
|
|
4509
4563
|
const dialog = new LoginDialogComponent(this.ui, providerOption.id, () => restoreEditor(), providerOption.name, `${providerOption.name} setup`);
|
|
4510
|
-
dialog.showInfo(`${providerOption.method?.name ?? "Authentication"} is configured outside
|
|
4564
|
+
dialog.showInfo(`${providerOption.method?.name ?? "Authentication"} is configured outside ${APP_NAME}.`, [], true);
|
|
4511
4565
|
this.editorContainer.clear();
|
|
4512
4566
|
this.editorContainer.addChild(dialog);
|
|
4513
4567
|
this.ui.setFocus(dialog);
|
|
@@ -4794,7 +4848,9 @@ export class InteractiveMode {
|
|
|
4794
4848
|
this.showStatus(`Session exported to: ${filePath}`);
|
|
4795
4849
|
}
|
|
4796
4850
|
else {
|
|
4797
|
-
const filePath = await this.session.exportToHtml(outputPath
|
|
4851
|
+
const filePath = await this.session.exportToHtml(outputPath, {
|
|
4852
|
+
themeName: theme.name,
|
|
4853
|
+
});
|
|
4798
4854
|
this.showStatus(`Session exported to: ${filePath}`);
|
|
4799
4855
|
}
|
|
4800
4856
|
}
|
|
@@ -4885,7 +4941,7 @@ export class InteractiveMode {
|
|
|
4885
4941
|
// Export to a temp file
|
|
4886
4942
|
const tmpFile = path.join(os.tmpdir(), "session.html");
|
|
4887
4943
|
try {
|
|
4888
|
-
await this.session.exportToHtml(tmpFile);
|
|
4944
|
+
await this.session.exportToHtml(tmpFile, { themeName: theme.name });
|
|
4889
4945
|
}
|
|
4890
4946
|
catch (error) {
|
|
4891
4947
|
this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -5326,7 +5382,7 @@ export class InteractiveMode {
|
|
|
5326
5382
|
// Ignore, will be emitted as an event
|
|
5327
5383
|
}
|
|
5328
5384
|
}
|
|
5329
|
-
stop() {
|
|
5385
|
+
stop(fullscreenExitOutput = this.settingsManager.getFullscreenExitOutput()) {
|
|
5330
5386
|
if (this.settingsManager.getShowTerminalProgress()) {
|
|
5331
5387
|
this.ui.terminal.setProgress(false);
|
|
5332
5388
|
}
|
|
@@ -5339,7 +5395,7 @@ export class InteractiveMode {
|
|
|
5339
5395
|
this.unsubscribe();
|
|
5340
5396
|
}
|
|
5341
5397
|
if (this.isInitialized) {
|
|
5342
|
-
this.stopInteractiveTui();
|
|
5398
|
+
this.stopInteractiveTui(fullscreenExitOutput);
|
|
5343
5399
|
this.isInitialized = false;
|
|
5344
5400
|
}
|
|
5345
5401
|
this.unregisterSignalHandlers();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ModelsRefreshResult } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ModelRuntime } from "../../core/model-runtime.ts";
|
|
3
|
+
type ModelCatalogRuntime = Pick<ModelRuntime, "refresh">;
|
|
4
|
+
/** Share concurrent interactive all-catalog refreshes while keeping each caller's cancellation independent. */
|
|
5
|
+
export declare function refreshModelCatalogs(modelRuntime: ModelCatalogRuntime, signal: AbortSignal): Promise<ModelsRefreshResult>;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { raceWithAbortSignal } from "../../utils/abort.js";
|
|
2
|
+
class ModelCatalogRefreshCoordinator {
|
|
3
|
+
activeByRuntime = new WeakMap();
|
|
4
|
+
refresh(modelRuntime, signal) {
|
|
5
|
+
signal.throwIfAborted();
|
|
6
|
+
let active = this.activeByRuntime.get(modelRuntime);
|
|
7
|
+
if (!active) {
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
let created;
|
|
10
|
+
const operation = modelRuntime.refresh({ signal: controller.signal });
|
|
11
|
+
const promise = raceWithAbortSignal(operation, controller.signal).finally(() => {
|
|
12
|
+
if (this.activeByRuntime.get(modelRuntime) === created) {
|
|
13
|
+
this.activeByRuntime.delete(modelRuntime);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
created = { controller, promise, waiters: 0 };
|
|
17
|
+
active = created;
|
|
18
|
+
this.activeByRuntime.set(modelRuntime, active);
|
|
19
|
+
}
|
|
20
|
+
active.waiters++;
|
|
21
|
+
return raceWithAbortSignal(active.promise, signal).finally(() => {
|
|
22
|
+
active.waiters--;
|
|
23
|
+
if (active.waiters === 0 && this.activeByRuntime.get(modelRuntime) === active) {
|
|
24
|
+
active.controller.abort();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const modelCatalogRefreshCoordinator = new ModelCatalogRefreshCoordinator();
|
|
30
|
+
/** Share concurrent interactive all-catalog refreshes while keeping each caller's cancellation independent. */
|
|
31
|
+
export function refreshModelCatalogs(modelRuntime, signal) {
|
|
32
|
+
return modelCatalogRefreshCoordinator.refresh(modelRuntime, signal);
|
|
33
|
+
}
|
|
@@ -7,17 +7,25 @@ type ThemeResult = {
|
|
|
7
7
|
};
|
|
8
8
|
export declare class InteractiveThemeController {
|
|
9
9
|
private readonly ui;
|
|
10
|
-
private readonly
|
|
10
|
+
private readonly getSettingsManager;
|
|
11
11
|
private readonly showError;
|
|
12
12
|
private readonly onChanged;
|
|
13
|
+
private currentThemeSetting;
|
|
13
14
|
private terminalTheme;
|
|
14
15
|
private activeThemeName;
|
|
15
16
|
private autoSyncEnabled;
|
|
16
17
|
private terminalColorSchemeUnsubscribe;
|
|
17
|
-
constructor(ui: TUI,
|
|
18
|
+
constructor(ui: TUI, options: {
|
|
19
|
+
getSettingsManager: () => SettingsManager;
|
|
20
|
+
showError: (message: string) => void;
|
|
21
|
+
onChanged: () => void;
|
|
22
|
+
initialThemeSetting?: string;
|
|
23
|
+
});
|
|
18
24
|
rebindTui(): void;
|
|
19
25
|
applyFromSettings(): Promise<void>;
|
|
26
|
+
getThemeSelection(): string | undefined;
|
|
20
27
|
setThemeName(themeName: string, showError?: boolean): ThemeResult;
|
|
28
|
+
setThemeSetting(themeSetting: string): Promise<void>;
|
|
21
29
|
setThemeInstance(themeInstance: Theme): ThemeResult;
|
|
22
30
|
preview(themeSettingOrName: string): void;
|
|
23
31
|
disableAutoSync(): void;
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { detectTerminalBackgroundFromEnv, detectTerminalBackgroundTheme, detectTerminalThemeForAuto, initTheme, parseAutoThemeSetting, resolveThemeSetting, setTheme, setThemeInstance, } from "./theme.js";
|
|
2
2
|
export class InteractiveThemeController {
|
|
3
3
|
ui;
|
|
4
|
-
|
|
4
|
+
getSettingsManager;
|
|
5
5
|
showError;
|
|
6
6
|
onChanged;
|
|
7
|
+
currentThemeSetting;
|
|
7
8
|
terminalTheme = detectTerminalBackgroundFromEnv().theme;
|
|
8
9
|
activeThemeName;
|
|
9
10
|
autoSyncEnabled = false;
|
|
10
11
|
terminalColorSchemeUnsubscribe;
|
|
11
|
-
constructor(ui,
|
|
12
|
+
constructor(ui, options) {
|
|
12
13
|
this.ui = ui;
|
|
13
|
-
this.
|
|
14
|
-
this.showError = showError;
|
|
15
|
-
this.onChanged = onChanged;
|
|
16
|
-
this.
|
|
14
|
+
this.getSettingsManager = options.getSettingsManager;
|
|
15
|
+
this.showError = options.showError;
|
|
16
|
+
this.onChanged = options.onChanged;
|
|
17
|
+
this.currentThemeSetting = options.initialThemeSetting;
|
|
18
|
+
this.activeThemeName = resolveThemeSetting(this.currentThemeSetting ?? this.getSettingsManager().getThemeSetting(), this.terminalTheme);
|
|
17
19
|
initTheme(this.activeThemeName, true);
|
|
18
20
|
this.bindTerminalColorSchemeListener();
|
|
19
21
|
}
|
|
@@ -23,7 +25,8 @@ export class InteractiveThemeController {
|
|
|
23
25
|
this.ui.setTerminalColorSchemeNotifications(this.autoSyncEnabled);
|
|
24
26
|
}
|
|
25
27
|
async applyFromSettings() {
|
|
26
|
-
const
|
|
28
|
+
const settingsManager = this.getSettingsManager();
|
|
29
|
+
const themeSetting = this.currentThemeSetting ?? settingsManager.getThemeSetting();
|
|
27
30
|
const autoTheme = parseAutoThemeSetting(themeSetting);
|
|
28
31
|
if (autoTheme) {
|
|
29
32
|
this.terminalTheme = await detectTerminalThemeForAuto({ ui: this.ui, timeoutMs: 100 });
|
|
@@ -41,13 +44,24 @@ export class InteractiveThemeController {
|
|
|
41
44
|
if (!this.applyThemeName(detection.theme).success)
|
|
42
45
|
return;
|
|
43
46
|
if (detection.confidence === "high") {
|
|
44
|
-
|
|
45
|
-
await
|
|
47
|
+
settingsManager.setTheme(detection.theme);
|
|
48
|
+
await settingsManager.flush();
|
|
46
49
|
}
|
|
47
50
|
}
|
|
51
|
+
getThemeSelection() {
|
|
52
|
+
return this.currentThemeSetting ?? this.getSettingsManager().getThemeSetting() ?? this.activeThemeName;
|
|
53
|
+
}
|
|
48
54
|
setThemeName(themeName, showError = false) {
|
|
49
55
|
this.setAutoSync(false);
|
|
50
|
-
|
|
56
|
+
const result = this.applyThemeName(themeName, showError);
|
|
57
|
+
if (result.success) {
|
|
58
|
+
this.currentThemeSetting = themeName;
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
async setThemeSetting(themeSetting) {
|
|
63
|
+
this.currentThemeSetting = themeSetting;
|
|
64
|
+
await this.applyFromSettings();
|
|
51
65
|
}
|
|
52
66
|
setThemeInstance(themeInstance) {
|
|
53
67
|
this.setAutoSync(false);
|
|
@@ -97,7 +111,7 @@ export class InteractiveThemeController {
|
|
|
97
111
|
if (!this.autoSyncEnabled)
|
|
98
112
|
return;
|
|
99
113
|
this.terminalTheme = terminalTheme;
|
|
100
|
-
const autoTheme = parseAutoThemeSetting(this.
|
|
114
|
+
const autoTheme = parseAutoThemeSetting(this.currentThemeSetting ?? this.getSettingsManager().getThemeSetting());
|
|
101
115
|
if (!autoTheme) {
|
|
102
116
|
this.setAutoSync(false);
|
|
103
117
|
return;
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"colors": {
|
|
36
36
|
"type": "object",
|
|
37
|
-
"description": "Theme color definitions (thinkingMax
|
|
37
|
+
"description": "Theme color definitions (thinkingMax and scrollbarThumb are optional and use compatible fallbacks)",
|
|
38
38
|
"required": [
|
|
39
39
|
"accent",
|
|
40
40
|
"border",
|
|
@@ -137,6 +137,14 @@
|
|
|
137
137
|
"$ref": "#/$defs/colorValue",
|
|
138
138
|
"description": "Selected item background"
|
|
139
139
|
},
|
|
140
|
+
"searchMatchBg": {
|
|
141
|
+
"$ref": "#/$defs/colorValue",
|
|
142
|
+
"description": "Transcript search match background and current-match text (falls back to selectedBg when omitted)"
|
|
143
|
+
},
|
|
144
|
+
"searchMatchText": {
|
|
145
|
+
"$ref": "#/$defs/colorValue",
|
|
146
|
+
"description": "Transcript search match text and current-match background (falls back to text when omitted)"
|
|
147
|
+
},
|
|
140
148
|
"userMessageBg": {
|
|
141
149
|
"$ref": "#/$defs/colorValue",
|
|
142
150
|
"description": "User message background"
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
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
|
-
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" | "scrollbarThumb" | "userMessageBg" | "customMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
|
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" | "searchMatchText" | "bashMode";
|
|
5
|
+
export type ThemeBg = "selectedBg" | "scrollbarThumb" | "searchMatchBg" | "userMessageBg" | "customMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
|
6
|
+
type OptionalThemeColor = "thinkingMax" | "searchMatchText";
|
|
7
|
+
type OptionalThemeBg = "scrollbarThumb" | "searchMatchBg";
|
|
6
8
|
type ColorMode = "truecolor" | "256color";
|
|
7
9
|
export declare class Theme {
|
|
8
10
|
readonly name?: string;
|
|
@@ -11,7 +13,7 @@ export declare class Theme {
|
|
|
11
13
|
private fgColors;
|
|
12
14
|
private bgColors;
|
|
13
15
|
private mode;
|
|
14
|
-
constructor(fgColors: Record<ThemeColor, string | number
|
|
16
|
+
constructor(fgColors: Record<Exclude<ThemeColor, OptionalThemeColor>, string | number> & Partial<Record<OptionalThemeColor, string | number>>, bgColors: Record<Exclude<ThemeBg, OptionalThemeBg>, string | number> & Partial<Record<OptionalThemeBg, string | number>>, mode: ColorMode, options?: {
|
|
15
17
|
name?: string;
|
|
16
18
|
sourcePath?: string;
|
|
17
19
|
sourceInfo?: SourceInfo;
|