gsd-pi 2.25.0 → 2.26.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/README.md +11 -2
- package/dist/headless.js +24 -4
- package/dist/resources/extensions/async-jobs/index.ts +9 -1
- package/dist/resources/extensions/bg-shell/index.ts +3 -2
- package/dist/resources/extensions/gsd/auto-recovery.ts +7 -4
- package/dist/resources/extensions/gsd/auto-worktree.ts +14 -3
- package/dist/resources/extensions/gsd/auto.ts +81 -12
- package/dist/resources/extensions/gsd/doctor-proactive.ts +7 -6
- package/dist/resources/extensions/gsd/doctor.ts +24 -1
- package/dist/resources/extensions/gsd/files.ts +13 -2
- package/dist/resources/extensions/gsd/guided-flow.ts +19 -9
- package/dist/resources/extensions/gsd/index.ts +48 -7
- package/dist/resources/extensions/gsd/migrate/writer.ts +39 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
- package/dist/resources/extensions/gsd/preferences.ts +2 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +2 -2
- package/dist/resources/extensions/gsd/roadmap-slices.ts +45 -1
- package/dist/resources/extensions/gsd/state.ts +17 -6
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
- package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
- package/dist/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
- package/dist/resources/extensions/gsd/types.ts +2 -0
- package/dist/resources/extensions/search-the-web/native-search.ts +4 -0
- package/dist/resources/extensions/shared/path-display.ts +19 -0
- package/package.json +1 -6
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +25 -0
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +27 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +32 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/keybindings.js +1 -1
- package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +12 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.js +7 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +2 -2
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +8 -3
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +2 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +5 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +41 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +301 -62
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +63 -30
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts +8 -0
- package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/tests/path-display.test.js +60 -0
- package/packages/pi-coding-agent/dist/tests/path-display.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/utils/clipboard-image.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/utils/clipboard-image.js +32 -6
- package/packages/pi-coding-agent/dist/utils/clipboard-image.js.map +1 -1
- package/packages/pi-coding-agent/dist/utils/path-display.d.ts +34 -0
- package/packages/pi-coding-agent/dist/utils/path-display.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/utils/path-display.js +36 -0
- package/packages/pi-coding-agent/dist/utils/path-display.js.map +1 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +36 -0
- package/packages/pi-coding-agent/src/core/keybindings.ts +1 -1
- package/packages/pi-coding-agent/src/core/lsp/client.ts +11 -1
- package/packages/pi-coding-agent/src/core/lsp/index.ts +7 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +17 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/core/system-prompt.ts +2 -1
- package/packages/pi-coding-agent/src/index.ts +15 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +347 -62
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +40 -4
- package/packages/pi-coding-agent/src/tests/path-display.test.ts +85 -0
- package/packages/pi-coding-agent/src/utils/clipboard-image.ts +33 -6
- package/packages/pi-coding-agent/src/utils/path-display.ts +36 -0
- package/src/resources/extensions/async-jobs/index.ts +9 -1
- package/src/resources/extensions/bg-shell/index.ts +3 -2
- package/src/resources/extensions/gsd/auto-recovery.ts +7 -4
- package/src/resources/extensions/gsd/auto-worktree.ts +14 -3
- package/src/resources/extensions/gsd/auto.ts +81 -12
- package/src/resources/extensions/gsd/doctor-proactive.ts +7 -6
- package/src/resources/extensions/gsd/doctor.ts +24 -1
- package/src/resources/extensions/gsd/files.ts +13 -2
- package/src/resources/extensions/gsd/guided-flow.ts +19 -9
- package/src/resources/extensions/gsd/index.ts +48 -7
- package/src/resources/extensions/gsd/migrate/writer.ts +39 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
- package/src/resources/extensions/gsd/preferences.ts +2 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +2 -2
- package/src/resources/extensions/gsd/roadmap-slices.ts +45 -1
- package/src/resources/extensions/gsd/state.ts +17 -6
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
- package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
- package/src/resources/extensions/gsd/types.ts +2 -0
- package/src/resources/extensions/search-the-web/native-search.ts +4 -0
- package/src/resources/extensions/shared/path-display.ts +19 -0
|
@@ -38,10 +38,22 @@ interface ScopedModelItem {
|
|
|
38
38
|
thinkingLevel?: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* A navigable row — either a provider group header or a selectable model entry.
|
|
43
|
+
*/
|
|
44
|
+
type ListRow =
|
|
45
|
+
| { kind: "header"; provider: string; count: number }
|
|
46
|
+
| { kind: "model"; item: ModelItem };
|
|
47
|
+
|
|
41
48
|
type ModelScope = "all" | "scoped";
|
|
42
49
|
|
|
43
50
|
/**
|
|
44
|
-
* Component that renders a model selector with search
|
|
51
|
+
* Component that renders a grouped model selector with search.
|
|
52
|
+
*
|
|
53
|
+
* Browsing (no search): models are grouped under provider headers.
|
|
54
|
+
* - Current model's provider is shown first; remaining providers sorted alphabetically.
|
|
55
|
+
* - Arrow keys navigate all rows; headers are skipped during selection.
|
|
56
|
+
* Searching: reverts to a flat fuzzy-filtered list (same as before), with [provider] badges.
|
|
45
57
|
*/
|
|
46
58
|
export class ModelSelectorComponent extends Container implements Focusable {
|
|
47
59
|
private searchInput: Input;
|
|
@@ -59,8 +71,17 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|
|
59
71
|
private allModels: ModelItem[] = [];
|
|
60
72
|
private scopedModelItems: ModelItem[] = [];
|
|
61
73
|
private activeModels: ModelItem[] = [];
|
|
74
|
+
|
|
75
|
+
// Grouped (browse) state
|
|
76
|
+
private groupedRows: ListRow[] = [];
|
|
77
|
+
private modelRowIndices: number[] = []; // indices into groupedRows that are "model" kind
|
|
78
|
+
private selectedGroupIndex: number = 0; // index into groupedRows (can be model or header)
|
|
79
|
+
|
|
80
|
+
// Search (flat) state
|
|
62
81
|
private filteredModels: ModelItem[] = [];
|
|
63
|
-
private
|
|
82
|
+
private selectedFlatIndex: number = 0;
|
|
83
|
+
|
|
84
|
+
private isSearching: boolean = false;
|
|
64
85
|
private currentModel?: Model<any>;
|
|
65
86
|
private settingsManager: SettingsManager;
|
|
66
87
|
private modelRegistry: ModelRegistry;
|
|
@@ -116,9 +137,13 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|
|
116
137
|
this.searchInput.setValue(initialSearchInput);
|
|
117
138
|
}
|
|
118
139
|
this.searchInput.onSubmit = () => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
140
|
+
if (this.isSearching) {
|
|
141
|
+
if (this.filteredModels[this.selectedFlatIndex]) {
|
|
142
|
+
this.handleSelect(this.filteredModels[this.selectedFlatIndex].model);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
const model = this.getSelectedModel();
|
|
146
|
+
if (model) this.handleSelect(model);
|
|
122
147
|
}
|
|
123
148
|
};
|
|
124
149
|
this.addChild(this.searchInput);
|
|
@@ -137,8 +162,11 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|
|
137
162
|
// Load models and do initial render
|
|
138
163
|
this.loadModels().then(() => {
|
|
139
164
|
if (initialSearchInput) {
|
|
165
|
+
this.isSearching = true;
|
|
140
166
|
this.filterModels(initialSearchInput);
|
|
141
167
|
} else {
|
|
168
|
+
this.buildGroupedRows();
|
|
169
|
+
this.jumpToCurrentModel();
|
|
142
170
|
this.updateList();
|
|
143
171
|
}
|
|
144
172
|
// Request re-render after models are loaded
|
|
@@ -171,12 +199,14 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|
|
171
199
|
this.scopedModelItems = [];
|
|
172
200
|
this.activeModels = [];
|
|
173
201
|
this.filteredModels = [];
|
|
202
|
+
this.groupedRows = [];
|
|
203
|
+
this.modelRowIndices = [];
|
|
174
204
|
this.errorMessage = error instanceof Error ? error.message : String(error);
|
|
175
205
|
return;
|
|
176
206
|
}
|
|
177
207
|
|
|
178
|
-
this.allModels = this.
|
|
179
|
-
this.scopedModelItems = this.
|
|
208
|
+
this.allModels = this.sortModelsWithinProvider(models);
|
|
209
|
+
this.scopedModelItems = this.sortModelsWithinProvider(
|
|
180
210
|
this.scopedModels.map((scoped) => ({
|
|
181
211
|
provider: scoped.model.provider,
|
|
182
212
|
id: scoped.model.id,
|
|
@@ -185,18 +215,20 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|
|
185
215
|
);
|
|
186
216
|
this.activeModels = this.scope === "scoped" ? this.scopedModelItems : this.allModels;
|
|
187
217
|
this.filteredModels = this.activeModels;
|
|
188
|
-
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
|
|
189
218
|
}
|
|
190
219
|
|
|
191
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Sort models within each provider: current model first, then by name desc.
|
|
222
|
+
* Provider ordering is handled separately in buildGroupedRows().
|
|
223
|
+
*/
|
|
224
|
+
private sortModelsWithinProvider(models: ModelItem[]): ModelItem[] {
|
|
192
225
|
const sorted = [...models];
|
|
193
|
-
// Sort: current model first, then by name descending (newest first), then by provider
|
|
194
226
|
sorted.sort((a, b) => {
|
|
195
227
|
const aIsCurrent = modelsAreEqual(this.currentModel, a.model);
|
|
196
228
|
const bIsCurrent = modelsAreEqual(this.currentModel, b.model);
|
|
197
229
|
if (aIsCurrent && !bIsCurrent) return -1;
|
|
198
230
|
if (!aIsCurrent && bIsCurrent) return 1;
|
|
199
|
-
//
|
|
231
|
+
// Within provider: newest/largest model name first
|
|
200
232
|
const nameCmp = b.model.name.localeCompare(a.model.name);
|
|
201
233
|
if (nameCmp !== 0) return nameCmp;
|
|
202
234
|
return a.provider.localeCompare(b.provider);
|
|
@@ -204,6 +236,79 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|
|
204
236
|
return sorted;
|
|
205
237
|
}
|
|
206
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Build the grouped rows array for browse mode.
|
|
241
|
+
* Current model's provider comes first; remaining providers sorted alphabetically.
|
|
242
|
+
*/
|
|
243
|
+
private buildGroupedRows(): void {
|
|
244
|
+
// Group models by provider
|
|
245
|
+
const byProvider = new Map<string, ModelItem[]>();
|
|
246
|
+
for (const item of this.activeModels) {
|
|
247
|
+
let group = byProvider.get(item.provider);
|
|
248
|
+
if (!group) {
|
|
249
|
+
group = [];
|
|
250
|
+
byProvider.set(item.provider, group);
|
|
251
|
+
}
|
|
252
|
+
group.push(item);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Determine provider order: current model's provider first, rest alphabetically
|
|
256
|
+
const currentProvider = this.currentModel?.provider;
|
|
257
|
+
const providers = Array.from(byProvider.keys()).sort((a, b) => {
|
|
258
|
+
if (a === currentProvider) return -1;
|
|
259
|
+
if (b === currentProvider) return 1;
|
|
260
|
+
return a.localeCompare(b);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const rows: ListRow[] = [];
|
|
264
|
+
const modelIndices: number[] = [];
|
|
265
|
+
|
|
266
|
+
for (const provider of providers) {
|
|
267
|
+
const items = byProvider.get(provider)!;
|
|
268
|
+
rows.push({ kind: "header", provider, count: items.length });
|
|
269
|
+
for (const item of items) {
|
|
270
|
+
modelIndices.push(rows.length);
|
|
271
|
+
rows.push({ kind: "model", item });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.groupedRows = rows;
|
|
276
|
+
this.modelRowIndices = modelIndices;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Move selectedGroupIndex to point at the current model (or first model).
|
|
281
|
+
*/
|
|
282
|
+
private jumpToCurrentModel(): void {
|
|
283
|
+
if (this.groupedRows.length === 0) {
|
|
284
|
+
this.selectedGroupIndex = 0;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// Find the current model in grouped rows
|
|
288
|
+
for (let i = 0; i < this.groupedRows.length; i++) {
|
|
289
|
+
const row = this.groupedRows[i];
|
|
290
|
+
if (row.kind === "model" && modelsAreEqual(this.currentModel, row.item.model)) {
|
|
291
|
+
this.selectedGroupIndex = i;
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Fall back to first model row
|
|
296
|
+
if (this.modelRowIndices.length > 0) {
|
|
297
|
+
this.selectedGroupIndex = this.modelRowIndices[0];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get the currently selected model from grouped or flat state.
|
|
303
|
+
*/
|
|
304
|
+
private getSelectedModel(): Model<any> | undefined {
|
|
305
|
+
if (this.isSearching) {
|
|
306
|
+
return this.filteredModels[this.selectedFlatIndex]?.model;
|
|
307
|
+
}
|
|
308
|
+
const row = this.groupedRows[this.selectedGroupIndex];
|
|
309
|
+
return row?.kind === "model" ? row.item.model : undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
207
312
|
private getScopeText(): string {
|
|
208
313
|
const allText = this.scope === "all" ? theme.fg("accent", "all") : theme.fg("muted", "all");
|
|
209
314
|
const scopedText = this.scope === "scoped" ? theme.fg("accent", "scoped") : theme.fg("muted", "scoped");
|
|
@@ -218,8 +323,16 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|
|
218
323
|
if (this.scope === scope) return;
|
|
219
324
|
this.scope = scope;
|
|
220
325
|
this.activeModels = this.scope === "scoped" ? this.scopedModelItems : this.allModels;
|
|
221
|
-
|
|
222
|
-
|
|
326
|
+
|
|
327
|
+
if (this.isSearching) {
|
|
328
|
+
this.selectedFlatIndex = 0;
|
|
329
|
+
this.filterModels(this.searchInput.getValue());
|
|
330
|
+
} else {
|
|
331
|
+
this.buildGroupedRows();
|
|
332
|
+
this.jumpToCurrentModel();
|
|
333
|
+
this.updateList();
|
|
334
|
+
}
|
|
335
|
+
|
|
223
336
|
if (this.scopeText) {
|
|
224
337
|
this.scopeText.setText(this.getScopeText());
|
|
225
338
|
}
|
|
@@ -229,26 +342,51 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|
|
229
342
|
this.filteredModels = query
|
|
230
343
|
? fuzzyFilter(this.activeModels, query, ({ id, provider }) => `${id} ${provider}`)
|
|
231
344
|
: this.activeModels;
|
|
232
|
-
this.
|
|
345
|
+
this.selectedFlatIndex = Math.min(this.selectedFlatIndex, Math.max(0, this.filteredModels.length - 1));
|
|
233
346
|
this.updateList();
|
|
234
347
|
}
|
|
235
348
|
|
|
236
349
|
private updateList(): void {
|
|
237
350
|
this.listContainer.clear();
|
|
238
351
|
|
|
352
|
+
if (this.errorMessage) {
|
|
353
|
+
const errorLines = this.errorMessage.split("\n");
|
|
354
|
+
for (const line of errorLines) {
|
|
355
|
+
this.listContainer.addChild(new Text(theme.fg("error", line), 0, 0));
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (this.isSearching) {
|
|
361
|
+
this.renderFlatList();
|
|
362
|
+
} else {
|
|
363
|
+
this.renderGroupedList();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Flat fuzzy-search results, same as original behaviour */
|
|
368
|
+
private renderFlatList(): void {
|
|
239
369
|
const maxVisible = 10;
|
|
370
|
+
|
|
371
|
+
if (this.filteredModels.length === 0) {
|
|
372
|
+
this.listContainer.addChild(new Text(theme.fg("muted", " No matching models"), 0, 0));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
240
376
|
const startIndex = Math.max(
|
|
241
377
|
0,
|
|
242
|
-
Math.min(
|
|
378
|
+
Math.min(
|
|
379
|
+
this.selectedFlatIndex - Math.floor(maxVisible / 2),
|
|
380
|
+
this.filteredModels.length - maxVisible,
|
|
381
|
+
),
|
|
243
382
|
);
|
|
244
383
|
const endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);
|
|
245
384
|
|
|
246
|
-
// Show visible slice of filtered models
|
|
247
385
|
for (let i = startIndex; i < endIndex; i++) {
|
|
248
386
|
const item = this.filteredModels[i];
|
|
249
387
|
if (!item) continue;
|
|
250
388
|
|
|
251
|
-
const isSelected = i === this.
|
|
389
|
+
const isSelected = i === this.selectedFlatIndex;
|
|
252
390
|
const isCurrent = modelsAreEqual(this.currentModel, item.model);
|
|
253
391
|
|
|
254
392
|
const ctx = formatTokenCount(item.model.contextWindow);
|
|
@@ -256,7 +394,7 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|
|
256
394
|
const providerBadge = theme.fg("muted", `[${item.provider}]`);
|
|
257
395
|
const checkmark = isCurrent ? theme.fg("success", " ✓") : "";
|
|
258
396
|
|
|
259
|
-
let line
|
|
397
|
+
let line: string;
|
|
260
398
|
if (isSelected) {
|
|
261
399
|
const prefix = theme.fg("accent", "→ ");
|
|
262
400
|
line = `${prefix}${theme.fg("accent", item.id)} ${ctxBadge} ${providerBadge}${checkmark}`;
|
|
@@ -267,40 +405,110 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|
|
267
405
|
this.listContainer.addChild(new Text(line, 0, 0));
|
|
268
406
|
}
|
|
269
407
|
|
|
270
|
-
// Add scroll indicator if needed
|
|
271
408
|
if (startIndex > 0 || endIndex < this.filteredModels.length) {
|
|
272
|
-
|
|
273
|
-
|
|
409
|
+
this.listContainer.addChild(
|
|
410
|
+
new Text(theme.fg("muted", ` (${this.selectedFlatIndex + 1}/${this.filteredModels.length})`), 0, 0),
|
|
411
|
+
);
|
|
274
412
|
}
|
|
275
413
|
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
414
|
+
// Detail line for selected model
|
|
415
|
+
const selected = this.filteredModels[this.selectedFlatIndex];
|
|
416
|
+
if (selected) {
|
|
417
|
+
this.listContainer.addChild(new Spacer(1));
|
|
418
|
+
this.listContainer.addChild(new Text(theme.fg("muted", ` ${this.modelDetailLine(selected.model)}`), 0, 0));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Grouped browse view: provider headers + model rows, windowed around selection.
|
|
424
|
+
* Shows enough rows to fill ~10 visible lines; headers count as one line each.
|
|
425
|
+
*/
|
|
426
|
+
private renderGroupedList(): void {
|
|
427
|
+
const maxVisible = 12;
|
|
428
|
+
|
|
429
|
+
if (this.groupedRows.length === 0) {
|
|
430
|
+
this.listContainer.addChild(new Text(theme.fg("muted", " No models available"), 0, 0));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Window around selectedGroupIndex
|
|
435
|
+
const startIndex = Math.max(
|
|
436
|
+
0,
|
|
437
|
+
Math.min(
|
|
438
|
+
this.selectedGroupIndex - Math.floor(maxVisible / 2),
|
|
439
|
+
this.groupedRows.length - maxVisible,
|
|
440
|
+
),
|
|
441
|
+
);
|
|
442
|
+
const endIndex = Math.min(startIndex + maxVisible, this.groupedRows.length);
|
|
443
|
+
|
|
444
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
445
|
+
const row = this.groupedRows[i];
|
|
446
|
+
if (!row) continue;
|
|
447
|
+
|
|
448
|
+
if (row.kind === "header") {
|
|
449
|
+
// Provider group header — always unselectable
|
|
450
|
+
const providerLabel = theme.fg("borderAccent", row.provider);
|
|
451
|
+
const count = theme.fg("muted", ` (${row.count})`);
|
|
452
|
+
// Add blank line before header if not the very first visible row
|
|
453
|
+
if (i > startIndex) {
|
|
454
|
+
this.listContainer.addChild(new Text("", 0, 0));
|
|
455
|
+
}
|
|
456
|
+
this.listContainer.addChild(new Text(` ${providerLabel}${count}`, 0, 0));
|
|
457
|
+
} else {
|
|
458
|
+
// Model row
|
|
459
|
+
const isSelected = i === this.selectedGroupIndex;
|
|
460
|
+
const isCurrent = modelsAreEqual(this.currentModel, row.item.model);
|
|
461
|
+
|
|
462
|
+
const ctx = formatTokenCount(row.item.model.contextWindow);
|
|
463
|
+
const ctxBadge = theme.fg("muted", ` ${ctx}`);
|
|
464
|
+
const checkmark = isCurrent ? theme.fg("success", " ✓") : "";
|
|
465
|
+
|
|
466
|
+
let line: string;
|
|
467
|
+
if (isSelected) {
|
|
468
|
+
line = ` ${theme.fg("accent", "→")} ${theme.fg("accent", row.item.id)}${ctxBadge}${checkmark}`;
|
|
469
|
+
} else {
|
|
470
|
+
line = ` ${row.item.id}${ctxBadge}${checkmark}`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
this.listContainer.addChild(new Text(line, 0, 0));
|
|
298
474
|
}
|
|
299
475
|
}
|
|
476
|
+
|
|
477
|
+
// Scroll indicator
|
|
478
|
+
if (startIndex > 0 || endIndex < this.groupedRows.length) {
|
|
479
|
+
const modelPos = this.modelRowIndices.indexOf(this.selectedGroupIndex) + 1;
|
|
480
|
+
const totalModels = this.modelRowIndices.length;
|
|
481
|
+
this.listContainer.addChild(
|
|
482
|
+
new Text(theme.fg("muted", ` (${modelPos}/${totalModels})`), 0, 0),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Detail line for selected model
|
|
487
|
+
const selectedModel = this.getSelectedModel();
|
|
488
|
+
if (selectedModel) {
|
|
489
|
+
this.listContainer.addChild(new Spacer(1));
|
|
490
|
+
this.listContainer.addChild(
|
|
491
|
+
new Text(theme.fg("muted", ` ${this.modelDetailLine(selectedModel)}`), 0, 0),
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private modelDetailLine(m: Model<any>): string {
|
|
497
|
+
return [
|
|
498
|
+
m.name,
|
|
499
|
+
`ctx: ${formatTokenCount(m.contextWindow)}`,
|
|
500
|
+
`out: ${formatTokenCount(m.maxTokens)}`,
|
|
501
|
+
m.reasoning ? "thinking" : "",
|
|
502
|
+
m.input.includes("image") ? "vision" : "",
|
|
503
|
+
]
|
|
504
|
+
.filter(Boolean)
|
|
505
|
+
.join(" · ");
|
|
300
506
|
}
|
|
301
507
|
|
|
302
508
|
handleInput(keyData: string): void {
|
|
303
509
|
const kb = getEditorKeybindings();
|
|
510
|
+
|
|
511
|
+
// Tab: scope toggle
|
|
304
512
|
if (kb.matches(keyData, "tab")) {
|
|
305
513
|
if (this.scopedModelItems.length > 0) {
|
|
306
514
|
const nextScope: ModelScope = this.scope === "all" ? "scoped" : "all";
|
|
@@ -311,34 +519,111 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|
|
311
519
|
}
|
|
312
520
|
return;
|
|
313
521
|
}
|
|
314
|
-
|
|
522
|
+
|
|
523
|
+
// Navigation keys
|
|
315
524
|
if (kb.matches(keyData, "selectUp")) {
|
|
525
|
+
this.moveUp();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (kb.matches(keyData, "selectDown")) {
|
|
529
|
+
this.moveDown();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Confirm
|
|
534
|
+
if (kb.matches(keyData, "selectConfirm")) {
|
|
535
|
+
const model = this.getSelectedModel();
|
|
536
|
+
if (model) this.handleSelect(model);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Cancel
|
|
541
|
+
if (kb.matches(keyData, "selectCancel")) {
|
|
542
|
+
this.onCancelCallback();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Everything else: feed to search input
|
|
547
|
+
const prevQuery = this.searchInput.getValue();
|
|
548
|
+
this.searchInput.handleInput(keyData);
|
|
549
|
+
const newQuery = this.searchInput.getValue();
|
|
550
|
+
|
|
551
|
+
if (newQuery !== prevQuery) {
|
|
552
|
+
const entering = !prevQuery && !!newQuery;
|
|
553
|
+
const leaving = !!prevQuery && !newQuery;
|
|
554
|
+
|
|
555
|
+
if (entering) {
|
|
556
|
+
// Entering search mode: remember current model position
|
|
557
|
+
this.isSearching = true;
|
|
558
|
+
this.selectedFlatIndex = 0;
|
|
559
|
+
} else if (leaving) {
|
|
560
|
+
// Leaving search mode: return to grouped view, restore position
|
|
561
|
+
this.isSearching = false;
|
|
562
|
+
this.buildGroupedRows();
|
|
563
|
+
this.jumpToCurrentModel();
|
|
564
|
+
}
|
|
565
|
+
if (this.isSearching) {
|
|
566
|
+
this.filterModels(newQuery);
|
|
567
|
+
} else {
|
|
568
|
+
this.updateList();
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Move selection up, skipping headers in grouped mode */
|
|
574
|
+
private moveUp(): void {
|
|
575
|
+
if (this.isSearching) {
|
|
316
576
|
if (this.filteredModels.length === 0) return;
|
|
317
|
-
this.
|
|
577
|
+
this.selectedFlatIndex =
|
|
578
|
+
this.selectedFlatIndex === 0
|
|
579
|
+
? this.filteredModels.length - 1
|
|
580
|
+
: this.selectedFlatIndex - 1;
|
|
318
581
|
this.updateList();
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (this.groupedRows.length === 0) return;
|
|
586
|
+
let next = this.selectedGroupIndex - 1;
|
|
587
|
+
// Wrap
|
|
588
|
+
if (next < 0) next = this.groupedRows.length - 1;
|
|
589
|
+
// Skip headers
|
|
590
|
+
while (next > 0 && this.groupedRows[next]?.kind === "header") {
|
|
591
|
+
next--;
|
|
592
|
+
}
|
|
593
|
+
// If landed on header at 0, wrap to bottom
|
|
594
|
+
if (this.groupedRows[next]?.kind === "header") {
|
|
595
|
+
next = this.groupedRows.length - 1;
|
|
319
596
|
}
|
|
320
|
-
|
|
321
|
-
|
|
597
|
+
this.selectedGroupIndex = next;
|
|
598
|
+
this.updateList();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Move selection down, skipping headers in grouped mode */
|
|
602
|
+
private moveDown(): void {
|
|
603
|
+
if (this.isSearching) {
|
|
322
604
|
if (this.filteredModels.length === 0) return;
|
|
323
|
-
this.
|
|
605
|
+
this.selectedFlatIndex =
|
|
606
|
+
this.selectedFlatIndex === this.filteredModels.length - 1
|
|
607
|
+
? 0
|
|
608
|
+
: this.selectedFlatIndex + 1;
|
|
324
609
|
this.updateList();
|
|
610
|
+
return;
|
|
325
611
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
else if (kb.matches(keyData, "selectCancel")) {
|
|
335
|
-
this.onCancelCallback();
|
|
612
|
+
|
|
613
|
+
if (this.groupedRows.length === 0) return;
|
|
614
|
+
let next = this.selectedGroupIndex + 1;
|
|
615
|
+
// Wrap
|
|
616
|
+
if (next >= this.groupedRows.length) next = 0;
|
|
617
|
+
// Skip headers
|
|
618
|
+
while (next < this.groupedRows.length - 1 && this.groupedRows[next]?.kind === "header") {
|
|
619
|
+
next++;
|
|
336
620
|
}
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
this.
|
|
340
|
-
this.filterModels(this.searchInput.getValue());
|
|
621
|
+
// If landed on header at end, wrap to first model
|
|
622
|
+
if (this.groupedRows[next]?.kind === "header") {
|
|
623
|
+
next = this.modelRowIndices[0] ?? 0;
|
|
341
624
|
}
|
|
625
|
+
this.selectedGroupIndex = next;
|
|
626
|
+
this.updateList();
|
|
342
627
|
}
|
|
343
628
|
|
|
344
629
|
private handleSelect(model: Model<any>): void {
|
|
@@ -2063,6 +2063,12 @@ export class InteractiveMode {
|
|
|
2063
2063
|
this.handleThinkingCommand(arg);
|
|
2064
2064
|
return;
|
|
2065
2065
|
}
|
|
2066
|
+
if (text === "/edit-mode" || text.startsWith("/edit-mode ")) {
|
|
2067
|
+
const arg = text.startsWith("/edit-mode ") ? text.slice(11).trim() : undefined;
|
|
2068
|
+
this.editor.setText("");
|
|
2069
|
+
this.handleEditModeCommand(arg);
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2066
2072
|
if (text === "/debug") {
|
|
2067
2073
|
this.handleDebugCommand();
|
|
2068
2074
|
this.editor.setText("");
|
|
@@ -2891,6 +2897,27 @@ export class InteractiveMode {
|
|
|
2891
2897
|
this.showThinkingSelector();
|
|
2892
2898
|
}
|
|
2893
2899
|
|
|
2900
|
+
private handleEditModeCommand(arg?: string): void {
|
|
2901
|
+
const modes = ["standard", "hashline"] as const;
|
|
2902
|
+
|
|
2903
|
+
if (arg) {
|
|
2904
|
+
const mode = arg.toLowerCase();
|
|
2905
|
+
if (!modes.includes(mode as typeof modes[number])) {
|
|
2906
|
+
this.showStatus(`Invalid edit mode "${arg}". Available: standard, hashline`);
|
|
2907
|
+
return;
|
|
2908
|
+
}
|
|
2909
|
+
this.session.setEditMode(mode as "standard" | "hashline");
|
|
2910
|
+
this.showStatus(`Edit mode: ${mode}${mode === "hashline" ? " (LINE#ID anchored edits)" : " (text-match edits)"}`);
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
// Toggle
|
|
2915
|
+
const current = this.session.editMode;
|
|
2916
|
+
const next = current === "standard" ? "hashline" : "standard";
|
|
2917
|
+
this.session.setEditMode(next);
|
|
2918
|
+
this.showStatus(`Edit mode: ${next}${next === "hashline" ? " (LINE#ID anchored edits)" : " (text-match edits)"}`);
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2894
2921
|
private showThinkingSelector(): void {
|
|
2895
2922
|
const availableLevels = this.session.getAvailableThinkingLevels();
|
|
2896
2923
|
this.showSelector((done) => {
|
|
@@ -3883,12 +3910,16 @@ export class InteractiveMode {
|
|
|
3883
3910
|
const selector = new OAuthSelectorComponent(
|
|
3884
3911
|
mode,
|
|
3885
3912
|
this.session.modelRegistry.authStorage,
|
|
3886
|
-
|
|
3913
|
+
(providerId: string) => {
|
|
3887
3914
|
done();
|
|
3888
3915
|
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3916
|
+
// OAuthSelectorComponent calls this synchronously (no await),
|
|
3917
|
+
// so we must catch async errors here to prevent unhandled rejections
|
|
3918
|
+
// when the user cancels the login dialog (#821).
|
|
3919
|
+
const handleAsync = async () => {
|
|
3920
|
+
if (mode === "login") {
|
|
3921
|
+
await this.showLoginDialog(providerId);
|
|
3922
|
+
} else {
|
|
3892
3923
|
// Logout flow
|
|
3893
3924
|
const providerInfo = this.session.modelRegistry.authStorage
|
|
3894
3925
|
.getOAuthProviders()
|
|
@@ -3919,6 +3950,11 @@ export class InteractiveMode {
|
|
|
3919
3950
|
this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3920
3951
|
}
|
|
3921
3952
|
}
|
|
3953
|
+
};
|
|
3954
|
+
handleAsync().catch(() => {
|
|
3955
|
+
// Swallow — showLoginDialog already handles its own errors.
|
|
3956
|
+
// This prevents unhandled rejections when login is cancelled.
|
|
3957
|
+
});
|
|
3922
3958
|
},
|
|
3923
3959
|
() => {
|
|
3924
3960
|
done();
|