@oh-my-pi/pi-tui 18.4.0 → 18.4.2
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 +42 -0
- package/dist/types/chat/image-loading.d.ts +24 -0
- package/dist/types/chat/transcript-entry.d.ts +9 -0
- package/dist/types/chrome/transcript-container.d.ts +24 -3
- package/dist/types/overlays/agents-hub.d.ts +7 -4
- package/dist/types/overlays/model-browser.d.ts +15 -0
- package/dist/types/overlays/rewind-selector.d.ts +4 -3
- package/dist/types/overlays/usage-dashboard.d.ts +9 -2
- package/dist/types/prompt/composer-attachments.d.ts +7 -0
- package/dist/types/prompt/composer-cache.d.ts +34 -16
- package/dist/types/prompt/composer.d.ts +15 -18
- package/dist/types/prompt/model-mention-autocomplete.d.ts +4 -1
- package/dist/types/prompt/welcome.d.ts +5 -2
- package/dist/types/render/width-aware-text.d.ts +6 -0
- package/dist/types/status-line/component.d.ts +13 -4
- package/dist/types/status-line/metrics.d.ts +0 -1
- package/dist/types/status-line/startup.d.ts +38 -0
- package/dist/types/status-line/types.d.ts +0 -2
- package/dist/types/terminal-capabilities.d.ts +10 -6
- package/dist/types/terminal.d.ts +7 -0
- package/dist/types/theme/color.d.ts +1 -0
- package/dist/types/theme/session-color.d.ts +3 -3
- package/dist/types/theme/shimmer.d.ts +1 -1
- package/dist/types/theme/theme-class.d.ts +1 -1
- package/dist/types/tools/output-meta.d.ts +2 -1
- package/dist/types/tools/vibe.d.ts +4 -0
- package/dist/types/tui.d.ts +3 -2
- package/package.json +9 -9
- package/src/chat/assistant-message.ts +27 -21
- package/src/chat/image-loading.ts +81 -0
- package/src/chat/tool-execution.ts +53 -17
- package/src/chat/transcript-entry.ts +16 -0
- package/src/chrome/transcript-container.ts +282 -86
- package/src/components/editor.ts +28 -6
- package/src/components/image.ts +25 -9
- package/src/components/loader.ts +37 -17
- package/src/overlays/agent-hub.ts +16 -6
- package/src/overlays/agents-hub.ts +7 -18
- package/src/overlays/copy-selector.ts +8 -31
- package/src/overlays/model-browser.ts +106 -25
- package/src/overlays/model-hub.ts +49 -16
- package/src/overlays/rewind-selector.ts +60 -13
- package/src/overlays/usage-dashboard.ts +47 -6
- package/src/prompt/composer-attachments.ts +54 -7
- package/src/prompt/composer-cache.ts +217 -196
- package/src/prompt/composer.ts +45 -72
- package/src/prompt/custom-editor.ts +24 -6
- package/src/prompt/model-mention-autocomplete.ts +22 -10
- package/src/prompt/welcome.ts +31 -28
- package/src/render/width-aware-text.ts +9 -0
- package/src/status-line/component.ts +286 -182
- package/src/status-line/metrics.ts +3 -21
- package/src/status-line/segments.ts +68 -79
- package/src/status-line/startup.ts +181 -0
- package/src/status-line/types.ts +0 -2
- package/src/terminal-capabilities.ts +12 -6
- package/src/terminal.ts +75 -20
- package/src/theme/color.ts +15 -0
- package/src/theme/session-color.ts +26 -4
- package/src/theme/shimmer.ts +55 -76
- package/src/theme/theme-class.ts +29 -12
- package/src/tools/output-meta.ts +11 -3
- package/src/tools/vibe.ts +15 -2
- package/src/tui.ts +208 -154
- package/src/utils.ts +15 -0
package/src/prompt/composer.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { getComposerStyle } from "../components/composer/registry";
|
|
2
2
|
import { Spacer } from "../components/spacer";
|
|
3
|
+
import type { StatusLineComponent } from "../status-line/component";
|
|
4
|
+
import type { StatusLineSession } from "../status-line/host";
|
|
5
|
+
import { createStartupStatusLine, type StatusLineStartupData } from "../status-line/startup";
|
|
3
6
|
import { isInsideTerminalMultiplexer } from "../terminal-multiplexer";
|
|
4
7
|
import { ProcessTerminal, type Terminal } from "../terminal";
|
|
5
8
|
import {
|
|
@@ -12,7 +15,7 @@ import {
|
|
|
12
15
|
type TUIOptions,
|
|
13
16
|
type ViewportSize,
|
|
14
17
|
} from "../tui";
|
|
15
|
-
import { sliceWithWidth,
|
|
18
|
+
import { sliceWithWidth, visibleWidth } from "../utils";
|
|
16
19
|
import { postmortem } from "@oh-my-pi/pi-utils";
|
|
17
20
|
import { CustomEditor } from "./custom-editor";
|
|
18
21
|
import type { WordCompletionMethod } from "./word-completion";
|
|
@@ -56,28 +59,22 @@ export interface ComposerWelcomeUpdate {
|
|
|
56
59
|
readonly modelName?: string;
|
|
57
60
|
readonly providerName?: string;
|
|
58
61
|
readonly recentSessions?: readonly RecentSession[];
|
|
59
|
-
|
|
62
|
+
/** Detected project servers; `null` means LSP is disabled and hides the welcome section. */
|
|
63
|
+
readonly lspServers?: readonly LspServerInfo[] | null;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
67
|
+
* Status-bar inputs persisted by the last session. The first frame renders
|
|
68
|
+
* them through a startup {@link StatusLineComponent} at the live width until
|
|
69
|
+
* the session-aware status line attaches.
|
|
66
70
|
*/
|
|
67
|
-
export interface
|
|
68
|
-
|
|
69
|
-
/** ANSI wrapper of the editor border at snapshot time (session accent or thinking color). */
|
|
71
|
+
export interface ComposerStatusCache {
|
|
72
|
+
/** ANSI wrapper of the editor border when persisted (session accent or thinking color). */
|
|
70
73
|
readonly borderColor?: {
|
|
71
74
|
readonly prefix: string;
|
|
72
75
|
readonly suffix: string;
|
|
73
76
|
};
|
|
74
|
-
|
|
75
|
-
readonly topBorder?: {
|
|
76
|
-
readonly content: string;
|
|
77
|
-
readonly width: number;
|
|
78
|
-
};
|
|
79
|
-
/** Standalone bottom-bar rows (`pi`/`claude` shapes), gap row included. */
|
|
80
|
-
readonly bottomLines: readonly string[];
|
|
77
|
+
readonly statusLine: StatusLineStartupData;
|
|
81
78
|
}
|
|
82
79
|
|
|
83
80
|
/** Optional dependencies and initial state for a standalone composer. */
|
|
@@ -87,7 +84,7 @@ export interface ComposerOptions {
|
|
|
87
84
|
readonly tuiOptions?: TUIOptions;
|
|
88
85
|
readonly preferences?: Partial<ComposerPreferences>;
|
|
89
86
|
readonly welcome?: ComposerWelcomeUpdate;
|
|
90
|
-
readonly status?:
|
|
87
|
+
readonly status?: ComposerStatusCache;
|
|
91
88
|
readonly exit?: (code: number) => void;
|
|
92
89
|
readonly now?: () => number;
|
|
93
90
|
}
|
|
@@ -117,30 +114,16 @@ export interface ComposerStartOptions {
|
|
|
117
114
|
readonly deferInput?: boolean;
|
|
118
115
|
}
|
|
119
116
|
|
|
120
|
-
/**
|
|
121
|
-
* Mount slot for the session-aware status component below the editor. Shows
|
|
122
|
-
* placeholder rows during startup until the real component mounts.
|
|
123
|
-
*/
|
|
117
|
+
/** Mount slot below the editor: the startup status line, then the session-aware one. */
|
|
124
118
|
class StatusHost implements Component {
|
|
125
|
-
#lines: readonly string[] = [];
|
|
126
119
|
#component: Component | undefined;
|
|
127
120
|
|
|
128
|
-
get mounted(): boolean {
|
|
129
|
-
return this.#component !== undefined;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
setLines(lines: readonly string[]): void {
|
|
133
|
-
this.#lines = lines;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
121
|
setComponent(component: Component): void {
|
|
137
122
|
this.#component = component;
|
|
138
|
-
this.#lines = [];
|
|
139
123
|
}
|
|
140
124
|
|
|
141
125
|
render(width: number): readonly string[] {
|
|
142
|
-
|
|
143
|
-
return this.#lines.map(line => truncateToWidth(line, width));
|
|
126
|
+
return this.#component?.render(width) ?? [];
|
|
144
127
|
}
|
|
145
128
|
}
|
|
146
129
|
|
|
@@ -220,11 +203,12 @@ export class Composer implements TerminalFrameProvider {
|
|
|
220
203
|
#modelName = "";
|
|
221
204
|
#providerName = "";
|
|
222
205
|
#recentSessions: RecentSession[] = [];
|
|
223
|
-
#lspServers: LspServerInfo[] = [];
|
|
206
|
+
#lspServers: LspServerInfo[] | null = [];
|
|
224
207
|
#headerBefore: readonly Component[] = [];
|
|
225
208
|
#headerAfter: readonly Component[] = [];
|
|
226
209
|
#runtimeChildren: readonly Component[] = [];
|
|
227
|
-
|
|
210
|
+
/** Cache-driven status line shown until {@link setStatusComponent} mounts the session's. */
|
|
211
|
+
#startupStatus: StatusLineComponent | undefined;
|
|
228
212
|
#runtimeMounted = false;
|
|
229
213
|
// Composer-owned history id space. Transcript batch ids restart across
|
|
230
214
|
// container clears/swaps; the composer translates them into one monotonic
|
|
@@ -291,7 +275,6 @@ export class Composer implements TerminalFrameProvider {
|
|
|
291
275
|
this.#exit = options.exit ?? (code => postmortem.exitProcess(code));
|
|
292
276
|
this.#now = options.now ?? Date.now;
|
|
293
277
|
this.#preferences = { ...COMPOSER_DEFAULTS, ...options.preferences };
|
|
294
|
-
this.#statusSnapshot = options.status;
|
|
295
278
|
this.#applyWelcomeUpdate(options.welcome ?? {});
|
|
296
279
|
|
|
297
280
|
this.ui = new TUI(
|
|
@@ -318,7 +301,13 @@ export class Composer implements TerminalFrameProvider {
|
|
|
318
301
|
} catch {
|
|
319
302
|
// Extension-defined styles arrive with the session; InteractiveMode reapplies them.
|
|
320
303
|
}
|
|
321
|
-
|
|
304
|
+
if (options.status) {
|
|
305
|
+
const { borderColor, statusLine } = options.status;
|
|
306
|
+
if (borderColor) this.editor.borderColor = text => `${borderColor.prefix}${text}${borderColor.suffix}`;
|
|
307
|
+
this.#startupStatus = createStartupStatusLine(statusLine);
|
|
308
|
+
this.#statusHost.setComponent(this.#startupStatus);
|
|
309
|
+
this.#startupStatus.attachToEditor(this.editor, getComposerStyle(this.#preferences.composerShape));
|
|
310
|
+
}
|
|
322
311
|
// Emergency controls stay active until InteractiveMode installs configured bindings.
|
|
323
312
|
// They deliberately mirror the interactive editor's contract so a stalled startup
|
|
324
313
|
// never behaves differently from a healthy one: Ctrl+C clears the draft and a second
|
|
@@ -380,12 +369,15 @@ export class Composer implements TerminalFrameProvider {
|
|
|
380
369
|
// editor drifts up above a band of blank rows (#11007).
|
|
381
370
|
this.#transientChromeFloor = Math.min(this.#transientChromeFloor ?? transientRows, transientRows);
|
|
382
371
|
const belowFloor = after.length - transientRows + this.#transientChromeFloor;
|
|
372
|
+
const now = performance.now();
|
|
373
|
+
const frame: AnimationFrame = { now, tick: Math.floor(now / 80) };
|
|
374
|
+
// Retirement measures the same live blocks the viewport lays out below;
|
|
375
|
+
// one open frame renders each of them once for both.
|
|
376
|
+
transcript.beginFrame(frame);
|
|
383
377
|
const history = this.#offerHistory(transcript, width, rows, preRoots.length + belowFloor);
|
|
384
378
|
const headerVisible = !this.#headerRetired && this.#offeredHistory?.source !== "header";
|
|
385
379
|
const headerRows = headerVisible ? this.#header.render(width) : [];
|
|
386
380
|
const before = [...headerRows, ...preRoots];
|
|
387
|
-
const now = performance.now();
|
|
388
|
-
const frame: AnimationFrame = { now, tick: Math.floor(now / 80) };
|
|
389
381
|
// The live tail is laid out against the same baseline retirement is
|
|
390
382
|
// billed against, so its compaction allocator (one row per block, no
|
|
391
383
|
// inter-block blanks) engages only when a block genuinely cannot retire.
|
|
@@ -635,7 +627,9 @@ export class Composer implements TerminalFrameProvider {
|
|
|
635
627
|
// retires first so transcript prefixes can follow in order.
|
|
636
628
|
const renderedHeader = this.#header.render(width);
|
|
637
629
|
if (renderedHeader.length > 0) {
|
|
638
|
-
|
|
630
|
+
// Only the comparison below reads the height, so the walk stops at
|
|
631
|
+
// the budget instead of rendering every replayed block (#12933).
|
|
632
|
+
const liveRows = transcript.liveRowCount(width, Math.max(0, rows - renderedHeader.length - chromeRows));
|
|
639
633
|
if (!this.#historyFlush && renderedHeader.length + chromeRows + liveRows <= rows) return undefined;
|
|
640
634
|
this.#offeredHistory = {
|
|
641
635
|
id: this.#nextHistoryId++,
|
|
@@ -789,7 +783,7 @@ export class Composer implements TerminalFrameProvider {
|
|
|
789
783
|
autocomplete: this.#preferences.spellingAutocomplete,
|
|
790
784
|
autocorrect: this.#preferences.spellingAutocorrect,
|
|
791
785
|
});
|
|
792
|
-
this.#
|
|
786
|
+
this.#startupStatus?.attachToEditor(this.editor, getComposerStyle(this.#preferences.composerShape));
|
|
793
787
|
if (this.#preferences.quiet) {
|
|
794
788
|
this.#welcome?.stopIntro();
|
|
795
789
|
this.#welcome = undefined;
|
|
@@ -834,42 +828,20 @@ export class Composer implements TerminalFrameProvider {
|
|
|
834
828
|
}
|
|
835
829
|
|
|
836
830
|
/**
|
|
837
|
-
* Mount the session-aware status component into the slot below the editor
|
|
838
|
-
*
|
|
831
|
+
* Mount the session-aware status component into the slot below the editor,
|
|
832
|
+
* retiring the startup status line; the caller installs the real top-border
|
|
839
833
|
* provider through its composer-shape sync.
|
|
840
834
|
*/
|
|
841
|
-
setStatusComponent(component:
|
|
835
|
+
setStatusComponent<TSession extends StatusLineSession>(component: StatusLineComponent<TSession>): void {
|
|
836
|
+
if (this.#startupStatus) component.adoptGitStatus(this.#startupStatus);
|
|
837
|
+
this.#disposeStartupStatus();
|
|
842
838
|
this.#statusHost.setComponent(component);
|
|
843
|
-
this.#statusSnapshot = undefined;
|
|
844
839
|
this.editor.setTopBorderProvider(undefined);
|
|
845
840
|
}
|
|
846
841
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
if (!border) return undefined;
|
|
851
|
-
if (border.width <= availableWidth) return { content: border.content, width: border.width };
|
|
852
|
-
const content = truncateToWidth(border.content, availableWidth);
|
|
853
|
-
return { content, width: visibleWidth(content) };
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
/** Install the cached chrome for the current shape; a shape mismatch clears it. */
|
|
857
|
-
#applyStatusSnapshot(): void {
|
|
858
|
-
if (this.#statusHost.mounted) return;
|
|
859
|
-
const snapshot = this.#statusSnapshot;
|
|
860
|
-
if (!snapshot || snapshot.shape !== this.#preferences.composerShape) {
|
|
861
|
-
this.editor.setTopBorderProvider(undefined);
|
|
862
|
-
this.#statusHost.setLines([]);
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
if (snapshot.borderColor) {
|
|
866
|
-
const { prefix, suffix } = snapshot.borderColor;
|
|
867
|
-
this.editor.borderColor = text => `${prefix}${text}${suffix}`;
|
|
868
|
-
}
|
|
869
|
-
this.editor.setTopBorderProvider(
|
|
870
|
-
snapshot.topBorder ? availableWidth => this.#speculativeTopBorder(availableWidth) : undefined,
|
|
871
|
-
);
|
|
872
|
-
this.#statusHost.setLines(snapshot.bottomLines);
|
|
842
|
+
#disposeStartupStatus(): void {
|
|
843
|
+
this.#startupStatus?.dispose();
|
|
844
|
+
this.#startupStatus = undefined;
|
|
873
845
|
}
|
|
874
846
|
|
|
875
847
|
/** Mount or replace session-aware root children while preserving the header and status hosts. */
|
|
@@ -908,6 +880,7 @@ export class Composer implements TerminalFrameProvider {
|
|
|
908
880
|
stop(): void {
|
|
909
881
|
if (!this.#started || this.#stopped || this.#transferred) return;
|
|
910
882
|
this.#welcome?.stopIntro();
|
|
883
|
+
this.#disposeStartupStatus();
|
|
911
884
|
this.ui.stop();
|
|
912
885
|
this.#stopped = true;
|
|
913
886
|
}
|
|
@@ -917,7 +890,7 @@ export class Composer implements TerminalFrameProvider {
|
|
|
917
890
|
if (update.modelName !== undefined) this.#modelName = update.modelName;
|
|
918
891
|
if (update.providerName !== undefined) this.#providerName = update.providerName;
|
|
919
892
|
if (update.recentSessions !== undefined) this.#recentSessions = [...update.recentSessions];
|
|
920
|
-
if (update.lspServers !== undefined) this.#lspServers = [...update.lspServers];
|
|
893
|
+
if (update.lspServers !== undefined) this.#lspServers = update.lspServers && [...update.lspServers];
|
|
921
894
|
}
|
|
922
895
|
|
|
923
896
|
#ensureWelcome(): void {
|
|
@@ -14,6 +14,7 @@ import { imageAttachmentSource } from "./image-source";
|
|
|
14
14
|
import { isVideoPath } from "./video";
|
|
15
15
|
import {
|
|
16
16
|
attachmentSgr,
|
|
17
|
+
type ChipKind,
|
|
17
18
|
COMPOSER_TOKEN_REGEX,
|
|
18
19
|
chipLabel,
|
|
19
20
|
collapseImageMarkers,
|
|
@@ -21,6 +22,8 @@ import {
|
|
|
21
22
|
collapseSkillTokens,
|
|
22
23
|
composerTokenRegex,
|
|
23
24
|
modelChipStyle,
|
|
25
|
+
PLACEHOLDER_REGEX,
|
|
26
|
+
referencedAttachments,
|
|
24
27
|
renderPlaceholders,
|
|
25
28
|
skillChipLabel,
|
|
26
29
|
skillChipStyle,
|
|
@@ -662,14 +665,29 @@ export class CustomEditor extends Editor {
|
|
|
662
665
|
) {
|
|
663
666
|
return cached.chips;
|
|
664
667
|
}
|
|
665
|
-
const
|
|
668
|
+
const recorded = new Map<string, ChipKind>();
|
|
669
|
+
if (this.pendingImages.length > 0) {
|
|
670
|
+
for (const [label, expansion] of this.atoms) {
|
|
671
|
+
const kind = expansion.startsWith("[Image #")
|
|
672
|
+
? "image"
|
|
673
|
+
: expansion.startsWith("[Video #")
|
|
674
|
+
? "video"
|
|
675
|
+
: undefined;
|
|
676
|
+
if (kind !== undefined && expansion.match(PLACEHOLDER_REGEX)?.[0] === expansion) {
|
|
677
|
+
recorded.set(label, kind);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
for (const entry of this.pendingTexts) {
|
|
682
|
+
// A reused label belongs to the atom currently expanding it, not a deleted paste.
|
|
683
|
+
if (!recorded.has(entry.label)) recorded.set(entry.label, "paste");
|
|
684
|
+
}
|
|
685
|
+
const refs = referencedAttachments(this.getText(), recorded);
|
|
666
686
|
const chips: ComposerChipDescriptor[] = [];
|
|
667
687
|
for (let i = 0; i < this.pendingImages.length; i++) {
|
|
668
688
|
const n = i + 1;
|
|
669
|
-
const video =
|
|
670
|
-
|
|
671
|
-
const image =
|
|
672
|
-
text.includes(chipLabel("image", n)) || text.includes(`[Image #${n}]`) || text.includes(`[Image #${n},`);
|
|
689
|
+
const video = refs.video.has(n);
|
|
690
|
+
const image = refs.image.has(n);
|
|
673
691
|
if (!video && !image) continue;
|
|
674
692
|
chips.push({
|
|
675
693
|
kind: video ? "video" : "image",
|
|
@@ -679,7 +697,7 @@ export class CustomEditor extends Editor {
|
|
|
679
697
|
});
|
|
680
698
|
}
|
|
681
699
|
for (const entry of this.pendingTexts) {
|
|
682
|
-
if (!
|
|
700
|
+
if (!refs.paste.has(entry.n)) continue;
|
|
683
701
|
chips.push({ kind: "paste", n: entry.n, text: entry });
|
|
684
702
|
}
|
|
685
703
|
this.#composerChipsCache = {
|
|
@@ -4,9 +4,11 @@ import type { ModelBrowserRegistry, ModelBrowserSource } from "../overlays/model
|
|
|
4
4
|
import { modelMentionDisplayName } from "./model-mention-syntax";
|
|
5
5
|
import type {
|
|
6
6
|
buildSearchAffinity as BuildSearchAffinity,
|
|
7
|
-
buildSessionModelScope as BuildSessionModelScope,
|
|
8
7
|
ModelBrowserItem,
|
|
9
8
|
rankModelItems as RankModelItems,
|
|
9
|
+
SearchAffinity,
|
|
10
|
+
SessionModelScope,
|
|
11
|
+
SessionModelScopeCache as SessionModelScopeCacheClass,
|
|
10
12
|
} from "../overlays/model-browser";
|
|
11
13
|
import { theme } from "../theme/theme";
|
|
12
14
|
|
|
@@ -17,7 +19,7 @@ const MAX_MODEL_MENTION_SUGGESTIONS = 20;
|
|
|
17
19
|
export type ModelMentionCandidateSource = (query: string) => ReadonlyArray<ModelBrowserItem>;
|
|
18
20
|
|
|
19
21
|
interface ModelBrowserModules {
|
|
20
|
-
|
|
22
|
+
SessionModelScopeCache: typeof SessionModelScopeCacheClass;
|
|
21
23
|
buildSearchAffinity: typeof BuildSearchAffinity;
|
|
22
24
|
rankModelItems: typeof RankModelItems;
|
|
23
25
|
}
|
|
@@ -75,20 +77,30 @@ export function applyModelMentionCompletion(
|
|
|
75
77
|
};
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Create a session-scoped model candidate lookup using picker ordering. The
|
|
82
|
+
* scope and search affinity are reused across queries until their inputs change.
|
|
83
|
+
*/
|
|
79
84
|
export function createModelMentionSource(host: {
|
|
80
85
|
source: ModelBrowserSource;
|
|
81
86
|
registry: ModelBrowserRegistry;
|
|
82
87
|
scopedModels: () => ReadonlyArray<Model>;
|
|
83
88
|
}): ModelMentionCandidateSource {
|
|
89
|
+
let scopeCache: SessionModelScopeCacheClass | undefined;
|
|
90
|
+
let affinityScope: SessionModelScope | undefined;
|
|
91
|
+
let affinityProviderOrder: readonly string[] | undefined;
|
|
92
|
+
let affinity: SearchAffinity | undefined;
|
|
84
93
|
return query => {
|
|
85
|
-
const {
|
|
86
|
-
|
|
94
|
+
const { SessionModelScopeCache, buildSearchAffinity, rankModelItems } = loadModelBrowser();
|
|
95
|
+
scopeCache ??= new SessionModelScopeCache(host.source, host.registry);
|
|
96
|
+
const scope = scopeCache.get(host.scopedModels());
|
|
87
97
|
if (!query.trim()) return scope.items;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
98
|
+
const providerOrder = host.source.modelProviderOrder;
|
|
99
|
+
if (!affinity || affinityScope !== scope || affinityProviderOrder !== providerOrder) {
|
|
100
|
+
affinity = buildSearchAffinity(providerOrder, scope.roles, scope.mruOrder);
|
|
101
|
+
affinityScope = scope;
|
|
102
|
+
affinityProviderOrder = providerOrder;
|
|
103
|
+
}
|
|
104
|
+
return rankModelItems(query, scope.items, { roles: scope.roles, mruOrder: scope.mruOrder, affinity });
|
|
93
105
|
};
|
|
94
106
|
}
|
package/src/prompt/welcome.ts
CHANGED
|
@@ -186,7 +186,8 @@ export class WelcomeComponent implements Component {
|
|
|
186
186
|
private modelName: string,
|
|
187
187
|
private providerName: string,
|
|
188
188
|
private recentSessions: RecentSession[] = [],
|
|
189
|
-
|
|
189
|
+
/** Detected project servers; `null` means LSP is disabled and hides the section. */
|
|
190
|
+
private lspServers: LspServerInfo[] | null = [],
|
|
190
191
|
) {}
|
|
191
192
|
get tip(): string | undefined {
|
|
192
193
|
this.#nagRoll ??= Math.random();
|
|
@@ -272,7 +273,7 @@ export class WelcomeComponent implements Component {
|
|
|
272
273
|
this.invalidate();
|
|
273
274
|
}
|
|
274
275
|
|
|
275
|
-
setLspServers(servers: LspServerInfo[]): void {
|
|
276
|
+
setLspServers(servers: LspServerInfo[] | null): void {
|
|
276
277
|
this.lspServers = servers;
|
|
277
278
|
this.invalidate();
|
|
278
279
|
}
|
|
@@ -365,29 +366,6 @@ export class WelcomeComponent implements Component {
|
|
|
365
366
|
sessionLines.push("");
|
|
366
367
|
}
|
|
367
368
|
|
|
368
|
-
// LSP servers content
|
|
369
|
-
const lspLines: string[] = [];
|
|
370
|
-
if (this.lspServers.length === 0) {
|
|
371
|
-
lspLines.push(` ${theme.fg("dim", "No LSP servers")}`);
|
|
372
|
-
} else {
|
|
373
|
-
for (const server of this.lspServers.slice(0, WELCOME_LSP_SLOTS)) {
|
|
374
|
-
const icon =
|
|
375
|
-
server.status === "ready"
|
|
376
|
-
? theme.styledSymbol("status.enabled", "success")
|
|
377
|
-
: server.status === "available"
|
|
378
|
-
? theme.styledSymbol("status.enabled", "dim")
|
|
379
|
-
: server.status === "connecting"
|
|
380
|
-
? theme.styledSymbol("status.pending", "muted")
|
|
381
|
-
: theme.styledSymbol("status.error", "error");
|
|
382
|
-
const exts = server.fileTypes.slice(0, 3).join(" ");
|
|
383
|
-
lspLines.push(` ${icon} ${theme.fg("muted", server.name)} ${theme.fg("dim", exts)}`);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
// Pad to the fixed slot count so the box height doesn't depend on server count.
|
|
387
|
-
while (lspLines.length < WELCOME_LSP_SLOTS) {
|
|
388
|
-
lspLines.push("");
|
|
389
|
-
}
|
|
390
|
-
|
|
391
369
|
// Right column
|
|
392
370
|
const rightLines = [
|
|
393
371
|
` ${theme.bold(theme.fg("accent", "Tips"))}`,
|
|
@@ -395,9 +373,7 @@ export class WelcomeComponent implements Component {
|
|
|
395
373
|
` ${theme.fg("dim", "/")}${theme.fg("muted", " for commands")}`,
|
|
396
374
|
` ${theme.fg("dim", "!")}${theme.fg("muted", " to run bash")}`,
|
|
397
375
|
` ${theme.fg("dim", "$")}${theme.fg("muted", " to run python")}`,
|
|
398
|
-
separator,
|
|
399
|
-
` ${theme.bold(theme.fg("accent", "LSP Servers"))}`,
|
|
400
|
-
...lspLines,
|
|
376
|
+
...this.#renderLspSection(separator),
|
|
401
377
|
separator,
|
|
402
378
|
` ${theme.bold(theme.fg("accent", "Recent sessions"))}`,
|
|
403
379
|
...sessionLines,
|
|
@@ -452,6 +428,33 @@ export class WelcomeComponent implements Component {
|
|
|
452
428
|
return lines;
|
|
453
429
|
}
|
|
454
430
|
|
|
431
|
+
/** Right-column LSP rows padded to a fixed height; empty when LSP is disabled. */
|
|
432
|
+
#renderLspSection(separator: string): string[] {
|
|
433
|
+
if (this.lspServers === null) return [];
|
|
434
|
+
const lspLines: string[] = [];
|
|
435
|
+
if (this.lspServers.length === 0) {
|
|
436
|
+
lspLines.push(` ${theme.fg("dim", "No LSP servers")}`);
|
|
437
|
+
} else {
|
|
438
|
+
for (const server of this.lspServers.slice(0, WELCOME_LSP_SLOTS)) {
|
|
439
|
+
const icon =
|
|
440
|
+
server.status === "ready"
|
|
441
|
+
? theme.styledSymbol("status.enabled", "success")
|
|
442
|
+
: server.status === "available"
|
|
443
|
+
? theme.styledSymbol("status.enabled", "dim")
|
|
444
|
+
: server.status === "connecting"
|
|
445
|
+
? theme.styledSymbol("status.pending", "muted")
|
|
446
|
+
: theme.styledSymbol("status.error", "error");
|
|
447
|
+
const exts = server.fileTypes.slice(0, 3).join(" ");
|
|
448
|
+
lspLines.push(` ${icon} ${theme.fg("muted", server.name)} ${theme.fg("dim", exts)}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// Pad to the fixed slot count so the box height doesn't depend on server count.
|
|
452
|
+
while (lspLines.length < WELCOME_LSP_SLOTS) {
|
|
453
|
+
lspLines.push("");
|
|
454
|
+
}
|
|
455
|
+
return [separator, ` ${theme.bold(theme.fg("accent", "LSP Servers"))}`, ...lspLines];
|
|
456
|
+
}
|
|
457
|
+
|
|
455
458
|
/**
|
|
456
459
|
* Render the per-instance tip line: the `customMessageLabel`-themed `Tip:`
|
|
457
460
|
* label followed by a `muted` body, the whole line italicized. Returns `[]`
|
|
@@ -47,6 +47,15 @@ export class WidthAwareText implements Component {
|
|
|
47
47
|
this.#inner.invalidate();
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Re-run the formatter on the next render because its inputs changed.
|
|
52
|
+
* Unlike {@link invalidate}, the inner `Text` keeps its wrap cache, so a
|
|
53
|
+
* reformat that yields the same string skips re-wrapping and re-tinting.
|
|
54
|
+
*/
|
|
55
|
+
reformat(): void {
|
|
56
|
+
this.#cachedText = undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
render(width: number): readonly string[] {
|
|
51
60
|
const paddingX = this.#ignoreTight ? this.#paddingX : getPaddingX(this.#paddingX);
|
|
52
61
|
const contentWidth = Math.max(1, width - paddingX * 2);
|