git-coco 0.62.2 → 0.62.3

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.
@@ -61,7 +61,7 @@ import { pathToFileURL } from 'url';
61
61
  /**
62
62
  * Current build version from package.json
63
63
  */
64
- const BUILD_VERSION = "0.62.2";
64
+ const BUILD_VERSION = "0.62.3";
65
65
 
66
66
  const isInteractive = (config) => {
67
67
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -34351,7 +34351,13 @@ function sidebarTabLabel(tab) {
34351
34351
  * Sliding window keeps the cursor in view as the user navigates a long
34352
34352
  * list; truncation hints surface the count of hidden rows.
34353
34353
  */
34354
- function renderSelectableSidebarRows(h, Text, items, selectedIndex, focused, width, theme, toRowText, keyPrefix, visibleCount) {
34354
+ function renderSelectableSidebarRows(h, Text, items, selectedIndex, focused, width, theme, toRowText, keyPrefix, visibleCount,
34355
+ // Optional per-row foreground colour (e.g. the current branch in
34356
+ // green). Applied only when the row is NOT the active selection —
34357
+ // the selection's inverse/background styling owns the row's colour in
34358
+ // that case, and layering a foreground under `inverse` would swap it
34359
+ // into an unexpected background tint.
34360
+ rowColor) {
34355
34361
  if (items.length === 0)
34356
34362
  return [];
34357
34363
  const window = getSidebarVisibleWindow(items.length, selectedIndex, visibleCount);
@@ -34368,10 +34374,12 @@ function renderSelectableSidebarRows(h, Text, items, selectedIndex, focused, wid
34368
34374
  break;
34369
34375
  const isSelected = focused && index === selectedIndex;
34370
34376
  const text = toRowText(items[index], index);
34377
+ const color = isSelected ? undefined : rowColor?.(items[index], index);
34371
34378
  elements.push(h(Text, {
34372
34379
  key: `${keyPrefix}-row-${index}`,
34373
34380
  backgroundColor: isSelected && !theme.noColor ? theme.colors.selection : undefined,
34374
34381
  inverse: isSelected,
34382
+ color,
34375
34383
  }, truncateCells(` ${text}`, width - 4)));
34376
34384
  }
34377
34385
  if (window.truncatedBelow > 0) {
@@ -34481,7 +34489,11 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
34481
34489
  ? spin
34482
34490
  : branchRowMarker(branch, { ascii: theme.ascii }).glyph;
34483
34491
  return `${glyph} ${branch.shortName}`;
34484
- }, 'tab-branches', visibleListCount),
34492
+ }, 'tab-branches', visibleListCount,
34493
+ // Paint the checked-out branch green so "where am I?" reads at a
34494
+ // glance, matching the green HEAD marker the branches surface
34495
+ // already uses. NO_COLOR themes fall back to the `*` glyph alone.
34496
+ (branch) => (branch.current && !theme.noColor ? theme.colors.success : undefined)),
34485
34497
  ];
34486
34498
  }
34487
34499
  if (tab === 'tags') {
@@ -34982,11 +34994,16 @@ function renderBranchesSurface(ctx, spinnerFrame = 0) {
34982
34994
  const truncated = truncateCells(fullText, Math.max(20, width - 4));
34983
34995
  // If truncation chopped into the timestamp/divergence portion,
34984
34996
  // fall back to a single Text to keep the visible width honest.
34997
+ // The checked-out branch is painted green (matching its green
34998
+ // HEAD marker) so "where am I?" reads at a glance. NO_COLOR
34999
+ // themes fall back to the `*` glyph alone.
35000
+ const currentColor = branch.current && !theme.noColor ? theme.colors.success : undefined;
34985
35001
  if (truncated !== fullText) {
34986
35002
  return h(Text, {
34987
35003
  key: `branch-${index}`,
34988
35004
  bold: isSelected,
34989
35005
  dimColor: lineDim,
35006
+ color: currentColor,
34990
35007
  }, truncated);
34991
35008
  }
34992
35009
  return h(Text, {
@@ -35001,7 +35018,12 @@ function renderBranchesSurface(ctx, spinnerFrame = 0) {
35001
35018
  // no-upstream kinds return undefined from
35002
35019
  // `getBranchRowMarkerColor`, so those markers inherit the
35003
35020
  // row's dim and read as quiet chrome.
35004
- h(Text, { color: glyphColor, dimColor: glyphColor ? false : undefined }, glyph), trailingName, h(Text, { dimColor: true }, timestampPadded), trailingDivergence);
35021
+ h(Text, { color: glyphColor, dimColor: glyphColor ? false : undefined }, glyph),
35022
+ // Name span: green for the current branch (dimColor:false keeps
35023
+ // it bright), otherwise it inherits the row's normal styling.
35024
+ currentColor
35025
+ ? h(Text, { color: currentColor, dimColor: false }, trailingName)
35026
+ : trailingName, h(Text, { dimColor: true }, timestampPadded), trailingDivergence);
35005
35027
  });
35006
35028
  // Scroll indicators — same "N more above/below" pattern as the
35007
35029
  // sidebar and help overlay so the user knows the list continues.
@@ -38800,6 +38822,15 @@ function renderReflogSurface(ctx) {
38800
38822
  * Extracted from `src/commands/log/inkRuntime.ts` as part of phase 5a.1
38801
38823
  * of #890. No behavior change.
38802
38824
  */
38825
+ const GAP = 2; // cells between columns
38826
+ /** Truncate to `w` cells, then pad to `w` (left = padEnd, right = padStart). */
38827
+ function cell(value, w, align = 'left') {
38828
+ const t = truncateCells(value, w);
38829
+ // padStart/padEnd count code units; refs / ages / counts / branch
38830
+ // names are ASCII in practice, matching the branches surface's
38831
+ // padEnd-based column alignment.
38832
+ return align === 'right' ? t.padStart(w) : t.padEnd(w);
38833
+ }
38803
38834
  function renderStashSurface(ctx, spinnerFrame = 0) {
38804
38835
  const { h, components, state, context, contextStatus, bodyRows, width, theme } = ctx;
38805
38836
  const { Box, Text } = components;
@@ -38810,7 +38841,9 @@ function renderStashSurface(ctx, spinnerFrame = 0) {
38810
38841
  ? allStashes.filter((stash) => matchesPromotedFilter([stash.ref, stash.message], state.filter))
38811
38842
  : allStashes;
38812
38843
  const selected = Math.max(0, Math.min(state.selectedStashIndex, Math.max(0, stashes.length - 1)));
38813
- const listRows = Math.max(4, bodyRows - 4);
38844
+ // One extra row reserved (vs the other surfaces' `- 4`) for the column
38845
+ // header row below.
38846
+ const listRows = Math.max(4, bodyRows - 5);
38814
38847
  const startIndex = Math.max(0, selected - Math.floor(listRows / 2));
38815
38848
  const visible = stashes.slice(startIndex, startIndex + listRows);
38816
38849
  const filterLabel = state.filter ? ` | filter: ${state.filter}` : '';
@@ -38820,10 +38853,67 @@ function renderStashSurface(ctx, spinnerFrame = 0) {
38820
38853
  const emptyLabel = formatLogInkStashEmpty({ filter: state.filter });
38821
38854
  const loadingLabel = formatLogInkLoading({ resource: 'stashes' });
38822
38855
  const now = getRenderNow();
38823
- // Available width for a row: box width minus the 2-cell horizontal
38824
- // padding. Truncate to it (with a small floor) instead of a magic 140
38825
- // so the richer meta degrades gracefully on narrow terminals.
38826
- const rowWidth = Math.max(20, width - 2);
38856
+ // Usable interior width: panel width minus the border (2) and the
38857
+ // paddingX:1 (2). The previous `width - 2` under-counted the border
38858
+ // and made every near-full row overflow by 2 cells and wrap — the
38859
+ // single biggest readability hit in the old list.
38860
+ const contentWidth = Math.max(20, width - 4);
38861
+ const ageOf = (s) => formatCompactRelativeDate(s.date, now);
38862
+ // Column widths derived from the visible window (#833 pattern) so rows
38863
+ // align without re-measuring the whole list. Each column is at least
38864
+ // as wide as its header label so the header never truncates ("age",
38865
+ // "branch", "files"); age tops out at 5 ("today" is the longest value
38866
+ // formatCompactRelativeDate emits).
38867
+ const refCol = visible.length
38868
+ ? Math.min(11, Math.max(3, ...visible.map((s) => cellWidth(s.ref))))
38869
+ : 9;
38870
+ const ageCol = visible.length
38871
+ ? Math.min(5, Math.max(3, ...visible.map((s) => cellWidth(ageOf(s)))))
38872
+ : 3;
38873
+ const branchColMax = visible.length
38874
+ ? Math.max(0, ...visible.map((s) => cellWidth(s.branch || '')))
38875
+ : 0;
38876
+ const filesCol = visible.length
38877
+ ? Math.max(5, ...visible.map((s) => String(s.files.length).length))
38878
+ : 5;
38879
+ // Responsive degradation. Keep ref + message always; when the message
38880
+ // floor is threatened, shed columns the preview pane already shows —
38881
+ // branch first, then age, then the file count — before squeezing the
38882
+ // message.
38883
+ // Widen to fit the "branch" header when any stash carries a branch;
38884
+ // stays 0 (column dropped) when none do.
38885
+ let branchCol = branchColMax > 0 ? Math.min(18, Math.max(6, branchColMax)) : 0;
38886
+ let showAge = true;
38887
+ let showFiles = true;
38888
+ const fixedWidth = () => 2 + refCol +
38889
+ (showAge ? GAP + ageCol : 0) +
38890
+ (branchCol > 0 ? GAP + branchCol : 0) +
38891
+ (showFiles ? GAP + filesCol : 0) +
38892
+ GAP; // gap before the message column
38893
+ let messageWidth = contentWidth - fixedWidth();
38894
+ if (messageWidth < 24 && branchCol > 0) {
38895
+ branchCol = 0;
38896
+ messageWidth = contentWidth - fixedWidth();
38897
+ }
38898
+ if (messageWidth < 16 && showAge) {
38899
+ showAge = false;
38900
+ messageWidth = contentWidth - fixedWidth();
38901
+ }
38902
+ if (messageWidth < 12 && showFiles) {
38903
+ showFiles = false;
38904
+ messageWidth = contentWidth - fixedWidth();
38905
+ }
38906
+ messageWidth = Math.max(8, messageWidth);
38907
+ // Column header. Right-aligned labels over the right-aligned numeric
38908
+ // columns (age, files) so header and data share an edge.
38909
+ let headerText = ` ${cell('ref', refCol)}`;
38910
+ if (showAge)
38911
+ headerText += `${' '.repeat(GAP)}${cell('age', ageCol, 'right')}`;
38912
+ if (branchCol > 0)
38913
+ headerText += `${' '.repeat(GAP)}${cell('branch', branchCol)}`;
38914
+ if (showFiles)
38915
+ headerText += `${' '.repeat(GAP)}${cell('files', filesCol, 'right')}`;
38916
+ headerText += `${' '.repeat(GAP)}message`;
38827
38917
  const lines = loading
38828
38918
  ? [h(Text, { key: 'stash-loading', dimColor: true }, loadingLabel)]
38829
38919
  : stashes.length === 0
@@ -38832,32 +38922,22 @@ function renderStashSurface(ctx, spinnerFrame = 0) {
38832
38922
  const index = startIndex + offset;
38833
38923
  const isSelected = index === selected;
38834
38924
  const cursor = isSelected ? '>' : ' ';
38835
- // Surface the metadata the StashEntry already carries — origin
38836
- // branch, file count, and relative age — between the ref and the
38837
- // message, so the list answers "which stash is this?" without an
38838
- // Enter→diff round trip.
38839
- const age = formatCompactRelativeDate(stash.date, now);
38840
- const fileCount = stash.files.length;
38841
- const meta = [
38842
- stash.branch ? `on ${stash.branch}` : '',
38843
- fileCount > 0 ? `${fileCount} file${fileCount === 1 ? '' : 's'}` : '',
38844
- age,
38845
- ].filter(Boolean).join(' · ');
38846
- const rowText = meta
38847
- ? `${cursor} ${stash.ref.padEnd(11)} ${meta} ${stash.message}`
38848
- : `${cursor} ${stash.ref.padEnd(11)} ${stash.message}`;
38925
+ const deleting = isPendingItemAction(state.pendingItemAction, 'stash', stash.ref);
38849
38926
  // The `stash@{N}` ref is an identifier, not a status icon, so a
38850
38927
  // delete-in-flight appends an accent spinner at the row's end
38851
- // (2 cells reserved from the width budget).
38852
- const deleting = isPendingItemAction(state.pendingItemAction, 'stash', stash.ref);
38928
+ // (2 cells reserved from the message budget).
38853
38929
  const spinnerSpan = deleting
38854
38930
  ? h(Text, { color: theme.noColor ? undefined : theme.colors.accent, dimColor: false }, ` ${inlineSpinnerGlyph(spinnerFrame, theme.ascii)}`)
38855
38931
  : null;
38932
+ const message = truncateCells(stash.message, messageWidth - (deleting ? 2 : 0));
38933
+ // ref + message read as primary; age / branch / file-count are
38934
+ // dim metadata (kept dim even on the bold selected row so the
38935
+ // message stays the focal point).
38856
38936
  return h(Text, {
38857
38937
  key: `stash-${index}`,
38858
38938
  bold: isSelected,
38859
38939
  dimColor: !isSelected,
38860
- }, truncateCells(rowText, rowWidth - (deleting ? 2 : 0)), spinnerSpan);
38940
+ }, `${cursor} `, cell(stash.ref, refCol), showAge ? h(Text, { dimColor: true }, `${' '.repeat(GAP)}${cell(ageOf(stash), ageCol, 'right')}`) : null, branchCol > 0 ? h(Text, { dimColor: true }, `${' '.repeat(GAP)}${cell(stash.branch || '', branchCol)}`) : null, showFiles ? h(Text, { dimColor: true }, `${' '.repeat(GAP)}${cell(String(stash.files.length), filesCol, 'right')}`) : null, `${' '.repeat(GAP)}${message}`, spinnerSpan);
38861
38941
  });
38862
38942
  const stashHasMoreAbove = startIndex > 0 && stashes.length > 0;
38863
38943
  const stashHasMoreBelow = startIndex + listRows < stashes.length;
@@ -38868,7 +38948,11 @@ function renderStashSurface(ctx, spinnerFrame = 0) {
38868
38948
  flexShrink: 0,
38869
38949
  paddingX: 1,
38870
38950
  width,
38871
- }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Stash', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...(stashHasMoreAbove
38951
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Stash', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme),
38952
+ // Column header — only when there are rows to label.
38953
+ ...(!loading && stashes.length > 0
38954
+ ? [h(Text, { key: 'stash-col-header', dimColor: true }, truncateCells(headerText, contentWidth))]
38955
+ : []), ...(stashHasMoreAbove
38872
38956
  ? [h(Text, { key: 'stash-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
38873
38957
  : []), ...lines, ...(stashHasMoreBelow
38874
38958
  ? [h(Text, { key: 'stash-more-below', dimColor: true }, ` ↓ ${stashes.length - (startIndex + listRows)} more below`)]
package/dist/index.js CHANGED
@@ -78,7 +78,7 @@ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
78
78
  /**
79
79
  * Current build version from package.json
80
80
  */
81
- const BUILD_VERSION = "0.62.2";
81
+ const BUILD_VERSION = "0.62.3";
82
82
 
83
83
  const isInteractive = (config) => {
84
84
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -34368,7 +34368,13 @@ function sidebarTabLabel(tab) {
34368
34368
  * Sliding window keeps the cursor in view as the user navigates a long
34369
34369
  * list; truncation hints surface the count of hidden rows.
34370
34370
  */
34371
- function renderSelectableSidebarRows(h, Text, items, selectedIndex, focused, width, theme, toRowText, keyPrefix, visibleCount) {
34371
+ function renderSelectableSidebarRows(h, Text, items, selectedIndex, focused, width, theme, toRowText, keyPrefix, visibleCount,
34372
+ // Optional per-row foreground colour (e.g. the current branch in
34373
+ // green). Applied only when the row is NOT the active selection —
34374
+ // the selection's inverse/background styling owns the row's colour in
34375
+ // that case, and layering a foreground under `inverse` would swap it
34376
+ // into an unexpected background tint.
34377
+ rowColor) {
34372
34378
  if (items.length === 0)
34373
34379
  return [];
34374
34380
  const window = getSidebarVisibleWindow(items.length, selectedIndex, visibleCount);
@@ -34385,10 +34391,12 @@ function renderSelectableSidebarRows(h, Text, items, selectedIndex, focused, wid
34385
34391
  break;
34386
34392
  const isSelected = focused && index === selectedIndex;
34387
34393
  const text = toRowText(items[index], index);
34394
+ const color = isSelected ? undefined : rowColor?.(items[index], index);
34388
34395
  elements.push(h(Text, {
34389
34396
  key: `${keyPrefix}-row-${index}`,
34390
34397
  backgroundColor: isSelected && !theme.noColor ? theme.colors.selection : undefined,
34391
34398
  inverse: isSelected,
34399
+ color,
34392
34400
  }, truncateCells(` ${text}`, width - 4)));
34393
34401
  }
34394
34402
  if (window.truncatedBelow > 0) {
@@ -34498,7 +34506,11 @@ function renderActiveSidebarContent(h, Text, tab, state, context, contextStatus,
34498
34506
  ? spin
34499
34507
  : branchRowMarker(branch, { ascii: theme.ascii }).glyph;
34500
34508
  return `${glyph} ${branch.shortName}`;
34501
- }, 'tab-branches', visibleListCount),
34509
+ }, 'tab-branches', visibleListCount,
34510
+ // Paint the checked-out branch green so "where am I?" reads at a
34511
+ // glance, matching the green HEAD marker the branches surface
34512
+ // already uses. NO_COLOR themes fall back to the `*` glyph alone.
34513
+ (branch) => (branch.current && !theme.noColor ? theme.colors.success : undefined)),
34502
34514
  ];
34503
34515
  }
34504
34516
  if (tab === 'tags') {
@@ -34999,11 +35011,16 @@ function renderBranchesSurface(ctx, spinnerFrame = 0) {
34999
35011
  const truncated = truncateCells(fullText, Math.max(20, width - 4));
35000
35012
  // If truncation chopped into the timestamp/divergence portion,
35001
35013
  // fall back to a single Text to keep the visible width honest.
35014
+ // The checked-out branch is painted green (matching its green
35015
+ // HEAD marker) so "where am I?" reads at a glance. NO_COLOR
35016
+ // themes fall back to the `*` glyph alone.
35017
+ const currentColor = branch.current && !theme.noColor ? theme.colors.success : undefined;
35002
35018
  if (truncated !== fullText) {
35003
35019
  return h(Text, {
35004
35020
  key: `branch-${index}`,
35005
35021
  bold: isSelected,
35006
35022
  dimColor: lineDim,
35023
+ color: currentColor,
35007
35024
  }, truncated);
35008
35025
  }
35009
35026
  return h(Text, {
@@ -35018,7 +35035,12 @@ function renderBranchesSurface(ctx, spinnerFrame = 0) {
35018
35035
  // no-upstream kinds return undefined from
35019
35036
  // `getBranchRowMarkerColor`, so those markers inherit the
35020
35037
  // row's dim and read as quiet chrome.
35021
- h(Text, { color: glyphColor, dimColor: glyphColor ? false : undefined }, glyph), trailingName, h(Text, { dimColor: true }, timestampPadded), trailingDivergence);
35038
+ h(Text, { color: glyphColor, dimColor: glyphColor ? false : undefined }, glyph),
35039
+ // Name span: green for the current branch (dimColor:false keeps
35040
+ // it bright), otherwise it inherits the row's normal styling.
35041
+ currentColor
35042
+ ? h(Text, { color: currentColor, dimColor: false }, trailingName)
35043
+ : trailingName, h(Text, { dimColor: true }, timestampPadded), trailingDivergence);
35022
35044
  });
35023
35045
  // Scroll indicators — same "N more above/below" pattern as the
35024
35046
  // sidebar and help overlay so the user knows the list continues.
@@ -38817,6 +38839,15 @@ function renderReflogSurface(ctx) {
38817
38839
  * Extracted from `src/commands/log/inkRuntime.ts` as part of phase 5a.1
38818
38840
  * of #890. No behavior change.
38819
38841
  */
38842
+ const GAP = 2; // cells between columns
38843
+ /** Truncate to `w` cells, then pad to `w` (left = padEnd, right = padStart). */
38844
+ function cell(value, w, align = 'left') {
38845
+ const t = truncateCells(value, w);
38846
+ // padStart/padEnd count code units; refs / ages / counts / branch
38847
+ // names are ASCII in practice, matching the branches surface's
38848
+ // padEnd-based column alignment.
38849
+ return align === 'right' ? t.padStart(w) : t.padEnd(w);
38850
+ }
38820
38851
  function renderStashSurface(ctx, spinnerFrame = 0) {
38821
38852
  const { h, components, state, context, contextStatus, bodyRows, width, theme } = ctx;
38822
38853
  const { Box, Text } = components;
@@ -38827,7 +38858,9 @@ function renderStashSurface(ctx, spinnerFrame = 0) {
38827
38858
  ? allStashes.filter((stash) => matchesPromotedFilter([stash.ref, stash.message], state.filter))
38828
38859
  : allStashes;
38829
38860
  const selected = Math.max(0, Math.min(state.selectedStashIndex, Math.max(0, stashes.length - 1)));
38830
- const listRows = Math.max(4, bodyRows - 4);
38861
+ // One extra row reserved (vs the other surfaces' `- 4`) for the column
38862
+ // header row below.
38863
+ const listRows = Math.max(4, bodyRows - 5);
38831
38864
  const startIndex = Math.max(0, selected - Math.floor(listRows / 2));
38832
38865
  const visible = stashes.slice(startIndex, startIndex + listRows);
38833
38866
  const filterLabel = state.filter ? ` | filter: ${state.filter}` : '';
@@ -38837,10 +38870,67 @@ function renderStashSurface(ctx, spinnerFrame = 0) {
38837
38870
  const emptyLabel = formatLogInkStashEmpty({ filter: state.filter });
38838
38871
  const loadingLabel = formatLogInkLoading({ resource: 'stashes' });
38839
38872
  const now = getRenderNow();
38840
- // Available width for a row: box width minus the 2-cell horizontal
38841
- // padding. Truncate to it (with a small floor) instead of a magic 140
38842
- // so the richer meta degrades gracefully on narrow terminals.
38843
- const rowWidth = Math.max(20, width - 2);
38873
+ // Usable interior width: panel width minus the border (2) and the
38874
+ // paddingX:1 (2). The previous `width - 2` under-counted the border
38875
+ // and made every near-full row overflow by 2 cells and wrap — the
38876
+ // single biggest readability hit in the old list.
38877
+ const contentWidth = Math.max(20, width - 4);
38878
+ const ageOf = (s) => formatCompactRelativeDate(s.date, now);
38879
+ // Column widths derived from the visible window (#833 pattern) so rows
38880
+ // align without re-measuring the whole list. Each column is at least
38881
+ // as wide as its header label so the header never truncates ("age",
38882
+ // "branch", "files"); age tops out at 5 ("today" is the longest value
38883
+ // formatCompactRelativeDate emits).
38884
+ const refCol = visible.length
38885
+ ? Math.min(11, Math.max(3, ...visible.map((s) => cellWidth(s.ref))))
38886
+ : 9;
38887
+ const ageCol = visible.length
38888
+ ? Math.min(5, Math.max(3, ...visible.map((s) => cellWidth(ageOf(s)))))
38889
+ : 3;
38890
+ const branchColMax = visible.length
38891
+ ? Math.max(0, ...visible.map((s) => cellWidth(s.branch || '')))
38892
+ : 0;
38893
+ const filesCol = visible.length
38894
+ ? Math.max(5, ...visible.map((s) => String(s.files.length).length))
38895
+ : 5;
38896
+ // Responsive degradation. Keep ref + message always; when the message
38897
+ // floor is threatened, shed columns the preview pane already shows —
38898
+ // branch first, then age, then the file count — before squeezing the
38899
+ // message.
38900
+ // Widen to fit the "branch" header when any stash carries a branch;
38901
+ // stays 0 (column dropped) when none do.
38902
+ let branchCol = branchColMax > 0 ? Math.min(18, Math.max(6, branchColMax)) : 0;
38903
+ let showAge = true;
38904
+ let showFiles = true;
38905
+ const fixedWidth = () => 2 + refCol +
38906
+ (showAge ? GAP + ageCol : 0) +
38907
+ (branchCol > 0 ? GAP + branchCol : 0) +
38908
+ (showFiles ? GAP + filesCol : 0) +
38909
+ GAP; // gap before the message column
38910
+ let messageWidth = contentWidth - fixedWidth();
38911
+ if (messageWidth < 24 && branchCol > 0) {
38912
+ branchCol = 0;
38913
+ messageWidth = contentWidth - fixedWidth();
38914
+ }
38915
+ if (messageWidth < 16 && showAge) {
38916
+ showAge = false;
38917
+ messageWidth = contentWidth - fixedWidth();
38918
+ }
38919
+ if (messageWidth < 12 && showFiles) {
38920
+ showFiles = false;
38921
+ messageWidth = contentWidth - fixedWidth();
38922
+ }
38923
+ messageWidth = Math.max(8, messageWidth);
38924
+ // Column header. Right-aligned labels over the right-aligned numeric
38925
+ // columns (age, files) so header and data share an edge.
38926
+ let headerText = ` ${cell('ref', refCol)}`;
38927
+ if (showAge)
38928
+ headerText += `${' '.repeat(GAP)}${cell('age', ageCol, 'right')}`;
38929
+ if (branchCol > 0)
38930
+ headerText += `${' '.repeat(GAP)}${cell('branch', branchCol)}`;
38931
+ if (showFiles)
38932
+ headerText += `${' '.repeat(GAP)}${cell('files', filesCol, 'right')}`;
38933
+ headerText += `${' '.repeat(GAP)}message`;
38844
38934
  const lines = loading
38845
38935
  ? [h(Text, { key: 'stash-loading', dimColor: true }, loadingLabel)]
38846
38936
  : stashes.length === 0
@@ -38849,32 +38939,22 @@ function renderStashSurface(ctx, spinnerFrame = 0) {
38849
38939
  const index = startIndex + offset;
38850
38940
  const isSelected = index === selected;
38851
38941
  const cursor = isSelected ? '>' : ' ';
38852
- // Surface the metadata the StashEntry already carries — origin
38853
- // branch, file count, and relative age — between the ref and the
38854
- // message, so the list answers "which stash is this?" without an
38855
- // Enter→diff round trip.
38856
- const age = formatCompactRelativeDate(stash.date, now);
38857
- const fileCount = stash.files.length;
38858
- const meta = [
38859
- stash.branch ? `on ${stash.branch}` : '',
38860
- fileCount > 0 ? `${fileCount} file${fileCount === 1 ? '' : 's'}` : '',
38861
- age,
38862
- ].filter(Boolean).join(' · ');
38863
- const rowText = meta
38864
- ? `${cursor} ${stash.ref.padEnd(11)} ${meta} ${stash.message}`
38865
- : `${cursor} ${stash.ref.padEnd(11)} ${stash.message}`;
38942
+ const deleting = isPendingItemAction(state.pendingItemAction, 'stash', stash.ref);
38866
38943
  // The `stash@{N}` ref is an identifier, not a status icon, so a
38867
38944
  // delete-in-flight appends an accent spinner at the row's end
38868
- // (2 cells reserved from the width budget).
38869
- const deleting = isPendingItemAction(state.pendingItemAction, 'stash', stash.ref);
38945
+ // (2 cells reserved from the message budget).
38870
38946
  const spinnerSpan = deleting
38871
38947
  ? h(Text, { color: theme.noColor ? undefined : theme.colors.accent, dimColor: false }, ` ${inlineSpinnerGlyph(spinnerFrame, theme.ascii)}`)
38872
38948
  : null;
38949
+ const message = truncateCells(stash.message, messageWidth - (deleting ? 2 : 0));
38950
+ // ref + message read as primary; age / branch / file-count are
38951
+ // dim metadata (kept dim even on the bold selected row so the
38952
+ // message stays the focal point).
38873
38953
  return h(Text, {
38874
38954
  key: `stash-${index}`,
38875
38955
  bold: isSelected,
38876
38956
  dimColor: !isSelected,
38877
- }, truncateCells(rowText, rowWidth - (deleting ? 2 : 0)), spinnerSpan);
38957
+ }, `${cursor} `, cell(stash.ref, refCol), showAge ? h(Text, { dimColor: true }, `${' '.repeat(GAP)}${cell(ageOf(stash), ageCol, 'right')}`) : null, branchCol > 0 ? h(Text, { dimColor: true }, `${' '.repeat(GAP)}${cell(stash.branch || '', branchCol)}`) : null, showFiles ? h(Text, { dimColor: true }, `${' '.repeat(GAP)}${cell(String(stash.files.length), filesCol, 'right')}`) : null, `${' '.repeat(GAP)}${message}`, spinnerSpan);
38878
38958
  });
38879
38959
  const stashHasMoreAbove = startIndex > 0 && stashes.length > 0;
38880
38960
  const stashHasMoreBelow = startIndex + listRows < stashes.length;
@@ -38885,7 +38965,11 @@ function renderStashSurface(ctx, spinnerFrame = 0) {
38885
38965
  flexShrink: 0,
38886
38966
  paddingX: 1,
38887
38967
  width,
38888
- }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Stash', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...(stashHasMoreAbove
38968
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Stash', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme),
38969
+ // Column header — only when there are rows to label.
38970
+ ...(!loading && stashes.length > 0
38971
+ ? [h(Text, { key: 'stash-col-header', dimColor: true }, truncateCells(headerText, contentWidth))]
38972
+ : []), ...(stashHasMoreAbove
38889
38973
  ? [h(Text, { key: 'stash-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
38890
38974
  : []), ...lines, ...(stashHasMoreBelow
38891
38975
  ? [h(Text, { key: 'stash-more-below', dimColor: true }, ` ↓ ${stashes.length - (startIndex + listRows)} more below`)]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-coco",
3
- "version": "0.62.2",
3
+ "version": "0.62.3",
4
4
  "description": "zero-effort git commits with coco.",
5
5
  "author": "gfargo <ghfargo@gmail.com>",
6
6
  "license": "MIT",