@vaclav-synacek/pi-coding-agent-termux 0.50.7-1 → 0.51.1-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 +94 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +2 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/session-picker.d.ts.map +1 -1
- package/dist/cli/session-picker.js +3 -1
- package/dist/cli/session-picker.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +5 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +76 -15
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/index.d.ts +3 -3
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +19 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +42 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +88 -6
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js +4 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/extensions/wrapper.d.ts.map +1 -1
- package/dist/core/extensions/wrapper.js +1 -1
- package/dist/core/extensions/wrapper.js.map +1 -1
- package/dist/core/keybindings.d.ts +1 -1
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +8 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +27 -18
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +11 -9
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/resource-loader.d.ts +24 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +88 -19
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +2 -0
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +5 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +16 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +1 -0
- package/dist/core/skills.js.map +1 -1
- package/dist/core/tools/bash.d.ts +11 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +18 -3
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts +2 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts +2 -0
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +2 -0
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +7 -7
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +5 -5
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +2 -0
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts +2 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/write.d.ts +2 -0
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +0 -7
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
- package/dist/modes/interactive/components/daxnuts.js +1 -1
- package/dist/modes/interactive/components/daxnuts.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +4 -1
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.d.ts +4 -2
- package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.js +13 -4
- package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts +12 -2
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +188 -57
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +12 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts +6 -0
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +43 -16
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +24 -15
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +4 -0
- 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 +4 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +52 -10
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/clipboard-native.d.ts +7 -0
- package/dist/utils/clipboard-native.d.ts.map +1 -0
- package/dist/utils/clipboard-native.js +14 -0
- package/dist/utils/clipboard-native.js.map +1 -0
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +14 -0
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/custom-provider.md +2 -1
- package/docs/extensions.md +57 -9
- package/docs/keybindings.md +9 -0
- package/docs/models.md +43 -14
- package/docs/rpc.md +188 -1
- package/docs/settings.md +5 -1
- package/docs/termux.md +127 -0
- package/examples/extensions/README.md +9 -0
- package/examples/extensions/antigravity-image-gen.ts +1 -1
- package/examples/extensions/bash-spawn-hook.ts +30 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/custom-provider-qwen-cli/index.ts +345 -0
- package/examples/extensions/custom-provider-qwen-cli/package.json +16 -0
- package/examples/extensions/dynamic-resources/SKILL.md +8 -0
- package/examples/extensions/dynamic-resources/dynamic.json +79 -0
- package/examples/extensions/dynamic-resources/dynamic.md +5 -0
- package/examples/extensions/dynamic-resources/index.ts +15 -0
- package/examples/extensions/hello.ts +1 -1
- package/examples/extensions/question.ts +1 -1
- package/examples/extensions/questionnaire.ts +1 -1
- package/examples/extensions/rpc-demo.ts +124 -0
- package/examples/extensions/sandbox/index.ts +1 -1
- package/examples/extensions/shutdown-command.ts +2 -2
- package/examples/extensions/ssh.ts +4 -4
- package/examples/extensions/subagent/index.ts +1 -1
- package/examples/extensions/titlebar-spinner.ts +58 -0
- package/examples/extensions/todo.ts +1 -1
- package/examples/extensions/tool-override.ts +1 -1
- package/examples/extensions/truncated-tool.ts +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/rpc-extension-ui.ts +632 -0
- package/examples/sdk/06-extensions.ts +1 -1
- package/examples/sdk/12-full-control.ts +1 -0
- package/package.json +6 -5
|
@@ -3,10 +3,11 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { unlink } from "node:fs/promises";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import { Container, getEditorKeybindings, Input, matchesKey, Spacer, Text, truncateToWidth, visibleWidth, } from "@mariozechner/pi-tui";
|
|
6
|
+
import { KeybindingsManager } from "../../../core/keybindings.js";
|
|
6
7
|
import { theme } from "../theme/theme.js";
|
|
7
8
|
import { DynamicBorder } from "./dynamic-border.js";
|
|
8
|
-
import { keyHint } from "./keybinding-hints.js";
|
|
9
|
-
import { filterAndSortSessions } from "./session-selector-search.js";
|
|
9
|
+
import { appKey, appKeyHint, keyHint } from "./keybinding-hints.js";
|
|
10
|
+
import { filterAndSortSessions, hasSessionName } from "./session-selector-search.js";
|
|
10
11
|
function shortenPath(path) {
|
|
11
12
|
const home = os.homedir();
|
|
12
13
|
if (!path)
|
|
@@ -23,20 +24,24 @@ function formatSessionDate(date) {
|
|
|
23
24
|
const diffHours = Math.floor(diffMs / 3600000);
|
|
24
25
|
const diffDays = Math.floor(diffMs / 86400000);
|
|
25
26
|
if (diffMins < 1)
|
|
26
|
-
return "
|
|
27
|
+
return "now";
|
|
27
28
|
if (diffMins < 60)
|
|
28
|
-
return `${diffMins}
|
|
29
|
+
return `${diffMins}m`;
|
|
29
30
|
if (diffHours < 24)
|
|
30
|
-
return `${diffHours}
|
|
31
|
-
if (diffDays === 1)
|
|
32
|
-
return "1 day ago";
|
|
31
|
+
return `${diffHours}h`;
|
|
33
32
|
if (diffDays < 7)
|
|
34
|
-
return `${diffDays}
|
|
35
|
-
|
|
33
|
+
return `${diffDays}d`;
|
|
34
|
+
if (diffDays < 30)
|
|
35
|
+
return `${Math.floor(diffDays / 7)}w`;
|
|
36
|
+
if (diffDays < 365)
|
|
37
|
+
return `${Math.floor(diffDays / 30)}mo`;
|
|
38
|
+
return `${Math.floor(diffDays / 365)}y`;
|
|
36
39
|
}
|
|
37
40
|
class SessionSelectorHeader {
|
|
38
41
|
scope;
|
|
39
42
|
sortMode;
|
|
43
|
+
nameFilter;
|
|
44
|
+
keybindings;
|
|
40
45
|
requestRender;
|
|
41
46
|
loading = false;
|
|
42
47
|
loadProgress = null;
|
|
@@ -45,9 +50,11 @@ class SessionSelectorHeader {
|
|
|
45
50
|
statusMessage = null;
|
|
46
51
|
statusTimeout = null;
|
|
47
52
|
showRenameHint = false;
|
|
48
|
-
constructor(scope, sortMode, requestRender) {
|
|
53
|
+
constructor(scope, sortMode, nameFilter, keybindings, requestRender) {
|
|
49
54
|
this.scope = scope;
|
|
50
55
|
this.sortMode = sortMode;
|
|
56
|
+
this.nameFilter = nameFilter;
|
|
57
|
+
this.keybindings = keybindings;
|
|
51
58
|
this.requestRender = requestRender;
|
|
52
59
|
}
|
|
53
60
|
setScope(scope) {
|
|
@@ -56,6 +63,9 @@ class SessionSelectorHeader {
|
|
|
56
63
|
setSortMode(sortMode) {
|
|
57
64
|
this.sortMode = sortMode;
|
|
58
65
|
}
|
|
66
|
+
setNameFilter(nameFilter) {
|
|
67
|
+
this.nameFilter = nameFilter;
|
|
68
|
+
}
|
|
59
69
|
setLoading(loading) {
|
|
60
70
|
this.loading = loading;
|
|
61
71
|
// Progress is scoped to the current load; clear whenever the loading state is set
|
|
@@ -94,8 +104,10 @@ class SessionSelectorHeader {
|
|
|
94
104
|
render(width) {
|
|
95
105
|
const title = this.scope === "current" ? "Resume Session (Current Folder)" : "Resume Session (All)";
|
|
96
106
|
const leftText = theme.bold(title);
|
|
97
|
-
const sortLabel = this.sortMode === "recent" ? "Recent" : "Fuzzy";
|
|
107
|
+
const sortLabel = this.sortMode === "threaded" ? "Threaded" : this.sortMode === "recent" ? "Recent" : "Fuzzy";
|
|
98
108
|
const sortText = theme.fg("muted", "Sort: ") + theme.fg("accent", sortLabel);
|
|
109
|
+
const nameLabel = this.nameFilter === "all" ? "All" : "Named";
|
|
110
|
+
const nameText = theme.fg("muted", "Name: ") + theme.fg("accent", nameLabel);
|
|
99
111
|
let scopeText;
|
|
100
112
|
if (this.loading) {
|
|
101
113
|
const progressText = this.loadProgress ? `${this.loadProgress.loaded}/${this.loadProgress.total}` : "...";
|
|
@@ -107,7 +119,7 @@ class SessionSelectorHeader {
|
|
|
107
119
|
else {
|
|
108
120
|
scopeText = `${theme.fg("muted", "○ Current Folder | ")}${theme.fg("accent", "◉ All")}`;
|
|
109
121
|
}
|
|
110
|
-
const rightText = truncateToWidth(`${scopeText} ${sortText}`, width, "");
|
|
122
|
+
const rightText = truncateToWidth(`${scopeText} ${nameText} ${sortText}`, width, "");
|
|
111
123
|
const availableLeft = Math.max(0, width - visibleWidth(rightText) - 1);
|
|
112
124
|
const left = truncateToWidth(leftText, availableLeft, "");
|
|
113
125
|
const spacing = Math.max(0, width - visibleWidth(left) - visibleWidth(rightText));
|
|
@@ -130,6 +142,7 @@ class SessionSelectorHeader {
|
|
|
130
142
|
const hint1 = keyHint("tab", "scope") + sep + theme.fg("muted", 're:<pattern> regex · "phrase" exact');
|
|
131
143
|
const hint2Parts = [
|
|
132
144
|
keyHint("toggleSessionSort", "sort"),
|
|
145
|
+
appKeyHint(this.keybindings, "toggleSessionNamedFilter", "named"),
|
|
133
146
|
keyHint("deleteSession", "delete"),
|
|
134
147
|
keyHint("toggleSessionPath", `path ${pathState}`),
|
|
135
148
|
];
|
|
@@ -143,20 +156,71 @@ class SessionSelectorHeader {
|
|
|
143
156
|
return [`${left}${" ".repeat(spacing)}${rightText}`, hintLine1, hintLine2];
|
|
144
157
|
}
|
|
145
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Build a tree structure from sessions based on parentSessionPath.
|
|
161
|
+
* Returns root nodes sorted by modified date (descending).
|
|
162
|
+
*/
|
|
163
|
+
function buildSessionTree(sessions) {
|
|
164
|
+
const byPath = new Map();
|
|
165
|
+
for (const session of sessions) {
|
|
166
|
+
byPath.set(session.path, { session, children: [] });
|
|
167
|
+
}
|
|
168
|
+
const roots = [];
|
|
169
|
+
for (const session of sessions) {
|
|
170
|
+
const node = byPath.get(session.path);
|
|
171
|
+
const parentPath = session.parentSessionPath;
|
|
172
|
+
if (parentPath && byPath.has(parentPath)) {
|
|
173
|
+
byPath.get(parentPath).children.push(node);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
roots.push(node);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Sort children and roots by modified date (descending)
|
|
180
|
+
const sortNodes = (nodes) => {
|
|
181
|
+
nodes.sort((a, b) => b.session.modified.getTime() - a.session.modified.getTime());
|
|
182
|
+
for (const node of nodes) {
|
|
183
|
+
sortNodes(node.children);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
sortNodes(roots);
|
|
187
|
+
return roots;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Flatten tree into display list with tree structure metadata.
|
|
191
|
+
*/
|
|
192
|
+
function flattenSessionTree(roots) {
|
|
193
|
+
const result = [];
|
|
194
|
+
const walk = (node, depth, ancestorContinues, isLast) => {
|
|
195
|
+
result.push({ session: node.session, depth, isLast, ancestorContinues });
|
|
196
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
197
|
+
const childIsLast = i === node.children.length - 1;
|
|
198
|
+
// Only show continuation line for non-root ancestors
|
|
199
|
+
const continues = depth > 0 ? !isLast : false;
|
|
200
|
+
walk(node.children[i], depth + 1, [...ancestorContinues, continues], childIsLast);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
for (let i = 0; i < roots.length; i++) {
|
|
204
|
+
walk(roots[i], 0, [], i === roots.length - 1);
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
146
208
|
/**
|
|
147
209
|
* Custom session list component with multi-line items and search
|
|
148
210
|
*/
|
|
149
211
|
class SessionList {
|
|
150
212
|
getSelectedSessionPath() {
|
|
151
213
|
const selected = this.filteredSessions[this.selectedIndex];
|
|
152
|
-
return selected?.path;
|
|
214
|
+
return selected?.session.path;
|
|
153
215
|
}
|
|
154
216
|
allSessions = [];
|
|
155
217
|
filteredSessions = [];
|
|
156
218
|
selectedIndex = 0;
|
|
157
219
|
searchInput;
|
|
158
220
|
showCwd = false;
|
|
159
|
-
sortMode = "
|
|
221
|
+
sortMode = "threaded";
|
|
222
|
+
nameFilter = "all";
|
|
223
|
+
keybindings;
|
|
160
224
|
showPath = false;
|
|
161
225
|
confirmingDeletePath = null;
|
|
162
226
|
currentSessionFilePath;
|
|
@@ -165,12 +229,13 @@ class SessionList {
|
|
|
165
229
|
onExit = () => { };
|
|
166
230
|
onToggleScope;
|
|
167
231
|
onToggleSort;
|
|
232
|
+
onToggleNameFilter;
|
|
168
233
|
onTogglePath;
|
|
169
234
|
onDeleteConfirmationChange;
|
|
170
235
|
onDeleteSession;
|
|
171
236
|
onRenameSession;
|
|
172
237
|
onError;
|
|
173
|
-
maxVisible =
|
|
238
|
+
maxVisible = 10; // Max sessions visible (one line each)
|
|
174
239
|
// Focusable implementation - propagate to searchInput for IME cursor positioning
|
|
175
240
|
_focused = false;
|
|
176
241
|
get focused() {
|
|
@@ -180,19 +245,22 @@ class SessionList {
|
|
|
180
245
|
this._focused = value;
|
|
181
246
|
this.searchInput.focused = value;
|
|
182
247
|
}
|
|
183
|
-
constructor(sessions, showCwd, sortMode, currentSessionFilePath) {
|
|
248
|
+
constructor(sessions, showCwd, sortMode, nameFilter, keybindings, currentSessionFilePath) {
|
|
184
249
|
this.allSessions = sessions;
|
|
185
|
-
this.filteredSessions =
|
|
250
|
+
this.filteredSessions = [];
|
|
186
251
|
this.searchInput = new Input();
|
|
187
252
|
this.showCwd = showCwd;
|
|
188
253
|
this.sortMode = sortMode;
|
|
254
|
+
this.nameFilter = nameFilter;
|
|
255
|
+
this.keybindings = keybindings;
|
|
189
256
|
this.currentSessionFilePath = currentSessionFilePath;
|
|
257
|
+
this.filterSessions("");
|
|
190
258
|
// Handle Enter in search input - select current item
|
|
191
259
|
this.searchInput.onSubmit = () => {
|
|
192
260
|
if (this.filteredSessions[this.selectedIndex]) {
|
|
193
261
|
const selected = this.filteredSessions[this.selectedIndex];
|
|
194
262
|
if (this.onSelect) {
|
|
195
|
-
this.onSelect(selected.path);
|
|
263
|
+
this.onSelect(selected.session.path);
|
|
196
264
|
}
|
|
197
265
|
}
|
|
198
266
|
};
|
|
@@ -201,13 +269,33 @@ class SessionList {
|
|
|
201
269
|
this.sortMode = sortMode;
|
|
202
270
|
this.filterSessions(this.searchInput.getValue());
|
|
203
271
|
}
|
|
272
|
+
setNameFilter(nameFilter) {
|
|
273
|
+
this.nameFilter = nameFilter;
|
|
274
|
+
this.filterSessions(this.searchInput.getValue());
|
|
275
|
+
}
|
|
204
276
|
setSessions(sessions, showCwd) {
|
|
205
277
|
this.allSessions = sessions;
|
|
206
278
|
this.showCwd = showCwd;
|
|
207
279
|
this.filterSessions(this.searchInput.getValue());
|
|
208
280
|
}
|
|
209
281
|
filterSessions(query) {
|
|
210
|
-
|
|
282
|
+
const trimmed = query.trim();
|
|
283
|
+
const nameFiltered = this.nameFilter === "all" ? this.allSessions : this.allSessions.filter((session) => hasSessionName(session));
|
|
284
|
+
if (this.sortMode === "threaded" && !trimmed) {
|
|
285
|
+
// Threaded mode without search: show tree structure
|
|
286
|
+
const roots = buildSessionTree(nameFiltered);
|
|
287
|
+
this.filteredSessions = flattenSessionTree(roots);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// Other modes or with search: flat list
|
|
291
|
+
const filtered = filterAndSortSessions(nameFiltered, query, this.sortMode, "all");
|
|
292
|
+
this.filteredSessions = filtered.map((session) => ({
|
|
293
|
+
session,
|
|
294
|
+
depth: 0,
|
|
295
|
+
isLast: true,
|
|
296
|
+
ancestorContinues: [],
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
211
299
|
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));
|
|
212
300
|
}
|
|
213
301
|
setConfirmingDeletePath(path) {
|
|
@@ -219,11 +307,11 @@ class SessionList {
|
|
|
219
307
|
if (!selected)
|
|
220
308
|
return;
|
|
221
309
|
// Prevent deleting current session
|
|
222
|
-
if (this.currentSessionFilePath && selected.path === this.currentSessionFilePath) {
|
|
310
|
+
if (this.currentSessionFilePath && selected.session.path === this.currentSessionFilePath) {
|
|
223
311
|
this.onError?.("Cannot delete the currently active session");
|
|
224
312
|
return;
|
|
225
313
|
}
|
|
226
|
-
this.setConfirmingDeletePath(selected.path);
|
|
314
|
+
this.setConfirmingDeletePath(selected.session.path);
|
|
227
315
|
}
|
|
228
316
|
invalidate() { }
|
|
229
317
|
render(width) {
|
|
@@ -232,37 +320,68 @@ class SessionList {
|
|
|
232
320
|
lines.push(...this.searchInput.render(width));
|
|
233
321
|
lines.push(""); // Blank line after search
|
|
234
322
|
if (this.filteredSessions.length === 0) {
|
|
235
|
-
|
|
323
|
+
let emptyMessage;
|
|
324
|
+
if (this.nameFilter === "named") {
|
|
325
|
+
const toggleKey = appKey(this.keybindings, "toggleSessionNamedFilter");
|
|
326
|
+
if (this.showCwd) {
|
|
327
|
+
emptyMessage = ` No named sessions found. Press ${toggleKey} to show all.`;
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
emptyMessage = ` No named sessions in current folder. Press ${toggleKey} to show all, or Tab to view all.`;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else if (this.showCwd) {
|
|
236
334
|
// "All" scope - no sessions anywhere that match filter
|
|
237
|
-
|
|
335
|
+
emptyMessage = " No sessions found";
|
|
238
336
|
}
|
|
239
337
|
else {
|
|
240
338
|
// "Current folder" scope - hint to try "all"
|
|
241
|
-
|
|
339
|
+
emptyMessage = " No sessions in current folder. Press Tab to view all.";
|
|
242
340
|
}
|
|
341
|
+
lines.push(theme.fg("muted", truncateToWidth(emptyMessage, width, "…")));
|
|
243
342
|
return lines;
|
|
244
343
|
}
|
|
245
344
|
// Calculate visible range with scrolling
|
|
246
345
|
const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredSessions.length - this.maxVisible));
|
|
247
346
|
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);
|
|
248
|
-
// Render visible sessions (
|
|
347
|
+
// Render visible sessions (one line each with tree structure)
|
|
249
348
|
for (let i = startIndex; i < endIndex; i++) {
|
|
250
|
-
const
|
|
349
|
+
const node = this.filteredSessions[i];
|
|
350
|
+
const session = node.session;
|
|
251
351
|
const isSelected = i === this.selectedIndex;
|
|
252
352
|
const isConfirmingDelete = session.path === this.confirmingDeletePath;
|
|
253
|
-
|
|
353
|
+
const isCurrent = this.currentSessionFilePath === session.path;
|
|
354
|
+
// Build tree prefix
|
|
355
|
+
const prefix = this.buildTreePrefix(node);
|
|
356
|
+
// Session display text (name or first message)
|
|
254
357
|
const hasName = !!session.name;
|
|
255
358
|
const displayText = session.name ?? session.firstMessage;
|
|
256
359
|
const normalizedMessage = displayText.replace(/\n/g, " ").trim();
|
|
257
|
-
//
|
|
258
|
-
|
|
360
|
+
// Right side: message count and age
|
|
361
|
+
const age = formatSessionDate(session.modified);
|
|
362
|
+
const msgCount = String(session.messageCount);
|
|
363
|
+
let rightPart = `${msgCount} ${age}`;
|
|
364
|
+
if (this.showCwd && session.cwd) {
|
|
365
|
+
rightPart = `${shortenPath(session.cwd)} ${rightPart}`;
|
|
366
|
+
}
|
|
367
|
+
if (this.showPath) {
|
|
368
|
+
rightPart = `${shortenPath(session.path)} ${rightPart}`;
|
|
369
|
+
}
|
|
370
|
+
// Cursor
|
|
259
371
|
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
|
|
260
|
-
|
|
261
|
-
const
|
|
372
|
+
// Calculate available width for message
|
|
373
|
+
const prefixWidth = visibleWidth(prefix);
|
|
374
|
+
const rightWidth = visibleWidth(rightPart) + 2; // +2 for spacing
|
|
375
|
+
const availableForMsg = width - 2 - prefixWidth - rightWidth; // -2 for cursor
|
|
376
|
+
const truncatedMsg = truncateToWidth(normalizedMessage, Math.max(10, availableForMsg), "…");
|
|
377
|
+
// Style message
|
|
262
378
|
let messageColor = null;
|
|
263
379
|
if (isConfirmingDelete) {
|
|
264
380
|
messageColor = "error";
|
|
265
381
|
}
|
|
382
|
+
else if (isCurrent) {
|
|
383
|
+
messageColor = "accent";
|
|
384
|
+
}
|
|
266
385
|
else if (hasName) {
|
|
267
386
|
messageColor = "warning";
|
|
268
387
|
}
|
|
@@ -270,27 +389,16 @@ class SessionList {
|
|
|
270
389
|
if (isSelected) {
|
|
271
390
|
styledMsg = theme.bold(styledMsg);
|
|
272
391
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const metadata = ` ${metadataParts.join(" · ")}`;
|
|
282
|
-
const truncatedMetadata = truncateToWidth(metadata, width, "");
|
|
283
|
-
const metadataLine = theme.fg(isConfirmingDelete ? "error" : "dim", truncatedMetadata);
|
|
284
|
-
lines.push(messageLine);
|
|
285
|
-
lines.push(metadataLine);
|
|
286
|
-
// Optional third line: file path (when showPath is enabled)
|
|
287
|
-
if (this.showPath) {
|
|
288
|
-
const pathText = ` ${shortenPath(session.path)}`;
|
|
289
|
-
const truncatedPath = truncateToWidth(pathText, width, "…");
|
|
290
|
-
const pathLine = theme.fg(isConfirmingDelete ? "error" : "muted", truncatedPath);
|
|
291
|
-
lines.push(pathLine);
|
|
392
|
+
// Build line
|
|
393
|
+
const leftPart = cursor + theme.fg("dim", prefix) + styledMsg;
|
|
394
|
+
const leftWidth = visibleWidth(leftPart);
|
|
395
|
+
const spacing = Math.max(1, width - leftWidth - visibleWidth(rightPart));
|
|
396
|
+
const styledRight = theme.fg(isConfirmingDelete ? "error" : "dim", rightPart);
|
|
397
|
+
let line = leftPart + " ".repeat(spacing) + styledRight;
|
|
398
|
+
if (isSelected) {
|
|
399
|
+
line = theme.bg("selectedBg", line);
|
|
292
400
|
}
|
|
293
|
-
lines.push(
|
|
401
|
+
lines.push(truncateToWidth(line, width));
|
|
294
402
|
}
|
|
295
403
|
// Add scroll indicator if needed
|
|
296
404
|
if (startIndex > 0 || endIndex < this.filteredSessions.length) {
|
|
@@ -300,6 +408,14 @@ class SessionList {
|
|
|
300
408
|
}
|
|
301
409
|
return lines;
|
|
302
410
|
}
|
|
411
|
+
buildTreePrefix(node) {
|
|
412
|
+
if (node.depth === 0) {
|
|
413
|
+
return "";
|
|
414
|
+
}
|
|
415
|
+
const parts = node.ancestorContinues.map((continues) => (continues ? "│ " : " "));
|
|
416
|
+
const branch = node.isLast ? "└─ " : "├─ ";
|
|
417
|
+
return parts.join("") + branch;
|
|
418
|
+
}
|
|
303
419
|
handleInput(keyData) {
|
|
304
420
|
const kb = getEditorKeybindings();
|
|
305
421
|
// Handle delete confirmation state first - intercept all keys
|
|
@@ -328,6 +444,10 @@ class SessionList {
|
|
|
328
444
|
this.onToggleSort?.();
|
|
329
445
|
return;
|
|
330
446
|
}
|
|
447
|
+
if (this.keybindings.matches(keyData, "toggleSessionNamedFilter")) {
|
|
448
|
+
this.onToggleNameFilter?.();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
331
451
|
// Ctrl+P: toggle path display
|
|
332
452
|
if (kb.matches(keyData, "toggleSessionPath")) {
|
|
333
453
|
this.showPath = !this.showPath;
|
|
@@ -343,7 +463,7 @@ class SessionList {
|
|
|
343
463
|
if (matchesKey(keyData, "ctrl+r")) {
|
|
344
464
|
const selected = this.filteredSessions[this.selectedIndex];
|
|
345
465
|
if (selected) {
|
|
346
|
-
this.onRenameSession?.(selected.path);
|
|
466
|
+
this.onRenameSession?.(selected.session.path);
|
|
347
467
|
}
|
|
348
468
|
return;
|
|
349
469
|
}
|
|
@@ -378,7 +498,7 @@ class SessionList {
|
|
|
378
498
|
else if (kb.matches(keyData, "selectConfirm")) {
|
|
379
499
|
const selected = this.filteredSessions[this.selectedIndex];
|
|
380
500
|
if (selected && this.onSelect) {
|
|
381
|
-
this.onSelect(selected.path);
|
|
501
|
+
this.onSelect(selected.session.path);
|
|
382
502
|
}
|
|
383
503
|
}
|
|
384
504
|
// Escape - cancel
|
|
@@ -449,8 +569,10 @@ export class SessionSelectorComponent extends Container {
|
|
|
449
569
|
canRename = true;
|
|
450
570
|
sessionList;
|
|
451
571
|
header;
|
|
572
|
+
keybindings;
|
|
452
573
|
scope = "current";
|
|
453
|
-
sortMode = "
|
|
574
|
+
sortMode = "threaded";
|
|
575
|
+
nameFilter = "all";
|
|
454
576
|
currentSessions = null;
|
|
455
577
|
allSessions = null;
|
|
456
578
|
currentSessionsLoader;
|
|
@@ -492,17 +614,18 @@ export class SessionSelectorComponent extends Container {
|
|
|
492
614
|
}
|
|
493
615
|
constructor(currentSessionsLoader, allSessionsLoader, onSelect, onCancel, onExit, requestRender, options, currentSessionFilePath) {
|
|
494
616
|
super();
|
|
617
|
+
this.keybindings = options?.keybindings ?? KeybindingsManager.create();
|
|
495
618
|
this.currentSessionsLoader = currentSessionsLoader;
|
|
496
619
|
this.allSessionsLoader = allSessionsLoader;
|
|
497
620
|
this.onCancel = onCancel;
|
|
498
621
|
this.requestRender = requestRender;
|
|
499
|
-
this.header = new SessionSelectorHeader(this.scope, this.sortMode, this.requestRender);
|
|
622
|
+
this.header = new SessionSelectorHeader(this.scope, this.sortMode, this.nameFilter, this.keybindings, this.requestRender);
|
|
500
623
|
const renameSession = options?.renameSession;
|
|
501
624
|
this.renameSession = renameSession;
|
|
502
625
|
this.canRename = !!renameSession;
|
|
503
626
|
this.header.setShowRenameHint(options?.showRenameHint ?? this.canRename);
|
|
504
627
|
// Create session list (starts empty, will be populated after load)
|
|
505
|
-
this.sessionList = new SessionList([], false, this.sortMode, currentSessionFilePath);
|
|
628
|
+
this.sessionList = new SessionList([], false, this.sortMode, this.nameFilter, this.keybindings, currentSessionFilePath);
|
|
506
629
|
this.buildBaseLayout(this.sessionList);
|
|
507
630
|
this.renameInput.onSubmit = (value) => {
|
|
508
631
|
void this.confirmRename(value);
|
|
@@ -523,6 +646,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
523
646
|
};
|
|
524
647
|
this.sessionList.onToggleScope = () => this.toggleScope();
|
|
525
648
|
this.sessionList.onToggleSort = () => this.toggleSortMode();
|
|
649
|
+
this.sessionList.onToggleNameFilter = () => this.toggleNameFilter();
|
|
526
650
|
this.sessionList.onRenameSession = (sessionPath) => {
|
|
527
651
|
if (!renameSession)
|
|
528
652
|
return;
|
|
@@ -684,11 +808,18 @@ export class SessionSelectorComponent extends Container {
|
|
|
684
808
|
}
|
|
685
809
|
}
|
|
686
810
|
toggleSortMode() {
|
|
687
|
-
|
|
811
|
+
// Cycle: threaded -> recent -> relevance -> threaded
|
|
812
|
+
this.sortMode = this.sortMode === "threaded" ? "recent" : this.sortMode === "recent" ? "relevance" : "threaded";
|
|
688
813
|
this.header.setSortMode(this.sortMode);
|
|
689
814
|
this.sessionList.setSortMode(this.sortMode);
|
|
690
815
|
this.requestRender();
|
|
691
816
|
}
|
|
817
|
+
toggleNameFilter() {
|
|
818
|
+
this.nameFilter = this.nameFilter === "all" ? "named" : "all";
|
|
819
|
+
this.header.setNameFilter(this.nameFilter);
|
|
820
|
+
this.sessionList.setNameFilter(this.nameFilter);
|
|
821
|
+
this.requestRender();
|
|
822
|
+
}
|
|
692
823
|
async refreshSessionsAfterMutation() {
|
|
693
824
|
await this.loadScope(this.scope, "refresh");
|
|
694
825
|
}
|