@timurproko/a1 0.2.5-dev.683 → 0.2.5-dev.698
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 +71 -15
- package/dist/app/session-shell/share-operation.d.ts +3 -0
- package/dist/app/session-shell/share-operation.js +11 -0
- package/dist/composition/settings-route-host.js +1 -1
- package/dist/contracts/presentation/index.d.ts +5 -0
- package/dist/contracts/presentation/index.js +2 -0
- 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.js +10 -5
- 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 +17 -17
- 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 +15 -6
- 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 +19 -6
- 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/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 +136 -109
- package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +8 -5
- package/dist/integrations/pi/components/upstream/components/thinking-selector.js +63 -26
- package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +29 -20
- package/dist/integrations/pi/components/upstream/components/tree-selector.js +245 -210
- 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.js +6 -7
- package/dist/ui/components/index.d.ts +1 -1
- package/dist/ui/components/index.js +1 -1
- package/dist/ui/components/list-view.d.ts +3 -3
- package/dist/ui/components/list-view.js +8 -9
- package/dist/ui/components/shortcut-hints.d.ts +2 -0
- package/dist/ui/components/shortcut-hints.js +17 -1
- package/dist/ui/components/theme.d.ts +1 -1
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { DynamicBorder } from "../../../startup-public.js";
|
|
11
11
|
import { Container, getKeybindings, Input, Spacer, Text } from "@earendil-works/pi-tui";
|
|
12
12
|
import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
|
|
13
|
-
import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
|
|
13
|
+
import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalShortcutHints } from "../../theme.js";
|
|
14
14
|
import { CountdownTimer } from "./countdown-timer.js";
|
|
15
15
|
/** Bare-A1 extension text input using the common modal shortcut row. */
|
|
16
16
|
export class ExtensionInputComponent extends Container {
|
|
@@ -32,7 +32,7 @@ export class ExtensionInputComponent extends Container {
|
|
|
32
32
|
this.#onCancel = onCancel;
|
|
33
33
|
this.#baseTitle = title;
|
|
34
34
|
const theme = piTheme();
|
|
35
|
-
this.#titleText = new Text(theme.fg("accent", title), 0, 0);
|
|
35
|
+
this.#titleText = new Text(theme.fg("accent", theme.bold(title)), 0, 0);
|
|
36
36
|
const header = addPiModalHeader(this, new DynamicBorder(), this.#titleText);
|
|
37
37
|
if (opts?.description) {
|
|
38
38
|
this.addChild(new Spacer(1));
|
|
@@ -42,7 +42,7 @@ export class ExtensionInputComponent extends Container {
|
|
|
42
42
|
this.#countdown = opts?.timeout !== undefined && opts.timeout > 0 && opts.tui !== undefined
|
|
43
43
|
? new CountdownTimer(opts.timeout, opts.tui, seconds => {
|
|
44
44
|
const current = piTheme();
|
|
45
|
-
this.#titleText.setText(current.fg("accent", `${this.#baseTitle} (${seconds}s)`));
|
|
45
|
+
this.#titleText.setText(current.fg("accent", current.bold(`${this.#baseTitle} (${seconds}s)`)));
|
|
46
46
|
}, () => this.#onCancel())
|
|
47
47
|
: undefined;
|
|
48
48
|
if (opts?.initialValue !== undefined)
|
|
@@ -52,9 +52,8 @@ export class ExtensionInputComponent extends Container {
|
|
|
52
52
|
const keys = getKeybindings();
|
|
53
53
|
this.addChild(new Text(renderPiModalShortcutHints([
|
|
54
54
|
{ key: keys.getKeys("tui.select.confirm").join("/"), action: "submit" },
|
|
55
|
-
|
|
55
|
+
DIALOG_CLOSE_SHORTCUT_HINT,
|
|
56
56
|
]), 0, 0));
|
|
57
|
-
this.addChild(new Spacer(1));
|
|
58
57
|
this.addChild(new DynamicBorder());
|
|
59
58
|
adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1, header });
|
|
60
59
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { DynamicBorder } from "../../../startup-public.js";
|
|
11
11
|
import { Container, getKeybindings, Spacer, Text } from "@earendil-works/pi-tui";
|
|
12
12
|
import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
|
|
13
|
-
import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
|
|
13
|
+
import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalShortcutHints } from "../../theme.js";
|
|
14
14
|
import { CountdownTimer } from "./countdown-timer.js";
|
|
15
15
|
/** Bare-A1 extension selector using the common modal shortcut row. */
|
|
16
16
|
export class ExtensionSelectorComponent extends Container {
|
|
@@ -50,9 +50,8 @@ export class ExtensionSelectorComponent extends Container {
|
|
|
50
50
|
this.addChild(new Text(renderPiModalShortcutHints([
|
|
51
51
|
{ key: "↑↓", action: "navigate" },
|
|
52
52
|
{ key: keys.getKeys("tui.select.confirm").join("/"), action: "select" },
|
|
53
|
-
|
|
53
|
+
DIALOG_CLOSE_SHORTCUT_HINT,
|
|
54
54
|
]), 0, 0));
|
|
55
|
-
this.addChild(new Spacer(1));
|
|
56
55
|
this.addChild(new DynamicBorder());
|
|
57
56
|
adoptPiModalFrame(this, { topIndex: 0, bottomIndex: this.children.length - 1, header });
|
|
58
57
|
this.#updateList();
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* search, bulk/provider/reorder actions, dirty state, Ctrl+S persistence, refresh status,
|
|
6
6
|
* cancellation, and focus while remapping theme and public helper imports; local key labels preserve
|
|
7
7
|
* pinned platform formatting before layout without changing binding identities, and bare A1 uses the
|
|
8
|
-
* shared semantic modal shortcut row and compact padded modal frame
|
|
9
|
-
*
|
|
8
|
+
* shared semantic modal shortcut row and compact padded modal frame while immediately closing on the
|
|
9
|
+
* implicit Ctrl+C selection-cancel alias without advertising it.
|
|
10
|
+
* Deviations: owned-modal-shortcut-hints, owned-dialog-ctrl-c-cancel.
|
|
10
11
|
*/
|
|
11
12
|
interface ScopedModel {
|
|
12
13
|
readonly provider: string;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Container, fuzzyFilter, getKeybindings, Input,
|
|
1
|
+
import { Container, fuzzyFilter, getKeybindings, Input, Spacer, Text, } from "@earendil-works/pi-tui";
|
|
2
2
|
import { DynamicBorder } from "../../../startup-public.js";
|
|
3
3
|
import { getModelSearchText } from "../model-search.js";
|
|
4
4
|
import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
|
|
5
|
-
import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
|
|
5
|
+
import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalShortcutHints } from "../../theme.js";
|
|
6
6
|
// Rationale: upstream's capitalized hint formatter is private to the package; the same mapping lives here.
|
|
7
7
|
function keyDisplayText(keybinding) {
|
|
8
8
|
return getKeybindings().getKeys(keybinding)
|
|
@@ -179,6 +179,7 @@ export class ScopedModelsSelectorComponent extends Container {
|
|
|
179
179
|
{ key: reorderKeys, action: "reorder" },
|
|
180
180
|
{ key: keyDisplayText("app.models.save"), action: "save" },
|
|
181
181
|
{ action: countText },
|
|
182
|
+
DIALOG_CLOSE_SHORTCUT_HINT,
|
|
182
183
|
]);
|
|
183
184
|
return this.isDirty ? `${hint} ${theme.fg("warning", "(unsaved)")}` : hint;
|
|
184
185
|
}
|
|
@@ -321,19 +322,7 @@ export class ScopedModelsSelectorComponent extends Container {
|
|
|
321
322
|
this.footerText.setText(this.getFooterText());
|
|
322
323
|
return;
|
|
323
324
|
}
|
|
324
|
-
|
|
325
|
-
if (matchesKey(data, Key.ctrl("c"))) {
|
|
326
|
-
if (this.searchInput.getValue()) {
|
|
327
|
-
this.searchInput.setValue("");
|
|
328
|
-
this.refresh();
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
this.callbacks.onCancel();
|
|
332
|
-
}
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
// Escape - cancel
|
|
336
|
-
if (matchesKey(data, Key.escape)) {
|
|
325
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
337
326
|
this.callbacks.onCancel();
|
|
338
327
|
return;
|
|
339
328
|
}
|
|
@@ -45,6 +45,7 @@ declare class SessionList implements Component, Focusable {
|
|
|
45
45
|
private setConfirmingDeletePath;
|
|
46
46
|
private startDeleteConfirmationForSelectedSession;
|
|
47
47
|
private isCurrentSessionPath;
|
|
48
|
+
private sessionPathText;
|
|
48
49
|
invalidate(): void;
|
|
49
50
|
render(width: number): string[];
|
|
50
51
|
private buildTreePrefix;
|
|
@@ -59,6 +60,7 @@ export declare class SessionSelectorComponent extends Container implements Focus
|
|
|
59
60
|
private canRename;
|
|
60
61
|
private sessionList;
|
|
61
62
|
private header;
|
|
63
|
+
private footer;
|
|
62
64
|
private keybindings;
|
|
63
65
|
private scope;
|
|
64
66
|
private sortMode;
|
|
@@ -2,20 +2,25 @@
|
|
|
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, and bottom dynamic feedback and
|
|
10
|
+
* shortcut footer. Selected session rows use the blue selection surface, success-green primary title,
|
|
11
|
+
* muted metadata, and ordinary text weight.
|
|
12
|
+
* Deviations: owned-modal-shortcut-hints, owned-resume-session-dialog,
|
|
13
|
+
* owned-standard-dialog-selection.
|
|
9
14
|
*/
|
|
10
15
|
import { spawnSync } from "node:child_process";
|
|
11
16
|
import { existsSync, realpathSync } from "node:fs";
|
|
12
17
|
import { unlink } from "node:fs/promises";
|
|
13
18
|
import * as os from "node:os";
|
|
14
|
-
import { Container, getKeybindings, Input, Spacer, Text, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
|
|
19
|
+
import { Container, getKeybindings, Input, matchesKey, sliceByColumn, Spacer, Text, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
|
|
15
20
|
import { KeybindingsManager } from "../adjacent/core/keybindings.js";
|
|
16
21
|
import { DynamicBorder } from "../../../startup-public.js";
|
|
17
22
|
import { addPiModalHeader, adoptPiModalFrame } from "../../modal-frame.js";
|
|
18
|
-
import { piTheme, renderPiModalShortcutHints } from "../../theme.js";
|
|
23
|
+
import { DIALOG_CLOSE_SHORTCUT_HINT, piTheme, renderPiModalShortcutHints } from "../../theme.js";
|
|
19
24
|
const theme = new Proxy({}, {
|
|
20
25
|
get(_target, property) {
|
|
21
26
|
const active = piTheme();
|
|
@@ -29,6 +34,16 @@ function keyText(keybinding, keybindings) {
|
|
|
29
34
|
function shortcutHint(keybinding, action, keybindings) {
|
|
30
35
|
return { key: keyText(keybinding, keybindings), action };
|
|
31
36
|
}
|
|
37
|
+
/** Keep the canonical close control complete while clipping preceding state-specific guidance. */
|
|
38
|
+
function withCloseHint(content, width) {
|
|
39
|
+
const close = renderPiModalShortcutHints([DIALOG_CLOSE_SHORTCUT_HINT]);
|
|
40
|
+
const closeWidth = visibleWidth(close);
|
|
41
|
+
if (width <= closeWidth)
|
|
42
|
+
return truncateToWidth(close, width, "");
|
|
43
|
+
const bodyWidth = Math.max(0, width - closeWidth - 2);
|
|
44
|
+
const body = truncateToWidth(content, bodyWidth, "…");
|
|
45
|
+
return body.length === 0 ? close : `${body} ${close}`;
|
|
46
|
+
}
|
|
32
47
|
import { filterAndSortSessions, hasSessionName } from "./session-selector-search.js";
|
|
33
48
|
function shortenPath(path) {
|
|
34
49
|
const home = os.homedir();
|
|
@@ -59,6 +74,27 @@ function formatSessionDate(date) {
|
|
|
59
74
|
return `${Math.floor(diffDays / 30)}mo`;
|
|
60
75
|
return `${Math.floor(diffDays / 365)}y`;
|
|
61
76
|
}
|
|
77
|
+
function fitToWidth(value, width, ellipsis = "") {
|
|
78
|
+
const available = Math.max(0, width);
|
|
79
|
+
const clipped = truncateToWidth(value, available, ellipsis);
|
|
80
|
+
return `${clipped}${" ".repeat(Math.max(0, available - visibleWidth(clipped)))}`;
|
|
81
|
+
}
|
|
82
|
+
function fitPathToWidth(value, width, preserveTail) {
|
|
83
|
+
if (!preserveTail || visibleWidth(value) <= width)
|
|
84
|
+
return fitToWidth(value, width, "…");
|
|
85
|
+
const tailWidth = Math.max(0, width - 1);
|
|
86
|
+
const tail = sliceByColumn(value, Math.max(0, visibleWidth(value) - tailWidth), tailWidth, true);
|
|
87
|
+
return fitToWidth(`…${tail}`, width);
|
|
88
|
+
}
|
|
89
|
+
function renderSelectedRow(value) {
|
|
90
|
+
const marker = "\u0000";
|
|
91
|
+
const wrapper = theme.bg("selectedBg", marker);
|
|
92
|
+
const markerIndex = wrapper.indexOf(marker);
|
|
93
|
+
const on = wrapper.slice(0, markerIndex);
|
|
94
|
+
const off = wrapper.slice(markerIndex + marker.length);
|
|
95
|
+
const reasserted = value.replace(/\u001b\[[0-?]*[ -/]*m/gu, sequence => `${sequence}${on}`);
|
|
96
|
+
return `${on}${reasserted}${off}`;
|
|
97
|
+
}
|
|
62
98
|
function canonicalizePath(path) {
|
|
63
99
|
if (!path)
|
|
64
100
|
return path;
|
|
@@ -75,8 +111,6 @@ class SessionSelectorHeader {
|
|
|
75
111
|
nameFilter;
|
|
76
112
|
requestRender;
|
|
77
113
|
keybindings;
|
|
78
|
-
loading = false;
|
|
79
|
-
loadProgress = null;
|
|
80
114
|
showPath = false;
|
|
81
115
|
confirmingDeletePath = null;
|
|
82
116
|
statusMessage = null;
|
|
@@ -98,14 +132,6 @@ class SessionSelectorHeader {
|
|
|
98
132
|
setNameFilter(nameFilter) {
|
|
99
133
|
this.nameFilter = nameFilter;
|
|
100
134
|
}
|
|
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
135
|
setShowPath(showPath) {
|
|
110
136
|
this.showPath = showPath;
|
|
111
137
|
}
|
|
@@ -134,64 +160,58 @@ class SessionSelectorHeader {
|
|
|
134
160
|
}
|
|
135
161
|
invalidate() { }
|
|
136
162
|
render(width) {
|
|
137
|
-
const title =
|
|
138
|
-
const
|
|
139
|
-
|
|
163
|
+
const title = theme.fg("accent", theme.bold("Resume Session"));
|
|
164
|
+
const scopeText = theme.fg("muted", "Filter: ")
|
|
165
|
+
+ theme.fg(this.scope === "current" ? "accent" : "dim", "current")
|
|
166
|
+
+ theme.fg("muted", " | ")
|
|
167
|
+
+ theme.fg(this.scope === "all" ? "accent" : "dim", "all");
|
|
168
|
+
const nameText = theme.fg("muted", "Name: ") + theme.fg("accent", this.nameFilter);
|
|
169
|
+
const sortLabel = this.sortMode === "relevance" ? "fuzzy" : this.sortMode;
|
|
140
170
|
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;
|
|
171
|
+
const status = `${scopeText} ${nameText} ${sortText}`;
|
|
172
|
+
return [truncateToWidth(title, width, ""), truncateToWidth(status, width, "")];
|
|
173
|
+
}
|
|
174
|
+
renderFooter(width) {
|
|
161
175
|
if (this.confirmingDeletePath !== null) {
|
|
162
176
|
const confirmHint = theme.fg("error", "Delete session? ") + renderPiModalShortcutHints([
|
|
163
177
|
shortcutHint("tui.select.confirm", "confirm", this.keybindings),
|
|
164
|
-
shortcutHint("tui.select.cancel", "cancel", this.keybindings),
|
|
165
178
|
]);
|
|
166
|
-
|
|
167
|
-
hintLine2 = "";
|
|
179
|
+
return [withCloseHint(confirmHint, width)];
|
|
168
180
|
}
|
|
169
|
-
|
|
181
|
+
if (this.statusMessage) {
|
|
170
182
|
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
|
-
|
|
183
|
+
return [withCloseHint(theme.fg(color, this.statusMessage.message), width)];
|
|
184
|
+
}
|
|
185
|
+
const pathState = this.showPath ? "(on)" : "(off)";
|
|
186
|
+
const hint1 = renderPiModalShortcutHints([
|
|
187
|
+
shortcutHint("tui.input.tab", "scope", this.keybindings),
|
|
188
|
+
{ action: "re:<pattern> regex" },
|
|
189
|
+
{ action: '"phrase" exact' },
|
|
190
|
+
]);
|
|
191
|
+
const hint2Parts = [
|
|
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
|
+
hint2Parts.push(shortcutHint("app.session.rename", "rename", this.keybindings));
|
|
199
|
+
return [
|
|
200
|
+
truncateToWidth(hint1, width, "…"),
|
|
201
|
+
withCloseHint(renderPiModalShortcutHints(hint2Parts), width),
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
class SessionSelectorFooter {
|
|
206
|
+
presentation;
|
|
207
|
+
constructor(presentation) {
|
|
208
|
+
this.presentation = presentation;
|
|
209
|
+
}
|
|
210
|
+
invalidate() {
|
|
211
|
+
this.presentation.invalidate();
|
|
212
|
+
}
|
|
213
|
+
render(width) {
|
|
214
|
+
return this.presentation.renderFooter(width);
|
|
195
215
|
}
|
|
196
216
|
}
|
|
197
217
|
/**
|
|
@@ -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,15 @@
|
|
|
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 without advertising that alias. All list and border colors use the owned theme
|
|
12
|
+
* and its explicit color mode. Selected rows use the shared bare-A1 blue selection surface only around
|
|
13
|
+
* the rendered item span, with the existing accent arrow, normal-text level, and preserved
|
|
14
|
+
* current/default/description roles. The comparison profile retains the public pinned component.
|
|
12
15
|
* Deviations: owned-modal-shortcut-hints, owned-level-cycle-shortcut, owned-thinking-selector-heading,
|
|
13
|
-
* owned-thinking-selector-controls.
|
|
16
|
+
* owned-thinking-selector-controls, owned-dialog-ctrl-c-cancel, owned-standard-dialog-selection.
|
|
14
17
|
*/
|
|
15
18
|
import { Container, type Focusable, SelectList } from "@earendil-works/pi-tui";
|
|
16
19
|
export type ThinkingSelectorLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|