@oh-my-pi/pi-coding-agent 16.4.8 → 16.5.0
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 +63 -0
- package/dist/cli.js +3502 -3444
- package/dist/types/cli/args.d.ts +5 -0
- package/dist/types/cli/gallery-fixtures/shell.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +15 -0
- package/dist/types/config/model-resolver.d.ts +4 -3
- package/dist/types/config/model-roles.d.ts +8 -0
- package/dist/types/config/settings-schema.d.ts +46 -14
- package/dist/types/extensibility/extensions/types.d.ts +1 -1
- package/dist/types/launch/broker.d.ts +2 -0
- package/dist/types/launch/client.d.ts +22 -0
- package/dist/types/launch/paths.d.ts +4 -0
- package/dist/types/launch/presence.d.ts +8 -0
- package/dist/types/launch/protocol.d.ts +170 -0
- package/dist/types/launch/terminal-output.d.ts +7 -0
- package/dist/types/modes/components/agent-hub.d.ts +1 -1
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-browser.d.ts +17 -2
- package/dist/types/modes/components/model-hub.d.ts +0 -10
- package/dist/types/modes/components/model-picker.d.ts +44 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/print-mode.d.ts +14 -8
- package/dist/types/modes/print-mode.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/sdk.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +42 -0
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/session/session-entries.d.ts +6 -0
- package/dist/types/thinking.d.ts +2 -2
- package/dist/types/tiny/models.d.ts +1 -1
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +28 -2
- package/dist/types/tools/browser/tab-protocol.d.ts +6 -0
- package/dist/types/tools/browser/tab-worker.d.ts +6 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/launch.d.ts +121 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/terminal-output.d.ts +5 -0
- package/dist/types/vibe/runtime.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +0 -8
- package/package.json +20 -20
- package/src/cli/args.ts +11 -0
- package/src/cli/flag-tables.ts +9 -0
- package/src/cli/gallery-fixtures/shell.ts +82 -1
- package/src/cli.ts +10 -0
- package/src/commands/launch.ts +17 -0
- package/src/config/model-resolver.ts +110 -31
- package/src/config/model-roles.ts +14 -0
- package/src/config/settings-schema.ts +71 -7
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +2 -2
- package/src/eval/__tests__/completion-bridge.test.ts +1 -1
- package/src/eval/completion-bridge.ts +4 -4
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/extensions/model-api.ts +1 -1
- package/src/extensibility/extensions/types.ts +1 -1
- package/src/internal-urls/docs-index.ts +4 -4
- package/src/launch/broker.ts +1017 -0
- package/src/launch/client.ts +344 -0
- package/src/launch/paths.ts +17 -0
- package/src/launch/presence.ts +82 -0
- package/src/launch/protocol.ts +386 -0
- package/src/launch/terminal-output.ts +46 -0
- package/src/main.ts +50 -1
- package/src/modes/acp/acp-agent.ts +8 -1
- package/src/modes/components/agent-hub.ts +101 -31
- package/src/modes/components/compaction-summary-message.ts +8 -2
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/model-browser.ts +111 -21
- package/src/modes/components/model-hub.ts +23 -116
- package/src/modes/components/model-picker.ts +233 -0
- package/src/modes/components/snapcompact-shape-preview-doc.md +7 -11
- package/src/modes/components/status-line/component.test.ts +41 -2
- package/src/modes/components/status-line/component.ts +4 -0
- package/src/modes/components/status-line/segments.ts +6 -0
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/controllers/command-controller.ts +9 -1
- package/src/modes/controllers/event-controller.ts +31 -32
- package/src/modes/controllers/selector-controller.ts +96 -22
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +7 -3
- package/src/modes/print-mode.test.ts +71 -0
- package/src/modes/print-mode.ts +51 -2
- package/src/modes/theme/theme.ts +9 -0
- package/src/modes/utils/ui-helpers.ts +25 -4
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/scout.md +1 -1
- package/src/prompts/system/plan-yolo-handoff.md +5 -0
- package/src/prompts/system/prewalk-checklist.md +7 -0
- package/src/prompts/system/prewalk-continue.md +1 -0
- package/src/prompts/system/prewalk-plan.md +13 -0
- package/src/prompts/system/system-prompt.md +9 -7
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/bash.md +6 -4
- package/src/prompts/tools/browser.md +4 -4
- package/src/prompts/tools/launch.md +25 -0
- package/src/sdk.ts +8 -2
- package/src/session/agent-session.ts +550 -87
- package/src/session/session-context.test.ts +10 -5
- package/src/session/session-context.ts +25 -8
- package/src/session/session-entries.ts +6 -0
- package/src/slash-commands/builtin-registry.ts +25 -0
- package/src/task/agents.ts +2 -2
- package/src/thinking.ts +10 -3
- package/src/tiny/models.ts +7 -7
- package/src/tools/bash-interactive.ts +5 -8
- package/src/tools/bash.ts +38 -24
- package/src/tools/browser/cmux/cmux-tab.ts +17 -2
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +66 -6
- package/src/tools/browser/tab-protocol.ts +6 -0
- package/src/tools/browser/tab-supervisor.ts +17 -1
- package/src/tools/browser/tab-worker.ts +140 -21
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/index.ts +5 -0
- package/src/tools/inspect-image.ts +2 -2
- package/src/tools/launch.ts +643 -0
- package/src/tools/render-utils.ts +3 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/terminal-output.ts +141 -0
- package/src/tts/speech-enhancer.ts +2 -2
- package/src/utils/image-vision-fallback.ts +3 -3
- package/src/vibe/runtime.ts +2 -2
- package/src/web/search/provider.ts +0 -10
- package/src/web/search/providers/public.ts +2 -4
- package/src/web/search/types.ts +0 -10
- package/dist/types/web/search/providers/bing.d.ts +0 -14
- package/dist/types/web/search/providers/yahoo.d.ts +0 -14
- package/src/web/search/providers/bing.ts +0 -197
- package/src/web/search/providers/yahoo.ts +0 -179
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* {@link ModelBrowser} body. The Roles view manages assignments directly:
|
|
7
7
|
* pick a role, pick a model, adjust thinking in an inline strip, or clear the
|
|
8
8
|
* role back to auto-selection. Locked providers forward to the /login flow.
|
|
9
|
-
* Fully mouse-navigable (hover, wheel, click).
|
|
9
|
+
* Fully mouse-navigable (hover, wheel, click). Session-only switching lives
|
|
10
|
+
* in the compact alt+p picker ({@link ./model-picker}).
|
|
10
11
|
*/
|
|
11
12
|
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
12
13
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
@@ -27,15 +28,9 @@ import {
|
|
|
27
28
|
visibleWidth,
|
|
28
29
|
} from "@oh-my-pi/pi-tui";
|
|
29
30
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
30
|
-
import { getModelMatchPreferences, resolveModelRoleValue } from "../../config/model-resolver";
|
|
31
31
|
import { getKnownRoleIds, getRoleInfo } from "../../config/model-roles";
|
|
32
32
|
import type { Settings } from "../../config/settings";
|
|
33
|
-
import {
|
|
34
|
-
AUTO_THINKING,
|
|
35
|
-
type ConfiguredThinkingLevel,
|
|
36
|
-
getConfiguredThinkingLevelMetadata,
|
|
37
|
-
parseConfiguredThinkingLevel,
|
|
38
|
-
} from "../../thinking";
|
|
33
|
+
import { AUTO_THINKING, type ConfiguredThinkingLevel, getConfiguredThinkingLevelMetadata } from "../../thinking";
|
|
39
34
|
import { theme } from "../theme/theme";
|
|
40
35
|
import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
|
|
41
36
|
import {
|
|
@@ -43,15 +38,13 @@ import {
|
|
|
43
38
|
ModelBrowser,
|
|
44
39
|
type ModelBrowserItem,
|
|
45
40
|
type RoleAssignments,
|
|
41
|
+
resolveRoleAssignments,
|
|
46
42
|
sortModelItems,
|
|
47
43
|
thinkingLevelGlyph,
|
|
48
44
|
} from "./model-browser";
|
|
49
45
|
import { bottomBorder, dividerSplit, row, splitBodyWidth, splitRow, topBorderSplit } from "./overlay-box";
|
|
50
46
|
import { renderSegmentTrack } from "./segment-track";
|
|
51
47
|
|
|
52
|
-
/** `roles` is the full /models hub; `pick` is a one-shot session/embedded picker. */
|
|
53
|
-
export type ModelHubMode = "roles" | "pick";
|
|
54
|
-
|
|
55
48
|
/**
|
|
56
49
|
* A row of the Roles view: a role, a model/wildcard chain-key header, one of a
|
|
57
50
|
* chain's fallback entries, or the trailing "+ New role…". Fallback rows under
|
|
@@ -89,8 +82,6 @@ export interface ModelHubCallbacks {
|
|
|
89
82
|
onUnassign: (role: string) => void;
|
|
90
83
|
/** Persist a `retry.fallbackChains` entry — keyed by a role, `provider/model-id`, or `provider/*`; an empty chain clears the key. */
|
|
91
84
|
onFallbackChainChange?: (role: string, chain: string[]) => void;
|
|
92
|
-
/** Pick-mode activation: session-only switch or embedded pick. */
|
|
93
|
-
onPick?: (model: Model, selector: string) => void;
|
|
94
85
|
/** Locked provider activation: forward to the /login flow. */
|
|
95
86
|
onLoginRequest?: (providerId: string) => void;
|
|
96
87
|
/** Persist a new quick-switch cycle order (the ctrl+p role cycle). */
|
|
@@ -99,14 +90,8 @@ export interface ModelHubCallbacks {
|
|
|
99
90
|
}
|
|
100
91
|
|
|
101
92
|
export interface ModelHubOptions {
|
|
102
|
-
mode?: ModelHubMode;
|
|
103
|
-
/** Session token count; in pick mode, models with smaller context windows are disabled. */
|
|
104
|
-
currentContextTokens?: number;
|
|
105
93
|
/** Preselect this provider's sidebar entry (e.g. when reopening after /login). */
|
|
106
94
|
initialProviderId?: string;
|
|
107
|
-
/** Status-row hint shown in pick mode. */
|
|
108
|
-
pickerHint?: string;
|
|
109
|
-
initialQuery?: string;
|
|
110
95
|
}
|
|
111
96
|
|
|
112
97
|
interface SidebarEntry {
|
|
@@ -158,8 +143,6 @@ const RECENT_LIMIT = 15;
|
|
|
158
143
|
const SIDEBAR_MIN_WIDTH = 18;
|
|
159
144
|
const SIDEBAR_MAX_WIDTH = 26;
|
|
160
145
|
|
|
161
|
-
const PICK_MODE_HINT = "Session-only switch — role models stay unchanged";
|
|
162
|
-
|
|
163
146
|
/**
|
|
164
147
|
* Providers already auto-refreshed this process. Selecting a provider fetches
|
|
165
148
|
* its live model list at most once per application lifetime (surviving hub
|
|
@@ -182,8 +165,6 @@ export class ModelHubComponent implements Component {
|
|
|
182
165
|
#registry: ModelRegistry;
|
|
183
166
|
#scopedModels: ReadonlyArray<ScopedModelItem>;
|
|
184
167
|
#callbacks: ModelHubCallbacks;
|
|
185
|
-
#mode: ModelHubMode;
|
|
186
|
-
#pickerHint: string;
|
|
187
168
|
|
|
188
169
|
#browser: ModelBrowser;
|
|
189
170
|
#roles: RoleAssignments = {};
|
|
@@ -250,14 +231,9 @@ export class ModelHubComponent implements Component {
|
|
|
250
231
|
this.#registry = registry;
|
|
251
232
|
this.#scopedModels = scopedModels;
|
|
252
233
|
this.#callbacks = callbacks;
|
|
253
|
-
this.#mode = options.mode ?? "roles";
|
|
254
|
-
this.#pickerHint = options.pickerHint ?? PICK_MODE_HINT;
|
|
255
234
|
|
|
256
235
|
this.#browser = new ModelBrowser(settings, {
|
|
257
|
-
currentContextTokens: options.currentContextTokens,
|
|
258
|
-
disableOverContext: this.#mode === "pick",
|
|
259
236
|
emptyText: () => this.#emptyStateMessage(),
|
|
260
|
-
initialQuery: options.initialQuery,
|
|
261
237
|
});
|
|
262
238
|
this.#browser.onActivate = item => this.#activateItem(item);
|
|
263
239
|
this.#browser.onCancel = () => this.#callbacks.onCancel();
|
|
@@ -275,10 +251,6 @@ export class ModelHubComponent implements Component {
|
|
|
275
251
|
this.#setActiveEntry("all");
|
|
276
252
|
}
|
|
277
253
|
|
|
278
|
-
if (this.#mode === "pick") {
|
|
279
|
-
this.#focus = "list";
|
|
280
|
-
}
|
|
281
|
-
|
|
282
254
|
// Reconcile with cached discovery state in the background. A --models
|
|
283
255
|
// scope is registry-independent, so the offline reload would only repeat
|
|
284
256
|
// the synchronous hydration above.
|
|
@@ -314,62 +286,10 @@ export class ModelHubComponent implements Component {
|
|
|
314
286
|
return getKnownRoleIds(this.#settings).filter(role => !getRoleInfo(role, this.#settings).hidden);
|
|
315
287
|
}
|
|
316
288
|
|
|
317
|
-
#getResolvedRoleThinkingLevel(
|
|
318
|
-
role: string,
|
|
319
|
-
resolved: { explicitThinkingLevel: boolean; thinkingLevel?: ConfiguredThinkingLevel },
|
|
320
|
-
): ConfiguredThinkingLevel {
|
|
321
|
-
if (resolved.explicitThinkingLevel && resolved.thinkingLevel !== undefined) {
|
|
322
|
-
return resolved.thinkingLevel;
|
|
323
|
-
}
|
|
324
|
-
if (role === "default") {
|
|
325
|
-
return parseConfiguredThinkingLevel(this.#settings.get("defaultThinkingLevel")) ?? ThinkingLevel.Inherit;
|
|
326
|
-
}
|
|
327
|
-
return ThinkingLevel.Inherit;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
289
|
/** Resolve every known role: configured values first, auto-selection for the rest. */
|
|
331
290
|
#reloadRoles(autoCandidates: ReadonlyArray<Model>): void {
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
const matchPreferences = getModelMatchPreferences(this.#settings);
|
|
335
|
-
const knownRoles = getKnownRoleIds(this.#settings);
|
|
336
|
-
const configuredRoles = new Set<string>();
|
|
337
|
-
|
|
338
|
-
for (const role of knownRoles) {
|
|
339
|
-
const roleValue = this.#settings.getModelRole(role);
|
|
340
|
-
if (!roleValue) continue;
|
|
341
|
-
configuredRoles.add(role);
|
|
342
|
-
const resolved = resolveModelRoleValue(roleValue, allModels, {
|
|
343
|
-
settings: this.#settings,
|
|
344
|
-
matchPreferences,
|
|
345
|
-
});
|
|
346
|
-
if (resolved.model) {
|
|
347
|
-
nextRoles[role] = {
|
|
348
|
-
model: resolved.model,
|
|
349
|
-
thinkingLevel: this.#getResolvedRoleThinkingLevel(role, resolved),
|
|
350
|
-
autoSelected: false,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (autoCandidates.length > 0) {
|
|
356
|
-
const candidates = [...autoCandidates];
|
|
357
|
-
for (const role of knownRoles) {
|
|
358
|
-
if (configuredRoles.has(role)) continue;
|
|
359
|
-
const resolved = resolveModelRoleValue(`pi/${role}`, candidates, {
|
|
360
|
-
settings: this.#settings,
|
|
361
|
-
matchPreferences,
|
|
362
|
-
});
|
|
363
|
-
if (!resolved.model) continue;
|
|
364
|
-
nextRoles[role] = {
|
|
365
|
-
model: resolved.model,
|
|
366
|
-
thinkingLevel: this.#getResolvedRoleThinkingLevel(role, resolved),
|
|
367
|
-
autoSelected: true,
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
this.#roles = nextRoles;
|
|
291
|
+
const allModels = this.#scopedModels.length > 0 ? autoCandidates : this.#registry.getAll();
|
|
292
|
+
this.#roles = resolveRoleAssignments(this.#settings, allModels, autoCandidates);
|
|
373
293
|
}
|
|
374
294
|
|
|
375
295
|
/** Rebuild items, roles, and the sidebar from the registry's in-memory state. */
|
|
@@ -475,16 +395,15 @@ export class ModelHubComponent implements Component {
|
|
|
475
395
|
|
|
476
396
|
// Roles leads the fixed section so downward hops from Recent head into
|
|
477
397
|
// model scopes instead of being captured by the roles view.
|
|
478
|
-
const fixed: SidebarEntry[] = [
|
|
479
|
-
|
|
480
|
-
fixed.push({
|
|
398
|
+
const fixed: SidebarEntry[] = [
|
|
399
|
+
{
|
|
481
400
|
id: "roles",
|
|
482
401
|
kind: "roles",
|
|
483
402
|
label: "Roles",
|
|
484
403
|
annotation: `${assignedCount}/${visibleRoles.length}`,
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
404
|
+
},
|
|
405
|
+
{ id: "all", kind: "all", label: "All models", annotation: String(availableModels.length) },
|
|
406
|
+
];
|
|
488
407
|
|
|
489
408
|
this.#fixedEntries = fixed;
|
|
490
409
|
this.#unlockedProviderEntries = [...unlocked]
|
|
@@ -811,10 +730,6 @@ export class ModelHubComponent implements Component {
|
|
|
811
730
|
// ═══════════════════════════════════════════════════════════════════════
|
|
812
731
|
|
|
813
732
|
#activateItem(item: ModelBrowserItem): void {
|
|
814
|
-
if (this.#mode === "pick") {
|
|
815
|
-
this.#callbacks.onPick?.(item.model, item.selector);
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
733
|
if (this.#assigning) {
|
|
819
734
|
const target = this.#assigning;
|
|
820
735
|
this.#assigning = null;
|
|
@@ -917,7 +832,7 @@ export class ModelHubComponent implements Component {
|
|
|
917
832
|
const strip = this.#strip;
|
|
918
833
|
this.#strip = null;
|
|
919
834
|
this.#chipRanges = [];
|
|
920
|
-
if (strip?.kind === "thinking" && strip.returnToRoles
|
|
835
|
+
if (strip?.kind === "thinking" && strip.returnToRoles) {
|
|
921
836
|
this.#setActiveEntry("roles");
|
|
922
837
|
this.#focus = "list";
|
|
923
838
|
}
|
|
@@ -1030,14 +945,12 @@ export class ModelHubComponent implements Component {
|
|
|
1030
945
|
}
|
|
1031
946
|
this.#setFallbackChain(target.role, chain);
|
|
1032
947
|
this.#browser.setQuery("");
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
if (rowIndex >= 0) this.#roleIndex = rowIndex;
|
|
1040
|
-
}
|
|
948
|
+
this.#setActiveEntry("roles");
|
|
949
|
+
this.#focus = "list";
|
|
950
|
+
const rowIndex = this.#rolesRows.findIndex(
|
|
951
|
+
row => row.kind === "fallback" && row.role === target.role && row.selector === selector,
|
|
952
|
+
);
|
|
953
|
+
if (rowIndex >= 0) this.#roleIndex = rowIndex;
|
|
1041
954
|
}
|
|
1042
955
|
|
|
1043
956
|
/** Persist `role`'s chain through the host callback and rebuild dependent state. */
|
|
@@ -1076,10 +989,8 @@ export class ModelHubComponent implements Component {
|
|
|
1076
989
|
#cancelAssign(): void {
|
|
1077
990
|
this.#assigning = null;
|
|
1078
991
|
this.#browser.setQuery("");
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
this.#focus = "list";
|
|
1082
|
-
}
|
|
992
|
+
this.#setActiveEntry("roles");
|
|
993
|
+
this.#focus = "list";
|
|
1083
994
|
}
|
|
1084
995
|
|
|
1085
996
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -1655,7 +1566,7 @@ export class ModelHubComponent implements Component {
|
|
|
1655
1566
|
let text: string;
|
|
1656
1567
|
switch (entry.kind) {
|
|
1657
1568
|
case "recent":
|
|
1658
|
-
text =
|
|
1569
|
+
text = `Recently used models${scopedSuffix}`;
|
|
1659
1570
|
break;
|
|
1660
1571
|
case "roles":
|
|
1661
1572
|
text = "Model roles — f adds a retry fallback, cleared roles fall back to auto-selection";
|
|
@@ -1670,7 +1581,7 @@ export class ModelHubComponent implements Component {
|
|
|
1670
1581
|
}
|
|
1671
1582
|
break;
|
|
1672
1583
|
default:
|
|
1673
|
-
text =
|
|
1584
|
+
text = `All available models${scopedSuffix}`;
|
|
1674
1585
|
break;
|
|
1675
1586
|
}
|
|
1676
1587
|
if (this.#configError && entry.kind !== "provider") {
|
|
@@ -1895,9 +1806,6 @@ export class ModelHubComponent implements Component {
|
|
|
1895
1806
|
}
|
|
1896
1807
|
const arrows = this.#focus === "scope" ? "↑/↓ providers · → models" : "↑/↓ models · ← providers";
|
|
1897
1808
|
const refresh = entry.kind === "provider" ? " · F5 refresh" : "";
|
|
1898
|
-
if (this.#mode === "pick") {
|
|
1899
|
-
return `Enter use for this session · ${arrows} · type to search${refresh} · Esc close`;
|
|
1900
|
-
}
|
|
1901
1809
|
return `Enter assign roles · ${arrows} · type to search${refresh} · Esc close`;
|
|
1902
1810
|
}
|
|
1903
1811
|
|
|
@@ -1992,9 +1900,8 @@ export class ModelHubComponent implements Component {
|
|
|
1992
1900
|
|
|
1993
1901
|
const sidebarLines = this.#renderSidebar(sidebarWidth, contentRows);
|
|
1994
1902
|
|
|
1995
|
-
const title = this.#mode === "pick" ? "Switch Model" : "Models";
|
|
1996
1903
|
const out: string[] = [];
|
|
1997
|
-
out.push(topBorderSplit(width,
|
|
1904
|
+
out.push(topBorderSplit(width, "Models", sidebarWidth));
|
|
1998
1905
|
this.#contentRowStart = out.length;
|
|
1999
1906
|
for (let i = 0; i < contentRows; i++) {
|
|
2000
1907
|
out.push(splitRow(sidebarLines[i] ?? "", bodyLines[i] ?? "", width, sidebarWidth));
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact session-model picker (alt+p / `/switch`): a bottom-anchored
|
|
3
|
+
* floating overlay hosting just a {@link ModelBrowser} — no provider sidebar.
|
|
4
|
+
* Model entries switch the current session only; a search beginning with `@`
|
|
5
|
+
* exposes the configured ctrl+p quick roles.
|
|
6
|
+
*/
|
|
7
|
+
import type { Model } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import type { Component, TUI } from "@oh-my-pi/pi-tui";
|
|
9
|
+
import type { ModelRegistry } from "../../config/model-registry";
|
|
10
|
+
import type { Settings } from "../../config/settings";
|
|
11
|
+
import type { ResolvedRoleModel } from "../../session/agent-session";
|
|
12
|
+
import { theme } from "../theme/theme";
|
|
13
|
+
import {
|
|
14
|
+
buildBrowserItems,
|
|
15
|
+
ModelBrowser,
|
|
16
|
+
type ModelBrowserItem,
|
|
17
|
+
resolveRoleAssignments,
|
|
18
|
+
sortModelItems,
|
|
19
|
+
} from "./model-browser";
|
|
20
|
+
import type { ScopedModelItem } from "./model-hub";
|
|
21
|
+
import { bottomBorder, row, topBorder } from "./overlay-box";
|
|
22
|
+
import { resolveSegmentPalette } from "./segment-track";
|
|
23
|
+
|
|
24
|
+
export interface ModelPickerCallbacks {
|
|
25
|
+
/** A model was chosen for a session-only switch. `selector` is `provider/id`. */
|
|
26
|
+
onPick: (model: Model, selector: string) => void;
|
|
27
|
+
/** A configured ctrl+p quick role was chosen. */
|
|
28
|
+
onPickRole?: (entry: ResolvedRoleModel) => void;
|
|
29
|
+
/** The picker was dismissed. */
|
|
30
|
+
onCancel: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ModelPickerOptions {
|
|
34
|
+
/** Session token count; models with smaller context windows are disabled. */
|
|
35
|
+
currentContextTokens?: number;
|
|
36
|
+
/** `provider/id` of the session's active model; highlighted and preselected. */
|
|
37
|
+
currentSelector?: string;
|
|
38
|
+
/** Resolved role models in the same order used by the ctrl+p quick-role cycle. */
|
|
39
|
+
quickRoles?: ReadonlyArray<ResolvedRoleModel>;
|
|
40
|
+
/** Complete ctrl+p order, including unavailable roles, to preserve segment colors. */
|
|
41
|
+
quickRoleOrder?: ReadonlyArray<string>;
|
|
42
|
+
/** Active quick role, highlighted when the search begins with `@`. */
|
|
43
|
+
currentQuickRole?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Fixed chrome rows: top border, status row, footer, bottom border. */
|
|
47
|
+
const CHROME_ROWS = 4;
|
|
48
|
+
/** Rows the browser renders around its list window (search + blank, blank + two detail rows). */
|
|
49
|
+
const BROWSER_FRAME_ROWS = 5;
|
|
50
|
+
/** Minimum rows for the browser list window on short terminals. */
|
|
51
|
+
const MIN_VISIBLE = 5;
|
|
52
|
+
/** Fraction of the terminal height the floating overlay occupies. */
|
|
53
|
+
const HEIGHT_FRACTION = 0.4;
|
|
54
|
+
|
|
55
|
+
const STATUS_HINT = "Session-only switch — role models stay unchanged";
|
|
56
|
+
const QUICK_ROLE_STATUS_HINT = "Quick role switch — applies its model and thinking for this session";
|
|
57
|
+
const FOOTER_HINT = "↑/↓ models · Enter use for this session · type to search · @ quick roles · Esc close";
|
|
58
|
+
const QUICK_ROLE_FOOTER_HINT = "↑/↓ roles · Enter apply role model · type to search · Esc close";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The alt+p picker component. Hosted as a non-fullscreen bottom-anchored
|
|
62
|
+
* overlay (`ui.showOverlay(..., { anchor: "bottom-center" })`); keyboard-only,
|
|
63
|
+
* since mouse tracking is reserved for fullscreen overlays.
|
|
64
|
+
*/
|
|
65
|
+
export class ModelPickerComponent implements Component {
|
|
66
|
+
#tui: TUI;
|
|
67
|
+
#settings: Settings;
|
|
68
|
+
#registry: ModelRegistry;
|
|
69
|
+
#scopedModels: ReadonlyArray<ScopedModelItem>;
|
|
70
|
+
#browser: ModelBrowser;
|
|
71
|
+
#configError: string | undefined;
|
|
72
|
+
#currentSelector: string | undefined;
|
|
73
|
+
#currentQuickRoleSelector: string | undefined;
|
|
74
|
+
#modelItems: ModelBrowserItem[] = [];
|
|
75
|
+
#quickRoleItems: ModelBrowserItem[] = [];
|
|
76
|
+
#quickRoles = new Map<string, ResolvedRoleModel>();
|
|
77
|
+
#roleMode = false;
|
|
78
|
+
|
|
79
|
+
constructor(
|
|
80
|
+
tui: TUI,
|
|
81
|
+
settings: Settings,
|
|
82
|
+
registry: ModelRegistry,
|
|
83
|
+
scopedModels: ReadonlyArray<ScopedModelItem>,
|
|
84
|
+
callbacks: ModelPickerCallbacks,
|
|
85
|
+
options: ModelPickerOptions = {},
|
|
86
|
+
) {
|
|
87
|
+
this.#tui = tui;
|
|
88
|
+
this.#settings = settings;
|
|
89
|
+
this.#registry = registry;
|
|
90
|
+
this.#scopedModels = scopedModels;
|
|
91
|
+
this.#currentSelector = options.currentSelector;
|
|
92
|
+
this.#currentQuickRoleSelector = options.currentQuickRole ? `@${options.currentQuickRole}` : undefined;
|
|
93
|
+
this.#quickRoleItems = this.#buildQuickRoleItems(
|
|
94
|
+
options.quickRoles ?? [],
|
|
95
|
+
options.quickRoleOrder ?? options.quickRoles?.map(entry => entry.role) ?? [],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
this.#browser = new ModelBrowser(settings, {
|
|
99
|
+
currentContextTokens: options.currentContextTokens,
|
|
100
|
+
disableOverContext: true,
|
|
101
|
+
emptyText: () => (this.#roleMode ? " No quick roles in the Ctrl+P cycle" : undefined),
|
|
102
|
+
});
|
|
103
|
+
this.#browser.onActivate = item => {
|
|
104
|
+
const quickRole = this.#quickRoles.get(item.selector);
|
|
105
|
+
if (quickRole) {
|
|
106
|
+
callbacks.onPickRole?.(quickRole);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
callbacks.onPick(item.model, item.selector);
|
|
110
|
+
};
|
|
111
|
+
this.#browser.onCancel = () => callbacks.onCancel();
|
|
112
|
+
this.#browser.onQueryChange = query => this.#syncItemsForQuery(query);
|
|
113
|
+
|
|
114
|
+
// Hydrate synchronously from the current registry snapshot so the first
|
|
115
|
+
// Enter after opening acts on cached models instead of being dropped
|
|
116
|
+
// while the offline refresh promise is still pending.
|
|
117
|
+
this.#syncFromRegistryState();
|
|
118
|
+
if (options.currentSelector) {
|
|
119
|
+
this.#browser.selectSelector(options.currentSelector);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Reconcile with cached discovery state in the background. A --models
|
|
123
|
+
// scope is registry-independent, so the offline reload would only repeat
|
|
124
|
+
// the synchronous hydration above.
|
|
125
|
+
if (this.#scopedModels.length === 0) {
|
|
126
|
+
this.#registry
|
|
127
|
+
.refresh("offline")
|
|
128
|
+
.then(() => this.#syncFromRegistryState())
|
|
129
|
+
.catch(error => {
|
|
130
|
+
this.#configError = error instanceof Error ? error.message : String(error);
|
|
131
|
+
})
|
|
132
|
+
.finally(() => this.#tui.requestRender());
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
invalidate(): void {}
|
|
137
|
+
|
|
138
|
+
/** Rebuild model items and role chips from the registry's in-memory state. */
|
|
139
|
+
#syncFromRegistryState(): void {
|
|
140
|
+
let models: ReadonlyArray<Model>;
|
|
141
|
+
if (this.#scopedModels.length > 0) {
|
|
142
|
+
models = this.#scopedModels.map(scoped => scoped.model);
|
|
143
|
+
this.#configError = undefined;
|
|
144
|
+
} else {
|
|
145
|
+
const loadError = this.#registry.getError();
|
|
146
|
+
this.#configError = loadError ? String(loadError) : undefined;
|
|
147
|
+
try {
|
|
148
|
+
models = this.#registry.getAvailable();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
this.#configError = error instanceof Error ? error.message : String(error);
|
|
151
|
+
models = [];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const allModels = this.#scopedModels.length > 0 ? models : this.#registry.getAll();
|
|
156
|
+
const roles = resolveRoleAssignments(this.#settings, allModels, models);
|
|
157
|
+
const storage = this.#settings.getStorage();
|
|
158
|
+
const mruOrder = storage?.getModelUsageOrder() ?? [];
|
|
159
|
+
this.#modelItems = buildBrowserItems(models);
|
|
160
|
+
sortModelItems(this.#modelItems, { roles, mruOrder });
|
|
161
|
+
this.#browser.setRoles(roles);
|
|
162
|
+
this.#browser.setMruOrder(mruOrder);
|
|
163
|
+
this.#browser.setPerfStats(storage?.getModelPerf() ?? new Map());
|
|
164
|
+
this.#syncItemsForQuery(this.#browser.query, true);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Build virtual `@role` rows, colored by their ctrl+p segment position. */
|
|
168
|
+
#buildQuickRoleItems(
|
|
169
|
+
quickRoles: ReadonlyArray<ResolvedRoleModel>,
|
|
170
|
+
quickRoleOrder: ReadonlyArray<string>,
|
|
171
|
+
): ModelBrowserItem[] {
|
|
172
|
+
const order = quickRoleOrder.length > 0 ? quickRoleOrder : quickRoles.map(entry => entry.role);
|
|
173
|
+
const palette = resolveSegmentPalette(order.length);
|
|
174
|
+
return quickRoles.map((entry, index) => {
|
|
175
|
+
const selector = `@${entry.role}`;
|
|
176
|
+
this.#quickRoles.set(selector, entry);
|
|
177
|
+
const orderIndex = order.indexOf(entry.role);
|
|
178
|
+
return {
|
|
179
|
+
provider: "",
|
|
180
|
+
id: selector,
|
|
181
|
+
model: entry.model,
|
|
182
|
+
selector,
|
|
183
|
+
labelColor: palette[(orderIndex >= 0 ? orderIndex : index) % palette.length],
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Switch browser content only when a leading `@` changes the search mode. */
|
|
189
|
+
#syncItemsForQuery(query: string, refresh = false): void {
|
|
190
|
+
const roleMode = query.startsWith("@");
|
|
191
|
+
const modeChanged = roleMode !== this.#roleMode;
|
|
192
|
+
if (!modeChanged && !refresh) return;
|
|
193
|
+
|
|
194
|
+
this.#roleMode = roleMode;
|
|
195
|
+
this.#browser.setShowProvider(!roleMode);
|
|
196
|
+
this.#browser.setDisableOverContext(!roleMode);
|
|
197
|
+
this.#browser.setPreserveQueryOrder(roleMode);
|
|
198
|
+
const currentSelector = roleMode ? this.#currentQuickRoleSelector : this.#currentSelector;
|
|
199
|
+
this.#browser.setCurrentSelector(currentSelector);
|
|
200
|
+
this.#browser.setItems(roleMode ? this.#quickRoleItems : this.#modelItems);
|
|
201
|
+
if (modeChanged && currentSelector) {
|
|
202
|
+
this.#browser.selectSelector(currentSelector);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
handleInput(data: string): void {
|
|
207
|
+
// Mouse tracking is off outside fullscreen overlays; drop any stray SGR
|
|
208
|
+
// reports instead of feeding them to the search input.
|
|
209
|
+
if (data.startsWith("\x1b[<")) return;
|
|
210
|
+
this.#browser.handleInput(data);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
render(width: number): string[] {
|
|
214
|
+
const termRows = Math.max(16, this.#tui.terminal?.rows || process.stdout.rows || 40);
|
|
215
|
+
const listBudget = Math.floor(termRows * HEIGHT_FRACTION) - CHROME_ROWS - BROWSER_FRAME_ROWS;
|
|
216
|
+
this.#browser.setMaxVisible(Math.max(MIN_VISIBLE, listBudget));
|
|
217
|
+
|
|
218
|
+
const inner = Math.max(1, width - 4);
|
|
219
|
+
const status = this.#configError
|
|
220
|
+
? theme.fg("error", ` ${this.#configError}`)
|
|
221
|
+
: theme.fg("muted", ` ${this.#roleMode ? QUICK_ROLE_STATUS_HINT : STATUS_HINT}`);
|
|
222
|
+
|
|
223
|
+
const out: string[] = [];
|
|
224
|
+
out.push(topBorder(width, "Switch Model"));
|
|
225
|
+
out.push(row(status, width));
|
|
226
|
+
for (const line of this.#browser.render(inner)) {
|
|
227
|
+
out.push(row(line, width));
|
|
228
|
+
}
|
|
229
|
+
out.push(row(theme.fg("dim", this.#roleMode ? QUICK_ROLE_FOOTER_HINT : FOOTER_HINT), width));
|
|
230
|
+
out.push(bottomBorder(width));
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
Fix the settings overlay crash. Wheeling past the last row throws.
|
|
1
|
+
¶user:Fix the settings overlay crash. Wheeling past the last row throws.
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
//Reading the select-list hit test
|
|
6
|
-
read(path="src/select-list.ts:140-180")
|
|
3
|
+
¶call:read(path="src/select-list.ts:140-180")//Reading the select-list hit test
|
|
7
4
|
<out>
|
|
8
5
|
162: const index = Math.floor(line / rowHeight); index is never checked against bounds.
|
|
9
6
|
</out>
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
Found it. The hit test indexes past the filtered list; clamping to the last row fixes the crash.
|
|
8
|
+
¶ai:Found it. The hit test indexes past the filtered list; clamping to the last row fixes the crash.
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
Does the fix survive filtering?
|
|
10
|
+
¶user:Does the fix survive filtering?
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
¶think:Check whether the clamp runs before or after filtering.
|
|
13
|
+
|
|
14
|
+
¶ai:Yes. The clamp applies after the filter pass, so a narrowed list keeps the hit map in sync. Added a regression test that wheels past the last row with a filter active and asserts no throw.
|
|
@@ -4,15 +4,43 @@ import type { AgentSession } from "../../../session/agent-session";
|
|
|
4
4
|
import { getThemeByName, setThemeInstance } from "../../theme/theme";
|
|
5
5
|
import { StatusLineComponent } from "./component";
|
|
6
6
|
|
|
7
|
-
function makeSessionWithLastMessage(lastMessage: unknown) {
|
|
7
|
+
function makeSessionWithLastMessage(lastMessage: unknown, prewalkArmed: boolean = false) {
|
|
8
8
|
return {
|
|
9
|
-
messages: [lastMessage],
|
|
9
|
+
messages: lastMessage ? [lastMessage] : [],
|
|
10
10
|
model: { contextWindow: 128000 },
|
|
11
11
|
contextUsageRevision: 0,
|
|
12
12
|
systemPrompt: [],
|
|
13
13
|
agent: { state: { tools: [] } },
|
|
14
14
|
skills: [],
|
|
15
15
|
getContextUsage: () => ({ tokens: 42, contextWindow: 128000 }),
|
|
16
|
+
state: {
|
|
17
|
+
messages: lastMessage ? [lastMessage] : [],
|
|
18
|
+
model: { contextWindow: 128000 },
|
|
19
|
+
},
|
|
20
|
+
sessionManager: {
|
|
21
|
+
getUsageStatistics: () => ({
|
|
22
|
+
input: 0,
|
|
23
|
+
output: 0,
|
|
24
|
+
cacheRead: 0,
|
|
25
|
+
cacheWrite: 0,
|
|
26
|
+
totalTokens: 0,
|
|
27
|
+
orchestrationInput: 0,
|
|
28
|
+
orchestrationOutput: 0,
|
|
29
|
+
orchestrationCacheRead: 0,
|
|
30
|
+
premiumRequests: 0,
|
|
31
|
+
cost: 0,
|
|
32
|
+
tokensPerSecond: null,
|
|
33
|
+
}),
|
|
34
|
+
getSessionName: () => "test-session",
|
|
35
|
+
},
|
|
36
|
+
getPrewalkState: () => (prewalkArmed ? { target: { id: "cheap-model", provider: "openai" } } : undefined),
|
|
37
|
+
getAsyncJobSnapshot: () => undefined,
|
|
38
|
+
isAdvisorActive: () => false,
|
|
39
|
+
isFastModeActive: () => false,
|
|
40
|
+
configuredThinkingLevel: () => undefined,
|
|
41
|
+
modelRegistry: {
|
|
42
|
+
isUsingOAuth: () => false,
|
|
43
|
+
},
|
|
16
44
|
};
|
|
17
45
|
}
|
|
18
46
|
|
|
@@ -41,4 +69,15 @@ describe("StatusLineComponent", () => {
|
|
|
41
69
|
|
|
42
70
|
expect(statusLine.getCachedContextBreakdown()).toEqual({ usedTokens: 42, contextWindow: 128000 });
|
|
43
71
|
});
|
|
72
|
+
|
|
73
|
+
it("renders Prewalk annotation when prewalk is armed", () => {
|
|
74
|
+
const statusLine = new StatusLineComponent(makeSessionWithLastMessage(null, true) as unknown as AgentSession);
|
|
75
|
+
|
|
76
|
+
// By default preset, 'mode' segment is included in left/right segments.
|
|
77
|
+
// Let's get the border and see if Prewalk is rendered.
|
|
78
|
+
const border = statusLine.getTopBorder(100);
|
|
79
|
+
// SGR codes might be included, so we check if the stripped content contains "Prewalk"
|
|
80
|
+
const stripped = border.content.replace(/\x1b\[[0-9;]*m/g, "");
|
|
81
|
+
expect(stripped).toContain("Prewalk");
|
|
82
|
+
});
|
|
44
83
|
});
|
|
@@ -1051,6 +1051,10 @@ export class StatusLineComponent implements Component {
|
|
|
1051
1051
|
compactThinkingLevel: this.#resolveSettings().compactThinkingLevel ?? false,
|
|
1052
1052
|
planMode: this.#planModeStatus,
|
|
1053
1053
|
loopMode: this.#loopModeStatus,
|
|
1054
|
+
prewalk:
|
|
1055
|
+
typeof this.session.getPrewalkState === "function" && this.session.getPrewalkState()
|
|
1056
|
+
? { enabled: true }
|
|
1057
|
+
: null,
|
|
1054
1058
|
goalMode: this.#goalModeStatus,
|
|
1055
1059
|
vibeMode: this.#vibeModeStatus,
|
|
1056
1060
|
collab: this.#collabStatus,
|
|
@@ -208,6 +208,12 @@ const modeSegment: StatusLineSegment = {
|
|
|
208
208
|
return { content: theme.fg(color, content), visible: true };
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
const prewalk = ctx.prewalk;
|
|
212
|
+
if (prewalk?.enabled) {
|
|
213
|
+
const content = withIcon(theme.icon.prewalk, "Prewalk");
|
|
214
|
+
return { content: theme.fg("accent", content), visible: true };
|
|
215
|
+
}
|
|
216
|
+
|
|
211
217
|
const goal = ctx.goalMode;
|
|
212
218
|
if (goal && (goal.enabled || goal.paused)) {
|
|
213
219
|
return renderGoalMode(ctx, goal);
|
|
@@ -1200,7 +1200,15 @@ export class CommandController {
|
|
|
1200
1200
|
this.ctx.rebuildChatFromMessages();
|
|
1201
1201
|
|
|
1202
1202
|
this.ctx.statusLine.invalidate();
|
|
1203
|
-
|
|
1203
|
+
// Same as the auto-compaction rebuild: a collapsed transcript is an
|
|
1204
|
+
// intentional replacement, so drop the stale pre-compaction scrollback
|
|
1205
|
+
// instead of repainting the shrunken frame below it. With collapse
|
|
1206
|
+
// disabled the full history stays inline and scrollback is kept.
|
|
1207
|
+
if (this.ctx.settings.get("display.collapseCompacted")) {
|
|
1208
|
+
this.ctx.ui.requestRender(true, { clearScrollback: true });
|
|
1209
|
+
} else {
|
|
1210
|
+
this.ctx.ui.requestRender();
|
|
1211
|
+
}
|
|
1204
1212
|
} catch (error) {
|
|
1205
1213
|
if (error instanceof CompactionCancelledError) {
|
|
1206
1214
|
outcome = "cancelled";
|