@mariozechner/pi-coding-agent 0.43.0 → 0.45.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 +29 -0
- package/README.md +28 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +7 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/list-models.d.ts.map +1 -1
- package/dist/cli/list-models.js +1 -1
- package/dist/cli/list-models.js.map +1 -1
- package/dist/core/agent-session.d.ts +5 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +8 -5
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +8 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +2 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +13 -3
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +2 -0
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +2 -0
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/session-manager.d.ts +13 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +37 -2
- package/dist/core/session-manager.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +5 -6
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +121 -104
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +30 -9
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +41 -2
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +22 -8
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +7 -1
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +7 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/docs/extensions.md +21 -0
- package/docs/session.md +12 -0
- package/docs/tui.md +1 -0
- package/examples/extensions/README.md +2 -0
- package/examples/extensions/notify.ts +25 -0
- package/examples/extensions/preset.ts +3 -3
- package/examples/extensions/sandbox/index.ts +318 -0
- package/examples/extensions/sandbox/package-lock.json +92 -0
- package/examples/extensions/sandbox/package.json +19 -0
- package/examples/extensions/tools.ts +7 -6
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -1,12 +1,61 @@
|
|
|
1
1
|
import { Container, fuzzyFilter, getEditorKeybindings, Input, Key, matchesKey, Spacer, Text, } from "@mariozechner/pi-tui";
|
|
2
2
|
import { theme } from "../theme/theme.js";
|
|
3
3
|
import { DynamicBorder } from "./dynamic-border.js";
|
|
4
|
+
function isEnabled(enabledIds, id) {
|
|
5
|
+
return enabledIds === null || enabledIds.includes(id);
|
|
6
|
+
}
|
|
7
|
+
function toggle(enabledIds, id) {
|
|
8
|
+
if (enabledIds === null)
|
|
9
|
+
return [id]; // First toggle: start with only this one
|
|
10
|
+
const index = enabledIds.indexOf(id);
|
|
11
|
+
if (index >= 0)
|
|
12
|
+
return [...enabledIds.slice(0, index), ...enabledIds.slice(index + 1)];
|
|
13
|
+
return [...enabledIds, id];
|
|
14
|
+
}
|
|
15
|
+
function enableAll(enabledIds, allIds, targetIds) {
|
|
16
|
+
if (enabledIds === null)
|
|
17
|
+
return null; // Already all enabled
|
|
18
|
+
const targets = targetIds ?? allIds;
|
|
19
|
+
const result = [...enabledIds];
|
|
20
|
+
for (const id of targets) {
|
|
21
|
+
if (!result.includes(id))
|
|
22
|
+
result.push(id);
|
|
23
|
+
}
|
|
24
|
+
return result.length === allIds.length ? null : result;
|
|
25
|
+
}
|
|
26
|
+
function clearAll(enabledIds, allIds, targetIds) {
|
|
27
|
+
if (enabledIds === null) {
|
|
28
|
+
return targetIds ? allIds.filter((id) => !targetIds.includes(id)) : [];
|
|
29
|
+
}
|
|
30
|
+
const targets = new Set(targetIds ?? enabledIds);
|
|
31
|
+
return enabledIds.filter((id) => !targets.has(id));
|
|
32
|
+
}
|
|
33
|
+
function move(enabledIds, allIds, id, delta) {
|
|
34
|
+
const list = enabledIds ?? [...allIds];
|
|
35
|
+
const index = list.indexOf(id);
|
|
36
|
+
if (index < 0)
|
|
37
|
+
return list;
|
|
38
|
+
const newIndex = index + delta;
|
|
39
|
+
if (newIndex < 0 || newIndex >= list.length)
|
|
40
|
+
return list;
|
|
41
|
+
const result = [...list];
|
|
42
|
+
[result[index], result[newIndex]] = [result[newIndex], result[index]];
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
function getSortedIds(enabledIds, allIds) {
|
|
46
|
+
if (enabledIds === null)
|
|
47
|
+
return allIds;
|
|
48
|
+
const enabledSet = new Set(enabledIds);
|
|
49
|
+
return [...enabledIds, ...allIds.filter((id) => !enabledSet.has(id))];
|
|
50
|
+
}
|
|
4
51
|
/**
|
|
5
52
|
* Component for enabling/disabling models for Ctrl+P cycling.
|
|
6
53
|
* Changes are session-only until explicitly persisted with Ctrl+S.
|
|
7
54
|
*/
|
|
8
55
|
export class ScopedModelsSelectorComponent extends Container {
|
|
9
|
-
|
|
56
|
+
modelsById = new Map();
|
|
57
|
+
allIds = [];
|
|
58
|
+
enabledIds = null;
|
|
10
59
|
filteredItems = [];
|
|
11
60
|
selectedIndex = 0;
|
|
12
61
|
searchInput;
|
|
@@ -18,27 +67,13 @@ export class ScopedModelsSelectorComponent extends Container {
|
|
|
18
67
|
constructor(config, callbacks) {
|
|
19
68
|
super();
|
|
20
69
|
this.callbacks = callbacks;
|
|
21
|
-
// Group models by provider for organized display
|
|
22
|
-
const modelsByProvider = new Map();
|
|
23
70
|
for (const model of config.allModels) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
71
|
+
const fullId = `${model.provider}/${model.id}`;
|
|
72
|
+
this.modelsById.set(fullId, model);
|
|
73
|
+
this.allIds.push(fullId);
|
|
27
74
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
for (const model of models) {
|
|
31
|
-
const fullId = `${provider}/${model.id}`;
|
|
32
|
-
// If no filter defined, all models are enabled by default
|
|
33
|
-
const isEnabled = !config.hasEnabledModelsFilter || config.enabledModelIds.has(fullId);
|
|
34
|
-
this.items.push({
|
|
35
|
-
fullId,
|
|
36
|
-
model,
|
|
37
|
-
enabled: isEnabled,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
this.filteredItems = this.getSortedItems();
|
|
75
|
+
this.enabledIds = config.hasEnabledModelsFilter ? [...config.enabledModelIds] : null;
|
|
76
|
+
this.filteredItems = this.buildItems();
|
|
42
77
|
// Header
|
|
43
78
|
this.addChild(new DynamicBorder());
|
|
44
79
|
this.addChild(new Spacer(1));
|
|
@@ -59,35 +94,29 @@ export class ScopedModelsSelectorComponent extends Container {
|
|
|
59
94
|
this.addChild(new DynamicBorder());
|
|
60
95
|
this.updateList();
|
|
61
96
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
97
|
+
buildItems() {
|
|
98
|
+
return getSortedIds(this.enabledIds, this.allIds).map((id) => ({
|
|
99
|
+
fullId: id,
|
|
100
|
+
model: this.modelsById.get(id),
|
|
101
|
+
enabled: isEnabled(this.enabledIds, id),
|
|
102
|
+
}));
|
|
67
103
|
}
|
|
68
104
|
getFooterText() {
|
|
69
|
-
const enabledCount = this.
|
|
70
|
-
const allEnabled =
|
|
71
|
-
const countText = allEnabled ? "all enabled" : `${enabledCount}/${this.
|
|
72
|
-
const parts = ["Enter toggle", "^A all", "^X clear", "^P provider", "^S save", countText];
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return theme.fg("dim", ` ${parts.join(" · ")}`);
|
|
77
|
-
}
|
|
78
|
-
updateFooter() {
|
|
79
|
-
this.footerText.setText(this.getFooterText());
|
|
105
|
+
const enabledCount = this.enabledIds?.length ?? this.allIds.length;
|
|
106
|
+
const allEnabled = this.enabledIds === null;
|
|
107
|
+
const countText = allEnabled ? "all enabled" : `${enabledCount}/${this.allIds.length} enabled`;
|
|
108
|
+
const parts = ["Enter toggle", "^A all", "^X clear", "^P provider", "Alt+↑↓ reorder", "^S save", countText];
|
|
109
|
+
return this.isDirty
|
|
110
|
+
? theme.fg("dim", ` ${parts.join(" · ")} `) + theme.fg("warning", "(unsaved)")
|
|
111
|
+
: theme.fg("dim", ` ${parts.join(" · ")}`);
|
|
80
112
|
}
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
this.filteredItems = fuzzyFilter(sorted, query, (item) => `${item.model.id} ${item.model.provider}`);
|
|
88
|
-
}
|
|
113
|
+
refresh() {
|
|
114
|
+
const query = this.searchInput.getValue();
|
|
115
|
+
const items = this.buildItems();
|
|
116
|
+
this.filteredItems = query ? fuzzyFilter(items, query, (i) => `${i.model.id} ${i.model.provider}`) : items;
|
|
89
117
|
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredItems.length - 1));
|
|
90
118
|
this.updateList();
|
|
119
|
+
this.footerText.setText(this.getFooterText());
|
|
91
120
|
}
|
|
92
121
|
updateList() {
|
|
93
122
|
this.listContainer.clear();
|
|
@@ -97,47 +126,20 @@ export class ScopedModelsSelectorComponent extends Container {
|
|
|
97
126
|
}
|
|
98
127
|
const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible));
|
|
99
128
|
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);
|
|
100
|
-
|
|
101
|
-
const allEnabled = this.items.every((i) => i.enabled);
|
|
129
|
+
const allEnabled = this.enabledIds === null;
|
|
102
130
|
for (let i = startIndex; i < endIndex; i++) {
|
|
103
131
|
const item = this.filteredItems[i];
|
|
104
|
-
if (!item)
|
|
105
|
-
continue;
|
|
106
132
|
const isSelected = i === this.selectedIndex;
|
|
107
133
|
const prefix = isSelected ? theme.fg("accent", "→ ") : " ";
|
|
108
134
|
const modelText = isSelected ? theme.fg("accent", item.model.id) : item.model.id;
|
|
109
135
|
const providerBadge = theme.fg("muted", ` [${item.model.provider}]`);
|
|
110
|
-
// Only show checkmarks when there's actually a filter
|
|
111
136
|
const status = allEnabled ? "" : item.enabled ? theme.fg("success", " ✓") : theme.fg("dim", " ✗");
|
|
112
137
|
this.listContainer.addChild(new Text(`${prefix}${modelText}${providerBadge}${status}`, 0, 0));
|
|
113
138
|
}
|
|
114
139
|
// Add scroll indicator if needed
|
|
115
140
|
if (startIndex > 0 || endIndex < this.filteredItems.length) {
|
|
116
|
-
|
|
117
|
-
this.listContainer.addChild(new Text(scrollInfo, 0, 0));
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
toggleItem(item) {
|
|
121
|
-
// If all models are currently enabled (no scope yet), first toggle starts fresh:
|
|
122
|
-
// clear all and enable only the selected model
|
|
123
|
-
const allEnabled = this.items.every((i) => i.enabled);
|
|
124
|
-
if (allEnabled) {
|
|
125
|
-
for (const i of this.items) {
|
|
126
|
-
i.enabled = false;
|
|
127
|
-
}
|
|
128
|
-
item.enabled = true;
|
|
129
|
-
this.isDirty = true;
|
|
130
|
-
this.callbacks.onClearAll();
|
|
131
|
-
this.callbacks.onModelToggle(item.fullId, true);
|
|
141
|
+
this.listContainer.addChild(new Text(theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredItems.length})`), 0, 0));
|
|
132
142
|
}
|
|
133
|
-
else {
|
|
134
|
-
item.enabled = !item.enabled;
|
|
135
|
-
this.isDirty = true;
|
|
136
|
-
this.callbacks.onModelToggle(item.fullId, item.enabled);
|
|
137
|
-
}
|
|
138
|
-
// Re-sort and re-filter to move item to correct section
|
|
139
|
-
this.filterItems(this.searchInput.getValue());
|
|
140
|
-
this.updateFooter();
|
|
141
143
|
}
|
|
142
144
|
handleInput(data) {
|
|
143
145
|
const kb = getEditorKeybindings();
|
|
@@ -156,69 +158,84 @@ export class ScopedModelsSelectorComponent extends Container {
|
|
|
156
158
|
this.updateList();
|
|
157
159
|
return;
|
|
158
160
|
}
|
|
161
|
+
// Alt+Up/Down - Reorder enabled models
|
|
162
|
+
if (matchesKey(data, Key.alt("up")) || matchesKey(data, Key.alt("down"))) {
|
|
163
|
+
const item = this.filteredItems[this.selectedIndex];
|
|
164
|
+
if (item && isEnabled(this.enabledIds, item.fullId)) {
|
|
165
|
+
const delta = matchesKey(data, Key.alt("up")) ? -1 : 1;
|
|
166
|
+
const enabledList = this.enabledIds ?? this.allIds;
|
|
167
|
+
const currentIndex = enabledList.indexOf(item.fullId);
|
|
168
|
+
const newIndex = currentIndex + delta;
|
|
169
|
+
// Only move if within bounds
|
|
170
|
+
if (newIndex >= 0 && newIndex < enabledList.length) {
|
|
171
|
+
this.enabledIds = move(this.enabledIds, this.allIds, item.fullId, delta);
|
|
172
|
+
this.isDirty = true;
|
|
173
|
+
this.selectedIndex += delta;
|
|
174
|
+
this.refresh();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
159
179
|
// Toggle on Enter
|
|
160
180
|
if (matchesKey(data, Key.enter)) {
|
|
161
181
|
const item = this.filteredItems[this.selectedIndex];
|
|
162
182
|
if (item) {
|
|
163
|
-
this.
|
|
183
|
+
const wasAllEnabled = this.enabledIds === null;
|
|
184
|
+
this.enabledIds = toggle(this.enabledIds, item.fullId);
|
|
185
|
+
this.isDirty = true;
|
|
186
|
+
if (wasAllEnabled)
|
|
187
|
+
this.callbacks.onClearAll();
|
|
188
|
+
this.callbacks.onModelToggle(item.fullId, isEnabled(this.enabledIds, item.fullId));
|
|
189
|
+
this.refresh();
|
|
164
190
|
}
|
|
165
191
|
return;
|
|
166
192
|
}
|
|
167
193
|
// Ctrl+A - Enable all (filtered if search active, otherwise all)
|
|
168
194
|
if (matchesKey(data, Key.ctrl("a"))) {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
item.enabled = true;
|
|
172
|
-
}
|
|
195
|
+
const targetIds = this.searchInput.getValue() ? this.filteredItems.map((i) => i.fullId) : undefined;
|
|
196
|
+
this.enabledIds = enableAll(this.enabledIds, this.allIds, targetIds);
|
|
173
197
|
this.isDirty = true;
|
|
174
|
-
this.callbacks.onEnableAll(
|
|
175
|
-
this.
|
|
176
|
-
this.updateFooter();
|
|
198
|
+
this.callbacks.onEnableAll(targetIds ?? this.allIds);
|
|
199
|
+
this.refresh();
|
|
177
200
|
return;
|
|
178
201
|
}
|
|
179
202
|
// Ctrl+X - Clear all (filtered if search active, otherwise all)
|
|
180
203
|
if (matchesKey(data, Key.ctrl("x"))) {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
item.enabled = false;
|
|
184
|
-
}
|
|
204
|
+
const targetIds = this.searchInput.getValue() ? this.filteredItems.map((i) => i.fullId) : undefined;
|
|
205
|
+
this.enabledIds = clearAll(this.enabledIds, this.allIds, targetIds);
|
|
185
206
|
this.isDirty = true;
|
|
186
207
|
this.callbacks.onClearAll();
|
|
187
|
-
this.
|
|
188
|
-
this.updateFooter();
|
|
208
|
+
this.refresh();
|
|
189
209
|
return;
|
|
190
210
|
}
|
|
191
211
|
// Ctrl+P - Toggle provider of current item
|
|
192
212
|
if (matchesKey(data, Key.ctrl("p"))) {
|
|
193
|
-
const
|
|
194
|
-
if (
|
|
195
|
-
const provider =
|
|
196
|
-
const
|
|
197
|
-
const allEnabled =
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
213
|
+
const item = this.filteredItems[this.selectedIndex];
|
|
214
|
+
if (item) {
|
|
215
|
+
const provider = item.model.provider;
|
|
216
|
+
const providerIds = this.allIds.filter((id) => this.modelsById.get(id).provider === provider);
|
|
217
|
+
const allEnabled = providerIds.every((id) => isEnabled(this.enabledIds, id));
|
|
218
|
+
this.enabledIds = allEnabled
|
|
219
|
+
? clearAll(this.enabledIds, this.allIds, providerIds)
|
|
220
|
+
: enableAll(this.enabledIds, this.allIds, providerIds);
|
|
202
221
|
this.isDirty = true;
|
|
203
|
-
this.callbacks.onToggleProvider(provider,
|
|
204
|
-
this.
|
|
205
|
-
this.updateFooter();
|
|
222
|
+
this.callbacks.onToggleProvider(provider, providerIds, !allEnabled);
|
|
223
|
+
this.refresh();
|
|
206
224
|
}
|
|
207
225
|
return;
|
|
208
226
|
}
|
|
209
227
|
// Ctrl+S - Save/persist to settings
|
|
210
228
|
if (matchesKey(data, Key.ctrl("s"))) {
|
|
211
|
-
|
|
212
|
-
this.callbacks.onPersist(enabledIds);
|
|
229
|
+
this.callbacks.onPersist(this.enabledIds ?? [...this.allIds]);
|
|
213
230
|
this.isDirty = false;
|
|
214
|
-
this.
|
|
231
|
+
this.footerText.setText(this.getFooterText());
|
|
215
232
|
return;
|
|
216
233
|
}
|
|
217
234
|
// Ctrl+C - clear search or cancel if empty
|
|
218
235
|
if (matchesKey(data, Key.ctrl("c"))) {
|
|
219
236
|
if (this.searchInput.getValue()) {
|
|
220
237
|
this.searchInput.setValue("");
|
|
221
|
-
this.
|
|
238
|
+
this.refresh();
|
|
222
239
|
}
|
|
223
240
|
else {
|
|
224
241
|
this.callbacks.onCancel();
|
|
@@ -232,7 +249,7 @@ export class ScopedModelsSelectorComponent extends Container {
|
|
|
232
249
|
}
|
|
233
250
|
// Pass everything else to search input
|
|
234
251
|
this.searchInput.handleInput(data);
|
|
235
|
-
this.
|
|
252
|
+
this.refresh();
|
|
236
253
|
}
|
|
237
254
|
getSearchInput() {
|
|
238
255
|
return this.searchInput;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scoped-models-selector.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/scoped-models-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Model } from \"@mariozechner/pi-ai\";\nimport {\n\tContainer,\n\tfuzzyFilter,\n\tgetEditorKeybindings,\n\tInput,\n\tKey,\n\tmatchesKey,\n\tSpacer,\n\tText,\n} from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ninterface ModelItem {\n\tfullId: string;\n\tmodel: Model<any>;\n\tenabled: boolean;\n}\n\nexport interface ModelsConfig {\n\tallModels: Model<any>[];\n\tenabledModelIds: Set<string>;\n\t/** true if enabledModels setting is defined (empty = all enabled) */\n\thasEnabledModelsFilter: boolean;\n}\n\nexport interface ModelsCallbacks {\n\t/** Called when a model is toggled (session-only, no persist) */\n\tonModelToggle: (modelId: string, enabled: boolean) => void;\n\t/** Called when user wants to persist current selection to settings */\n\tonPersist: (enabledModelIds: string[]) => void;\n\t/** Called when user enables all models. Returns list of all model IDs. */\n\tonEnableAll: (allModelIds: string[]) => void;\n\t/** Called when user clears all models */\n\tonClearAll: () => void;\n\t/** Called when user toggles all models for a provider. Returns affected model IDs. */\n\tonToggleProvider: (provider: string, modelIds: string[], enabled: boolean) => void;\n\tonCancel: () => void;\n}\n\n/**\n * Component for enabling/disabling models for Ctrl+P cycling.\n * Changes are session-only until explicitly persisted with Ctrl+S.\n */\nexport class ScopedModelsSelectorComponent extends Container {\n\tprivate items: ModelItem[] = [];\n\tprivate filteredItems: ModelItem[] = [];\n\tprivate selectedIndex = 0;\n\tprivate searchInput: Input;\n\tprivate listContainer: Container;\n\tprivate footerText: Text;\n\tprivate callbacks: ModelsCallbacks;\n\tprivate maxVisible = 15;\n\tprivate isDirty = false;\n\n\tconstructor(config: ModelsConfig, callbacks: ModelsCallbacks) {\n\t\tsuper();\n\t\tthis.callbacks = callbacks;\n\n\t\t// Group models by provider for organized display\n\t\tconst modelsByProvider = new Map<string, Model<any>[]>();\n\t\tfor (const model of config.allModels) {\n\t\t\tconst list = modelsByProvider.get(model.provider) ?? [];\n\t\t\tlist.push(model);\n\t\t\tmodelsByProvider.set(model.provider, list);\n\t\t}\n\n\t\t// Build items - group by provider\n\t\tfor (const [provider, models] of modelsByProvider) {\n\t\t\tfor (const model of models) {\n\t\t\t\tconst fullId = `${provider}/${model.id}`;\n\t\t\t\t// If no filter defined, all models are enabled by default\n\t\t\t\tconst isEnabled = !config.hasEnabledModelsFilter || config.enabledModelIds.has(fullId);\n\t\t\t\tthis.items.push({\n\t\t\t\t\tfullId,\n\t\t\t\t\tmodel,\n\t\t\t\t\tenabled: isEnabled,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tthis.filteredItems = this.getSortedItems();\n\n\t\t// Header\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Model Configuration\")), 0, 0));\n\t\tthis.addChild(new Text(theme.fg(\"muted\", \"Session-only. Ctrl+S to save to settings.\"), 0, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Search input\n\t\tthis.searchInput = new Input();\n\t\tthis.addChild(this.searchInput);\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// List container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\t// Footer hint\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.footerText = new Text(this.getFooterText(), 0, 0);\n\t\tthis.addChild(this.footerText);\n\n\t\tthis.addChild(new DynamicBorder());\n\n\t\tthis.updateList();\n\t}\n\n\t/** Get items sorted with enabled items first */\n\tprivate getSortedItems(): ModelItem[] {\n\t\tconst enabled = this.items.filter((i) => i.enabled);\n\t\tconst disabled = this.items.filter((i) => !i.enabled);\n\t\treturn [...enabled, ...disabled];\n\t}\n\n\tprivate getFooterText(): string {\n\t\tconst enabledCount = this.items.filter((i) => i.enabled).length;\n\t\tconst allEnabled = enabledCount === this.items.length;\n\t\tconst countText = allEnabled ? \"all enabled\" : `${enabledCount}/${this.items.length} enabled`;\n\t\tconst parts = [\"Enter toggle\", \"^A all\", \"^X clear\", \"^P provider\", \"^S save\", countText];\n\t\tif (this.isDirty) {\n\t\t\treturn theme.fg(\"dim\", ` ${parts.join(\" · \")} `) + theme.fg(\"warning\", \"(unsaved)\");\n\t\t}\n\t\treturn theme.fg(\"dim\", ` ${parts.join(\" · \")}`);\n\t}\n\n\tprivate updateFooter(): void {\n\t\tthis.footerText.setText(this.getFooterText());\n\t}\n\n\tprivate filterItems(query: string): void {\n\t\tconst sorted = this.getSortedItems();\n\t\tif (!query) {\n\t\t\tthis.filteredItems = sorted;\n\t\t} else {\n\t\t\tthis.filteredItems = fuzzyFilter(sorted, query, (item) => `${item.model.id} ${item.model.provider}`);\n\t\t}\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredItems.length - 1));\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tif (this.filteredItems.length === 0) {\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", \" No matching models\"), 0, 0));\n\t\t\treturn;\n\t\t}\n\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);\n\n\t\t// Only show status if there's a filter (not all models enabled)\n\t\tconst allEnabled = this.items.every((i) => i.enabled);\n\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredItems[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst prefix = isSelected ? theme.fg(\"accent\", \"→ \") : \" \";\n\t\t\tconst modelText = isSelected ? theme.fg(\"accent\", item.model.id) : item.model.id;\n\t\t\tconst providerBadge = theme.fg(\"muted\", ` [${item.model.provider}]`);\n\t\t\t// Only show checkmarks when there's actually a filter\n\t\t\tconst status = allEnabled ? \"\" : item.enabled ? theme.fg(\"success\", \" ✓\") : theme.fg(\"dim\", \" ✗\");\n\n\t\t\tthis.listContainer.addChild(new Text(`${prefix}${modelText}${providerBadge}${status}`, 0, 0));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredItems.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.filteredItems.length})`);\n\t\t\tthis.listContainer.addChild(new Text(scrollInfo, 0, 0));\n\t\t}\n\t}\n\n\tprivate toggleItem(item: ModelItem): void {\n\t\t// If all models are currently enabled (no scope yet), first toggle starts fresh:\n\t\t// clear all and enable only the selected model\n\t\tconst allEnabled = this.items.every((i) => i.enabled);\n\t\tif (allEnabled) {\n\t\t\tfor (const i of this.items) {\n\t\t\t\ti.enabled = false;\n\t\t\t}\n\t\t\titem.enabled = true;\n\t\t\tthis.isDirty = true;\n\t\t\tthis.callbacks.onClearAll();\n\t\t\tthis.callbacks.onModelToggle(item.fullId, true);\n\t\t} else {\n\t\t\titem.enabled = !item.enabled;\n\t\t\tthis.isDirty = true;\n\t\t\tthis.callbacks.onModelToggle(item.fullId, item.enabled);\n\t\t}\n\t\t// Re-sort and re-filter to move item to correct section\n\t\tthis.filterItems(this.searchInput.getValue());\n\t\tthis.updateFooter();\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst kb = getEditorKeybindings();\n\n\t\t// Navigation\n\t\tif (kb.matches(data, \"selectUp\")) {\n\t\t\tif (this.filteredItems.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.filteredItems.length - 1 : this.selectedIndex - 1;\n\t\t\tthis.updateList();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"selectDown\")) {\n\t\t\tif (this.filteredItems.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === this.filteredItems.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t\tthis.updateList();\n\t\t\treturn;\n\t\t}\n\n\t\t// Toggle on Enter\n\t\tif (matchesKey(data, Key.enter)) {\n\t\t\tconst item = this.filteredItems[this.selectedIndex];\n\t\t\tif (item) {\n\t\t\t\tthis.toggleItem(item);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+A - Enable all (filtered if search active, otherwise all)\n\t\tif (matchesKey(data, Key.ctrl(\"a\"))) {\n\t\t\tconst targets = this.searchInput.getValue() ? this.filteredItems : this.items;\n\t\t\tfor (const item of targets) {\n\t\t\t\titem.enabled = true;\n\t\t\t}\n\t\t\tthis.isDirty = true;\n\t\t\tthis.callbacks.onEnableAll(targets.map((i) => i.fullId));\n\t\t\tthis.filterItems(this.searchInput.getValue());\n\t\t\tthis.updateFooter();\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+X - Clear all (filtered if search active, otherwise all)\n\t\tif (matchesKey(data, Key.ctrl(\"x\"))) {\n\t\t\tconst targets = this.searchInput.getValue() ? this.filteredItems : this.items;\n\t\t\tfor (const item of targets) {\n\t\t\t\titem.enabled = false;\n\t\t\t}\n\t\t\tthis.isDirty = true;\n\t\t\tthis.callbacks.onClearAll();\n\t\t\tthis.filterItems(this.searchInput.getValue());\n\t\t\tthis.updateFooter();\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+P - Toggle provider of current item\n\t\tif (matchesKey(data, Key.ctrl(\"p\"))) {\n\t\t\tconst currentItem = this.filteredItems[this.selectedIndex];\n\t\t\tif (currentItem) {\n\t\t\t\tconst provider = currentItem.model.provider;\n\t\t\t\tconst providerItems = this.items.filter((i) => i.model.provider === provider);\n\t\t\t\tconst allEnabled = providerItems.every((i) => i.enabled);\n\t\t\t\tconst newState = !allEnabled;\n\t\t\t\tfor (const item of providerItems) {\n\t\t\t\t\titem.enabled = newState;\n\t\t\t\t}\n\t\t\t\tthis.isDirty = true;\n\t\t\t\tthis.callbacks.onToggleProvider(\n\t\t\t\t\tprovider,\n\t\t\t\t\tproviderItems.map((i) => i.fullId),\n\t\t\t\t\tnewState,\n\t\t\t\t);\n\t\t\t\tthis.filterItems(this.searchInput.getValue());\n\t\t\t\tthis.updateFooter();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+S - Save/persist to settings\n\t\tif (matchesKey(data, Key.ctrl(\"s\"))) {\n\t\t\tconst enabledIds = this.items.filter((i) => i.enabled).map((i) => i.fullId);\n\t\t\tthis.callbacks.onPersist(enabledIds);\n\t\t\tthis.isDirty = false;\n\t\t\tthis.updateFooter();\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+C - clear search or cancel if empty\n\t\tif (matchesKey(data, Key.ctrl(\"c\"))) {\n\t\t\tif (this.searchInput.getValue()) {\n\t\t\t\tthis.searchInput.setValue(\"\");\n\t\t\t\tthis.filterItems(\"\");\n\t\t\t} else {\n\t\t\t\tthis.callbacks.onCancel();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Escape - cancel\n\t\tif (matchesKey(data, Key.escape)) {\n\t\t\tthis.callbacks.onCancel();\n\t\t\treturn;\n\t\t}\n\n\t\t// Pass everything else to search input\n\t\tthis.searchInput.handleInput(data);\n\t\tthis.filterItems(this.searchInput.getValue());\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"scoped-models-selector.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/scoped-models-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Model } from \"@mariozechner/pi-ai\";\nimport {\n\tContainer,\n\tfuzzyFilter,\n\tgetEditorKeybindings,\n\tInput,\n\tKey,\n\tmatchesKey,\n\tSpacer,\n\tText,\n} from \"@mariozechner/pi-tui\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\n// EnabledIds: null = all enabled (no filter), string[] = explicit ordered list\ntype EnabledIds = string[] | null;\n\nfunction isEnabled(enabledIds: EnabledIds, id: string): boolean {\n\treturn enabledIds === null || enabledIds.includes(id);\n}\n\nfunction toggle(enabledIds: EnabledIds, id: string): EnabledIds {\n\tif (enabledIds === null) return [id]; // First toggle: start with only this one\n\tconst index = enabledIds.indexOf(id);\n\tif (index >= 0) return [...enabledIds.slice(0, index), ...enabledIds.slice(index + 1)];\n\treturn [...enabledIds, id];\n}\n\nfunction enableAll(enabledIds: EnabledIds, allIds: string[], targetIds?: string[]): EnabledIds {\n\tif (enabledIds === null) return null; // Already all enabled\n\tconst targets = targetIds ?? allIds;\n\tconst result = [...enabledIds];\n\tfor (const id of targets) {\n\t\tif (!result.includes(id)) result.push(id);\n\t}\n\treturn result.length === allIds.length ? null : result;\n}\n\nfunction clearAll(enabledIds: EnabledIds, allIds: string[], targetIds?: string[]): EnabledIds {\n\tif (enabledIds === null) {\n\t\treturn targetIds ? allIds.filter((id) => !targetIds.includes(id)) : [];\n\t}\n\tconst targets = new Set(targetIds ?? enabledIds);\n\treturn enabledIds.filter((id) => !targets.has(id));\n}\n\nfunction move(enabledIds: EnabledIds, allIds: string[], id: string, delta: number): EnabledIds {\n\tconst list = enabledIds ?? [...allIds];\n\tconst index = list.indexOf(id);\n\tif (index < 0) return list;\n\tconst newIndex = index + delta;\n\tif (newIndex < 0 || newIndex >= list.length) return list;\n\tconst result = [...list];\n\t[result[index], result[newIndex]] = [result[newIndex], result[index]];\n\treturn result;\n}\n\nfunction getSortedIds(enabledIds: EnabledIds, allIds: string[]): string[] {\n\tif (enabledIds === null) return allIds;\n\tconst enabledSet = new Set(enabledIds);\n\treturn [...enabledIds, ...allIds.filter((id) => !enabledSet.has(id))];\n}\n\ninterface ModelItem {\n\tfullId: string;\n\tmodel: Model<any>;\n\tenabled: boolean;\n}\n\nexport interface ModelsConfig {\n\tallModels: Model<any>[];\n\tenabledModelIds: Set<string>;\n\t/** true if enabledModels setting is defined (empty = all enabled) */\n\thasEnabledModelsFilter: boolean;\n}\n\nexport interface ModelsCallbacks {\n\t/** Called when a model is toggled (session-only, no persist) */\n\tonModelToggle: (modelId: string, enabled: boolean) => void;\n\t/** Called when user wants to persist current selection to settings */\n\tonPersist: (enabledModelIds: string[]) => void;\n\t/** Called when user enables all models. Returns list of all model IDs. */\n\tonEnableAll: (allModelIds: string[]) => void;\n\t/** Called when user clears all models */\n\tonClearAll: () => void;\n\t/** Called when user toggles all models for a provider. Returns affected model IDs. */\n\tonToggleProvider: (provider: string, modelIds: string[], enabled: boolean) => void;\n\tonCancel: () => void;\n}\n\n/**\n * Component for enabling/disabling models for Ctrl+P cycling.\n * Changes are session-only until explicitly persisted with Ctrl+S.\n */\nexport class ScopedModelsSelectorComponent extends Container {\n\tprivate modelsById: Map<string, Model<any>> = new Map();\n\tprivate allIds: string[] = [];\n\tprivate enabledIds: EnabledIds = null;\n\tprivate filteredItems: ModelItem[] = [];\n\tprivate selectedIndex = 0;\n\tprivate searchInput: Input;\n\tprivate listContainer: Container;\n\tprivate footerText: Text;\n\tprivate callbacks: ModelsCallbacks;\n\tprivate maxVisible = 15;\n\tprivate isDirty = false;\n\n\tconstructor(config: ModelsConfig, callbacks: ModelsCallbacks) {\n\t\tsuper();\n\t\tthis.callbacks = callbacks;\n\n\t\tfor (const model of config.allModels) {\n\t\t\tconst fullId = `${model.provider}/${model.id}`;\n\t\t\tthis.modelsById.set(fullId, model);\n\t\t\tthis.allIds.push(fullId);\n\t\t}\n\n\t\tthis.enabledIds = config.hasEnabledModelsFilter ? [...config.enabledModelIds] : null;\n\t\tthis.filteredItems = this.buildItems();\n\n\t\t// Header\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Model Configuration\")), 0, 0));\n\t\tthis.addChild(new Text(theme.fg(\"muted\", \"Session-only. Ctrl+S to save to settings.\"), 0, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Search input\n\t\tthis.searchInput = new Input();\n\t\tthis.addChild(this.searchInput);\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// List container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\t// Footer hint\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.footerText = new Text(this.getFooterText(), 0, 0);\n\t\tthis.addChild(this.footerText);\n\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.updateList();\n\t}\n\n\tprivate buildItems(): ModelItem[] {\n\t\treturn getSortedIds(this.enabledIds, this.allIds).map((id) => ({\n\t\t\tfullId: id,\n\t\t\tmodel: this.modelsById.get(id)!,\n\t\t\tenabled: isEnabled(this.enabledIds, id),\n\t\t}));\n\t}\n\n\tprivate getFooterText(): string {\n\t\tconst enabledCount = this.enabledIds?.length ?? this.allIds.length;\n\t\tconst allEnabled = this.enabledIds === null;\n\t\tconst countText = allEnabled ? \"all enabled\" : `${enabledCount}/${this.allIds.length} enabled`;\n\t\tconst parts = [\"Enter toggle\", \"^A all\", \"^X clear\", \"^P provider\", \"Alt+↑↓ reorder\", \"^S save\", countText];\n\t\treturn this.isDirty\n\t\t\t? theme.fg(\"dim\", ` ${parts.join(\" · \")} `) + theme.fg(\"warning\", \"(unsaved)\")\n\t\t\t: theme.fg(\"dim\", ` ${parts.join(\" · \")}`);\n\t}\n\n\tprivate refresh(): void {\n\t\tconst query = this.searchInput.getValue();\n\t\tconst items = this.buildItems();\n\t\tthis.filteredItems = query ? fuzzyFilter(items, query, (i) => `${i.model.id} ${i.model.provider}`) : items;\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredItems.length - 1));\n\t\tthis.updateList();\n\t\tthis.footerText.setText(this.getFooterText());\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tif (this.filteredItems.length === 0) {\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", \" No matching models\"), 0, 0));\n\t\t\treturn;\n\t\t}\n\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length);\n\t\tconst allEnabled = this.enabledIds === null;\n\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredItems[i]!;\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst prefix = isSelected ? theme.fg(\"accent\", \"→ \") : \" \";\n\t\t\tconst modelText = isSelected ? theme.fg(\"accent\", item.model.id) : item.model.id;\n\t\t\tconst providerBadge = theme.fg(\"muted\", ` [${item.model.provider}]`);\n\t\t\tconst status = allEnabled ? \"\" : item.enabled ? theme.fg(\"success\", \" ✓\") : theme.fg(\"dim\", \" ✗\");\n\t\t\tthis.listContainer.addChild(new Text(`${prefix}${modelText}${providerBadge}${status}`, 0, 0));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredItems.length) {\n\t\t\tthis.listContainer.addChild(\n\t\t\t\tnew Text(theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.filteredItems.length})`), 0, 0),\n\t\t\t);\n\t\t}\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst kb = getEditorKeybindings();\n\n\t\t// Navigation\n\t\tif (kb.matches(data, \"selectUp\")) {\n\t\t\tif (this.filteredItems.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.filteredItems.length - 1 : this.selectedIndex - 1;\n\t\t\tthis.updateList();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(data, \"selectDown\")) {\n\t\t\tif (this.filteredItems.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === this.filteredItems.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t\tthis.updateList();\n\t\t\treturn;\n\t\t}\n\n\t\t// Alt+Up/Down - Reorder enabled models\n\t\tif (matchesKey(data, Key.alt(\"up\")) || matchesKey(data, Key.alt(\"down\"))) {\n\t\t\tconst item = this.filteredItems[this.selectedIndex];\n\t\t\tif (item && isEnabled(this.enabledIds, item.fullId)) {\n\t\t\t\tconst delta = matchesKey(data, Key.alt(\"up\")) ? -1 : 1;\n\t\t\t\tconst enabledList = this.enabledIds ?? this.allIds;\n\t\t\t\tconst currentIndex = enabledList.indexOf(item.fullId);\n\t\t\t\tconst newIndex = currentIndex + delta;\n\t\t\t\t// Only move if within bounds\n\t\t\t\tif (newIndex >= 0 && newIndex < enabledList.length) {\n\t\t\t\t\tthis.enabledIds = move(this.enabledIds, this.allIds, item.fullId, delta);\n\t\t\t\t\tthis.isDirty = true;\n\t\t\t\t\tthis.selectedIndex += delta;\n\t\t\t\t\tthis.refresh();\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Toggle on Enter\n\t\tif (matchesKey(data, Key.enter)) {\n\t\t\tconst item = this.filteredItems[this.selectedIndex];\n\t\t\tif (item) {\n\t\t\t\tconst wasAllEnabled = this.enabledIds === null;\n\t\t\t\tthis.enabledIds = toggle(this.enabledIds, item.fullId);\n\t\t\t\tthis.isDirty = true;\n\t\t\t\tif (wasAllEnabled) this.callbacks.onClearAll();\n\t\t\t\tthis.callbacks.onModelToggle(item.fullId, isEnabled(this.enabledIds, item.fullId));\n\t\t\t\tthis.refresh();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+A - Enable all (filtered if search active, otherwise all)\n\t\tif (matchesKey(data, Key.ctrl(\"a\"))) {\n\t\t\tconst targetIds = this.searchInput.getValue() ? this.filteredItems.map((i) => i.fullId) : undefined;\n\t\t\tthis.enabledIds = enableAll(this.enabledIds, this.allIds, targetIds);\n\t\t\tthis.isDirty = true;\n\t\t\tthis.callbacks.onEnableAll(targetIds ?? this.allIds);\n\t\t\tthis.refresh();\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+X - Clear all (filtered if search active, otherwise all)\n\t\tif (matchesKey(data, Key.ctrl(\"x\"))) {\n\t\t\tconst targetIds = this.searchInput.getValue() ? this.filteredItems.map((i) => i.fullId) : undefined;\n\t\t\tthis.enabledIds = clearAll(this.enabledIds, this.allIds, targetIds);\n\t\t\tthis.isDirty = true;\n\t\t\tthis.callbacks.onClearAll();\n\t\t\tthis.refresh();\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+P - Toggle provider of current item\n\t\tif (matchesKey(data, Key.ctrl(\"p\"))) {\n\t\t\tconst item = this.filteredItems[this.selectedIndex];\n\t\t\tif (item) {\n\t\t\t\tconst provider = item.model.provider;\n\t\t\t\tconst providerIds = this.allIds.filter((id) => this.modelsById.get(id)!.provider === provider);\n\t\t\t\tconst allEnabled = providerIds.every((id) => isEnabled(this.enabledIds, id));\n\t\t\t\tthis.enabledIds = allEnabled\n\t\t\t\t\t? clearAll(this.enabledIds, this.allIds, providerIds)\n\t\t\t\t\t: enableAll(this.enabledIds, this.allIds, providerIds);\n\t\t\t\tthis.isDirty = true;\n\t\t\t\tthis.callbacks.onToggleProvider(provider, providerIds, !allEnabled);\n\t\t\t\tthis.refresh();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+S - Save/persist to settings\n\t\tif (matchesKey(data, Key.ctrl(\"s\"))) {\n\t\t\tthis.callbacks.onPersist(this.enabledIds ?? [...this.allIds]);\n\t\t\tthis.isDirty = false;\n\t\t\tthis.footerText.setText(this.getFooterText());\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+C - clear search or cancel if empty\n\t\tif (matchesKey(data, Key.ctrl(\"c\"))) {\n\t\t\tif (this.searchInput.getValue()) {\n\t\t\t\tthis.searchInput.setValue(\"\");\n\t\t\t\tthis.refresh();\n\t\t\t} else {\n\t\t\t\tthis.callbacks.onCancel();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Escape - cancel\n\t\tif (matchesKey(data, Key.escape)) {\n\t\t\tthis.callbacks.onCancel();\n\t\t\treturn;\n\t\t}\n\n\t\t// Pass everything else to search input\n\t\tthis.searchInput.handleInput(data);\n\t\tthis.refresh();\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/session-selector.ts"],"names":[],"mappings":"AACA,OAAO,EACN,KAAK,SAAS,EACd,SAAS,EAOT,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AA+EzF;;GAEG;AACH,cAAM,WAAY,YAAW,SAAS;IACrC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,OAAO,CAAS;IACjB,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,IAAI,CAAY;IAC9B,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,OAAO,CAAC,UAAU,CAAa;IAE/B,YAAY,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,OAAO,EAepD;IAED,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAI3D;IAED,OAAO,CAAC,cAAc;IAStB,UAAU,IAAI,IAAI,CAAG;IAErB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAwD9B;IAED,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAkCjC;CACD;AAED,KAAK,cAAc,GAAG,CAAC,UAAU,CAAC,EAAE,mBAAmB,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;AAEnF;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,SAAS;IACtD,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,eAAe,CAA8B;IACrD,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,qBAAqB,CAAiB;IAC9C,OAAO,CAAC,iBAAiB,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,aAAa,CAAa;IAElC,YACC,qBAAqB,EAAE,cAAc,EACrC,iBAAiB,EAAE,cAAc,EACjC,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,EACvC,QAAQ,EAAE,MAAM,IAAI,EACpB,MAAM,EAAE,MAAM,IAAI,EAClB,aAAa,EAAE,MAAM,IAAI,EA+BzB;IAED,OAAO,CAAC,mBAAmB;IAkB3B,OAAO,CAAC,WAAW;IAoCnB,cAAc,IAAI,WAAW,CAE5B;CACD","sourcesContent":["import * as os from \"node:os\";\nimport {\n\ttype Component,\n\tContainer,\n\tfuzzyFilter,\n\tgetEditorKeybindings,\n\tInput,\n\tSpacer,\n\ttruncateToWidth,\n\tvisibleWidth,\n} from \"@mariozechner/pi-tui\";\nimport type { SessionInfo, SessionListProgress } from \"../../../core/session-manager.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ntype SessionScope = \"current\" | \"all\";\n\nfunction shortenPath(path: string): string {\n\tconst home = os.homedir();\n\tif (!path) return path;\n\tif (path.startsWith(home)) {\n\t\treturn `~${path.slice(home.length)}`;\n\t}\n\treturn path;\n}\n\nfunction formatSessionDate(date: Date): string {\n\tconst now = new Date();\n\tconst diffMs = now.getTime() - date.getTime();\n\tconst diffMins = Math.floor(diffMs / 60000);\n\tconst diffHours = Math.floor(diffMs / 3600000);\n\tconst diffDays = Math.floor(diffMs / 86400000);\n\n\tif (diffMins < 1) return \"just now\";\n\tif (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? \"s\" : \"\"} ago`;\n\tif (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? \"s\" : \"\"} ago`;\n\tif (diffDays === 1) return \"1 day ago\";\n\tif (diffDays < 7) return `${diffDays} days ago`;\n\n\treturn date.toLocaleDateString();\n}\n\nclass SessionSelectorHeader implements Component {\n\tprivate scope: SessionScope;\n\tprivate loading = false;\n\tprivate loadProgress: { loaded: number; total: number } | null = null;\n\n\tconstructor(scope: SessionScope) {\n\t\tthis.scope = scope;\n\t}\n\n\tsetScope(scope: SessionScope): void {\n\t\tthis.scope = scope;\n\t}\n\n\tsetLoading(loading: boolean): void {\n\t\tthis.loading = loading;\n\t\tif (!loading) {\n\t\t\tthis.loadProgress = null;\n\t\t}\n\t}\n\n\tsetProgress(loaded: number, total: number): void {\n\t\tthis.loadProgress = { loaded, total };\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst title = this.scope === \"current\" ? \"Resume Session (Current Folder)\" : \"Resume Session (All)\";\n\t\tconst leftText = theme.bold(title);\n\t\tlet scopeText: string;\n\t\tif (this.loading) {\n\t\t\tconst progressText = this.loadProgress ? `${this.loadProgress.loaded}/${this.loadProgress.total}` : \"...\";\n\t\t\tscopeText = `${theme.fg(\"muted\", \"○ Current Folder | \")}${theme.fg(\"accent\", `Loading ${progressText}`)}`;\n\t\t} else {\n\t\t\tscopeText =\n\t\t\t\tthis.scope === \"current\"\n\t\t\t\t\t? `${theme.fg(\"accent\", \"◉ Current Folder\")}${theme.fg(\"muted\", \" | ○ All\")}`\n\t\t\t\t\t: `${theme.fg(\"muted\", \"○ Current Folder | \")}${theme.fg(\"accent\", \"◉ All\")}`;\n\t\t}\n\t\tconst rightText = truncateToWidth(scopeText, width, \"\");\n\t\tconst availableLeft = Math.max(0, width - visibleWidth(rightText) - 1);\n\t\tconst left = truncateToWidth(leftText, availableLeft, \"\");\n\t\tconst spacing = Math.max(0, width - visibleWidth(left) - visibleWidth(rightText));\n\t\tconst hint = theme.fg(\"muted\", \"Tab to toggle scope\");\n\t\treturn [`${left}${\" \".repeat(spacing)}${rightText}`, hint];\n\t}\n}\n\n/**\n * Custom session list component with multi-line items and search\n */\nclass SessionList implements Component {\n\tprivate allSessions: SessionInfo[] = [];\n\tprivate filteredSessions: SessionInfo[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate searchInput: Input;\n\tprivate showCwd = false;\n\tpublic onSelect?: (sessionPath: string) => void;\n\tpublic onCancel?: () => void;\n\tpublic onExit: () => void = () => {};\n\tpublic onToggleScope?: () => void;\n\tprivate maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)\n\n\tconstructor(sessions: SessionInfo[], showCwd: boolean) {\n\t\tthis.allSessions = sessions;\n\t\tthis.filteredSessions = sessions;\n\t\tthis.searchInput = new Input();\n\t\tthis.showCwd = showCwd;\n\n\t\t// Handle Enter in search input - select current item\n\t\tthis.searchInput.onSubmit = () => {\n\t\t\tif (this.filteredSessions[this.selectedIndex]) {\n\t\t\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\t\t\tif (this.onSelect) {\n\t\t\t\t\tthis.onSelect(selected.path);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\tsetSessions(sessions: SessionInfo[], showCwd: boolean): void {\n\t\tthis.allSessions = sessions;\n\t\tthis.showCwd = showCwd;\n\t\tthis.filterSessions(this.searchInput.getValue());\n\t}\n\n\tprivate filterSessions(query: string): void {\n\t\tthis.filteredSessions = fuzzyFilter(\n\t\t\tthis.allSessions,\n\t\t\tquery,\n\t\t\t(session) => `${session.id} ${session.allMessagesText} ${session.cwd}`,\n\t\t);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// Render search input\n\t\tlines.push(...this.searchInput.render(width));\n\t\tlines.push(\"\"); // Blank line after search\n\n\t\tif (this.filteredSessions.length === 0) {\n\t\t\tlines.push(theme.fg(\"muted\", \" No sessions found\"));\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredSessions.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);\n\n\t\t// Render visible sessions (2 lines per session + blank line)\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst session = this.filteredSessions[i];\n\t\t\tconst isSelected = i === this.selectedIndex;\n\n\t\t\t// Normalize first message to single line\n\t\t\tconst normalizedMessage = session.firstMessage.replace(/\\n/g, \" \").trim();\n\n\t\t\t// First line: cursor + message (truncate to visible width)\n\t\t\tconst cursor = isSelected ? theme.fg(\"accent\", \"› \") : \" \";\n\t\t\tconst maxMsgWidth = width - 2; // Account for cursor (2 visible chars)\n\t\t\tconst truncatedMsg = truncateToWidth(normalizedMessage, maxMsgWidth, \"...\");\n\t\t\tconst messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);\n\n\t\t\t// Second line: metadata (dimmed) - also truncate for safety\n\t\t\tconst modified = formatSessionDate(session.modified);\n\t\t\tconst msgCount = `${session.messageCount} message${session.messageCount !== 1 ? \"s\" : \"\"}`;\n\t\t\tconst metadataParts = [modified, msgCount];\n\t\t\tif (this.showCwd && session.cwd) {\n\t\t\t\tmetadataParts.push(shortenPath(session.cwd));\n\t\t\t}\n\t\t\tconst metadata = ` ${metadataParts.join(\" · \")}`;\n\t\t\tconst metadataLine = theme.fg(\"dim\", truncateToWidth(metadata, width, \"\"));\n\n\t\t\tlines.push(messageLine);\n\t\t\tlines.push(metadataLine);\n\t\t\tlines.push(\"\"); // Blank line between sessions\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredSessions.length) {\n\t\t\tconst scrollText = ` (${this.selectedIndex + 1}/${this.filteredSessions.length})`;\n\t\t\tconst scrollInfo = theme.fg(\"muted\", truncateToWidth(scrollText, width, \"\"));\n\t\t\tlines.push(scrollInfo);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\tif (kb.matches(keyData, \"tab\")) {\n\t\t\tif (this.onToggleScope) {\n\t\t\t\tthis.onToggleScope();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t// Up arrow\n\t\tif (kb.matches(keyData, \"selectUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t}\n\t\t// Down arrow\n\t\telse if (kb.matches(keyData, \"selectDown\")) {\n\t\t\tthis.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + 1);\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"selectConfirm\")) {\n\t\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\t\tif (selected && this.onSelect) {\n\t\t\t\tthis.onSelect(selected.path);\n\t\t\t}\n\t\t}\n\t\t// Escape - cancel\n\t\telse if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t\t// Pass everything else to search input\n\t\telse {\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tthis.filterSessions(this.searchInput.getValue());\n\t\t}\n\t}\n}\n\ntype SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;\n\n/**\n * Component that renders a session selector\n */\nexport class SessionSelectorComponent extends Container {\n\tprivate sessionList: SessionList;\n\tprivate header: SessionSelectorHeader;\n\tprivate scope: SessionScope = \"current\";\n\tprivate currentSessions: SessionInfo[] | null = null;\n\tprivate allSessions: SessionInfo[] | null = null;\n\tprivate currentSessionsLoader: SessionsLoader;\n\tprivate allSessionsLoader: SessionsLoader;\n\tprivate onCancel: () => void;\n\tprivate requestRender: () => void;\n\n\tconstructor(\n\t\tcurrentSessionsLoader: SessionsLoader,\n\t\tallSessionsLoader: SessionsLoader,\n\t\tonSelect: (sessionPath: string) => void,\n\t\tonCancel: () => void,\n\t\tonExit: () => void,\n\t\trequestRender: () => void,\n\t) {\n\t\tsuper();\n\t\tthis.currentSessionsLoader = currentSessionsLoader;\n\t\tthis.allSessionsLoader = allSessionsLoader;\n\t\tthis.onCancel = onCancel;\n\t\tthis.requestRender = requestRender;\n\t\tthis.header = new SessionSelectorHeader(this.scope);\n\n\t\t// Add header\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(this.header);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create session list (starts empty, will be populated after load)\n\t\tthis.sessionList = new SessionList([], false);\n\t\tthis.sessionList.onSelect = onSelect;\n\t\tthis.sessionList.onCancel = onCancel;\n\t\tthis.sessionList.onExit = onExit;\n\t\tthis.sessionList.onToggleScope = () => this.toggleScope();\n\n\t\tthis.addChild(this.sessionList);\n\n\t\t// Add bottom border\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Start loading current sessions immediately\n\t\tthis.loadCurrentSessions();\n\t}\n\n\tprivate loadCurrentSessions(): void {\n\t\tthis.header.setLoading(true);\n\t\tthis.requestRender();\n\t\tthis.currentSessionsLoader((loaded, total) => {\n\t\t\tthis.header.setProgress(loaded, total);\n\t\t\tthis.requestRender();\n\t\t}).then((sessions) => {\n\t\t\tthis.currentSessions = sessions;\n\t\t\tthis.header.setLoading(false);\n\t\t\tthis.sessionList.setSessions(sessions, false);\n\t\t\tthis.requestRender();\n\t\t\t// If no sessions found, cancel\n\t\t\tif (sessions.length === 0) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate toggleScope(): void {\n\t\tif (this.scope === \"current\") {\n\t\t\t// Switching to \"all\" - load if not already loaded\n\t\t\tif (this.allSessions === null) {\n\t\t\t\tthis.header.setLoading(true);\n\t\t\t\tthis.header.setScope(\"all\");\n\t\t\t\tthis.sessionList.setSessions([], true); // Clear list while loading\n\t\t\t\tthis.requestRender();\n\t\t\t\t// Load asynchronously with progress updates\n\t\t\t\tthis.allSessionsLoader((loaded, total) => {\n\t\t\t\t\tthis.header.setProgress(loaded, total);\n\t\t\t\t\tthis.requestRender();\n\t\t\t\t}).then((sessions) => {\n\t\t\t\t\tthis.allSessions = sessions;\n\t\t\t\t\tthis.header.setLoading(false);\n\t\t\t\t\tthis.scope = \"all\";\n\t\t\t\t\tthis.sessionList.setSessions(this.allSessions, true);\n\t\t\t\t\tthis.requestRender();\n\t\t\t\t\t// If no sessions in All scope either, cancel\n\t\t\t\t\tif (this.allSessions.length === 0 && (this.currentSessions?.length ?? 0) === 0) {\n\t\t\t\t\t\tthis.onCancel();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tthis.scope = \"all\";\n\t\t\t\tthis.sessionList.setSessions(this.allSessions, true);\n\t\t\t\tthis.header.setScope(this.scope);\n\t\t\t}\n\t\t} else {\n\t\t\t// Switching back to \"current\"\n\t\t\tthis.scope = \"current\";\n\t\t\tthis.sessionList.setSessions(this.currentSessions ?? [], false);\n\t\t\tthis.header.setScope(this.scope);\n\t\t}\n\t}\n\n\tgetSessionList(): SessionList {\n\t\treturn this.sessionList;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/session-selector.ts"],"names":[],"mappings":"AACA,OAAO,EACN,KAAK,SAAS,EACd,SAAS,EAOT,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AA+EzF;;GAEG;AACH,cAAM,WAAY,YAAW,SAAS;IACrC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,OAAO,CAAS;IACjB,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,IAAI,CAAY;IAC9B,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,OAAO,CAAC,UAAU,CAAa;IAE/B,YAAY,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,OAAO,EAepD;IAED,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAI3D;IAED,OAAO,CAAC,cAAc;IAStB,UAAU,IAAI,IAAI,CAAG;IAErB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAwE9B;IAED,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA0CjC;CACD;AAED,KAAK,cAAc,GAAG,CAAC,UAAU,CAAC,EAAE,mBAAmB,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;AAEnF;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,SAAS;IACtD,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,eAAe,CAA8B;IACrD,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,qBAAqB,CAAiB;IAC9C,OAAO,CAAC,iBAAiB,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,aAAa,CAAa;IAElC,YACC,qBAAqB,EAAE,cAAc,EACrC,iBAAiB,EAAE,cAAc,EACjC,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,EACvC,QAAQ,EAAE,MAAM,IAAI,EACpB,MAAM,EAAE,MAAM,IAAI,EAClB,aAAa,EAAE,MAAM,IAAI,EA+BzB;IAED,OAAO,CAAC,mBAAmB;IAc3B,OAAO,CAAC,WAAW;IAoCnB,cAAc,IAAI,WAAW,CAE5B;CACD","sourcesContent":["import * as os from \"node:os\";\nimport {\n\ttype Component,\n\tContainer,\n\tfuzzyFilter,\n\tgetEditorKeybindings,\n\tInput,\n\tSpacer,\n\ttruncateToWidth,\n\tvisibleWidth,\n} from \"@mariozechner/pi-tui\";\nimport type { SessionInfo, SessionListProgress } from \"../../../core/session-manager.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\ntype SessionScope = \"current\" | \"all\";\n\nfunction shortenPath(path: string): string {\n\tconst home = os.homedir();\n\tif (!path) return path;\n\tif (path.startsWith(home)) {\n\t\treturn `~${path.slice(home.length)}`;\n\t}\n\treturn path;\n}\n\nfunction formatSessionDate(date: Date): string {\n\tconst now = new Date();\n\tconst diffMs = now.getTime() - date.getTime();\n\tconst diffMins = Math.floor(diffMs / 60000);\n\tconst diffHours = Math.floor(diffMs / 3600000);\n\tconst diffDays = Math.floor(diffMs / 86400000);\n\n\tif (diffMins < 1) return \"just now\";\n\tif (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? \"s\" : \"\"} ago`;\n\tif (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? \"s\" : \"\"} ago`;\n\tif (diffDays === 1) return \"1 day ago\";\n\tif (diffDays < 7) return `${diffDays} days ago`;\n\n\treturn date.toLocaleDateString();\n}\n\nclass SessionSelectorHeader implements Component {\n\tprivate scope: SessionScope;\n\tprivate loading = false;\n\tprivate loadProgress: { loaded: number; total: number } | null = null;\n\n\tconstructor(scope: SessionScope) {\n\t\tthis.scope = scope;\n\t}\n\n\tsetScope(scope: SessionScope): void {\n\t\tthis.scope = scope;\n\t}\n\n\tsetLoading(loading: boolean): void {\n\t\tthis.loading = loading;\n\t\tif (!loading) {\n\t\t\tthis.loadProgress = null;\n\t\t}\n\t}\n\n\tsetProgress(loaded: number, total: number): void {\n\t\tthis.loadProgress = { loaded, total };\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst title = this.scope === \"current\" ? \"Resume Session (Current Folder)\" : \"Resume Session (All)\";\n\t\tconst leftText = theme.bold(title);\n\t\tlet scopeText: string;\n\t\tif (this.loading) {\n\t\t\tconst progressText = this.loadProgress ? `${this.loadProgress.loaded}/${this.loadProgress.total}` : \"...\";\n\t\t\tscopeText = `${theme.fg(\"muted\", \"○ Current Folder | \")}${theme.fg(\"accent\", `Loading ${progressText}`)}`;\n\t\t} else {\n\t\t\tscopeText =\n\t\t\t\tthis.scope === \"current\"\n\t\t\t\t\t? `${theme.fg(\"accent\", \"◉ Current Folder\")}${theme.fg(\"muted\", \" | ○ All\")}`\n\t\t\t\t\t: `${theme.fg(\"muted\", \"○ Current Folder | \")}${theme.fg(\"accent\", \"◉ All\")}`;\n\t\t}\n\t\tconst rightText = truncateToWidth(scopeText, width, \"\");\n\t\tconst availableLeft = Math.max(0, width - visibleWidth(rightText) - 1);\n\t\tconst left = truncateToWidth(leftText, availableLeft, \"\");\n\t\tconst spacing = Math.max(0, width - visibleWidth(left) - visibleWidth(rightText));\n\t\tconst hint = theme.fg(\"muted\", \"Tab to toggle scope\");\n\t\treturn [`${left}${\" \".repeat(spacing)}${rightText}`, hint];\n\t}\n}\n\n/**\n * Custom session list component with multi-line items and search\n */\nclass SessionList implements Component {\n\tprivate allSessions: SessionInfo[] = [];\n\tprivate filteredSessions: SessionInfo[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate searchInput: Input;\n\tprivate showCwd = false;\n\tpublic onSelect?: (sessionPath: string) => void;\n\tpublic onCancel?: () => void;\n\tpublic onExit: () => void = () => {};\n\tpublic onToggleScope?: () => void;\n\tprivate maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)\n\n\tconstructor(sessions: SessionInfo[], showCwd: boolean) {\n\t\tthis.allSessions = sessions;\n\t\tthis.filteredSessions = sessions;\n\t\tthis.searchInput = new Input();\n\t\tthis.showCwd = showCwd;\n\n\t\t// Handle Enter in search input - select current item\n\t\tthis.searchInput.onSubmit = () => {\n\t\t\tif (this.filteredSessions[this.selectedIndex]) {\n\t\t\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\t\t\tif (this.onSelect) {\n\t\t\t\t\tthis.onSelect(selected.path);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\tsetSessions(sessions: SessionInfo[], showCwd: boolean): void {\n\t\tthis.allSessions = sessions;\n\t\tthis.showCwd = showCwd;\n\t\tthis.filterSessions(this.searchInput.getValue());\n\t}\n\n\tprivate filterSessions(query: string): void {\n\t\tthis.filteredSessions = fuzzyFilter(\n\t\t\tthis.allSessions,\n\t\t\tquery,\n\t\t\t(session) => `${session.id} ${session.name ?? \"\"} ${session.allMessagesText} ${session.cwd}`,\n\t\t);\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// Render search input\n\t\tlines.push(...this.searchInput.render(width));\n\t\tlines.push(\"\"); // Blank line after search\n\n\t\tif (this.filteredSessions.length === 0) {\n\t\t\tif (this.showCwd) {\n\t\t\t\t// \"All\" scope - no sessions anywhere that match filter\n\t\t\t\tlines.push(theme.fg(\"muted\", \" No sessions found\"));\n\t\t\t} else {\n\t\t\t\t// \"Current folder\" scope - hint to try \"all\"\n\t\t\t\tlines.push(theme.fg(\"muted\", \" No sessions in current folder. Press Tab to view all.\"));\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredSessions.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);\n\n\t\t// Render visible sessions (2 lines per session + blank line)\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst session = this.filteredSessions[i];\n\t\t\tconst isSelected = i === this.selectedIndex;\n\n\t\t\t// Use session name if set, otherwise first message\n\t\t\tconst hasName = !!session.name;\n\t\t\tconst displayText = session.name ?? session.firstMessage;\n\t\t\tconst normalizedMessage = displayText.replace(/\\n/g, \" \").trim();\n\n\t\t\t// First line: cursor + message (truncate to visible width)\n\t\t\t// Use warning color for custom names to distinguish from first message\n\t\t\tconst cursor = isSelected ? theme.fg(\"accent\", \"› \") : \" \";\n\t\t\tconst maxMsgWidth = width - 2; // Account for cursor (2 visible chars)\n\t\t\tconst truncatedMsg = truncateToWidth(normalizedMessage, maxMsgWidth, \"...\");\n\t\t\tlet styledMsg = truncatedMsg;\n\t\t\tif (hasName) {\n\t\t\t\tstyledMsg = theme.fg(\"warning\", truncatedMsg);\n\t\t\t}\n\t\t\tif (isSelected) {\n\t\t\t\tstyledMsg = theme.bold(styledMsg);\n\t\t\t}\n\t\t\tconst messageLine = cursor + styledMsg;\n\n\t\t\t// Second line: metadata (dimmed) - also truncate for safety\n\t\t\tconst modified = formatSessionDate(session.modified);\n\t\t\tconst msgCount = `${session.messageCount} message${session.messageCount !== 1 ? \"s\" : \"\"}`;\n\t\t\tconst metadataParts = [modified, msgCount];\n\t\t\tif (this.showCwd && session.cwd) {\n\t\t\t\tmetadataParts.push(shortenPath(session.cwd));\n\t\t\t}\n\t\t\tconst metadata = ` ${metadataParts.join(\" · \")}`;\n\t\t\tconst metadataLine = theme.fg(\"dim\", truncateToWidth(metadata, width, \"\"));\n\n\t\t\tlines.push(messageLine);\n\t\t\tlines.push(metadataLine);\n\t\t\tlines.push(\"\"); // Blank line between sessions\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredSessions.length) {\n\t\t\tconst scrollText = ` (${this.selectedIndex + 1}/${this.filteredSessions.length})`;\n\t\t\tconst scrollInfo = theme.fg(\"muted\", truncateToWidth(scrollText, width, \"\"));\n\t\t\tlines.push(scrollInfo);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getEditorKeybindings();\n\t\tif (kb.matches(keyData, \"tab\")) {\n\t\t\tif (this.onToggleScope) {\n\t\t\t\tthis.onToggleScope();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t// Up arrow\n\t\tif (kb.matches(keyData, \"selectUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t}\n\t\t// Down arrow\n\t\telse if (kb.matches(keyData, \"selectDown\")) {\n\t\t\tthis.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + 1);\n\t\t}\n\t\t// Page up - jump up by maxVisible items\n\t\telse if (kb.matches(keyData, \"selectPageUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);\n\t\t}\n\t\t// Page down - jump down by maxVisible items\n\t\telse if (kb.matches(keyData, \"selectPageDown\")) {\n\t\t\tthis.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + this.maxVisible);\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"selectConfirm\")) {\n\t\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\t\tif (selected && this.onSelect) {\n\t\t\t\tthis.onSelect(selected.path);\n\t\t\t}\n\t\t}\n\t\t// Escape - cancel\n\t\telse if (kb.matches(keyData, \"selectCancel\")) {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t\t// Pass everything else to search input\n\t\telse {\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tthis.filterSessions(this.searchInput.getValue());\n\t\t}\n\t}\n}\n\ntype SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;\n\n/**\n * Component that renders a session selector\n */\nexport class SessionSelectorComponent extends Container {\n\tprivate sessionList: SessionList;\n\tprivate header: SessionSelectorHeader;\n\tprivate scope: SessionScope = \"current\";\n\tprivate currentSessions: SessionInfo[] | null = null;\n\tprivate allSessions: SessionInfo[] | null = null;\n\tprivate currentSessionsLoader: SessionsLoader;\n\tprivate allSessionsLoader: SessionsLoader;\n\tprivate onCancel: () => void;\n\tprivate requestRender: () => void;\n\n\tconstructor(\n\t\tcurrentSessionsLoader: SessionsLoader,\n\t\tallSessionsLoader: SessionsLoader,\n\t\tonSelect: (sessionPath: string) => void,\n\t\tonCancel: () => void,\n\t\tonExit: () => void,\n\t\trequestRender: () => void,\n\t) {\n\t\tsuper();\n\t\tthis.currentSessionsLoader = currentSessionsLoader;\n\t\tthis.allSessionsLoader = allSessionsLoader;\n\t\tthis.onCancel = onCancel;\n\t\tthis.requestRender = requestRender;\n\t\tthis.header = new SessionSelectorHeader(this.scope);\n\n\t\t// Add header\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(this.header);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create session list (starts empty, will be populated after load)\n\t\tthis.sessionList = new SessionList([], false);\n\t\tthis.sessionList.onSelect = onSelect;\n\t\tthis.sessionList.onCancel = onCancel;\n\t\tthis.sessionList.onExit = onExit;\n\t\tthis.sessionList.onToggleScope = () => this.toggleScope();\n\n\t\tthis.addChild(this.sessionList);\n\n\t\t// Add bottom border\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Start loading current sessions immediately\n\t\tthis.loadCurrentSessions();\n\t}\n\n\tprivate loadCurrentSessions(): void {\n\t\tthis.header.setLoading(true);\n\t\tthis.requestRender();\n\t\tthis.currentSessionsLoader((loaded, total) => {\n\t\t\tthis.header.setProgress(loaded, total);\n\t\t\tthis.requestRender();\n\t\t}).then((sessions) => {\n\t\t\tthis.currentSessions = sessions;\n\t\t\tthis.header.setLoading(false);\n\t\t\tthis.sessionList.setSessions(sessions, false);\n\t\t\tthis.requestRender();\n\t\t});\n\t}\n\n\tprivate toggleScope(): void {\n\t\tif (this.scope === \"current\") {\n\t\t\t// Switching to \"all\" - load if not already loaded\n\t\t\tif (this.allSessions === null) {\n\t\t\t\tthis.header.setLoading(true);\n\t\t\t\tthis.header.setScope(\"all\");\n\t\t\t\tthis.sessionList.setSessions([], true); // Clear list while loading\n\t\t\t\tthis.requestRender();\n\t\t\t\t// Load asynchronously with progress updates\n\t\t\t\tthis.allSessionsLoader((loaded, total) => {\n\t\t\t\t\tthis.header.setProgress(loaded, total);\n\t\t\t\t\tthis.requestRender();\n\t\t\t\t}).then((sessions) => {\n\t\t\t\t\tthis.allSessions = sessions;\n\t\t\t\t\tthis.header.setLoading(false);\n\t\t\t\t\tthis.scope = \"all\";\n\t\t\t\t\tthis.sessionList.setSessions(this.allSessions, true);\n\t\t\t\t\tthis.requestRender();\n\t\t\t\t\t// If no sessions in All scope either, cancel\n\t\t\t\t\tif (this.allSessions.length === 0 && (this.currentSessions?.length ?? 0) === 0) {\n\t\t\t\t\t\tthis.onCancel();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tthis.scope = \"all\";\n\t\t\t\tthis.sessionList.setSessions(this.allSessions, true);\n\t\t\t\tthis.header.setScope(this.scope);\n\t\t\t}\n\t\t} else {\n\t\t\t// Switching back to \"current\"\n\t\t\tthis.scope = \"current\";\n\t\t\tthis.sessionList.setSessions(this.currentSessions ?? [], false);\n\t\t\tthis.header.setScope(this.scope);\n\t\t}\n\t}\n\n\tgetSessionList(): SessionList {\n\t\treturn this.sessionList;\n\t}\n}\n"]}
|
|
@@ -106,7 +106,7 @@ class SessionList {
|
|
|
106
106
|
this.filterSessions(this.searchInput.getValue());
|
|
107
107
|
}
|
|
108
108
|
filterSessions(query) {
|
|
109
|
-
this.filteredSessions = fuzzyFilter(this.allSessions, query, (session) => `${session.id} ${session.allMessagesText} ${session.cwd}`);
|
|
109
|
+
this.filteredSessions = fuzzyFilter(this.allSessions, query, (session) => `${session.id} ${session.name ?? ""} ${session.allMessagesText} ${session.cwd}`);
|
|
110
110
|
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));
|
|
111
111
|
}
|
|
112
112
|
invalidate() { }
|
|
@@ -116,7 +116,14 @@ class SessionList {
|
|
|
116
116
|
lines.push(...this.searchInput.render(width));
|
|
117
117
|
lines.push(""); // Blank line after search
|
|
118
118
|
if (this.filteredSessions.length === 0) {
|
|
119
|
-
|
|
119
|
+
if (this.showCwd) {
|
|
120
|
+
// "All" scope - no sessions anywhere that match filter
|
|
121
|
+
lines.push(theme.fg("muted", " No sessions found"));
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// "Current folder" scope - hint to try "all"
|
|
125
|
+
lines.push(theme.fg("muted", " No sessions in current folder. Press Tab to view all."));
|
|
126
|
+
}
|
|
120
127
|
return lines;
|
|
121
128
|
}
|
|
122
129
|
// Calculate visible range with scrolling
|
|
@@ -126,13 +133,23 @@ class SessionList {
|
|
|
126
133
|
for (let i = startIndex; i < endIndex; i++) {
|
|
127
134
|
const session = this.filteredSessions[i];
|
|
128
135
|
const isSelected = i === this.selectedIndex;
|
|
129
|
-
//
|
|
130
|
-
const
|
|
136
|
+
// Use session name if set, otherwise first message
|
|
137
|
+
const hasName = !!session.name;
|
|
138
|
+
const displayText = session.name ?? session.firstMessage;
|
|
139
|
+
const normalizedMessage = displayText.replace(/\n/g, " ").trim();
|
|
131
140
|
// First line: cursor + message (truncate to visible width)
|
|
141
|
+
// Use warning color for custom names to distinguish from first message
|
|
132
142
|
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
|
|
133
143
|
const maxMsgWidth = width - 2; // Account for cursor (2 visible chars)
|
|
134
144
|
const truncatedMsg = truncateToWidth(normalizedMessage, maxMsgWidth, "...");
|
|
135
|
-
|
|
145
|
+
let styledMsg = truncatedMsg;
|
|
146
|
+
if (hasName) {
|
|
147
|
+
styledMsg = theme.fg("warning", truncatedMsg);
|
|
148
|
+
}
|
|
149
|
+
if (isSelected) {
|
|
150
|
+
styledMsg = theme.bold(styledMsg);
|
|
151
|
+
}
|
|
152
|
+
const messageLine = cursor + styledMsg;
|
|
136
153
|
// Second line: metadata (dimmed) - also truncate for safety
|
|
137
154
|
const modified = formatSessionDate(session.modified);
|
|
138
155
|
const msgCount = `${session.messageCount} message${session.messageCount !== 1 ? "s" : ""}`;
|
|
@@ -170,6 +187,14 @@ class SessionList {
|
|
|
170
187
|
else if (kb.matches(keyData, "selectDown")) {
|
|
171
188
|
this.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + 1);
|
|
172
189
|
}
|
|
190
|
+
// Page up - jump up by maxVisible items
|
|
191
|
+
else if (kb.matches(keyData, "selectPageUp")) {
|
|
192
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);
|
|
193
|
+
}
|
|
194
|
+
// Page down - jump down by maxVisible items
|
|
195
|
+
else if (kb.matches(keyData, "selectPageDown")) {
|
|
196
|
+
this.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + this.maxVisible);
|
|
197
|
+
}
|
|
173
198
|
// Enter
|
|
174
199
|
else if (kb.matches(keyData, "selectConfirm")) {
|
|
175
200
|
const selected = this.filteredSessions[this.selectedIndex];
|
|
@@ -240,10 +265,6 @@ export class SessionSelectorComponent extends Container {
|
|
|
240
265
|
this.header.setLoading(false);
|
|
241
266
|
this.sessionList.setSessions(sessions, false);
|
|
242
267
|
this.requestRender();
|
|
243
|
-
// If no sessions found, cancel
|
|
244
|
-
if (sessions.length === 0) {
|
|
245
|
-
this.onCancel();
|
|
246
|
-
}
|
|
247
268
|
});
|
|
248
269
|
}
|
|
249
270
|
toggleScope() {
|