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 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 regardless of focused pane and closes without exiting the modal.
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.2.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.5",
61
- "@mariozechner/pi-tui": "^0.70.5"
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;
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 = 140;
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 = 80;
90
+ export const MODAL_BASE_WIDTH = 100;
91
91
 
92
92
  /**
93
93
  * Calculate dynamic modal width based on content.
94
- * Formula: base width (80) + maxAgentNameLength clamped to min 80, max 140
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;
@@ -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, getSortOrderLabel, persistSortOrder, sortProfiles } from "./profile-sort-service.js";
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 splitPaneWidths(totalWidth: number, snapshotNameWidth: number): { left: number; right: number } {
145
- const safeWidth = Math.max(56, totalWidth);
146
- const preferredLeft = clamp(snapshotNameWidth + 6, 24, 32);
147
- const minLeft = Math.max(22, Math.min(26, safeWidth - 46));
148
- const maxLeft = Math.max(minLeft, Math.min(34, safeWidth - 40));
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(36, safeWidth - left);
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 renderOuterFrame(lines: string[], width: number, title: string, theme: ResolvedModalTheme): string[] {
198
- const frameWidth = Math.max(4, Math.floor(width));
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 colorPaneBorder(theme: ResolvedModalTheme, active: boolean, text: string): string {
216
- return theme.color(active ? "accent" : "borderMuted", text, { bold: active });
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 buildPaneTopBorder(theme: ResolvedModalTheme, width: number, title: string, active: boolean): string {
220
- const innerWidth = Math.max(1, width - 2);
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 buildPaneBottomBorder(theme: ResolvedModalTheme, width: number, active: boolean): string {
228
- return `${colorPaneBorder(theme, active, BOX.CORNER_BL)}${colorPaneBorder(theme, active, BOX.H_LINE.repeat(width - 2))}${colorPaneBorder(theme, active, BOX.CORNER_BR)}`;
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 buildPaneLine(theme: ResolvedModalTheme, width: number, content: string, active: boolean): string {
232
- const inner = fitText(content, width - 2);
233
- return `${colorPaneBorder(theme, active, BOX.V_LINE)}${inner}${colorPaneBorder(theme, active, BOX.V_LINE)}`;
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(8, " ")}`;
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 = 40;
304
+ let maxWidth = 8;
287
305
  for (const profile of data.profiles) {
288
- maxWidth = Math.max(maxWidth, visibleWidth(profile.name) + 18);
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 = totalWidth >= 60 ? "REASONING" : "REASON";
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, 18);
321
+ agent = clamp(agent, 10, 14);
307
322
 
308
- const temp = Math.max(
309
- 6,
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 = 16;
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(12, model);
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("TEMP", layout.temp), { bold: true }),
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(formatDisplayedFieldValue(agent, "temperature"), layout.temp)),
382
+ theme.color("text", alignRight(formatTemperatureValue(agent), layout.temp)),
374
383
  layout.gap,
375
- theme.color("text", fitText(formatDisplayedFieldValue(agent, "reasoningEffort"), layout.reasoning)),
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 contentWidth = Math.max(1, Math.floor(width));
419
- const paneAreaWidth = Math.max(40, contentWidth - OUTER_HORIZONTAL_PADDING * 2 - PANE_GAP);
420
- const paneWidths = splitPaneWidths(paneAreaWidth, computeProfileNameWidth(this.data));
421
- const footerLines = this.buildFooterLines(contentWidth);
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.buildSnapshotPaneBox(paneWidths.left, paneContentRows);
427
- const rightPaneLines = this.buildDetailsPaneBox(selectedProfile, paneWidths.right, paneContentRows);
428
- const paneRowCount = Math.max(leftPaneLines.length, rightPaneLines.length);
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 < paneRowCount; index += 1) {
432
- const left = leftPaneLines[index] ?? " ".repeat(paneWidths.left);
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(" ".repeat(contentWidth));
454
+ lines.push(buildGridSeparator(this.theme, paneWidths.left, paneWidths.right, "┴"));
439
455
  for (const footerLine of footerLines) {
440
- lines.push(fitText(footerLine, contentWidth));
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
- if (this.focusedPane === "snapshots") {
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 handleDetailsPaneInput(data: string): void {
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(buildPaneLine(this.theme, width, this.theme.color("dim", fitText(EMPTY_PROFILE_HINT, innerWidth)), isFocused));
606
- for (let index = 1; index < contentRows; index += 1) {
607
- lines.push(buildPaneLine(this.theme, width, " ".repeat(innerWidth), isFocused));
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(buildPaneLine(this.theme, width, " ".repeat(innerWidth), isFocused));
572
+ lines.push(" ".repeat(width));
617
573
  continue;
618
574
  }
619
575
 
620
576
  const isSelected = profile.id === this.selectedProfileId;
621
- const label = fitText(`${isSelected ? ">" : " "} ${profile.name}`, innerWidth);
577
+ const label = fitText(` ${isSelected ? ">" : " "} ${profile.name}`, width);
622
578
  if (isSelected) {
623
- const content = isFocused
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(buildPaneLine(this.theme, width, this.theme.color("text", label), isFocused));
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), innerWidth);
635
- lines.push(buildPaneLine(this.theme, width, this.theme.color("dim", indicator), isFocused));
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 + 1) {
639
- lines.push(buildPaneLine(this.theme, width, " ".repeat(innerWidth), isFocused));
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 buildDetailsPaneBox(profile: SavedProfile | null, width: number, contentRows: number): string[] {
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.lastDetailViewportRows = 1;
654
- lines.push(buildPaneLine(this.theme, width, renderMetadataLine(this.theme, "Name:", "-", innerWidth), isFocused));
655
- lines.push(buildPaneLine(this.theme, width, renderMetadataLine(this.theme, "Updated:", "-", innerWidth), isFocused));
656
- lines.push(buildPaneLine(this.theme, width, " ".repeat(innerWidth), isFocused));
657
- lines.push(buildPaneLine(this.theme, width, this.theme.color("dim", fitText(EMPTY_DETAILS_HINT, innerWidth)), isFocused));
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 = 5;
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 layout = buildTableLayout(profile, innerWidth);
617
+ const tableWidth = Math.max(1, width - 4);
618
+ const layout = buildTableLayout(profile, tableWidth);
673
619
 
674
- lines.push(buildPaneLine(this.theme, width, renderMetadataLine(this.theme, "Name:", profile.name, innerWidth), isFocused));
675
- lines.push(buildPaneLine(this.theme, width, renderMetadataLine(this.theme, "Updated:", formatTimestamp(profile.updatedAt), innerWidth), isFocused));
676
- lines.push(buildPaneLine(this.theme, width, " ".repeat(innerWidth), isFocused));
677
- lines.push(buildPaneLine(this.theme, width, buildTableHeaderLine(this.theme, layout), isFocused));
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(innerWidth);
683
- lines.push(buildPaneLine(this.theme, width, content, isFocused));
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), innerWidth);
688
- lines.push(buildPaneLine(this.theme, width, this.theme.color("dim", indicator), isFocused));
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 + 1) {
692
- lines.push(buildPaneLine(this.theme, width, " ".repeat(innerWidth), isFocused));
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
- row("NAVIGATION", "ACTIONS", true),
747
- row("[↑↓] Select Item", "[Enter] Apply Snapshot"),
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 - 6, MODAL_FALLBACK_VIEWPORT);
697
+ return Math.max(MODAL_MIN_HEIGHT - footerRows - 7, MODAL_FALLBACK_VIEWPORT);
771
698
  }
772
699
 
773
- return Math.max(8, calculateModalHeight(agentCount) - footerRows - 6);
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 - 1);
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 renderOuterFrame(contentInstance.render(Math.max(1, width - 2)), width, "MODEL PROFILES", resolvedTheme);
1049
+ return contentInstance.render(width);
1163
1050
  },
1164
1051
  invalidate(): void {
1165
1052
  contentInstance.invalidate();