@timurproko/a1 0.2.5-dev.683 → 0.2.5-dev.704
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/dist/app/session-shell/prompt-suggestion-controller.d.ts +2 -0
- package/dist/app/session-shell/prompt-suggestion-controller.js +10 -0
- package/dist/app/session-shell/session-shell-root.js +15 -2
- package/dist/app/session-shell/session-shell.js +80 -23
- package/dist/app/session-shell/share-operation.d.ts +3 -0
- package/dist/app/session-shell/share-operation.js +11 -0
- package/dist/composition/owned-ui.js +2 -0
- package/dist/composition/settings-route-host.js +1 -1
- package/dist/contracts/presentation/index.d.ts +11 -1
- package/dist/contracts/presentation/index.js +6 -2
- package/dist/features/owned-ui/project-trust-dialog.js +7 -5
- package/dist/features/owned-ui/reference-screen-app.d.ts +2 -2
- package/dist/features/owned-ui/reference-screen-app.js +10 -6
- package/dist/features/owned-ui/settings-app.d.ts +1 -1
- package/dist/features/owned-ui/settings-app.js +342 -66
- package/dist/integrations/pi/components/index.d.ts +3 -2
- package/dist/integrations/pi/components/index.js +2 -1
- package/dist/integrations/pi/components/modal-frame.d.ts +11 -1
- package/dist/integrations/pi/components/modal-frame.js +29 -1
- package/dist/integrations/pi/components/models-dialog.js +19 -19
- package/dist/integrations/pi/components/share-operation-dialog.d.ts +6 -0
- package/dist/integrations/pi/components/share-operation-dialog.js +40 -0
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +55 -8
- package/dist/integrations/pi/components/shell-extension-ui.d.ts +3 -0
- package/dist/integrations/pi/components/shell-extension-ui.js +8 -4
- package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +0 -4
- package/dist/integrations/pi/components/shell-selectors-dialogs.js +56 -34
- package/dist/integrations/pi/components/shell-session-info-reference.js +35 -2
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +3 -1
- package/dist/integrations/pi/components/shell-shared-facade.js +13 -1
- package/dist/integrations/pi/components/skills-dialog.js +21 -8
- package/dist/integrations/pi/components/theme.d.ts +8 -1
- package/dist/integrations/pi/components/theme.js +28 -1
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +10 -2
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +20 -6
- package/dist/integrations/pi/components/upstream/components/extension-editor.js +2 -2
- package/dist/integrations/pi/components/upstream/components/extension-input.js +4 -5
- package/dist/integrations/pi/components/upstream/components/extension-selector.js +2 -3
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +4 -2
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +48 -12
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +3 -2
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +4 -15
- package/dist/integrations/pi/components/upstream/components/session-selector.d.ts +2 -0
- package/dist/integrations/pi/components/upstream/components/session-selector.js +137 -110
- package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +9 -5
- package/dist/integrations/pi/components/upstream/components/thinking-selector.js +65 -26
- package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +31 -21
- package/dist/integrations/pi/components/upstream/components/tree-selector.js +255 -214
- package/dist/integrations/pi/components/upstream/components/trust-selector.js +13 -15
- package/dist/integrations/pi/engine/adapter.js +21 -2
- package/dist/integrations/pi/engine/workflow-runner.js +2 -1
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/apps/host.d.ts +4 -3
- package/dist/ui/apps/host.js +9 -4
- package/dist/ui/components/dialog-panel.d.ts +15 -0
- package/dist/ui/components/dialog-panel.js +55 -9
- package/dist/ui/components/index.d.ts +3 -3
- package/dist/ui/components/index.js +2 -2
- package/dist/ui/components/line-input.d.ts +2 -0
- package/dist/ui/components/line-input.js +2 -2
- package/dist/ui/components/list-view.d.ts +3 -3
- package/dist/ui/components/list-view.js +8 -9
- package/dist/ui/components/prompt-input.d.ts +2 -2
- package/dist/ui/components/prompt-input.js +5 -5
- package/dist/ui/components/shortcut-hints.d.ts +10 -3
- package/dist/ui/components/shortcut-hints.js +42 -6
- package/dist/ui/components/shortcuts.d.ts +1 -1
- package/dist/ui/components/shortcuts.js +4 -0
- package/dist/ui/components/theme.d.ts +1 -1
- package/dist/ui/components/value-menu.js +3 -1
- package/dist/ui/settings/manager.d.ts +1 -0
- package/dist/ui/settings/manager.js +9 -3
- package/package.json +1 -1
|
@@ -2,20 +2,26 @@
|
|
|
2
2
|
* Provenance: @earendil-works/pi-coding-agent 1.0.4 (MIT), commit 7c10bd4337495ee613f2224843ecdf349b80d1df,
|
|
3
3
|
* packages/coding-agent/src/modes/interactive/components/session-selector.ts.
|
|
4
4
|
* Modifications: Source-synchronized session selector port: preserve threaded/current/all scope,
|
|
5
|
-
* search, sort, named/path filters, rename, delete confirmation, active-session protection,
|
|
6
|
-
*
|
|
7
|
-
* canonical path handling, and the shared bare-A1 modal
|
|
8
|
-
*
|
|
5
|
+
* search, sort, named/path filters, rename, delete confirmation, active-session protection,
|
|
6
|
+
* progressive partial results, cancellation, focus, and disposal while remapping public helpers, owned
|
|
7
|
+
* keybindings/theme, canonical path handling, reverse-Tab scope cycling, and the shared bare-A1 modal
|
|
8
|
+
* frame with standalone title, standard outer rules, stable filter/status row, progressive result
|
|
9
|
+
* paging, aligned result columns, Session Tree selection roles, quiet in-field query syntax guidance,
|
|
10
|
+
* and one-line bottom dynamic feedback and searchable-dialog shortcut footer. Selected session rows
|
|
11
|
+
* use the blue selection surface, success-green primary title, muted metadata, and ordinary text
|
|
12
|
+
* weight.
|
|
13
|
+
* Deviations: owned-modal-shortcut-hints, owned-resume-session-dialog,
|
|
14
|
+
* owned-standard-dialog-selection.
|
|
9
15
|
*/
|
|
10
16
|
import { spawnSync } from "node:child_process";
|
|
11
17
|
import { existsSync, realpathSync } from "node:fs";
|
|
12
18
|
import { unlink } from "node:fs/promises";
|
|
13
19
|
import * as os from "node:os";
|
|
14
|
-
import { Container, getKeybindings, Input, Spacer, Text, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
|
|
20
|
+
import { Container, getKeybindings, Input, matchesKey, sliceByColumn, Spacer, Text, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
|
|
15
21
|
import { KeybindingsManager } from "../adjacent/core/keybindings.js";
|
|
16
22
|
import { DynamicBorder } from "../../../startup-public.js";
|
|
17
23
|
import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
|
|
18
|
-
import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
|
|
24
|
+
import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalShortcutHints } from "../../theme.js";
|
|
19
25
|
const theme = new Proxy({}, {
|
|
20
26
|
get(_target, property) {
|
|
21
27
|
const active = piTheme();
|
|
@@ -29,6 +35,16 @@ function keyText(keybinding, keybindings) {
|
|
|
29
35
|
function shortcutHint(keybinding, action, keybindings) {
|
|
30
36
|
return { key: keyText(keybinding, keybindings), action };
|
|
31
37
|
}
|
|
38
|
+
/** Keep the canonical close control complete while clipping preceding state-specific guidance. */
|
|
39
|
+
function withCloseHint(content, width) {
|
|
40
|
+
const close = renderPiModalShortcutHints([DIALOG_CLOSE_SHORTCUT_HINT]);
|
|
41
|
+
const closeWidth = visibleWidth(close);
|
|
42
|
+
if (width <= closeWidth)
|
|
43
|
+
return truncateToWidth(close, width, "");
|
|
44
|
+
const bodyWidth = Math.max(0, width - closeWidth - 2);
|
|
45
|
+
const body = truncateToWidth(content, bodyWidth, "…");
|
|
46
|
+
return body.length === 0 ? close : `${body} ${close}`;
|
|
47
|
+
}
|
|
32
48
|
import { filterAndSortSessions, hasSessionName } from "./session-selector-search.js";
|
|
33
49
|
function shortenPath(path) {
|
|
34
50
|
const home = os.homedir();
|
|
@@ -59,6 +75,27 @@ function formatSessionDate(date) {
|
|
|
59
75
|
return `${Math.floor(diffDays / 30)}mo`;
|
|
60
76
|
return `${Math.floor(diffDays / 365)}y`;
|
|
61
77
|
}
|
|
78
|
+
function fitToWidth(value, width, ellipsis = "") {
|
|
79
|
+
const available = Math.max(0, width);
|
|
80
|
+
const clipped = truncateToWidth(value, available, ellipsis);
|
|
81
|
+
return `${clipped}${" ".repeat(Math.max(0, available - visibleWidth(clipped)))}`;
|
|
82
|
+
}
|
|
83
|
+
function fitPathToWidth(value, width, preserveTail) {
|
|
84
|
+
if (!preserveTail || visibleWidth(value) <= width)
|
|
85
|
+
return fitToWidth(value, width, "…");
|
|
86
|
+
const tailWidth = Math.max(0, width - 1);
|
|
87
|
+
const tail = sliceByColumn(value, Math.max(0, visibleWidth(value) - tailWidth), tailWidth, true);
|
|
88
|
+
return fitToWidth(`…${tail}`, width);
|
|
89
|
+
}
|
|
90
|
+
function renderSelectedRow(value) {
|
|
91
|
+
const marker = "\u0000";
|
|
92
|
+
const wrapper = theme.bg("selectedBg", marker);
|
|
93
|
+
const markerIndex = wrapper.indexOf(marker);
|
|
94
|
+
const on = wrapper.slice(0, markerIndex);
|
|
95
|
+
const off = wrapper.slice(markerIndex + marker.length);
|
|
96
|
+
const reasserted = value.replace(/\u001b\[[0-?]*[ -/]*m/gu, sequence => `${sequence}${on}`);
|
|
97
|
+
return `${on}${reasserted}${off}`;
|
|
98
|
+
}
|
|
62
99
|
function canonicalizePath(path) {
|
|
63
100
|
if (!path)
|
|
64
101
|
return path;
|
|
@@ -75,8 +112,6 @@ class SessionSelectorHeader {
|
|
|
75
112
|
nameFilter;
|
|
76
113
|
requestRender;
|
|
77
114
|
keybindings;
|
|
78
|
-
loading = false;
|
|
79
|
-
loadProgress = null;
|
|
80
115
|
showPath = false;
|
|
81
116
|
confirmingDeletePath = null;
|
|
82
117
|
statusMessage = null;
|
|
@@ -98,14 +133,6 @@ class SessionSelectorHeader {
|
|
|
98
133
|
setNameFilter(nameFilter) {
|
|
99
134
|
this.nameFilter = nameFilter;
|
|
100
135
|
}
|
|
101
|
-
setLoading(loading) {
|
|
102
|
-
this.loading = loading;
|
|
103
|
-
// Progress is scoped to the current load; clear whenever the loading state is set
|
|
104
|
-
this.loadProgress = null;
|
|
105
|
-
}
|
|
106
|
-
setProgress(loaded, total) {
|
|
107
|
-
this.loadProgress = { loaded, total };
|
|
108
|
-
}
|
|
109
136
|
setShowPath(showPath) {
|
|
110
137
|
this.showPath = showPath;
|
|
111
138
|
}
|
|
@@ -134,64 +161,54 @@ class SessionSelectorHeader {
|
|
|
134
161
|
}
|
|
135
162
|
invalidate() { }
|
|
136
163
|
render(width) {
|
|
137
|
-
const title =
|
|
138
|
-
const
|
|
139
|
-
|
|
164
|
+
const title = theme.fg("accent", theme.bold("Resume Session"));
|
|
165
|
+
const scopeText = theme.fg("muted", "Filter: ")
|
|
166
|
+
+ theme.fg(this.scope === "current" ? "accent" : "dim", "current")
|
|
167
|
+
+ theme.fg("muted", " | ")
|
|
168
|
+
+ theme.fg(this.scope === "all" ? "accent" : "dim", "all");
|
|
169
|
+
const nameText = theme.fg("muted", "Name: ") + theme.fg("accent", this.nameFilter);
|
|
170
|
+
const sortLabel = this.sortMode === "relevance" ? "fuzzy" : this.sortMode;
|
|
140
171
|
const sortText = theme.fg("muted", "Sort: ") + theme.fg("accent", sortLabel);
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const progressText = this.loadProgress ? `${this.loadProgress.loaded}/${this.loadProgress.total}` : "...";
|
|
146
|
-
scopeText = `${theme.fg("muted", "○ Current Folder | ")}${theme.fg("accent", `Loading ${progressText}`)}`;
|
|
147
|
-
}
|
|
148
|
-
else if (this.scope === "current") {
|
|
149
|
-
scopeText = `${theme.fg("accent", "◉ Current Folder")}${theme.fg("muted", " | ○ All")}`;
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
scopeText = `${theme.fg("muted", "○ Current Folder | ")}${theme.fg("accent", "◉ All")}`;
|
|
153
|
-
}
|
|
154
|
-
const rightText = truncateToWidth(`${scopeText} ${nameText} ${sortText}`, width, "");
|
|
155
|
-
const availableLeft = Math.max(0, width - visibleWidth(rightText) - 1);
|
|
156
|
-
const left = truncateToWidth(leftText, availableLeft, "");
|
|
157
|
-
const spacing = Math.max(0, width - visibleWidth(left) - visibleWidth(rightText));
|
|
158
|
-
// Build hint lines - changes based on state (all branches truncate to width)
|
|
159
|
-
let hintLine1;
|
|
160
|
-
let hintLine2;
|
|
172
|
+
const status = `${scopeText} ${nameText} ${sortText}`;
|
|
173
|
+
return [truncateToWidth(title, width, ""), truncateToWidth(status, width, "")];
|
|
174
|
+
}
|
|
175
|
+
renderFooter(width) {
|
|
161
176
|
if (this.confirmingDeletePath !== null) {
|
|
162
177
|
const confirmHint = theme.fg("error", "Delete session? ") + renderPiModalShortcutHints([
|
|
163
178
|
shortcutHint("tui.select.confirm", "confirm", this.keybindings),
|
|
164
|
-
shortcutHint("tui.select.cancel", "cancel", this.keybindings),
|
|
165
179
|
]);
|
|
166
|
-
|
|
167
|
-
hintLine2 = "";
|
|
180
|
+
return [withCloseHint(confirmHint, width)];
|
|
168
181
|
}
|
|
169
|
-
|
|
182
|
+
if (this.statusMessage) {
|
|
170
183
|
const color = this.statusMessage.type === "error" ? "error" : "accent";
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
184
|
+
return [withCloseHint(theme.fg(color, this.statusMessage.message), width)];
|
|
185
|
+
}
|
|
186
|
+
const pathState = this.showPath ? "(on)" : "(off)";
|
|
187
|
+
const hints = [
|
|
188
|
+
{ key: "Type", action: "search" },
|
|
189
|
+
{ key: "↑↓", action: "navigate" },
|
|
190
|
+
shortcutHint("tui.select.confirm", "select", this.keybindings),
|
|
191
|
+
shortcutHint("tui.input.tab", "scope", this.keybindings),
|
|
192
|
+
shortcutHint("app.session.toggleSort", "sort", this.keybindings),
|
|
193
|
+
shortcutHint("app.session.toggleNamedFilter", "named", this.keybindings),
|
|
194
|
+
shortcutHint("app.session.delete", "delete", this.keybindings),
|
|
195
|
+
shortcutHint("app.session.togglePath", `path ${pathState}`, this.keybindings),
|
|
196
|
+
];
|
|
197
|
+
if (this.showRenameHint)
|
|
198
|
+
hints.push(shortcutHint("app.session.rename", "rename", this.keybindings));
|
|
199
|
+
return [withCloseHint(renderPiModalShortcutHints(hints), width)];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
class SessionSelectorFooter {
|
|
203
|
+
presentation;
|
|
204
|
+
constructor(presentation) {
|
|
205
|
+
this.presentation = presentation;
|
|
206
|
+
}
|
|
207
|
+
invalidate() {
|
|
208
|
+
this.presentation.invalidate();
|
|
209
|
+
}
|
|
210
|
+
render(width) {
|
|
211
|
+
return this.presentation.renderFooter(width);
|
|
195
212
|
}
|
|
196
213
|
}
|
|
197
214
|
/**
|
|
@@ -300,7 +317,10 @@ class SessionList {
|
|
|
300
317
|
constructor(sessions, showCwd, sortMode, nameFilter, keybindings, currentSessionFilePath) {
|
|
301
318
|
this.allSessions = sessions;
|
|
302
319
|
this.filteredSessions = [];
|
|
303
|
-
this.searchInput = new Input(
|
|
320
|
+
this.searchInput = new Input({
|
|
321
|
+
placeholder: 're:<pattern> regex, "phrase" exact',
|
|
322
|
+
placeholderStyle: text => text === "r" ? text : `\u001b[2m${text}\u001b[22m`,
|
|
323
|
+
});
|
|
304
324
|
this.showCwd = showCwd;
|
|
305
325
|
this.sortMode = sortMode;
|
|
306
326
|
this.nameFilter = nameFilter;
|
|
@@ -379,6 +399,11 @@ class SessionList {
|
|
|
379
399
|
return false;
|
|
380
400
|
return (canonicalizePath(path) ?? path) === this.currentSessionCanonicalPath;
|
|
381
401
|
}
|
|
402
|
+
sessionPathText(session) {
|
|
403
|
+
if (this.showPath)
|
|
404
|
+
return shortenPath(session.path);
|
|
405
|
+
return this.showCwd && session.cwd ? shortenPath(session.cwd) : "";
|
|
406
|
+
}
|
|
382
407
|
invalidate() { }
|
|
383
408
|
render(width) {
|
|
384
409
|
const lines = [];
|
|
@@ -410,7 +435,18 @@ class SessionList {
|
|
|
410
435
|
// Calculate visible range with scrolling
|
|
411
436
|
const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredSessions.length - this.maxVisible));
|
|
412
437
|
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);
|
|
413
|
-
|
|
438
|
+
const pathTexts = this.filteredSessions.map((node) => this.sessionPathText(node.session));
|
|
439
|
+
const longestPath = Math.max(0, ...pathTexts.map((path) => visibleWidth(path)));
|
|
440
|
+
const countColumnWidth = Math.max(1, ...this.filteredSessions.map((node) => String(node.session.messageCount).length));
|
|
441
|
+
const ageColumnWidth = Math.max(1, ...this.filteredSessions.map((node) => formatSessionDate(node.session.modified).length));
|
|
442
|
+
const trailingColumnsWidth = countColumnWidth + 1 + ageColumnWidth;
|
|
443
|
+
const columnGapWidth = 2;
|
|
444
|
+
const maxPathColumnWidth = Math.max(0, width - trailingColumnsWidth - columnGapWidth * 2 - 12);
|
|
445
|
+
const pathColumnWidth = Math.min(longestPath, Math.floor(width * 0.45), maxPathColumnWidth);
|
|
446
|
+
const metadataWidth = columnGapWidth + trailingColumnsWidth
|
|
447
|
+
+ (pathColumnWidth > 0 ? pathColumnWidth + columnGapWidth : 0);
|
|
448
|
+
const titleColumnWidth = Math.max(0, width - metadataWidth);
|
|
449
|
+
// Render visible sessions (one line each with stable title, path, count, and age columns)
|
|
414
450
|
for (let i = startIndex; i < endIndex; i++) {
|
|
415
451
|
const node = this.filteredSessions[i];
|
|
416
452
|
const session = node.session;
|
|
@@ -423,23 +459,14 @@ class SessionList {
|
|
|
423
459
|
const hasName = !!session.name;
|
|
424
460
|
const displayText = session.name ?? session.firstMessage;
|
|
425
461
|
const normalizedMessage = displayText.replace(/[\x00-\x1f\x7f]/g, " ").trim();
|
|
426
|
-
// Right side: message count and age
|
|
427
462
|
const age = formatSessionDate(session.modified);
|
|
428
463
|
const msgCount = String(session.messageCount);
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
433
|
-
if (this.showPath) {
|
|
434
|
-
rightPart = `${shortenPath(session.path)} ${rightPart}`;
|
|
435
|
-
}
|
|
436
|
-
// Cursor
|
|
437
|
-
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
|
|
438
|
-
// Calculate available width for message
|
|
464
|
+
const pathText = this.sessionPathText(session);
|
|
465
|
+
// Cursor and title stay inside their column so metadata always begins at one boundary.
|
|
466
|
+
const cursor = isSelected ? theme.fg("accent", "→ ") : " ";
|
|
439
467
|
const prefixWidth = visibleWidth(prefix);
|
|
440
|
-
const
|
|
441
|
-
const
|
|
442
|
-
const truncatedMsg = truncateToWidth(normalizedMessage, Math.max(10, availableForMsg), "…");
|
|
468
|
+
const availableForMsg = Math.max(0, titleColumnWidth - 2 - prefixWidth);
|
|
469
|
+
const truncatedMsg = truncateToWidth(normalizedMessage, availableForMsg, "…");
|
|
443
470
|
// Style message
|
|
444
471
|
let messageColor = null;
|
|
445
472
|
if (isConfirmingDelete) {
|
|
@@ -451,20 +478,22 @@ class SessionList {
|
|
|
451
478
|
else if (hasName) {
|
|
452
479
|
messageColor = "warning";
|
|
453
480
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
481
|
+
const styledMsg = isConfirmingDelete
|
|
482
|
+
? theme.fg("error", truncatedMsg)
|
|
483
|
+
: isSelected
|
|
484
|
+
? theme.fg("success", truncatedMsg)
|
|
485
|
+
: messageColor
|
|
486
|
+
? theme.fg(messageColor, truncatedMsg)
|
|
487
|
+
: truncatedMsg;
|
|
488
|
+
const metadataColor = isConfirmingDelete ? "error" : isSelected ? "muted" : "dim";
|
|
489
|
+
const titleColumn = fitToWidth(cursor + theme.fg("dim", prefix) + styledMsg, titleColumnWidth, "…");
|
|
490
|
+
const pathColumn = pathColumnWidth > 0
|
|
491
|
+
? `${theme.fg(metadataColor, fitPathToWidth(pathText, pathColumnWidth, this.showPath))}${" ".repeat(columnGapWidth)}`
|
|
492
|
+
: "";
|
|
493
|
+
const countColumn = theme.fg(metadataColor, msgCount.padStart(countColumnWidth));
|
|
494
|
+
const ageColumn = theme.fg(metadataColor, age.padStart(ageColumnWidth));
|
|
495
|
+
const fittedLine = fitToWidth(`${titleColumn}${" ".repeat(columnGapWidth)}${pathColumn}${countColumn} ${ageColumn}`, width);
|
|
496
|
+
lines.push(isSelected ? renderSelectedRow(fittedLine) : fittedLine);
|
|
468
497
|
}
|
|
469
498
|
// Add scroll indicator if needed
|
|
470
499
|
if (startIndex > 0 || endIndex < this.filteredSessions.length) {
|
|
@@ -499,7 +528,7 @@ class SessionList {
|
|
|
499
528
|
// Ignore all other keys while confirming
|
|
500
529
|
return;
|
|
501
530
|
}
|
|
502
|
-
if (kb.matches(keyData, "tui.input.tab")) {
|
|
531
|
+
if (matchesKey(keyData, "shift+tab") || kb.matches(keyData, "tui.input.tab")) {
|
|
503
532
|
if (this.onToggleScope) {
|
|
504
533
|
this.onToggleScope();
|
|
505
534
|
}
|
|
@@ -635,6 +664,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
635
664
|
canRename = true;
|
|
636
665
|
sessionList;
|
|
637
666
|
header;
|
|
667
|
+
footer;
|
|
638
668
|
keybindings;
|
|
639
669
|
scope = "current";
|
|
640
670
|
sortMode = "threaded";
|
|
@@ -665,18 +695,19 @@ export class SessionSelectorComponent extends Container {
|
|
|
665
695
|
}
|
|
666
696
|
buildBaseLayout(content, options) {
|
|
667
697
|
this.clear();
|
|
668
|
-
this.addChild(new Spacer(1));
|
|
669
698
|
const showHeader = options?.showHeader ?? true;
|
|
670
699
|
const modalHeader = showHeader
|
|
671
|
-
? addPiModalHeader(this, new DynamicBorder(
|
|
700
|
+
? addPiModalHeader(this, new DynamicBorder(), this.header)
|
|
672
701
|
: undefined;
|
|
673
702
|
if (!showHeader)
|
|
674
|
-
this.addChild(new DynamicBorder(
|
|
703
|
+
this.addChild(new DynamicBorder());
|
|
675
704
|
this.addChild(new Spacer(1));
|
|
676
705
|
this.addChild(content);
|
|
677
706
|
this.addChild(new Spacer(1));
|
|
678
|
-
|
|
679
|
-
|
|
707
|
+
if (showHeader)
|
|
708
|
+
this.addChild(this.footer);
|
|
709
|
+
this.addChild(new DynamicBorder());
|
|
710
|
+
const frame = { topIndex: 0, bottomIndex: this.children.length - 1 };
|
|
680
711
|
if (modalHeader === undefined)
|
|
681
712
|
adoptPiModalFrame(this, frame);
|
|
682
713
|
else
|
|
@@ -689,6 +720,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
689
720
|
this.allSessionsLoader = allSessionsLoader;
|
|
690
721
|
this.requestRender = requestRender;
|
|
691
722
|
this.header = new SessionSelectorHeader(this.scope, this.sortMode, this.nameFilter, this.requestRender, this.keybindings);
|
|
723
|
+
this.footer = new SessionSelectorFooter(this.header);
|
|
692
724
|
const renameSession = options?.renameSession;
|
|
693
725
|
this.renameSession = renameSession;
|
|
694
726
|
this.canRename = !!renameSession;
|
|
@@ -791,7 +823,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
791
823
|
panel.addChild(new Spacer(1));
|
|
792
824
|
panel.addChild(new Text(renderPiModalShortcutHints([
|
|
793
825
|
shortcutHint("tui.select.confirm", "to save", this.keybindings),
|
|
794
|
-
|
|
826
|
+
DIALOG_CLOSE_SHORTCUT_HINT,
|
|
795
827
|
]), 1, 0));
|
|
796
828
|
this.buildBaseLayout(panel, { showHeader: false });
|
|
797
829
|
this.requestRender();
|
|
@@ -837,10 +869,9 @@ export class SessionSelectorComponent extends Container {
|
|
|
837
869
|
this.allLoad = controller;
|
|
838
870
|
}
|
|
839
871
|
this.header.setScope(scope);
|
|
840
|
-
this.header.setLoading(true);
|
|
841
872
|
this.requestRender();
|
|
842
873
|
const isActive = () => (scope === "current" ? this.currentLoad : this.allLoad) === controller;
|
|
843
|
-
const onProgress = (
|
|
874
|
+
const onProgress = (_loaded, _total, partialSessions) => {
|
|
844
875
|
if (!isActive())
|
|
845
876
|
return;
|
|
846
877
|
if (partialSessions) {
|
|
@@ -856,7 +887,6 @@ export class SessionSelectorComponent extends Container {
|
|
|
856
887
|
}
|
|
857
888
|
if (scope !== this.scope)
|
|
858
889
|
return;
|
|
859
|
-
this.header.setProgress(loaded, total);
|
|
860
890
|
this.requestRender();
|
|
861
891
|
};
|
|
862
892
|
try {
|
|
@@ -875,7 +905,6 @@ export class SessionSelectorComponent extends Container {
|
|
|
875
905
|
}
|
|
876
906
|
if (scope !== this.scope)
|
|
877
907
|
return;
|
|
878
|
-
this.header.setLoading(false);
|
|
879
908
|
this.sessionList.setSessions(sessions, showCwd);
|
|
880
909
|
this.requestRender();
|
|
881
910
|
}
|
|
@@ -893,7 +922,6 @@ export class SessionSelectorComponent extends Container {
|
|
|
893
922
|
if (scope !== this.scope)
|
|
894
923
|
return;
|
|
895
924
|
const message = err instanceof Error ? err.message : String(err);
|
|
896
|
-
this.header.setLoading(false);
|
|
897
925
|
this.header.setStatusMessage({ type: "error", message: `Failed to load sessions: ${message}` }, 4000);
|
|
898
926
|
this.sessionList.setSessions([], showCwd);
|
|
899
927
|
this.requestRender();
|
|
@@ -923,7 +951,6 @@ export class SessionSelectorComponent extends Container {
|
|
|
923
951
|
const sessions = this.scope === "current" ? this.currentSessions : this.allSessions;
|
|
924
952
|
const loading = (this.scope === "current" ? this.currentLoad : this.allLoad) !== null;
|
|
925
953
|
this.header.setScope(this.scope);
|
|
926
|
-
this.header.setLoading(loading);
|
|
927
954
|
this.sessionList.setSessions(sessions ?? [], this.scope === "all");
|
|
928
955
|
this.requestRender();
|
|
929
956
|
if (sessions === null && !loading)
|
|
@@ -5,12 +5,16 @@
|
|
|
5
5
|
* selection, and focus while accepting the active bare-A1 cycle-key label from the shell, styling the
|
|
6
6
|
* title with the established bold semantic accent treatment, placing it directly below the top rule
|
|
7
7
|
* through the shared compact padded modal frame and its muted hint directly below it, deduplicating
|
|
8
|
-
* levels, and rendering
|
|
9
|
-
* Bare A1 persists defaults immediately on Space,
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* levels, and rendering exclusive radio-style default markers before level names, an item-adjacent
|
|
9
|
+
* active checkmark, and aligned muted descriptions. Bare A1 persists defaults immediately on Space,
|
|
10
|
+
* closes through the implicit Ctrl+C-capable selection-cancel action, and uses the shared compact
|
|
11
|
+
* semantic shortcut row with Type/search guidance without advertising that alias. All list and border
|
|
12
|
+
* colors use the owned theme and its explicit color mode. Selected rows use the shared bare-A1 blue
|
|
13
|
+
* selection surface only around the rendered item span, with the existing accent arrow, normal-text
|
|
14
|
+
* level, and preserved current/default/description roles. The comparison profile retains the public
|
|
15
|
+
* pinned component.
|
|
12
16
|
* Deviations: owned-modal-shortcut-hints, owned-level-cycle-shortcut, owned-thinking-selector-heading,
|
|
13
|
-
* owned-thinking-selector-controls.
|
|
17
|
+
* owned-thinking-selector-controls, owned-dialog-ctrl-c-cancel, owned-standard-dialog-selection.
|
|
14
18
|
*/
|
|
15
19
|
import { Container, type Focusable, SelectList } from "@earendil-works/pi-tui";
|
|
16
20
|
export type ThinkingSelectorLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
@@ -5,17 +5,21 @@
|
|
|
5
5
|
* selection, and focus while accepting the active bare-A1 cycle-key label from the shell, styling the
|
|
6
6
|
* title with the established bold semantic accent treatment, placing it directly below the top rule
|
|
7
7
|
* through the shared compact padded modal frame and its muted hint directly below it, deduplicating
|
|
8
|
-
* levels, and rendering
|
|
9
|
-
* Bare A1 persists defaults immediately on Space,
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* levels, and rendering exclusive radio-style default markers before level names, an item-adjacent
|
|
9
|
+
* active checkmark, and aligned muted descriptions. Bare A1 persists defaults immediately on Space,
|
|
10
|
+
* closes through the implicit Ctrl+C-capable selection-cancel action, and uses the shared compact
|
|
11
|
+
* semantic shortcut row with Type/search guidance without advertising that alias. All list and border
|
|
12
|
+
* colors use the owned theme and its explicit color mode. Selected rows use the shared bare-A1 blue
|
|
13
|
+
* selection surface only around the rendered item span, with the existing accent arrow, normal-text
|
|
14
|
+
* level, and preserved current/default/description roles. The comparison profile retains the public
|
|
15
|
+
* pinned component.
|
|
12
16
|
* Deviations: owned-modal-shortcut-hints, owned-level-cycle-shortcut, owned-thinking-selector-heading,
|
|
13
|
-
* owned-thinking-selector-controls.
|
|
17
|
+
* owned-thinking-selector-controls, owned-dialog-ctrl-c-cancel, owned-standard-dialog-selection.
|
|
14
18
|
*/
|
|
15
|
-
import { Container, fuzzyFilter, getKeybindings, Input,
|
|
19
|
+
import { Container, fuzzyFilter, getKeybindings, Input, SelectList, Spacer, Text, } from "@earendil-works/pi-tui";
|
|
16
20
|
import { DynamicBorder } from "../../../startup-public.js";
|
|
17
21
|
import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
|
|
18
|
-
import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
|
|
22
|
+
import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalListRow, renderPiModalShortcutHints } from "../../theme.js";
|
|
19
23
|
const THINKING_SELECT_LIST_LAYOUT = {
|
|
20
24
|
minPrimaryColumnWidth: 12,
|
|
21
25
|
maxPrimaryColumnWidth: 32,
|
|
@@ -29,6 +33,25 @@ const LEVEL_DESCRIPTIONS = {
|
|
|
29
33
|
xhigh: "Extra-high reasoning (~32k tokens)",
|
|
30
34
|
max: "Maximum reasoning",
|
|
31
35
|
};
|
|
36
|
+
class ThinkingSelectListPresentation {
|
|
37
|
+
list;
|
|
38
|
+
items;
|
|
39
|
+
constructor(list, items) {
|
|
40
|
+
this.list = list;
|
|
41
|
+
this.items = items;
|
|
42
|
+
}
|
|
43
|
+
invalidate() {
|
|
44
|
+
this.list.invalidate();
|
|
45
|
+
}
|
|
46
|
+
handleMouse(event) {
|
|
47
|
+
return this.list.handleMouse(event);
|
|
48
|
+
}
|
|
49
|
+
render(width) {
|
|
50
|
+
const selected = this.list.getSelectedItem();
|
|
51
|
+
const selectedIndex = selected === null ? -1 : this.items.findIndex((item) => item.value === selected.value);
|
|
52
|
+
return this.list.render(width).map((row, index) => renderPiModalListRow(row, width, index === selectedIndex));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
32
55
|
/** Bare-A1 thinking-level selector with profile-resolved shortcut presentation. */
|
|
33
56
|
export class OwnedThinkingSelectorComponent extends Container {
|
|
34
57
|
searchInput;
|
|
@@ -67,15 +90,17 @@ export class OwnedThinkingSelectorComponent extends Container {
|
|
|
67
90
|
this.searchInput.onSubmit = () => this.selectList.handleInput("\r");
|
|
68
91
|
this.addChild(this.searchInput);
|
|
69
92
|
this.addChild(new Spacer(1));
|
|
70
|
-
|
|
93
|
+
const initialList = this.buildSelectList(this.allItems, currentLevel);
|
|
94
|
+
this.selectList = initialList.list;
|
|
71
95
|
this.selectListContainer = new Container();
|
|
72
|
-
this.selectListContainer.addChild(
|
|
96
|
+
this.selectListContainer.addChild(initialList.presentation);
|
|
73
97
|
this.addChild(this.selectListContainer);
|
|
74
98
|
this.addChild(new Spacer(1));
|
|
75
99
|
this.addChild(new Text(renderPiModalShortcutHints([
|
|
100
|
+
{ key: "Type", action: "search" },
|
|
76
101
|
{ key: this.keyDisplayText("tui.select.confirm"), action: "select" },
|
|
77
102
|
{ key: "Space", action: "default" },
|
|
78
|
-
|
|
103
|
+
DIALOG_CLOSE_SHORTCUT_HINT,
|
|
79
104
|
]), 0, 0));
|
|
80
105
|
this.addChild(new DynamicBorder((text) => piTheme().fg("border", text)));
|
|
81
106
|
adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1, header });
|
|
@@ -89,29 +114,43 @@ export class OwnedThinkingSelectorComponent extends Container {
|
|
|
89
114
|
.join("/");
|
|
90
115
|
}
|
|
91
116
|
buildSelectList(items, preselect) {
|
|
117
|
+
const theme = piTheme();
|
|
92
118
|
const levelWidth = this.allItems.reduce((widest, item) => Math.max(widest, (item.label ?? item.value).length), 0);
|
|
119
|
+
const selectedDefaultMarker = theme.fg("accent", "◉");
|
|
120
|
+
const unselectedDefaultMarker = theme.fg("dim", "○");
|
|
93
121
|
const themedItems = items.map((item) => {
|
|
94
|
-
const level =
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const description = item.description ?
|
|
100
|
-
return { value: item.value, label: `${level}${currentMarker}${
|
|
122
|
+
const level = item.label ?? item.value;
|
|
123
|
+
const isCurrent = item.value === this.currentLevel;
|
|
124
|
+
const currentMarker = isCurrent ? ` ${theme.fg("success", "✓")}` : "";
|
|
125
|
+
const statePadding = " ".repeat(levelWidth - level.length + (isCurrent ? 0 : 2));
|
|
126
|
+
const defaultMarker = item.value === this.defaultThinkingLevel ? selectedDefaultMarker : unselectedDefaultMarker;
|
|
127
|
+
const description = item.description ? theme.fg("muted", item.description) : "";
|
|
128
|
+
return { value: item.value, label: `${defaultMarker} ${level}${currentMarker}${statePadding} ${description}` };
|
|
101
129
|
});
|
|
102
130
|
// Invariant: the bare selector uses the owned theme's explicit color mode, not upstream's host detection.
|
|
103
131
|
const list = new SelectList(themedItems, Math.max(1, themedItems.length), {
|
|
104
|
-
selectedPrefix: text =>
|
|
105
|
-
selectedText: text =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
132
|
+
selectedPrefix: text => theme.fg("accent", text),
|
|
133
|
+
selectedText: text => {
|
|
134
|
+
const arrowEnd = text.startsWith("→ ") ? 2 : 0;
|
|
135
|
+
const arrow = arrowEnd === 0 ? "" : theme.fg("accent", text.slice(0, arrowEnd));
|
|
136
|
+
const marker = text.includes(selectedDefaultMarker) ? selectedDefaultMarker : unselectedDefaultMarker;
|
|
137
|
+
const markerIndex = text.indexOf(marker, arrowEnd);
|
|
138
|
+
return markerIndex === -1
|
|
139
|
+
? arrow + theme.fg("text", text.slice(arrowEnd))
|
|
140
|
+
: arrow
|
|
141
|
+
+ theme.fg("text", text.slice(arrowEnd, markerIndex))
|
|
142
|
+
+ marker
|
|
143
|
+
+ theme.fg("text", text.slice(markerIndex + marker.length));
|
|
144
|
+
},
|
|
145
|
+
description: text => theme.fg("muted", text),
|
|
146
|
+
scrollInfo: text => theme.fg("muted", text),
|
|
147
|
+
noMatch: text => theme.fg("muted", text),
|
|
109
148
|
}, THINKING_SELECT_LIST_LAYOUT);
|
|
110
149
|
const currentIndex = themedItems.findIndex((item) => item.value === preselect);
|
|
111
150
|
if (currentIndex !== -1)
|
|
112
151
|
list.setSelectedIndex(currentIndex);
|
|
113
152
|
list.onSelect = (item) => this.onSelect(item.value);
|
|
114
|
-
return list;
|
|
153
|
+
return { list, presentation: new ThinkingSelectListPresentation(list, themedItems) };
|
|
115
154
|
}
|
|
116
155
|
applyFilter(query) {
|
|
117
156
|
const filtered = query
|
|
@@ -120,8 +159,8 @@ export class OwnedThinkingSelectorComponent extends Container {
|
|
|
120
159
|
const selectedValue = this.selectList.getSelectedItem()?.value;
|
|
121
160
|
const newList = this.buildSelectList(filtered, selectedValue);
|
|
122
161
|
this.selectListContainer.clear();
|
|
123
|
-
this.selectListContainer.addChild(newList);
|
|
124
|
-
this.selectList = newList;
|
|
162
|
+
this.selectListContainer.addChild(newList.presentation);
|
|
163
|
+
this.selectList = newList.list;
|
|
125
164
|
}
|
|
126
165
|
handleInput(keyData) {
|
|
127
166
|
const kb = getKeybindings();
|
|
@@ -134,7 +173,7 @@ export class OwnedThinkingSelectorComponent extends Container {
|
|
|
134
173
|
}
|
|
135
174
|
return;
|
|
136
175
|
}
|
|
137
|
-
if (
|
|
176
|
+
if (kb.matches(keyData, "tui.select.cancel")) {
|
|
138
177
|
this.onCancel();
|
|
139
178
|
return;
|
|
140
179
|
}
|