pi-model-profiles 0.2.0 → 0.3.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 +8 -1
- package/README.md +0 -1
- package/package.json +3 -3
- package/src/constants.ts +4 -4
- package/src/profile-modal.ts +133 -246
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
No unreleased changes.
|
|
11
11
|
|
|
12
|
+
## [0.3.0] - 2026-04-30
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Refined the model profiles modal layout with wider sizing, a single bordered grid, and clearer model table columns.
|
|
16
|
+
- Updated the public README screenshot and usage details.
|
|
17
|
+
- Bumped Pi peer dependency ranges to `^0.70.6`.
|
|
18
|
+
|
|
12
19
|
## [0.2.0] - 2026-04-26
|
|
13
20
|
|
|
14
21
|
### Added
|
|
@@ -18,7 +25,7 @@ No unreleased changes.
|
|
|
18
25
|
|
|
19
26
|
### Fixed
|
|
20
27
|
- Confirmation prompts now accept typed input before update or removal actions run.
|
|
21
|
-
- Sort menu keyboard handling now works
|
|
28
|
+
- Sort menu keyboard handling now works consistently and closes without exiting the modal.
|
|
22
29
|
- Profile update and removal command handlers now avoid duplicate scans and duplicate removal events.
|
|
23
30
|
|
|
24
31
|
## [0.1.0] - 2026-04-25
|
package/README.md
CHANGED
|
@@ -56,7 +56,6 @@ Modal shortcuts:
|
|
|
56
56
|
| Shortcut | Action |
|
|
57
57
|
|----------|--------|
|
|
58
58
|
| `↑` / `↓` | Move through snapshots |
|
|
59
|
-
| `Tab` / `→` | Switch between snapshots and details panes |
|
|
60
59
|
| `Enter` | Apply selected snapshot |
|
|
61
60
|
| `s` | Save current agent state as a new snapshot |
|
|
62
61
|
| `r` | Rename selected snapshot |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-model-profiles",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Pi extension for saving, importing, and applying agent model frontmatter profiles.",
|
|
6
6
|
"type": "module",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
]
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@mariozechner/pi-coding-agent": "^0.70.
|
|
61
|
-
"@mariozechner/pi-tui": "^0.70.
|
|
60
|
+
"@mariozechner/pi-coding-agent": "^0.70.6",
|
|
61
|
+
"@mariozechner/pi-tui": "^0.70.6"
|
|
62
62
|
},
|
|
63
63
|
"publishConfig": {
|
|
64
64
|
"access": "public"
|
package/src/constants.ts
CHANGED
|
@@ -77,21 +77,21 @@ export function calculateModalHeight(agentCount: number): number {
|
|
|
77
77
|
/**
|
|
78
78
|
* Minimum modal width in columns
|
|
79
79
|
*/
|
|
80
|
-
export const MODAL_MIN_WIDTH =
|
|
80
|
+
export const MODAL_MIN_WIDTH = 100;
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
83
|
* Maximum modal width in columns
|
|
84
84
|
*/
|
|
85
|
-
export const MODAL_MAX_WIDTH =
|
|
85
|
+
export const MODAL_MAX_WIDTH = 160;
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
88
|
* Base width for modal (borders, padding, labels)
|
|
89
89
|
*/
|
|
90
|
-
export const MODAL_BASE_WIDTH =
|
|
90
|
+
export const MODAL_BASE_WIDTH = 100;
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
93
|
* Calculate dynamic modal width based on content.
|
|
94
|
-
* Formula: base width (
|
|
94
|
+
* Formula: base width (100) + maxAgentNameLength clamped to min 100, max 160
|
|
95
95
|
*/
|
|
96
96
|
export function calculateModalWidth(maxAgentNameLength: number): number {
|
|
97
97
|
const calculatedWidth = MODAL_BASE_WIDTH + maxAgentNameLength;
|
package/src/profile-modal.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { MODAL_MIN_HEIGHT, calculateModalHeight, resolveModalOverlayOptions } fr
|
|
|
5
5
|
import { toErrorMessage } from "./errors.js";
|
|
6
6
|
import { loadModalTheme, BOX, type ResolvedModalTheme } from "./modal-theme.js";
|
|
7
7
|
import { formatProfileFieldValue } from "./profile-fields.js";
|
|
8
|
-
import { getAvailableSortOrders, getCurrentSortOrder,
|
|
8
|
+
import { getAvailableSortOrders, getCurrentSortOrder, persistSortOrder, sortProfiles } from "./profile-sort-service.js";
|
|
9
9
|
import type { AppliedProfileOutcome, ProfileSortOrder, ProfilesFile, SavedProfile, SavedProfileAgent } from "./types.js";
|
|
10
10
|
|
|
11
11
|
interface ThemeLike {
|
|
@@ -46,7 +46,6 @@ export type ProfileModalResult =
|
|
|
46
46
|
outcome: AppliedProfileOutcome;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
type FocusedPane = "snapshots" | "details";
|
|
50
49
|
type ConfirmationAction = "remove" | "update";
|
|
51
50
|
|
|
52
51
|
interface ConfirmationState {
|
|
@@ -74,8 +73,6 @@ interface TableColumnLayout {
|
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
const MODAL_FALLBACK_VIEWPORT = 10;
|
|
77
|
-
const OUTER_HORIZONTAL_PADDING = 1;
|
|
78
|
-
const PANE_GAP = 1;
|
|
79
76
|
const SNAPSHOT_TITLE = "SNAPSHOTS";
|
|
80
77
|
const DETAILS_TITLE = "DETAILS";
|
|
81
78
|
const ACTIVE_PANE_LABEL = "[ACTIVE]";
|
|
@@ -141,13 +138,14 @@ function centerLineInWidth(text: string, width: number): string {
|
|
|
141
138
|
return fitLineToWidth(`${" ".repeat(padding)}${clipped}`, safeWidth);
|
|
142
139
|
}
|
|
143
140
|
|
|
144
|
-
function
|
|
145
|
-
const safeWidth = Math.max(
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
const
|
|
141
|
+
function splitGridCellWidths(innerWidth: number, snapshotNameWidth: number): { left: number; right: number } {
|
|
142
|
+
const safeWidth = Math.max(3, innerWidth);
|
|
143
|
+
const minRight = Math.min(24, Math.max(1, safeWidth - 25));
|
|
144
|
+
const maxLeft = Math.max(1, safeWidth - minRight - 1);
|
|
145
|
+
const minLeft = Math.min(maxLeft, Math.max(1, Math.min(28, Math.floor(safeWidth * 0.35))));
|
|
146
|
+
const preferredLeft = clamp(snapshotNameWidth + 6, 28, 32);
|
|
149
147
|
const left = clamp(preferredLeft, minLeft, maxLeft);
|
|
150
|
-
const right = Math.max(
|
|
148
|
+
const right = Math.max(1, safeWidth - left - 1);
|
|
151
149
|
return { left, right };
|
|
152
150
|
}
|
|
153
151
|
|
|
@@ -194,43 +192,45 @@ function wrapText(text: string, width: number): string[] {
|
|
|
194
192
|
return lines.length > 0 ? lines : [""];
|
|
195
193
|
}
|
|
196
194
|
|
|
197
|
-
function
|
|
198
|
-
|
|
199
|
-
const innerWidth = Math.max(1, frameWidth - 2);
|
|
200
|
-
const colorBorder = (text: string): string => theme.color("accent", text);
|
|
201
|
-
const safeTitle = truncateToWidth(title, innerWidth, "…", true);
|
|
202
|
-
const titleWidth = innerWidth >= visibleWidth(safeTitle) + 2 ? visibleWidth(safeTitle) + 2 : visibleWidth(safeTitle);
|
|
203
|
-
const paddedTitle = innerWidth >= visibleWidth(safeTitle) + 2 ? ` ${theme.bold(safeTitle)} ` : theme.bold(safeTitle);
|
|
204
|
-
const fillWidth = Math.max(0, innerWidth - titleWidth);
|
|
205
|
-
const topLine = `${colorBorder(BOX.CORNER_TL)}${colorBorder(paddedTitle)}${colorBorder(BOX.H_LINE.repeat(fillWidth))}${colorBorder(BOX.CORNER_TR)}`;
|
|
206
|
-
const bottomLine = `${colorBorder(BOX.CORNER_BL)}${colorBorder(BOX.H_LINE.repeat(innerWidth))}${colorBorder(BOX.CORNER_BR)}`;
|
|
207
|
-
const contentLines = (lines.length > 0 ? lines : [""]).map((line) => {
|
|
208
|
-
const padded = fitText(line, innerWidth);
|
|
209
|
-
return `${colorBorder(BOX.V_LINE)}${padded}${colorBorder(BOX.V_LINE)}`;
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
return [topLine, ...contentLines, bottomLine];
|
|
195
|
+
function colorFrameBorder(theme: ResolvedModalTheme, text: string): string {
|
|
196
|
+
return theme.color("accent", text);
|
|
213
197
|
}
|
|
214
198
|
|
|
215
|
-
function
|
|
216
|
-
return theme
|
|
199
|
+
function buildTopBorder(theme: ResolvedModalTheme, innerWidth: number): string {
|
|
200
|
+
return colorFrameBorder(theme, `${BOX.CORNER_TL}${BOX.H_LINE.repeat(innerWidth)}${BOX.CORNER_TR}`);
|
|
217
201
|
}
|
|
218
202
|
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
const label = active ? `${title} ${ACTIVE_PANE_LABEL}` : title;
|
|
222
|
-
const labelText = truncateToWidth(` ${label} `, innerWidth, "…", true);
|
|
223
|
-
const fillWidth = Math.max(0, innerWidth - visibleWidth(labelText));
|
|
224
|
-
return `${colorPaneBorder(theme, active, BOX.CORNER_TL)}${colorPaneBorder(theme, active, labelText)}${colorPaneBorder(theme, active, BOX.H_LINE.repeat(fillWidth))}${colorPaneBorder(theme, active, BOX.CORNER_TR)}`;
|
|
203
|
+
function buildBottomBorder(theme: ResolvedModalTheme, innerWidth: number): string {
|
|
204
|
+
return colorFrameBorder(theme, `${BOX.CORNER_BL}${BOX.H_LINE.repeat(innerWidth)}${BOX.CORNER_BR}`);
|
|
225
205
|
}
|
|
226
206
|
|
|
227
|
-
function
|
|
228
|
-
return
|
|
207
|
+
function buildGridSeparator(theme: ResolvedModalTheme, leftWidth: number, rightWidth: number, join: "┬" | "┼" | "┴"): string {
|
|
208
|
+
return colorFrameBorder(theme, `${BOX.T_RIGHT}${BOX.H_LINE.repeat(leftWidth)}${join}${BOX.H_LINE.repeat(rightWidth)}${BOX.T_LEFT}`);
|
|
229
209
|
}
|
|
230
210
|
|
|
231
|
-
function
|
|
232
|
-
|
|
233
|
-
|
|
211
|
+
function buildFullWidthRow(theme: ResolvedModalTheme, innerWidth: number, content: string): string {
|
|
212
|
+
return `${colorFrameBorder(theme, BOX.V_LINE)}${fitText(content, innerWidth)}${colorFrameBorder(theme, BOX.V_LINE)}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function buildGridRow(theme: ResolvedModalTheme, leftWidth: number, rightWidth: number, left: string, right: string): string {
|
|
216
|
+
return `${colorFrameBorder(theme, BOX.V_LINE)}${fitText(left, leftWidth)}${colorFrameBorder(theme, BOX.V_LINE)}${fitText(right, rightWidth)}${colorFrameBorder(theme, BOX.V_LINE)}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildModalTitleLine(theme: ResolvedModalTheme, innerWidth: number): string {
|
|
220
|
+
const title = " MODEL PROFILES";
|
|
221
|
+
const close = "[Esc] Close";
|
|
222
|
+
const gap = " ".repeat(Math.max(1, innerWidth - visibleWidth(title) - visibleWidth(close)));
|
|
223
|
+
return `${theme.color("accent", title, { bold: true })}${gap}${theme.color("dim", close)}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function buildPaneTitleLine(theme: ResolvedModalTheme, title: string, active: boolean, suffix: string | null, width: number): string {
|
|
227
|
+
const activeLabel = active ? ` ${ACTIVE_PANE_LABEL}` : "";
|
|
228
|
+
const suffixLabel = suffix ? `: ${suffix}` : "";
|
|
229
|
+
return theme.color(active ? "accent" : "text", fitText(` ${title}${activeLabel}${suffixLabel}`, width), { bold: active });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function indentStyledLine(line: string, width: number): string {
|
|
233
|
+
return fitText(` ${line}`, width);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
function formatDisplayedFieldValue(agent: SavedProfileAgent, key: "model" | "temperature" | "reasoningEffort"): string {
|
|
@@ -238,6 +238,24 @@ function formatDisplayedFieldValue(agent: SavedProfileAgent, key: "model" | "tem
|
|
|
238
238
|
return raw === "(absent)" ? ABSENT_DISPLAY_VALUE : raw;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
function formatTemperatureValue(agent: SavedProfileAgent): string {
|
|
242
|
+
const value = formatDisplayedFieldValue(agent, "temperature");
|
|
243
|
+
const numeric = Number(value);
|
|
244
|
+
if (Number.isFinite(numeric) && Number.isInteger(numeric)) {
|
|
245
|
+
return String(numeric);
|
|
246
|
+
}
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function formatReasoningValue(agent: SavedProfileAgent): string {
|
|
251
|
+
const value = formatDisplayedFieldValue(agent, "reasoningEffort");
|
|
252
|
+
const normalized = value.trim().toLowerCase();
|
|
253
|
+
if (["extra-high", "extra high", "x-high", "very-high", "very high"].includes(normalized)) {
|
|
254
|
+
return "xhigh";
|
|
255
|
+
}
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
|
|
241
259
|
function buildProfileScrollIndicator(offset: number, totalItems: number, visibleItems: number): string {
|
|
242
260
|
const shownEnd = Math.min(totalItems, offset + visibleItems);
|
|
243
261
|
const remainingAbove = offset;
|
|
@@ -267,7 +285,7 @@ function buildAgentScrollIndicator(offset: number, totalItems: number, visibleIt
|
|
|
267
285
|
}
|
|
268
286
|
|
|
269
287
|
function renderMetadataLine(theme: ResolvedModalTheme, label: string, value: string, width: number): string {
|
|
270
|
-
const prefix = `${label.padEnd(
|
|
288
|
+
const prefix = `${label.padEnd(9, " ")}`;
|
|
271
289
|
const safeValueWidth = Math.max(1, width - visibleWidth(prefix));
|
|
272
290
|
const valueText = truncateToWidth(value, safeValueWidth, "…", true);
|
|
273
291
|
const trailing = " ".repeat(Math.max(0, width - visibleWidth(prefix) - visibleWidth(valueText)));
|
|
@@ -283,14 +301,11 @@ function computeProfileNameWidth(data: ProfilesFile): number {
|
|
|
283
301
|
}
|
|
284
302
|
|
|
285
303
|
function getMaximumContentWidth(data: ProfilesFile): number {
|
|
286
|
-
let maxWidth =
|
|
304
|
+
let maxWidth = 8;
|
|
287
305
|
for (const profile of data.profiles) {
|
|
288
|
-
maxWidth = Math.max(maxWidth, visibleWidth(profile.name)
|
|
306
|
+
maxWidth = Math.max(maxWidth, visibleWidth(profile.name));
|
|
289
307
|
for (const agent of profile.agents) {
|
|
290
308
|
maxWidth = Math.max(maxWidth, visibleWidth(agent.agentName));
|
|
291
|
-
maxWidth = Math.max(maxWidth, visibleWidth(formatDisplayedFieldValue(agent, "model")));
|
|
292
|
-
maxWidth = Math.max(maxWidth, visibleWidth(formatDisplayedFieldValue(agent, "temperature")));
|
|
293
|
-
maxWidth = Math.max(maxWidth, visibleWidth(formatDisplayedFieldValue(agent, "reasoningEffort")));
|
|
294
309
|
}
|
|
295
310
|
}
|
|
296
311
|
return maxWidth;
|
|
@@ -300,24 +315,18 @@ function buildTableLayout(profile: SavedProfile, totalWidth: number): TableColum
|
|
|
300
315
|
const gap = " ";
|
|
301
316
|
const gapWidth = visibleWidth(gap) * 3;
|
|
302
317
|
const available = Math.max(24, totalWidth - gapWidth);
|
|
303
|
-
const reasoningHeader =
|
|
318
|
+
const reasoningHeader = "REASONING";
|
|
304
319
|
|
|
305
320
|
let agent = Math.max("AGENT".length, ...profile.agents.map((entry) => visibleWidth(entry.agentName)));
|
|
306
|
-
agent = clamp(agent, 10,
|
|
321
|
+
agent = clamp(agent, 10, 14);
|
|
307
322
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
"TEMP".length,
|
|
311
|
-
...profile.agents.map((entry) => visibleWidth(formatDisplayedFieldValue(entry, "temperature"))),
|
|
312
|
-
);
|
|
323
|
+
let temp = Math.max("TEMPERATURE".length, ...profile.agents.map((entry) => visibleWidth(formatTemperatureValue(entry))));
|
|
324
|
+
temp = clamp(temp, "TEMPERATURE".length, 14);
|
|
313
325
|
|
|
314
|
-
let reasoning = Math.max(
|
|
315
|
-
reasoningHeader.length,
|
|
316
|
-
...profile.agents.map((entry) => visibleWidth(formatDisplayedFieldValue(entry, "reasoningEffort"))),
|
|
317
|
-
);
|
|
326
|
+
let reasoning = Math.max(reasoningHeader.length, ...profile.agents.map((entry) => visibleWidth(formatReasoningValue(entry))));
|
|
318
327
|
reasoning = clamp(reasoning, reasoningHeader.length, 12);
|
|
319
328
|
|
|
320
|
-
const minimumModel =
|
|
329
|
+
const minimumModel = 14;
|
|
321
330
|
let model = available - agent - temp - reasoning;
|
|
322
331
|
|
|
323
332
|
while (model < minimumModel && agent > 10) {
|
|
@@ -331,7 +340,7 @@ function buildTableLayout(profile: SavedProfile, totalWidth: number): TableColum
|
|
|
331
340
|
}
|
|
332
341
|
|
|
333
342
|
if (model < minimumModel) {
|
|
334
|
-
model = Math.max(
|
|
343
|
+
model = Math.max(10, model);
|
|
335
344
|
}
|
|
336
345
|
|
|
337
346
|
const totalUsed = agent + model + temp + reasoning;
|
|
@@ -354,7 +363,7 @@ function buildTableHeaderLine(theme: ResolvedModalTheme, layout: TableColumnLayo
|
|
|
354
363
|
layout.gap,
|
|
355
364
|
theme.color("accent", fitText("MODEL", layout.model), { bold: true }),
|
|
356
365
|
layout.gap,
|
|
357
|
-
theme.color("accent", alignRight("
|
|
366
|
+
theme.color("accent", alignRight("TEMPERATURE", layout.temp), { bold: true }),
|
|
358
367
|
layout.gap,
|
|
359
368
|
theme.color("accent", fitText(layout.reasoningHeader, layout.reasoning), { bold: true }),
|
|
360
369
|
].join("");
|
|
@@ -370,9 +379,9 @@ function buildTableDataLine(theme: ResolvedModalTheme, layout: TableColumnLayout
|
|
|
370
379
|
layout.gap,
|
|
371
380
|
theme.color("text", fitText(formatDisplayedFieldValue(agent, "model"), layout.model)),
|
|
372
381
|
layout.gap,
|
|
373
|
-
theme.color("text", alignRight(
|
|
382
|
+
theme.color("text", alignRight(formatTemperatureValue(agent), layout.temp)),
|
|
374
383
|
layout.gap,
|
|
375
|
-
theme.color("text", fitText(
|
|
384
|
+
theme.color("text", fitText(formatReasoningValue(agent), layout.reasoning)),
|
|
376
385
|
].join("");
|
|
377
386
|
}
|
|
378
387
|
|
|
@@ -381,9 +390,7 @@ class ProfileListModal {
|
|
|
381
390
|
private selectedProfileId: string | null;
|
|
382
391
|
private listScrollOffset = 0;
|
|
383
392
|
private detailScrollOffset = 0;
|
|
384
|
-
private focusedPane: FocusedPane = "snapshots";
|
|
385
393
|
private lastPaneContentRows = MODAL_FALLBACK_VIEWPORT;
|
|
386
|
-
private lastDetailViewportRows = 1;
|
|
387
394
|
private renameInput: Input | null = null;
|
|
388
395
|
private renameTargetId: string | null = null;
|
|
389
396
|
private confirmation: ConfirmationState | null = null;
|
|
@@ -415,30 +422,40 @@ class ProfileListModal {
|
|
|
415
422
|
}
|
|
416
423
|
|
|
417
424
|
render(width: number): string[] {
|
|
418
|
-
const
|
|
419
|
-
const
|
|
420
|
-
const paneWidths =
|
|
421
|
-
const footerLines = this.buildFooterLines(
|
|
425
|
+
const frameWidth = Math.max(4, Math.floor(width));
|
|
426
|
+
const innerWidth = Math.max(1, frameWidth - 2);
|
|
427
|
+
const paneWidths = splitGridCellWidths(innerWidth, computeProfileNameWidth(this.data));
|
|
428
|
+
const footerLines = this.buildFooterLines(innerWidth);
|
|
422
429
|
const selectedProfile = this.getSelectedProfile();
|
|
423
430
|
const agentCount = selectedProfile?.agents.length ?? 0;
|
|
424
431
|
const paneContentRows = this.resolvePaneContentRows(agentCount, footerLines.length);
|
|
425
432
|
this.lastPaneContentRows = paneContentRows;
|
|
426
|
-
const leftPaneLines = this.
|
|
427
|
-
const rightPaneLines = this.
|
|
428
|
-
const
|
|
429
|
-
const lines: string[] = [
|
|
433
|
+
const leftPaneLines = this.buildSnapshotPaneRows(paneWidths.left, paneContentRows);
|
|
434
|
+
const rightPaneLines = this.buildDetailsPaneRows(selectedProfile, paneWidths.right, paneContentRows);
|
|
435
|
+
const detailTitle = selectedProfile?.name ?? "No selection";
|
|
436
|
+
const lines: string[] = [
|
|
437
|
+
buildTopBorder(this.theme, innerWidth),
|
|
438
|
+
buildFullWidthRow(this.theme, innerWidth, buildModalTitleLine(this.theme, innerWidth)),
|
|
439
|
+
buildGridSeparator(this.theme, paneWidths.left, paneWidths.right, "┬"),
|
|
440
|
+
buildGridRow(
|
|
441
|
+
this.theme,
|
|
442
|
+
paneWidths.left,
|
|
443
|
+
paneWidths.right,
|
|
444
|
+
buildPaneTitleLine(this.theme, SNAPSHOT_TITLE, true, null, paneWidths.left),
|
|
445
|
+
buildPaneTitleLine(this.theme, DETAILS_TITLE, false, detailTitle, paneWidths.right),
|
|
446
|
+
),
|
|
447
|
+
buildGridSeparator(this.theme, paneWidths.left, paneWidths.right, "┼"),
|
|
448
|
+
];
|
|
430
449
|
|
|
431
|
-
for (let index = 0; index <
|
|
432
|
-
|
|
433
|
-
const right = rightPaneLines[index] ?? " ".repeat(paneWidths.right);
|
|
434
|
-
const content = `${" ".repeat(OUTER_HORIZONTAL_PADDING)}${left}${" ".repeat(PANE_GAP)}${right}${" ".repeat(OUTER_HORIZONTAL_PADDING)}`;
|
|
435
|
-
lines.push(fitText(content, contentWidth));
|
|
450
|
+
for (let index = 0; index < paneContentRows; index += 1) {
|
|
451
|
+
lines.push(buildGridRow(this.theme, paneWidths.left, paneWidths.right, leftPaneLines[index] ?? "", rightPaneLines[index] ?? ""));
|
|
436
452
|
}
|
|
437
453
|
|
|
438
|
-
lines.push(
|
|
454
|
+
lines.push(buildGridSeparator(this.theme, paneWidths.left, paneWidths.right, "┴"));
|
|
439
455
|
for (const footerLine of footerLines) {
|
|
440
|
-
lines.push(
|
|
456
|
+
lines.push(buildFullWidthRow(this.theme, innerWidth, footerLine));
|
|
441
457
|
}
|
|
458
|
+
lines.push(buildBottomBorder(this.theme, innerWidth));
|
|
442
459
|
|
|
443
460
|
return lines;
|
|
444
461
|
}
|
|
@@ -470,21 +487,6 @@ class ProfileListModal {
|
|
|
470
487
|
return;
|
|
471
488
|
}
|
|
472
489
|
|
|
473
|
-
if (matchesKey(data, "tab")) {
|
|
474
|
-
this.toggleFocusedPane();
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (matchesKey(data, "left")) {
|
|
479
|
-
this.setFocusedPane("snapshots");
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (matchesKey(data, "right")) {
|
|
484
|
-
this.setFocusedPane("details");
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
490
|
if (matchesKey(data, "r")) {
|
|
489
491
|
this.startRename();
|
|
490
492
|
return;
|
|
@@ -510,12 +512,7 @@ class ProfileListModal {
|
|
|
510
512
|
return;
|
|
511
513
|
}
|
|
512
514
|
|
|
513
|
-
|
|
514
|
-
this.handleSnapshotPaneInput(data);
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
this.handleDetailsPaneInput(data);
|
|
515
|
+
this.handleSnapshotPaneInput(data);
|
|
519
516
|
}
|
|
520
517
|
|
|
521
518
|
private handleSnapshotPaneInput(data: string): void {
|
|
@@ -554,145 +551,92 @@ class ProfileListModal {
|
|
|
554
551
|
}
|
|
555
552
|
}
|
|
556
553
|
|
|
557
|
-
private
|
|
558
|
-
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
559
|
-
this.scrollDetails(-1);
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (matchesKey(data, "down") || matchesKey(data, "j")) {
|
|
564
|
-
this.scrollDetails(1);
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if (matchesKey(data, "pageup")) {
|
|
569
|
-
this.scrollDetails(-Math.max(1, this.lastDetailViewportRows));
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (matchesKey(data, "pagedown")) {
|
|
574
|
-
this.scrollDetails(Math.max(1, this.lastDetailViewportRows));
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
if (matchesKey(data, "home")) {
|
|
579
|
-
this.scrollDetailsToBoundary("start");
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
if (matchesKey(data, "end")) {
|
|
584
|
-
this.scrollDetailsToBoundary("end");
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
if (matchesKey(data, "return")) {
|
|
589
|
-
this.applySelectedProfile();
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
private buildSnapshotPaneBox(width: number, contentRows: number): string[] {
|
|
554
|
+
private buildSnapshotPaneRows(width: number, contentRows: number): string[] {
|
|
594
555
|
const lines: string[] = [];
|
|
595
|
-
const innerWidth = Math.max(1, width - 2);
|
|
596
556
|
const profiles = this.getSortedProfiles();
|
|
597
|
-
const isFocused = this.focusedPane === "snapshots";
|
|
598
557
|
const needsIndicator = profiles.length > contentRows;
|
|
599
558
|
const viewportRows = Math.max(1, contentRows - (needsIndicator ? 1 : 0));
|
|
600
559
|
this.ensureSelectedVisible(viewportRows);
|
|
601
560
|
|
|
602
|
-
lines.push(buildPaneTopBorder(this.theme, width, SNAPSHOT_TITLE, isFocused));
|
|
603
|
-
|
|
604
561
|
if (profiles.length === 0) {
|
|
605
|
-
lines.push(
|
|
606
|
-
|
|
607
|
-
lines.push(
|
|
562
|
+
lines.push(this.theme.color("dim", fitText(` ${EMPTY_PROFILE_HINT}`, width)));
|
|
563
|
+
while (lines.length < contentRows) {
|
|
564
|
+
lines.push(" ".repeat(width));
|
|
608
565
|
}
|
|
609
|
-
lines.push(buildPaneBottomBorder(this.theme, width, isFocused));
|
|
610
566
|
return lines;
|
|
611
567
|
}
|
|
612
568
|
|
|
613
569
|
for (let index = 0; index < viewportRows; index += 1) {
|
|
614
570
|
const profile = profiles[this.listScrollOffset + index];
|
|
615
571
|
if (!profile) {
|
|
616
|
-
lines.push(
|
|
572
|
+
lines.push(" ".repeat(width));
|
|
617
573
|
continue;
|
|
618
574
|
}
|
|
619
575
|
|
|
620
576
|
const isSelected = profile.id === this.selectedProfileId;
|
|
621
|
-
const label = fitText(
|
|
577
|
+
const label = fitText(` ${isSelected ? ">" : " "} ${profile.name}`, width);
|
|
622
578
|
if (isSelected) {
|
|
623
|
-
|
|
624
|
-
? this.theme.color("selectedText", label, { background: "selectedBg", bold: true })
|
|
625
|
-
: this.theme.color("accent", label, { bold: true });
|
|
626
|
-
lines.push(buildPaneLine(this.theme, width, content, isFocused));
|
|
579
|
+
lines.push(this.theme.color("selectedText", label, { background: "selectedBg", bold: true }));
|
|
627
580
|
continue;
|
|
628
581
|
}
|
|
629
582
|
|
|
630
|
-
lines.push(
|
|
583
|
+
lines.push(this.theme.color("text", label));
|
|
631
584
|
}
|
|
632
585
|
|
|
633
586
|
if (needsIndicator) {
|
|
634
|
-
const indicator = alignRight(buildProfileScrollIndicator(this.listScrollOffset, profiles.length, viewportRows),
|
|
635
|
-
lines.push(
|
|
587
|
+
const indicator = alignRight(buildProfileScrollIndicator(this.listScrollOffset, profiles.length, viewportRows), width);
|
|
588
|
+
lines.push(this.theme.color("dim", indicator));
|
|
636
589
|
}
|
|
637
590
|
|
|
638
|
-
while (lines.length < contentRows
|
|
639
|
-
lines.push(
|
|
591
|
+
while (lines.length < contentRows) {
|
|
592
|
+
lines.push(" ".repeat(width));
|
|
640
593
|
}
|
|
641
594
|
|
|
642
|
-
lines.push(buildPaneBottomBorder(this.theme, width, isFocused));
|
|
643
595
|
return lines;
|
|
644
596
|
}
|
|
645
597
|
|
|
646
|
-
private
|
|
598
|
+
private buildDetailsPaneRows(profile: SavedProfile | null, width: number, contentRows: number): string[] {
|
|
647
599
|
const lines: string[] = [];
|
|
648
|
-
const innerWidth = Math.max(1, width - 2);
|
|
649
|
-
const isFocused = this.focusedPane === "details";
|
|
650
|
-
lines.push(buildPaneTopBorder(this.theme, width, DETAILS_TITLE, isFocused));
|
|
651
600
|
|
|
652
601
|
if (!profile) {
|
|
653
|
-
this.
|
|
654
|
-
lines.push(
|
|
655
|
-
lines.push(
|
|
656
|
-
lines.
|
|
657
|
-
|
|
658
|
-
while (lines.length < contentRows + 1) {
|
|
659
|
-
lines.push(buildPaneLine(this.theme, width, " ".repeat(innerWidth), isFocused));
|
|
602
|
+
lines.push(indentStyledLine(renderMetadataLine(this.theme, "Updated:", "-", Math.max(1, width - 2)), width));
|
|
603
|
+
lines.push(" ".repeat(width));
|
|
604
|
+
lines.push(this.theme.color("dim", fitText(` ${EMPTY_DETAILS_HINT}`, width)));
|
|
605
|
+
while (lines.length < contentRows) {
|
|
606
|
+
lines.push(" ".repeat(width));
|
|
660
607
|
}
|
|
661
|
-
lines.push(buildPaneBottomBorder(this.theme, width, isFocused));
|
|
662
608
|
return lines;
|
|
663
609
|
}
|
|
664
610
|
|
|
665
|
-
const fixedRowsBeforeData =
|
|
611
|
+
const fixedRowsBeforeData = 4;
|
|
666
612
|
const availableDataArea = Math.max(1, contentRows - fixedRowsBeforeData);
|
|
667
613
|
const needsIndicator = profile.agents.length > availableDataArea;
|
|
668
614
|
const viewportRows = Math.max(1, availableDataArea - (needsIndicator ? 1 : 0));
|
|
669
|
-
this.lastDetailViewportRows = viewportRows;
|
|
670
615
|
const maxOffset = Math.max(0, profile.agents.length - viewportRows);
|
|
671
616
|
this.detailScrollOffset = clamp(this.detailScrollOffset, 0, maxOffset);
|
|
672
|
-
const
|
|
617
|
+
const tableWidth = Math.max(1, width - 4);
|
|
618
|
+
const layout = buildTableLayout(profile, tableWidth);
|
|
673
619
|
|
|
674
|
-
lines.push(
|
|
675
|
-
lines.push(
|
|
676
|
-
lines.push(
|
|
677
|
-
lines.push(
|
|
678
|
-
lines.push(buildPaneLine(this.theme, width, buildTableSeparatorLine(this.theme, innerWidth), isFocused));
|
|
620
|
+
lines.push(indentStyledLine(renderMetadataLine(this.theme, "Updated:", formatTimestamp(profile.updatedAt), tableWidth), width));
|
|
621
|
+
lines.push(" ".repeat(width));
|
|
622
|
+
lines.push(indentStyledLine(buildTableHeaderLine(this.theme, layout), width));
|
|
623
|
+
lines.push(indentStyledLine(buildTableSeparatorLine(this.theme, tableWidth), width));
|
|
679
624
|
|
|
680
625
|
for (let index = 0; index < viewportRows; index += 1) {
|
|
681
626
|
const agent = profile.agents[this.detailScrollOffset + index];
|
|
682
|
-
const content = agent ? buildTableDataLine(this.theme, layout, agent) : " ".repeat(
|
|
683
|
-
lines.push(
|
|
627
|
+
const content = agent ? indentStyledLine(buildTableDataLine(this.theme, layout, agent), width) : " ".repeat(width);
|
|
628
|
+
lines.push(content);
|
|
684
629
|
}
|
|
685
630
|
|
|
686
631
|
if (needsIndicator) {
|
|
687
|
-
const indicator = centerText(buildAgentScrollIndicator(this.detailScrollOffset, profile.agents.length, viewportRows),
|
|
688
|
-
lines.push(
|
|
632
|
+
const indicator = centerText(buildAgentScrollIndicator(this.detailScrollOffset, profile.agents.length, viewportRows), width);
|
|
633
|
+
lines.push(this.theme.color("dim", indicator));
|
|
689
634
|
}
|
|
690
635
|
|
|
691
|
-
while (lines.length < contentRows
|
|
692
|
-
lines.push(
|
|
636
|
+
while (lines.length < contentRows) {
|
|
637
|
+
lines.push(" ".repeat(width));
|
|
693
638
|
}
|
|
694
639
|
|
|
695
|
-
lines.push(buildPaneBottomBorder(this.theme, width, isFocused));
|
|
696
640
|
return lines;
|
|
697
641
|
}
|
|
698
642
|
|
|
@@ -731,26 +675,9 @@ class ProfileListModal {
|
|
|
731
675
|
return wrapText(this.message.text, width).map((line) => this.theme.color(slot, fitText(line, width)));
|
|
732
676
|
}
|
|
733
677
|
|
|
734
|
-
const gap = " ";
|
|
735
|
-
const leftWidth = Math.max(24, Math.min(34, Math.floor((width - visibleWidth(gap)) * 0.44)));
|
|
736
|
-
const rightWidth = Math.max(24, width - visibleWidth(gap) - leftWidth);
|
|
737
|
-
const row = (left: string, right: string, accent = false): string => {
|
|
738
|
-
const leftText = accent ? this.theme.color("accent", fitText(left, leftWidth), { bold: true }) : this.theme.color("dim", fitText(left, leftWidth));
|
|
739
|
-
const rightText = accent ? this.theme.color("accent", fitText(right, rightWidth), { bold: true }) : this.theme.color("dim", fitText(right, rightWidth));
|
|
740
|
-
return `${leftText}${gap}${rightText}`;
|
|
741
|
-
};
|
|
742
|
-
|
|
743
|
-
const sortLabel = getSortOrderLabel(this.currentSortOrder);
|
|
744
|
-
|
|
745
678
|
return [
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
row("[Tab/→] Switch Pane", "[s] Save Current"),
|
|
749
|
-
row("[Esc] Close Modal", "[r] Rename Snapshot"),
|
|
750
|
-
row("", "[Del/Ctrl+D] Delete Snapshot"),
|
|
751
|
-
row("", "[Ctrl+U] Update Snapshot"),
|
|
752
|
-
row("", "[Ctrl+S] Sort Profiles"),
|
|
753
|
-
row("", `[Sort: ${sortLabel}]`),
|
|
679
|
+
this.theme.color("dim", fitText(" NAVIGATION: [↑↓] Select Item [Esc] Close Modal", width)),
|
|
680
|
+
this.theme.color("dim", fitText(" ACTIONS: [Enter] Apply [s] Save [r] Rename [Del] Delete [Ctrl+U] Update", width)),
|
|
754
681
|
];
|
|
755
682
|
}
|
|
756
683
|
|
|
@@ -767,14 +694,14 @@ class ProfileListModal {
|
|
|
767
694
|
const hasTerminalRows =
|
|
768
695
|
typeof process.stdout.rows === "number" && Number.isFinite(process.stdout.rows) && process.stdout.rows > 0;
|
|
769
696
|
if (!hasTerminalRows) {
|
|
770
|
-
return Math.max(MODAL_MIN_HEIGHT - footerRows -
|
|
697
|
+
return Math.max(MODAL_MIN_HEIGHT - footerRows - 7, MODAL_FALLBACK_VIEWPORT);
|
|
771
698
|
}
|
|
772
699
|
|
|
773
|
-
return Math.max(8, calculateModalHeight(agentCount) - footerRows -
|
|
700
|
+
return Math.max(8, calculateModalHeight(agentCount) - footerRows - 7);
|
|
774
701
|
}
|
|
775
702
|
|
|
776
703
|
private getSnapshotViewportRows(): number {
|
|
777
|
-
return Math.max(1, this.lastPaneContentRows
|
|
704
|
+
return Math.max(1, this.lastPaneContentRows);
|
|
778
705
|
}
|
|
779
706
|
|
|
780
707
|
private ensureSelectedVisible(viewportSize: number): void {
|
|
@@ -793,19 +720,6 @@ class ProfileListModal {
|
|
|
793
720
|
}
|
|
794
721
|
}
|
|
795
722
|
|
|
796
|
-
private setFocusedPane(nextPane: FocusedPane): void {
|
|
797
|
-
if (this.focusedPane === nextPane) {
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
this.focusedPane = nextPane;
|
|
801
|
-
this.requestRender();
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
private toggleFocusedPane(): void {
|
|
805
|
-
this.focusedPane = this.focusedPane === "snapshots" ? "details" : "snapshots";
|
|
806
|
-
this.requestRender();
|
|
807
|
-
}
|
|
808
|
-
|
|
809
723
|
private moveSelection(delta: number): void {
|
|
810
724
|
const profiles = this.getSortedProfiles();
|
|
811
725
|
if (profiles.length === 0) {
|
|
@@ -833,33 +747,6 @@ class ProfileListModal {
|
|
|
833
747
|
this.requestRender();
|
|
834
748
|
}
|
|
835
749
|
|
|
836
|
-
private scrollDetails(delta: number): void {
|
|
837
|
-
const profile = this.getSelectedProfile();
|
|
838
|
-
if (!profile) {
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
const maxOffset = Math.max(0, profile.agents.length - this.lastDetailViewportRows);
|
|
843
|
-
this.detailScrollOffset = clamp(this.detailScrollOffset + delta, 0, maxOffset);
|
|
844
|
-
this.requestRender();
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
private scrollDetailsToBoundary(boundary: "start" | "end"): void {
|
|
848
|
-
const profile = this.getSelectedProfile();
|
|
849
|
-
if (!profile) {
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
if (boundary === "start") {
|
|
854
|
-
this.detailScrollOffset = 0;
|
|
855
|
-
this.requestRender();
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
this.detailScrollOffset = Math.max(0, profile.agents.length - this.lastDetailViewportRows);
|
|
860
|
-
this.requestRender();
|
|
861
|
-
}
|
|
862
|
-
|
|
863
750
|
private startRename(): void {
|
|
864
751
|
const profile = this.getSelectedProfile();
|
|
865
752
|
if (!profile) {
|
|
@@ -1159,7 +1046,7 @@ export async function openProfilesModal(
|
|
|
1159
1046
|
|
|
1160
1047
|
return {
|
|
1161
1048
|
render(width: number): string[] {
|
|
1162
|
-
return
|
|
1049
|
+
return contentInstance.render(width);
|
|
1163
1050
|
},
|
|
1164
1051
|
invalidate(): void {
|
|
1165
1052
|
contentInstance.invalidate();
|