@oh-my-pi/pi-tui 18.4.1 → 18.4.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 +34 -0
- package/dist/types/chat/thinking-display.d.ts +2 -0
- package/dist/types/chrome/transcript-container.d.ts +15 -1
- package/dist/types/components/editor.d.ts +5 -0
- package/dist/types/loop-watchdog.d.ts +9 -0
- package/dist/types/overlays/model-browser.d.ts +15 -0
- package/dist/types/overlays/model-picker.d.ts +2 -1
- package/dist/types/overlays/rewind-selector.d.ts +2 -1
- package/dist/types/prompt/composer-attachments.d.ts +7 -0
- package/dist/types/prompt/model-mention-autocomplete.d.ts +4 -1
- package/dist/types/render/width-aware-text.d.ts +6 -0
- package/dist/types/setup/scenes/sign-in.d.ts +9 -9
- package/dist/types/setup/scenes/types.d.ts +0 -26
- package/dist/types/stdin-buffer.d.ts +10 -7
- package/dist/types/terminal.d.ts +16 -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/web-search-types.d.ts +90 -0
- package/dist/types/tools/web-search.d.ts +1 -168
- package/dist/types/tui.d.ts +3 -2
- package/package.json +9 -9
- package/src/chat/assistant-message.ts +192 -40
- package/src/chat/thinking-display.ts +73 -11
- package/src/chat/tool-execution.ts +24 -4
- package/src/chrome/transcript-container.ts +178 -31
- package/src/components/editor.ts +51 -15
- package/src/components/image.ts +15 -7
- package/src/components/loader.ts +37 -17
- package/src/components/markdown.ts +110 -56
- package/src/loop-watchdog.ts +38 -10
- package/src/overlays/agent-hub.ts +16 -6
- package/src/overlays/copy-selector.ts +5 -0
- package/src/overlays/model-browser.ts +106 -25
- package/src/overlays/model-picker.ts +9 -6
- package/src/overlays/rewind-selector.ts +10 -2
- package/src/overlays/settings-selector.ts +2 -10
- package/src/prompt/composer-attachments.ts +54 -7
- package/src/prompt/composer.ts +5 -2
- package/src/prompt/custom-editor.ts +24 -6
- package/src/prompt/model-mention-autocomplete.ts +22 -10
- package/src/render/width-aware-text.ts +9 -0
- package/src/setup/scenes/sign-in.ts +17 -12
- package/src/setup/scenes/types.ts +0 -27
- package/src/setup/wizard.ts +1 -1
- package/src/status-line/component.ts +229 -134
- package/src/status-line/segments.ts +34 -29
- package/src/stdin-buffer.ts +27 -21
- package/src/terminal.ts +60 -20
- package/src/theme/color.ts +15 -0
- package/src/theme/mermaid-cache.ts +10 -1
- 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/theme/tui-adapters.ts +14 -1
- package/src/tools/bash.ts +53 -13
- package/src/tools/edit.ts +110 -29
- package/src/tools/streaming-output.ts +79 -16
- package/src/tools/web-search-types.ts +99 -0
- package/src/tools/web-search.ts +1 -172
- package/src/tui.ts +143 -113
- package/src/utils.ts +15 -0
- package/dist/types/setup/scenes/providers.d.ts +0 -3
- package/dist/types/setup/scenes/web-search.d.ts +0 -22
- package/src/setup/scenes/providers.ts +0 -110
- package/src/setup/scenes/web-search.ts +0 -165
|
@@ -28,7 +28,8 @@ export interface ResolvedRoleModel {
|
|
|
28
28
|
|
|
29
29
|
/** Catalog refresh capability used by the session picker. */
|
|
30
30
|
export interface ModelPickerRegistry extends ModelBrowserRegistry {
|
|
31
|
-
|
|
31
|
+
/** Bring the catalog up to date without rebuilding a current one; `true` when the picker must re-read it. */
|
|
32
|
+
refreshIfStale(): Promise<boolean>;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export interface ModelPickerCallbacks {
|
|
@@ -172,13 +173,15 @@ export class ModelPickerComponent implements Component {
|
|
|
172
173
|
this.#browser.selectSelector(options.currentSelector);
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
// the
|
|
176
|
+
// Re-read only if the catalog moves (startup discovery landing, a
|
|
177
|
+
// models.yml edit): rebuilding a current catalog on every open blocks
|
|
178
|
+
// the first paint for seconds. A --models scope is registry-independent.
|
|
178
179
|
if (this.#scopedModels.length === 0) {
|
|
179
180
|
this.#registry
|
|
180
|
-
.
|
|
181
|
-
.then(
|
|
181
|
+
.refreshIfStale()
|
|
182
|
+
.then(changed => {
|
|
183
|
+
if (changed) this.#syncFromRegistryState();
|
|
184
|
+
})
|
|
182
185
|
.catch(error => {
|
|
183
186
|
this.#configError = error instanceof Error ? error.message : String(error);
|
|
184
187
|
})
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* variants at a fork and jump between user turns elsewhere, `f` opens a
|
|
23
23
|
* filter (typing narrows the current path to matching items; Esc leaves the
|
|
24
24
|
* filter with the selection kept), Enter rewinds to the outlined item, A loads
|
|
25
|
-
* earlier turns without changing selection
|
|
25
|
+
* earlier turns without changing selection (stepping above the oldest replayed
|
|
26
|
+
* turn loads them too), Esc cancels.
|
|
26
27
|
*/
|
|
27
28
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
28
29
|
import {
|
|
@@ -471,7 +472,10 @@ export class RewindSelectorComponent implements Component {
|
|
|
471
472
|
this.#move(delta, () => true);
|
|
472
473
|
}
|
|
473
474
|
|
|
474
|
-
/**
|
|
475
|
+
/**
|
|
476
|
+
* Step the main selection by `delta` to the nearest visible target passing
|
|
477
|
+
* `accept`; stepping above the replayed tail loads the earlier history first.
|
|
478
|
+
*/
|
|
475
479
|
#move(delta: -1 | 1, accept: (target: OutlineTarget) => boolean): void {
|
|
476
480
|
let index = this.#selected + delta;
|
|
477
481
|
while (index >= 0 && index < this.#targets.length) {
|
|
@@ -485,6 +489,10 @@ export class RewindSelectorComponent implements Component {
|
|
|
485
489
|
}
|
|
486
490
|
index += delta;
|
|
487
491
|
}
|
|
492
|
+
if (delta < 0 && this.#truncated) {
|
|
493
|
+
this.#loadFullHistory();
|
|
494
|
+
this.#move(delta, accept);
|
|
495
|
+
}
|
|
488
496
|
}
|
|
489
497
|
|
|
490
498
|
#isMainSelectable(index: number): boolean {
|
|
@@ -1139,15 +1139,7 @@ export class SettingsSelectorComponent implements Component {
|
|
|
1139
1139
|
return entries.map(([provider, limit]) => `${provider}: ${limit}`).join(", ");
|
|
1140
1140
|
}
|
|
1141
1141
|
|
|
1142
|
-
#getMultiSelectOptions(def: SettingDef & { type: "multiselect" }) {
|
|
1143
|
-
if (def.path !== "providers.webSearchOrder") return def.options;
|
|
1144
|
-
const excluded: unknown = this.#context.settings.get("providers.webSearchExclude");
|
|
1145
|
-
if (!Array.isArray(excluded)) return def.options;
|
|
1146
|
-
return def.options.filter(option => !excluded.includes(option.value));
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
1142
|
#createMultiSelect(def: SettingDef & { type: "multiselect" }, done: (value?: string) => void): Container {
|
|
1150
|
-
const options = this.#getMultiSelectOptions(def);
|
|
1151
1143
|
const current: unknown = this.#context.settings.get(def.path);
|
|
1152
1144
|
const initial = Array.isArray(current)
|
|
1153
1145
|
? current.filter((entry): entry is string => typeof entry === "string")
|
|
@@ -1155,7 +1147,7 @@ export class SettingsSelectorComponent implements Component {
|
|
|
1155
1147
|
return new MultiSelectSubmenu(
|
|
1156
1148
|
def.label,
|
|
1157
1149
|
def.description,
|
|
1158
|
-
options,
|
|
1150
|
+
def.options,
|
|
1159
1151
|
initial,
|
|
1160
1152
|
def.ordered,
|
|
1161
1153
|
value => {
|
|
@@ -1167,7 +1159,7 @@ export class SettingsSelectorComponent implements Component {
|
|
|
1167
1159
|
}
|
|
1168
1160
|
|
|
1169
1161
|
#formatMultiSelectValue(def: SettingDef & { type: "multiselect" }, value: unknown): string {
|
|
1170
|
-
const options =
|
|
1162
|
+
const { options } = def;
|
|
1171
1163
|
const labels = Array.isArray(value)
|
|
1172
1164
|
? value.flatMap(entry => {
|
|
1173
1165
|
if (typeof entry !== "string") return [];
|
|
@@ -50,9 +50,12 @@ const SKILL_ICONS = [...new Set(Object.values(SYMBOL_PRESETS).map(m => m["icon.e
|
|
|
50
50
|
/** Skill names as they appear in chips: word characters and dashes, dots only between segments. */
|
|
51
51
|
const SKILL_NAME_SOURCE = "[\\w-]+(?:\\.[\\w-]+)*";
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
/** Regex source for a chip glyph; word glyphs (ASCII preset) must not continue a preceding word. */
|
|
54
|
+
function glyphSource(icon: string): string {
|
|
55
|
+
return /^[a-z]+$/i.test(icon) ? `(?<![A-Za-z])${RegExp.escape(icon)}` : RegExp.escape(icon);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const SKILL_CHIP_SOURCE = `(?:${SKILL_ICONS.map(glyphSource).join("|")}) (${SKILL_NAME_SOURCE})(?![\\w-])`;
|
|
56
59
|
|
|
57
60
|
/**
|
|
58
61
|
* Replaces `/skill:<name>` tokens for known skills with compact chip labels and
|
|
@@ -96,11 +99,16 @@ export function collapseModelMentions(
|
|
|
96
99
|
|
|
97
100
|
const CHIP_ICON_KEY = { image: "chip.image", video: "chip.video", paste: "chip.paste" } as const;
|
|
98
101
|
|
|
102
|
+
/** Chip glyph the active theme renders for `kind`, including `symbols.overrides`. */
|
|
103
|
+
function activeChipIcon(kind: ChipKind): string {
|
|
104
|
+
return typeof theme === "undefined"
|
|
105
|
+
? SYMBOL_PRESETS.unicode[CHIP_ICON_KEY[kind]]
|
|
106
|
+
: theme.symbol(CHIP_ICON_KEY[kind]);
|
|
107
|
+
}
|
|
108
|
+
|
|
99
109
|
/** Compact atomic composer token for attachment `n` in the active symbol preset. */
|
|
100
110
|
export function chipLabel(kind: ChipKind, n: number): string {
|
|
101
|
-
|
|
102
|
-
typeof theme === "undefined" ? SYMBOL_PRESETS.unicode[CHIP_ICON_KEY[kind]] : theme.symbol(CHIP_ICON_KEY[kind]);
|
|
103
|
-
return `${icon} #${n}`;
|
|
111
|
+
return `${activeChipIcon(kind)} #${n}`;
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
/** Every glyph a chip token may start with, across all symbol presets. */
|
|
@@ -111,7 +119,7 @@ const CHIP_ICONS: Record<ChipKind, readonly string[]> = {
|
|
|
111
119
|
};
|
|
112
120
|
|
|
113
121
|
const CHIP_TOKEN_SOURCE = `(?:${[...CHIP_ICONS.image, ...CHIP_ICONS.video, ...CHIP_ICONS.paste]
|
|
114
|
-
.map(
|
|
122
|
+
.map(glyphSource)
|
|
115
123
|
.join("|")}) #[1-9]\\d*`;
|
|
116
124
|
|
|
117
125
|
/** Infers an attachment kind from a chip label emitted by any configured symbol preset. */
|
|
@@ -155,6 +163,45 @@ export const COMPOSER_TOKEN_REGEX = new RegExp(
|
|
|
155
163
|
"gu",
|
|
156
164
|
);
|
|
157
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Attachment indices referenced by a composer buffer, per kind. Image/video count compact chips
|
|
168
|
+
* and expanded markers; paste counts compact chips only, since `[Paste #N]` markers number the
|
|
169
|
+
* base editor's separate paste buffer. Registered labels take precedence over theme glyphs so
|
|
170
|
+
* tokens created before a theme switch keep their kind; full numbers prevent `#1` matching `#10`.
|
|
171
|
+
*/
|
|
172
|
+
export function referencedAttachments(
|
|
173
|
+
text: string,
|
|
174
|
+
recorded: ReadonlyMap<string, ChipKind>,
|
|
175
|
+
): Record<ChipKind, Set<number>> {
|
|
176
|
+
const kinds: readonly ChipKind[] = ["image", "video", "paste"];
|
|
177
|
+
const kindByIcon = new Map<string, ChipKind>();
|
|
178
|
+
for (const kind of kinds) for (const icon of CHIP_ICONS[kind]) kindByIcon.set(icon, kind);
|
|
179
|
+
for (const kind of kinds) kindByIcon.set(activeChipIcon(kind), kind);
|
|
180
|
+
const icons = [...kindByIcon.keys()].sort((a, b) => b.length - a.length).map(glyphSource);
|
|
181
|
+
const labels = [...recorded.keys()].sort((a, b) => b.length - a.length).map(label => RegExp.escape(label));
|
|
182
|
+
const recordedSource = labels.length > 0 ? labels.join("|") : "(?!)";
|
|
183
|
+
const scanner = new RegExp(
|
|
184
|
+
`${PLACEHOLDER_REGEX.source}|(${recordedSource})(?!\\d)|(${icons.join("|")}) #([1-9]\\d*)`,
|
|
185
|
+
"gu",
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const refs: Record<ChipKind, Set<number>> = { image: new Set(), video: new Set(), paste: new Set() };
|
|
189
|
+
for (const match of text.matchAll(scanner)) {
|
|
190
|
+
if (match[1] === "Image") refs.image.add(Number(match[2]));
|
|
191
|
+
else if (match[1] === "Video") refs.video.add(Number(match[2]));
|
|
192
|
+
const label = match[3];
|
|
193
|
+
if (label !== undefined) {
|
|
194
|
+
const kind = recorded.get(label);
|
|
195
|
+
if (kind !== undefined) refs[kind].add(Number(label.slice(label.lastIndexOf("#") + 1)));
|
|
196
|
+
} else {
|
|
197
|
+
const icon = match[4];
|
|
198
|
+
const kind = icon === undefined ? undefined : kindByIcon.get(icon);
|
|
199
|
+
if (kind !== undefined) refs[kind].add(Number(match[5]));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return refs;
|
|
203
|
+
}
|
|
204
|
+
|
|
158
205
|
/** Add the registered model-chip labels to the composer placeholder matcher. */
|
|
159
206
|
export function composerTokenRegex(mentionLabels: Iterable<string>): RegExp {
|
|
160
207
|
const labels = [...new Set(mentionLabels)].sort((a, b) => b.length - a.length);
|
package/src/prompt/composer.ts
CHANGED
|
@@ -369,12 +369,15 @@ export class Composer implements TerminalFrameProvider {
|
|
|
369
369
|
// editor drifts up above a band of blank rows (#11007).
|
|
370
370
|
this.#transientChromeFloor = Math.min(this.#transientChromeFloor ?? transientRows, transientRows);
|
|
371
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);
|
|
372
377
|
const history = this.#offerHistory(transcript, width, rows, preRoots.length + belowFloor);
|
|
373
378
|
const headerVisible = !this.#headerRetired && this.#offeredHistory?.source !== "header";
|
|
374
379
|
const headerRows = headerVisible ? this.#header.render(width) : [];
|
|
375
380
|
const before = [...headerRows, ...preRoots];
|
|
376
|
-
const now = performance.now();
|
|
377
|
-
const frame: AnimationFrame = { now, tick: Math.floor(now / 80) };
|
|
378
381
|
// The live tail is laid out against the same baseline retirement is
|
|
379
382
|
// billed against, so its compaction allocator (one row per block, no
|
|
380
383
|
// inter-block blanks) engages only when a block genuinely cannot retire.
|
|
@@ -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
|
}
|
|
@@ -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);
|
|
@@ -14,7 +14,7 @@ import { wrapTextWithAnsi } from "../../utils";
|
|
|
14
14
|
import { getAgentDbPath } from "@oh-my-pi/pi-utils";
|
|
15
15
|
import { OAuthSelectorComponent } from "../../overlays/oauth-selector";
|
|
16
16
|
import { theme } from "../../theme/theme";
|
|
17
|
-
import type {
|
|
17
|
+
import type { SetupScene, SetupSceneController, SetupSceneHost } from "./types";
|
|
18
18
|
|
|
19
19
|
function loginUrlLink(url: string): string {
|
|
20
20
|
return `\x1b]8;;${url}\x07Open login URL\x1b]8;;\x07`;
|
|
@@ -69,13 +69,15 @@ interface PromptState {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* "Sign in"
|
|
73
|
-
* OAuth.
|
|
74
|
-
*
|
|
72
|
+
* "Sign in" scene: lets the user authenticate one or more model providers via
|
|
73
|
+
* OAuth. It never auto-advances the wizard — the user may sign in to several
|
|
74
|
+
* providers and then continue with Esc.
|
|
75
75
|
*/
|
|
76
|
-
export class
|
|
77
|
-
readonly
|
|
78
|
-
|
|
76
|
+
export class SignInScene implements SetupSceneController {
|
|
77
|
+
readonly title = "Sign in to your providers";
|
|
78
|
+
get subtitle(): string {
|
|
79
|
+
return `Sign in to one or more providers. Press ${editorKey("tui.select.cancel")} when you're done.`;
|
|
80
|
+
}
|
|
79
81
|
|
|
80
82
|
#authStorage: AuthStorage;
|
|
81
83
|
#selector: OAuthSelectorComponent;
|
|
@@ -99,11 +101,6 @@ export class SignInTab implements SetupTab {
|
|
|
99
101
|
this.#selector = this.#createSelector();
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
/** Modal while an OAuth flow is running so the scene won't switch tabs or finish. */
|
|
103
|
-
get modal(): boolean {
|
|
104
|
-
return this.#loggingInProvider !== undefined;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
104
|
dispose(): void {
|
|
108
105
|
this.#disposed = true;
|
|
109
106
|
this.#selector.stopValidation();
|
|
@@ -373,3 +370,11 @@ export class SignInTab implements SetupTab {
|
|
|
373
370
|
this.#host.requestRender();
|
|
374
371
|
}
|
|
375
372
|
}
|
|
373
|
+
|
|
374
|
+
/** Onboarding scene for provider sign-in. */
|
|
375
|
+
export const providersSetupScene: SetupScene = {
|
|
376
|
+
id: "providers",
|
|
377
|
+
title: "Sign in to your providers",
|
|
378
|
+
minVersion: 1,
|
|
379
|
+
mount: host => new SignInScene(host),
|
|
380
|
+
};
|
|
@@ -5,7 +5,6 @@ import type { ComposerPreviewStatusSource } from "../../overlays/composer-shape-
|
|
|
5
5
|
import type { ComposerShape } from "../../overlays/composer-shape-registry";
|
|
6
6
|
import type { ModelBrowserSource } from "../../overlays/model-browser";
|
|
7
7
|
import type { SymbolPreset } from "../../theme/theme";
|
|
8
|
-
import type { SearchProviderId } from "../../tools/web-search";
|
|
9
8
|
import type { Component, TUI } from "../../tui";
|
|
10
9
|
|
|
11
10
|
/** Terminal capabilities used by setup overlays and the startup splash. */
|
|
@@ -21,7 +20,6 @@ export interface SetupHost extends SetupUiHost {
|
|
|
21
20
|
readonly composerShape: ComposerShape;
|
|
22
21
|
readonly symbolPreset: SymbolPreset;
|
|
23
22
|
readonly colorBlindMode: boolean;
|
|
24
|
-
readonly webSearchOrder: readonly string[];
|
|
25
23
|
readonly disabledProviders: readonly string[];
|
|
26
24
|
readonly authStorage: AuthStorage;
|
|
27
25
|
readonly modelSource: ModelBrowserSource;
|
|
@@ -33,8 +31,6 @@ export interface SetupHost extends SetupUiHost {
|
|
|
33
31
|
saveSymbolPreset(preset: SymbolPreset): void;
|
|
34
32
|
saveColorBlindMode(enabled: boolean): void;
|
|
35
33
|
saveTheme(mode: "dark" | "light", name: string): void;
|
|
36
|
-
isSearchProviderAvailable(id: SearchProviderId): Promise<boolean>;
|
|
37
|
-
saveSearchProvider(id: SearchProviderId | "auto"): void;
|
|
38
34
|
captureBrowserSession(request: OAuthBrowserSessionRequest, signal?: AbortSignal): Promise<string>;
|
|
39
35
|
copyToClipboard(text: string): Promise<void>;
|
|
40
36
|
openInBrowser(url: string): void;
|
|
@@ -78,29 +74,6 @@ export interface SetupSceneController extends Component {
|
|
|
78
74
|
routeMouse?(event: SgrMouseEvent, line: number, col: number): void;
|
|
79
75
|
}
|
|
80
76
|
|
|
81
|
-
/**
|
|
82
|
-
* A single panel inside a tabbed setup scene. The host scene owns the tab bar
|
|
83
|
-
* and forwards rendering/input to the active tab.
|
|
84
|
-
*/
|
|
85
|
-
export interface SetupTab {
|
|
86
|
-
readonly id: string;
|
|
87
|
-
readonly label: string;
|
|
88
|
-
/**
|
|
89
|
-
* While `true` the tab owns all keyboard input (e.g. an in-progress OAuth
|
|
90
|
-
* login). The parent scene MUST NOT switch tabs or finish while modal.
|
|
91
|
-
*/
|
|
92
|
-
readonly modal: boolean;
|
|
93
|
-
/** See {@link SetupSceneController.render}: `maxLines` is the tab-local row budget. */
|
|
94
|
-
render(width: number, maxLines?: number): readonly string[];
|
|
95
|
-
handleInput(data: string): void;
|
|
96
|
-
invalidate(): void;
|
|
97
|
-
/** Called when the tab becomes active (including initial mount). */
|
|
98
|
-
onActivate?(): void;
|
|
99
|
-
/** Mouse routing at tab-local coordinates; see {@link SetupSceneController.routeMouse}. */
|
|
100
|
-
routeMouse?(event: SgrMouseEvent, line: number, col: number): void;
|
|
101
|
-
dispose(): void;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
77
|
/** Versioned onboarding scene definition. */
|
|
105
78
|
export interface SetupScene {
|
|
106
79
|
id: string;
|
package/src/setup/wizard.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { SetupHost } from "./scenes/types";
|
|
|
3
3
|
import { composerSetupScene } from "./scenes/composer";
|
|
4
4
|
import { glyphSetupScene } from "./scenes/glyph";
|
|
5
5
|
import { modelSetupScene } from "./scenes/model";
|
|
6
|
-
import { providersSetupScene } from "./scenes/
|
|
6
|
+
import { providersSetupScene } from "./scenes/sign-in";
|
|
7
7
|
import { themeSetupScene } from "./scenes/theme";
|
|
8
8
|
import type { SetupScene } from "./scenes/types";
|
|
9
9
|
import { SetupWizardComponent } from "./wizard-overlay";
|