git-coco 0.56.0 → 0.57.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/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.56.0";
81
+ const BUILD_VERSION = "0.57.0";
82
82
 
83
83
  const isInteractive = (config) => {
84
84
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -2156,6 +2156,10 @@ const schema$1 = {
2156
2156
  "selection": {
2157
2157
  "type": "string"
2158
2158
  },
2159
+ "selectionForeground": {
2160
+ "type": "string",
2161
+ "description": "Foreground for text sitting on the `selection` background. Derived automatically from `selection` (black on light, white on dark) so the selected row stays readable regardless of the user's terminal default foreground — but can be overridden per theme via `options.colors`."
2162
+ },
2159
2163
  "success": {
2160
2164
  "type": "string"
2161
2165
  },
@@ -2199,7 +2203,25 @@ const schema$1 = {
2199
2203
  "vitesse-dark",
2200
2204
  "vesper",
2201
2205
  "flexoki",
2202
- "mellow"
2206
+ "mellow",
2207
+ "night-owl",
2208
+ "cobalt2",
2209
+ "oceanic-next",
2210
+ "catppuccin-macchiato",
2211
+ "gruvbox-light",
2212
+ "tokyo-night-day",
2213
+ "one-light",
2214
+ "ayu-light",
2215
+ "rose-pine-dawn",
2216
+ "everforest-light",
2217
+ "vitesse-light",
2218
+ "dayfox",
2219
+ "night-owl-light",
2220
+ "flexoki-light",
2221
+ "material-lighter",
2222
+ "papercolor-light",
2223
+ "modus-operandi",
2224
+ "quiet-light"
2203
2225
  ]
2204
2226
  }
2205
2227
  }
@@ -15229,10 +15251,19 @@ const CommitSplitPlanSchema = objectType({
15229
15251
  title: stringType().min(1),
15230
15252
  body: stringType().optional(),
15231
15253
  rationale: stringType().optional(),
15232
- files: arrayType(stringType()),
15233
- hunks: arrayType(stringType()),
15254
+ // Both optional: the model legitimately emits a group with *either*
15255
+ // `files` or `hunks` (a file-level vs hunk-level grouping), not always
15256
+ // both. Requiring both made Zod throw "Required" and the whole split
15257
+ // chain failed to parse before the refine could run. The refine below
15258
+ // still enforces "at least one", and every downstream consumer already
15259
+ // reads these as `group.files || []`. (Kept `.optional()` rather than
15260
+ // `.default([])` so the schema's input and output types stay identical
15261
+ // — `executeChainWithSchema` takes a `z.ZodSchema<T>`, which requires
15262
+ // that.)
15263
+ files: arrayType(stringType()).optional(),
15264
+ hunks: arrayType(stringType()).optional(),
15234
15265
  })
15235
- .refine((group) => group.files.length > 0 || group.hunks.length > 0, {
15266
+ .refine((group) => (group.files?.length ?? 0) > 0 || (group.hunks?.length ?? 0) > 0, {
15236
15267
  message: 'Each group must include at least one file or hunk',
15237
15268
  }))
15238
15269
  .min(1),
@@ -32197,6 +32228,10 @@ function renderBranchesSurface(h, components, state, context, contextStatus, bod
32197
32228
  // row's dim and read as quiet chrome.
32198
32229
  h(Text, { color: markerColor, dimColor: markerColor ? false : undefined }, marker.glyph), trailingName, h(Text, { dimColor: true }, timestampPadded), trailingDivergence);
32199
32230
  });
32231
+ // Scroll indicators — same "N more above/below" pattern as the
32232
+ // sidebar and help overlay so the user knows the list continues.
32233
+ const branchesHasMoreAbove = startIndex > 0 && localBranches.length > 0;
32234
+ const branchesHasMoreBelow = startIndex + listRows < localBranches.length;
32200
32235
  return h(Box, {
32201
32236
  borderColor: focusBorderColor(theme, focused),
32202
32237
  borderStyle: theme.borderStyle,
@@ -32204,7 +32239,11 @@ function renderBranchesSurface(h, components, state, context, contextStatus, bod
32204
32239
  flexShrink: 0,
32205
32240
  paddingX: 1,
32206
32241
  width,
32207
- }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Branches', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...lines);
32242
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Branches', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...(branchesHasMoreAbove
32243
+ ? [h(Text, { key: 'branches-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
32244
+ : []), ...lines, ...(branchesHasMoreBelow
32245
+ ? [h(Text, { key: 'branches-more-below', dimColor: true }, ` ↓ ${localBranches.length - (startIndex + listRows)} more below`)]
32246
+ : []));
32208
32247
  }
32209
32248
 
32210
32249
  /**
@@ -32977,12 +33016,19 @@ function renderDiffSurface(h, components, state, context, contextStatus, worktre
32977
33016
  // sees at a glance which file the cursor is inside.
32978
33017
  const isActive = absoluteIndex === activeStartLine;
32979
33018
  const arrow = theme.ascii ? '> ' : '▾ ';
33019
+ const activeHeader = isActive && focused && !theme.noColor;
32980
33020
  return h(Text, {
32981
33021
  key: `stash-diff-line-${absoluteIndex}`,
32982
33022
  bold: true,
32983
- color: theme.noColor ? undefined : theme.colors.accent,
32984
- backgroundColor: isActive && focused && !theme.noColor ? theme.colors.selection : undefined,
32985
- inverse: isActive && focused,
33023
+ // Active header sits on the selection bar with a
33024
+ // contrast-guaranteed foreground (matches history/status).
33025
+ // The old `inverse` swap turned the accent into the bar and
33026
+ // left the path in the selection color — low-contrast on
33027
+ // light themes (e.g. accent blue bar + light-gray text).
33028
+ color: activeHeader
33029
+ ? theme.colors.selectionForeground
33030
+ : (theme.noColor ? undefined : theme.colors.accent),
33031
+ backgroundColor: activeHeader ? theme.colors.selection : undefined,
32986
33032
  }, (() => {
32987
33033
  // Smart path truncation for the diff file header: keep
32988
33034
  // the leading arrow glyph and elide middle path
@@ -34060,7 +34106,7 @@ function formatHistoryFetchArgs(args) {
34060
34106
  * Returns the spans flat so the caller can splat them into the row's
34061
34107
  * outer Text alongside other segments without an extra wrapper.
34062
34108
  */
34063
- function renderTypedSubject(h, Text, text, theme, key) {
34109
+ function renderTypedSubject(h, Text, text, theme, key, suppressColor = false) {
34064
34110
  const parsed = parseConventionalCommitPrefix(text);
34065
34111
  if (!parsed) {
34066
34112
  return [h(Text, { key: `${key}-msg` }, text)];
@@ -34068,7 +34114,9 @@ function renderTypedSubject(h, Text, text, theme, key) {
34068
34114
  if (text.length < parsed.prefix.length) {
34069
34115
  return [h(Text, { key: `${key}-msg` }, text)];
34070
34116
  }
34071
- const color = getConventionalCommitColor(parsed, theme);
34117
+ // When the row is selected (inverted), suppress the type color so
34118
+ // text inherits the dark inverted foreground and stays readable.
34119
+ const color = suppressColor ? undefined : getConventionalCommitColor(parsed, theme);
34072
34120
  return [
34073
34121
  h(Text, { key: `${key}-type`, color, bold: parsed.breaking }, parsed.prefix),
34074
34122
  h(Text, { key: `${key}-rest` }, text.slice(parsed.prefix.length)),
@@ -34089,15 +34137,10 @@ function renderLaneSegmentSpans(h, Text, segments, theme, padTo, keyPrefix, opti
34089
34137
  const elements = [];
34090
34138
  let totalLen = 0;
34091
34139
  segments.forEach((seg, idx) => {
34092
- const laneColor = getLaneColor(seg.laneId, theme);
34140
+ const laneColor = options.suppressColor ? undefined : (getLaneColor(seg.laneId, theme) ?? muted);
34093
34141
  elements.push(h(Text, {
34094
34142
  key: `${keyPrefix}-${idx}`,
34095
- color: laneColor ?? muted,
34096
- // Ink does not cascade dimColor from a parent Text to children,
34097
- // so the caller's "this whole row should fade" intent has to
34098
- // travel here as an explicit flag (#831). Used for graph-only
34099
- // lane-closure rows, where the lane colors otherwise compete
34100
- // for attention with the commits they connect.
34143
+ color: laneColor,
34101
34144
  dimColor: options.forceDim || (theme.noColor && seg.laneId === undefined),
34102
34145
  }, seg.text));
34103
34146
  totalLen += seg.text.length;
@@ -34148,18 +34191,26 @@ function renderCommitHistoryRow(h, Text, commit, graph, graphWidth, selected, th
34148
34191
  const messageRoom = Math.max(8, totalWidth - fixedWidth - cellWidth(refsTrunc));
34149
34192
  const message = truncateCells(commit.message, messageRoom);
34150
34193
  const selectedBg = selected && !theme.noColor ? theme.colors.selection : undefined;
34151
- const accent = theme.noColor ? undefined : theme.colors.accent;
34152
- const muted = theme.noColor ? undefined : theme.colors.muted;
34194
+ // Don't use inverse it makes child colors unreadable. Instead, set a
34195
+ // background on the row AND an explicit, contrast-guaranteed foreground
34196
+ // (`selectionForeground`, derived from the selection bg) on the outer
34197
+ // span. Suppressing each child's own color to `undefined` then lets it
34198
+ // inherit that readable foreground — so the whole selected row stays
34199
+ // legible regardless of the user's terminal default foreground, which
34200
+ // is what the old "rely on the default fg" approach got wrong.
34201
+ const selectedFg = selected && !theme.noColor ? theme.colors.selectionForeground : undefined;
34202
+ const accent = selected ? undefined : (theme.noColor ? undefined : theme.colors.accent);
34203
+ const muted = selected ? undefined : (theme.noColor ? undefined : theme.colors.muted);
34153
34204
  // Lane-colored graph spans when full graph mode + non-ASCII rendering
34154
34205
  // is in play; otherwise fall back to the legacy single-muted span so
34155
34206
  // compact mode and legacy terminals stay visually unchanged.
34156
34207
  const graphChildren = laneSegments && !theme.ascii
34157
- ? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `c${index}`)
34158
- : [h(Text, { color: muted, dimColor: theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
34208
+ ? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `c${index}`, { suppressColor: selected })
34209
+ : [h(Text, { color: muted, dimColor: !selected && theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
34159
34210
  return h(Text, {
34160
34211
  key: `${commit.hash}-${index}`,
34161
34212
  backgroundColor: selectedBg,
34162
- inverse: selected,
34213
+ color: selectedFg,
34163
34214
  }, ...graphChildren, ' ',
34164
34215
  // "Just landed" marker — a single thick vertical bar in the
34165
34216
  // accent color before the short hash. Fades when the runtime
@@ -34181,11 +34232,11 @@ function renderCommitHistoryRow(h, Text, commit, graph, graphWidth, selected, th
34181
34232
  // Date column drops out entirely at `tight` density — no spacer
34182
34233
  // either, so the message column slides left into the freed cells.
34183
34234
  dateText
34184
- ? h(Text, { key: `${commit.hash}-${index}-date`, dimColor: true }, dateText, ' ')
34235
+ ? h(Text, { key: `${commit.hash}-${index}-date`, dimColor: !selected }, dateText, ' ')
34185
34236
  : null,
34186
34237
  // Branch chip prefix (full-graph mode only) lands right before the
34187
34238
  // message so the eye reads "branch · subject" as a unit.
34188
- chip.node, ...renderTypedSubject(h, Text, message, theme, `${commit.hash}-${index}-subj`), refsTrunc ? h(Text, { color: accent }, refsTrunc) : null);
34239
+ chip.node, ...renderTypedSubject(h, Text, message, theme, `${commit.hash}-${index}-subj`, selected), refsTrunc ? h(Text, { color: accent }, refsTrunc) : null);
34189
34240
  }
34190
34241
  /**
34191
34242
  * Stacked variant used at `rowMode='stacked'` (rail tier). Each
@@ -34200,9 +34251,13 @@ function renderCommitHistoryRow(h, Text, commit, graph, graphWidth, selected, th
34200
34251
  */
34201
34252
  function renderStackedCommitHistoryRow(h, Text, Box, commit, graph, graphWidth, selected, theme, index, panelWidth, fullGraph, now, laneSegments, isRecent = false, remoteNames) {
34202
34253
  const totalWidth = Math.max(20, panelWidth - 4);
34203
- const accent = theme.noColor ? undefined : theme.colors.accent;
34204
- const muted = theme.noColor ? undefined : theme.colors.muted;
34254
+ // Suppress child colors on selected rows so each span inherits the
34255
+ // contrast-guaranteed `selectionForeground` set on the line-1 span,
34256
+ // keeping the selected row readable against the selection bg.
34257
+ const accent = selected ? undefined : (theme.noColor ? undefined : theme.colors.accent);
34258
+ const muted = selected ? undefined : (theme.noColor ? undefined : theme.colors.muted);
34205
34259
  const selectedBg = selected && !theme.noColor ? theme.colors.selection : undefined;
34260
+ const selectedFg = selected && !theme.noColor ? theme.colors.selectionForeground : undefined;
34206
34261
  // Line 1 — subject row. Mostly mirrors the single-line layout but
34207
34262
  // skips the date and refs so the message has the whole tail to
34208
34263
  // itself. Branch chip rides between the hash and the subject the
@@ -34214,15 +34269,15 @@ function renderStackedCommitHistoryRow(h, Text, Box, commit, graph, graphWidth,
34214
34269
  const lineOneFixed = graphWidth + 1 + commit.shortHash.length + 1 + recentMarkerWidth + chip.width;
34215
34270
  const subject = truncateCells(commit.message, Math.max(8, totalWidth - lineOneFixed));
34216
34271
  const graphChildren = laneSegments && !theme.ascii
34217
- ? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `cs${index}`)
34218
- : [h(Text, { color: muted, dimColor: theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
34272
+ ? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `cs${index}`, { suppressColor: selected })
34273
+ : [h(Text, { color: muted, dimColor: !selected && theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
34219
34274
  const lineOne = h(Text, {
34220
34275
  key: `${commit.hash}-${index}-l1`,
34221
34276
  backgroundColor: selectedBg,
34222
- inverse: selected,
34277
+ color: selectedFg,
34223
34278
  }, ...graphChildren, ' ', isRecent
34224
34279
  ? h(Text, { color: accent, bold: true }, theme.ascii ? '* ' : '▎ ')
34225
- : null, h(Text, { color: accent, bold: selected || isRecent }, commit.shortHash), ' ', chip.node, ...renderTypedSubject(h, Text, subject, theme, `${commit.hash}-${index}-stk-subj`));
34280
+ : null, h(Text, { color: accent, bold: selected || isRecent }, commit.shortHash), ' ', chip.node, ...renderTypedSubject(h, Text, subject, theme, `${commit.hash}-${index}-stk-subj`, selected));
34226
34281
  // Line 2 — metadata row, padded to align with the start of the
34227
34282
  // shortHash on line 1 so the eye still groups them as one commit.
34228
34283
  // Selection background does not extend here so we don't get a thick
@@ -34275,8 +34330,11 @@ function renderPendingCommitRow(h, Text, worktree, selected, theme) {
34275
34330
  return h(Text, {
34276
34331
  key: 'pending-commit-row',
34277
34332
  bold: true,
34278
- color: theme.noColor ? undefined : theme.colors.accent,
34279
- inverse: selected,
34333
+ // On selection, swap to the contrast-guaranteed foreground so the
34334
+ // accent label doesn't wash out against the selection bar.
34335
+ color: selected && !theme.noColor
34336
+ ? theme.colors.selectionForeground
34337
+ : (theme.noColor ? undefined : theme.colors.accent),
34280
34338
  backgroundColor: selected && !theme.noColor ? theme.colors.selection : undefined,
34281
34339
  }, truncateCells(label, 140));
34282
34340
  }
@@ -34690,6 +34748,10 @@ function renderCommandPalette(h, components, state, width, theme, focused) {
34690
34748
  dimColor: !isSelected,
34691
34749
  }, truncateCells(line, width - 4));
34692
34750
  });
34751
+ // Scroll indicators for the palette list — same pattern as the
34752
+ // sidebar and help overlay so the user knows there's more content.
34753
+ const paletteHasMoreAbove = startIndex > 0 && filtered.length > 0;
34754
+ const paletteHasMoreBelow = startIndex + listRows < filtered.length;
34693
34755
  return h(Box, {
34694
34756
  borderColor: focusBorderColor(theme, focused),
34695
34757
  borderStyle: theme.borderStyle,
@@ -34698,7 +34760,11 @@ function renderCommandPalette(h, components, state, width, theme, focused) {
34698
34760
  paddingX: 1,
34699
34761
  }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Command palette', focused)), h(Text, { dimColor: true }, matchSummary)), h(Text, { color: theme.colors.accent }, truncateCells(inputLine, width - 4)), h(Text, { dimColor: true }, truncateCells(hint, width - 4)), h(Text, undefined, ''), ...(showingRecent
34700
34762
  ? [h(Text, { key: 'palette-recent-hint', dimColor: true }, '· marks recently-used')]
34701
- : []), ...itemLines);
34763
+ : []), ...(paletteHasMoreAbove
34764
+ ? [h(Text, { key: 'palette-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
34765
+ : []), ...itemLines, ...(paletteHasMoreBelow
34766
+ ? [h(Text, { key: 'palette-more-below', dimColor: true }, ` ↓ ${filtered.length - (startIndex + listRows)} more below`)]
34767
+ : []));
34702
34768
  }
34703
34769
  /**
34704
34770
  * Split-plan overlay (#907) — renders the proposed commit groups for
@@ -35554,6 +35620,8 @@ function renderStashSurface(h, components, state, context, contextStatus, bodyRo
35554
35620
  dimColor: !isSelected,
35555
35621
  }, truncateCells(`${cursor} ${stash.ref.padEnd(12)} ${stash.message}`, 140));
35556
35622
  });
35623
+ const stashHasMoreAbove = startIndex > 0 && stashes.length > 0;
35624
+ const stashHasMoreBelow = startIndex + listRows < stashes.length;
35557
35625
  return h(Box, {
35558
35626
  borderColor: focusBorderColor(theme, focused),
35559
35627
  borderStyle: theme.borderStyle,
@@ -35561,7 +35629,11 @@ function renderStashSurface(h, components, state, context, contextStatus, bodyRo
35561
35629
  flexShrink: 0,
35562
35630
  paddingX: 1,
35563
35631
  width,
35564
- }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Stash', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...lines);
35632
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Stash', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...(stashHasMoreAbove
35633
+ ? [h(Text, { key: 'stash-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
35634
+ : []), ...lines, ...(stashHasMoreBelow
35635
+ ? [h(Text, { key: 'stash-more-below', dimColor: true }, ` ↓ ${stashes.length - (startIndex + listRows)} more below`)]
35636
+ : []));
35565
35637
  }
35566
35638
 
35567
35639
  /**
@@ -35657,7 +35729,7 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
35657
35729
  bold: true,
35658
35730
  dimColor: !headerSelected && rowIndex > cursorRowIndex,
35659
35731
  backgroundColor: headerSelected && !theme.noColor ? theme.colors.selection : undefined,
35660
- inverse: headerSelected,
35732
+ color: headerSelected && !theme.noColor ? theme.colors.selectionForeground : undefined,
35661
35733
  }, truncateCells(text, 140));
35662
35734
  }
35663
35735
  const isSelected = !headerFocused && row.flatIndex === selectedIndex;
@@ -35677,8 +35749,11 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
35677
35749
  key: `status-file-${row.flatIndex}-${rowIndex}`,
35678
35750
  dimColor: !isSelected && rowIndex > cursorRowIndex,
35679
35751
  backgroundColor: isSelected && focused && !theme.noColor ? theme.colors.selection : undefined,
35680
- inverse: isSelected && focused,
35681
- }, ` ${cursorPart}`, ...(useDot ? [h(Text, { color: dotColor }, STAGE_STATUS_DOT), ' '] : []), tailTrunc);
35752
+ color: isSelected && focused && !theme.noColor ? theme.colors.selectionForeground : undefined,
35753
+ }, ` ${cursorPart}`,
35754
+ // Suppress the dot's own color on selected rows so it inherits the
35755
+ // contrast-guaranteed selection foreground set on the row span.
35756
+ ...(useDot ? [h(Text, { color: (isSelected && focused) ? undefined : dotColor }, STAGE_STATUS_DOT), ' '] : []), tailTrunc);
35682
35757
  });
35683
35758
  // When the mask narrows the list to nothing but the underlying repo
35684
35759
  // is non-clean, surface why the panel looks empty so the user can
@@ -35693,6 +35768,10 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
35693
35768
  : cleanHint
35694
35769
  ? [cleanHint]
35695
35770
  : ['Worktree clean'];
35771
+ // Scroll indicators for the status file list — same pattern as
35772
+ // branches and the sidebar so the user knows there's more content.
35773
+ const statusHasMoreAbove = windowStart > 0 && surfaceRows.length > 0;
35774
+ const statusHasMoreBelow = windowStart + listRows < surfaceRows.length;
35696
35775
  return h(Box, {
35697
35776
  borderColor: focusBorderColor(theme, focused),
35698
35777
  borderStyle: theme.borderStyle,
@@ -35708,7 +35787,11 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
35708
35787
  // never touch the filter.
35709
35788
  ...(isStatusFilterMaskActive(state.statusFilterMask)
35710
35789
  ? [h(Text, { key: 'status-mask-indicator', dimColor: true }, `filter: ${formatStatusFilterMask(state.statusFilterMask)} (1/2/3 to toggle)`)]
35711
- : []), ...renderedRows, ...fallbackLines.map((line, index) => h(Text, {
35790
+ : []), ...(statusHasMoreAbove
35791
+ ? [h(Text, { key: 'status-more-above', dimColor: true }, ` ↑ ${windowStart} more above`)]
35792
+ : []), ...renderedRows, ...(statusHasMoreBelow
35793
+ ? [h(Text, { key: 'status-more-below', dimColor: true }, ` ↓ ${surfaceRows.length - (windowStart + listRows)} more below`)]
35794
+ : []), ...fallbackLines.map((line, index) => h(Text, {
35712
35795
  key: `status-surface-fallback-${index}`,
35713
35796
  dimColor: index > 0,
35714
35797
  }, truncateCells(line, 140))));
@@ -35938,6 +36021,8 @@ function renderTagsSurface(h, components, state, context, contextStatus, bodyRow
35938
36021
  dimColor: !isSelected,
35939
36022
  }, before, formatHyperlink(namePadded, url), after);
35940
36023
  });
36024
+ const tagsHasMoreAbove = startIndex > 0 && tags.length > 0;
36025
+ const tagsHasMoreBelow = startIndex + listRows < tags.length;
35941
36026
  return h(Box, {
35942
36027
  borderColor: focusBorderColor(theme, focused),
35943
36028
  borderStyle: theme.borderStyle,
@@ -35945,7 +36030,11 @@ function renderTagsSurface(h, components, state, context, contextStatus, bodyRow
35945
36030
  flexShrink: 0,
35946
36031
  paddingX: 1,
35947
36032
  width,
35948
- }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Tags', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...lines);
36033
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Tags', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...(tagsHasMoreAbove
36034
+ ? [h(Text, { key: 'tags-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
36035
+ : []), ...lines, ...(tagsHasMoreBelow
36036
+ ? [h(Text, { key: 'tags-more-below', dimColor: true }, ` ↓ ${tags.length - (startIndex + listRows)} more below`)]
36037
+ : []));
35949
36038
  }
35950
36039
 
35951
36040
  /**
@@ -36464,12 +36553,17 @@ function renderInspectorActionsSection(h, Text, context, width, theme, options =
36464
36553
  h(Text, { key: 'actions-title' }, cursorActive ? '[Actions]' : 'Actions:'),
36465
36554
  ...actions.map((action, index) => {
36466
36555
  const isSelected = cursorActive && index === cursorIndex;
36556
+ // On the selected row, swap every span to the contrast-guaranteed
36557
+ // selection foreground so the key glyph / destructive marker don't
36558
+ // wash out against the selection bar; the row is already highlighted,
36559
+ // and the label text still conveys which actions are destructive.
36560
+ const selectedFg = isSelected && !theme.noColor ? theme.colors.selectionForeground : undefined;
36467
36561
  const keyCell = action.key.padEnd(KEY_COLUMN);
36468
36562
  const label = truncateCells(action.label, labelBudget);
36469
36563
  const children = [
36470
36564
  h(Text, {
36471
36565
  key: `actions-${index}-key`,
36472
- color: action.destructive ? theme.colors.danger : theme.colors.accent,
36566
+ color: selectedFg ?? (action.destructive ? theme.colors.danger : theme.colors.accent),
36473
36567
  }, keyCell),
36474
36568
  GAP,
36475
36569
  label,
@@ -36477,14 +36571,14 @@ function renderInspectorActionsSection(h, Text, context, width, theme, options =
36477
36571
  if (action.destructive) {
36478
36572
  children.push(h(Text, {
36479
36573
  key: `actions-${index}-mark`,
36480
- color: theme.colors.danger,
36574
+ color: selectedFg ?? theme.colors.danger,
36481
36575
  dimColor: false,
36482
36576
  }, DESTRUCTIVE_SUFFIX));
36483
36577
  }
36484
36578
  return h(Text, {
36485
36579
  key: `actions-${index}`,
36486
36580
  backgroundColor: isSelected && !theme.noColor ? theme.colors.selection : undefined,
36487
- inverse: isSelected,
36581
+ color: selectedFg,
36488
36582
  }, ...children);
36489
36583
  }),
36490
36584
  ];
@@ -36561,7 +36655,6 @@ function renderCommitFileList(h, Text, files, selectedIndex, focused, maxRows, w
36561
36655
  return h(Text, {
36562
36656
  key: `commit-file-${index}`,
36563
36657
  color: statusCodeColor(file.status, theme),
36564
- inverse: isSelected && focused && !theme.noColor,
36565
36658
  bold: isSelected,
36566
36659
  }, label);
36567
36660
  });
@@ -41153,8 +41246,9 @@ function LogInkApp(deps) {
41153
41246
  if (group.rationale)
41154
41247
  lines += 2;
41155
41248
  lines += (group.files?.length || 0) + 1;
41156
- if ((group.hunks?.length || 0) > 0)
41157
- lines += group.hunks.length + 1;
41249
+ const hunkCount = group.hunks?.length || 0;
41250
+ if (hunkCount > 0)
41251
+ lines += hunkCount + 1;
41158
41252
  return sum + lines;
41159
41253
  }, 0)
41160
41254
  : undefined,
@@ -41319,7 +41413,7 @@ function getColorLevel(env = process.env) {
41319
41413
  return '256';
41320
41414
  return '16';
41321
41415
  }
41322
- const TRUECOLOR_PRESETS = new Set(['catppuccin', 'gruvbox', 'dracula', 'nord', 'solarized-dark', 'tokyo-night', 'one-dark', 'rose-pine', 'kanagawa', 'everforest', 'monokai', 'synthwave', 'ayu-dark', 'palenight', 'github-dark', 'horizon', 'nightfox', 'carbonfox', 'tokyonight-storm', 'catppuccin-latte', 'solarized-light', 'github-light', 'iceberg', 'material-ocean', 'moonlight', 'poimandres', 'vitesse-dark', 'vesper', 'flexoki', 'mellow']);
41416
+ const TRUECOLOR_PRESETS = new Set(['catppuccin', 'gruvbox', 'dracula', 'nord', 'solarized-dark', 'tokyo-night', 'one-dark', 'rose-pine', 'kanagawa', 'everforest', 'monokai', 'synthwave', 'ayu-dark', 'palenight', 'github-dark', 'horizon', 'nightfox', 'carbonfox', 'tokyonight-storm', 'catppuccin-latte', 'solarized-light', 'github-light', 'iceberg', 'material-ocean', 'moonlight', 'poimandres', 'vitesse-dark', 'vesper', 'flexoki', 'mellow', 'night-owl', 'cobalt2', 'oceanic-next', 'catppuccin-macchiato', 'gruvbox-light', 'tokyo-night-day', 'one-light', 'ayu-light', 'rose-pine-dawn', 'everforest-light', 'vitesse-light', 'dayfox', 'night-owl-light', 'flexoki-light', 'material-lighter', 'papercolor-light', 'modus-operandi', 'quiet-light']);
41323
41417
  /**
41324
41418
  * `true` when the named preset relies on hex colors that look best under
41325
41419
  * 24-bit RGB. Used by `createLogInkTheme` to decide whether to downgrade
@@ -41328,6 +41422,45 @@ const TRUECOLOR_PRESETS = new Set(['catppuccin', 'gruvbox', 'dracula', 'nord', '
41328
41422
  function presetUsesTrueColor(preset) {
41329
41423
  return preset !== undefined && TRUECOLOR_PRESETS.has(preset);
41330
41424
  }
41425
+ /**
41426
+ * WCAG 2.x relative luminance of a `#rrggbb` color, 0 (black) … 1 (white).
41427
+ * Returns `null` for anything that isn't a 6-digit hex (e.g. ANSI-named
41428
+ * colors), so callers can fall back rather than guess.
41429
+ */
41430
+ function relativeLuminance(hex) {
41431
+ const match = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
41432
+ if (!match)
41433
+ return null;
41434
+ const int = parseInt(match[1], 16);
41435
+ const channel = (c) => {
41436
+ const x = c / 255;
41437
+ return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
41438
+ };
41439
+ const r = channel((int >> 16) & 0xff);
41440
+ const g = channel((int >> 8) & 0xff);
41441
+ const b = channel(int & 0xff);
41442
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
41443
+ }
41444
+ /**
41445
+ * Pick a foreground guaranteed to stay readable on `bg` — black for light
41446
+ * backgrounds, white for dark ones. The 0.179 threshold is the luminance
41447
+ * crossover where black and white yield identical contrast, so the choice
41448
+ * always maximizes it; every background clears WCAG AA (≥ 4.5:1).
41449
+ *
41450
+ * This is how the selected-row text stays legible across every theme:
41451
+ * coco controls the selection *background* but not the user's terminal
41452
+ * default foreground, so it must supply its own contrasting foreground
41453
+ * instead of hoping the terminal's happens to contrast. Returns
41454
+ * `undefined` for non-hex backgrounds (let the caller leave color alone).
41455
+ */
41456
+ function readableForegroundFor(bg) {
41457
+ if (!bg)
41458
+ return undefined;
41459
+ const luminance = relativeLuminance(bg);
41460
+ if (luminance === null)
41461
+ return undefined;
41462
+ return luminance > 0.179 ? '#000000' : '#ffffff';
41463
+ }
41331
41464
 
41332
41465
  const THEME_PRESET_COLORS = {
41333
41466
  default: {
@@ -41340,7 +41473,7 @@ const THEME_PRESET_COLORS = {
41340
41473
  gitModified: 'yellow',
41341
41474
  info: 'blue',
41342
41475
  muted: 'gray',
41343
- selection: 'cyan',
41476
+ selection: '#1a3a4a',
41344
41477
  success: 'green',
41345
41478
  warning: 'yellow',
41346
41479
  },
@@ -41764,6 +41897,258 @@ const THEME_PRESET_COLORS = {
41764
41897
  success: '#a3d4a0',
41765
41898
  warning: '#f0c674',
41766
41899
  },
41900
+ 'night-owl': {
41901
+ accent: '#82aaff',
41902
+ border: '#1d3b53',
41903
+ danger: '#ef5350',
41904
+ focusBorder: '#7fdbca',
41905
+ gitAdded: '#addb67',
41906
+ gitDeleted: '#ef5350',
41907
+ gitModified: '#ecc48d',
41908
+ info: '#82aaff',
41909
+ muted: '#637777',
41910
+ selection: '#1d3b53',
41911
+ success: '#addb67',
41912
+ warning: '#ecc48d',
41913
+ },
41914
+ cobalt2: {
41915
+ accent: '#ffc600',
41916
+ border: '#234e6d',
41917
+ danger: '#ff628c',
41918
+ focusBorder: '#9effff',
41919
+ gitAdded: '#3ad900',
41920
+ gitDeleted: '#ff628c',
41921
+ gitModified: '#ffc600',
41922
+ info: '#9effff',
41923
+ muted: '#627e99',
41924
+ selection: '#0d3a58',
41925
+ success: '#3ad900',
41926
+ warning: '#ffc600',
41927
+ },
41928
+ 'oceanic-next': {
41929
+ accent: '#6699cc',
41930
+ border: '#343d46',
41931
+ danger: '#ec5f67',
41932
+ focusBorder: '#5fb3b3',
41933
+ gitAdded: '#99c794',
41934
+ gitDeleted: '#ec5f67',
41935
+ gitModified: '#fac863',
41936
+ info: '#6699cc',
41937
+ muted: '#65737e',
41938
+ selection: '#4f5b66',
41939
+ success: '#99c794',
41940
+ warning: '#fac863',
41941
+ },
41942
+ 'catppuccin-macchiato': {
41943
+ accent: '#8aadf4',
41944
+ border: '#494d64',
41945
+ danger: '#ed8796',
41946
+ focusBorder: '#91d7e3',
41947
+ gitAdded: '#a6da95',
41948
+ gitDeleted: '#ed8796',
41949
+ gitModified: '#eed49f',
41950
+ info: '#8aadf4',
41951
+ muted: '#6e738d',
41952
+ selection: '#363a4f',
41953
+ success: '#a6da95',
41954
+ warning: '#eed49f',
41955
+ },
41956
+ 'gruvbox-light': {
41957
+ accent: '#076678',
41958
+ border: '#bdae93',
41959
+ danger: '#9d0006',
41960
+ focusBorder: '#427b58',
41961
+ gitAdded: '#79740e',
41962
+ gitDeleted: '#9d0006',
41963
+ gitModified: '#b57614',
41964
+ info: '#076678',
41965
+ muted: '#7c6f64',
41966
+ selection: '#ebdbb2',
41967
+ success: '#79740e',
41968
+ warning: '#b57614',
41969
+ },
41970
+ 'tokyo-night-day': {
41971
+ accent: '#2e7de9',
41972
+ border: '#b7c1e3',
41973
+ danger: '#f52a65',
41974
+ focusBorder: '#007197',
41975
+ gitAdded: '#587539',
41976
+ gitDeleted: '#f52a65',
41977
+ gitModified: '#8c6c3e',
41978
+ info: '#2e7de9',
41979
+ muted: '#848cb5',
41980
+ selection: '#b7c1e3',
41981
+ success: '#587539',
41982
+ warning: '#8c6c3e',
41983
+ },
41984
+ 'one-light': {
41985
+ accent: '#4078f2',
41986
+ border: '#d4d4d4',
41987
+ danger: '#e45649',
41988
+ focusBorder: '#0184bc',
41989
+ gitAdded: '#50a14f',
41990
+ gitDeleted: '#e45649',
41991
+ gitModified: '#c18401',
41992
+ info: '#4078f2',
41993
+ muted: '#a0a1a7',
41994
+ selection: '#e5e5e6',
41995
+ success: '#50a14f',
41996
+ warning: '#c18401',
41997
+ },
41998
+ 'ayu-light': {
41999
+ accent: '#fa8d3e',
42000
+ border: '#e6e6e6',
42001
+ danger: '#e65050',
42002
+ focusBorder: '#4cbf99',
42003
+ gitAdded: '#6cbf43',
42004
+ gitDeleted: '#e65050',
42005
+ gitModified: '#f2ae49',
42006
+ info: '#399ee6',
42007
+ muted: '#abb0b6',
42008
+ selection: '#d1e4f4',
42009
+ success: '#6cbf43',
42010
+ warning: '#f2ae49',
42011
+ },
42012
+ 'rose-pine-dawn': {
42013
+ accent: '#907aa9',
42014
+ border: '#dfdad9',
42015
+ danger: '#b4637a',
42016
+ focusBorder: '#56949f',
42017
+ gitAdded: '#286983',
42018
+ gitDeleted: '#b4637a',
42019
+ gitModified: '#ea9d34',
42020
+ info: '#56949f',
42021
+ muted: '#9893a5',
42022
+ selection: '#dfdad9',
42023
+ success: '#286983',
42024
+ warning: '#ea9d34',
42025
+ },
42026
+ 'everforest-light': {
42027
+ accent: '#8da101',
42028
+ border: '#ddd8be',
42029
+ danger: '#f85552',
42030
+ focusBorder: '#35a77c',
42031
+ gitAdded: '#8da101',
42032
+ gitDeleted: '#f85552',
42033
+ gitModified: '#dfa000',
42034
+ info: '#3a94c5',
42035
+ muted: '#939f91',
42036
+ selection: '#edeada',
42037
+ success: '#8da101',
42038
+ warning: '#dfa000',
42039
+ },
42040
+ 'vitesse-light': {
42041
+ accent: '#1e754f',
42042
+ border: '#e0e0e0',
42043
+ danger: '#ab5959',
42044
+ focusBorder: '#2993a3',
42045
+ gitAdded: '#1e754f',
42046
+ gitDeleted: '#ab5959',
42047
+ gitModified: '#b07d48',
42048
+ info: '#296aa3',
42049
+ muted: '#999fa6',
42050
+ selection: '#eaeaeb',
42051
+ success: '#1e754f',
42052
+ warning: '#b07d48',
42053
+ },
42054
+ dayfox: {
42055
+ accent: '#2848a9',
42056
+ border: '#e4dcd4',
42057
+ danger: '#a5222f',
42058
+ focusBorder: '#287980',
42059
+ gitAdded: '#396847',
42060
+ gitDeleted: '#a5222f',
42061
+ gitModified: '#ac5402',
42062
+ info: '#2848a9',
42063
+ muted: '#908479',
42064
+ selection: '#e7d2be',
42065
+ success: '#396847',
42066
+ warning: '#ac5402',
42067
+ },
42068
+ 'night-owl-light': {
42069
+ accent: '#288ed7',
42070
+ border: '#d9d9d9',
42071
+ danger: '#d3423e',
42072
+ focusBorder: '#2aa298',
42073
+ gitAdded: '#08916a',
42074
+ gitDeleted: '#d3423e',
42075
+ gitModified: '#daaa01',
42076
+ info: '#288ed7',
42077
+ muted: '#989fb1',
42078
+ selection: '#e4e8f0',
42079
+ success: '#08916a',
42080
+ warning: '#daaa01',
42081
+ },
42082
+ 'flexoki-light': {
42083
+ accent: '#205ea6',
42084
+ border: '#cecdc3',
42085
+ danger: '#af3029',
42086
+ focusBorder: '#24837b',
42087
+ gitAdded: '#66800b',
42088
+ gitDeleted: '#af3029',
42089
+ gitModified: '#ad8301',
42090
+ info: '#205ea6',
42091
+ muted: '#6f6e69',
42092
+ selection: '#e6e4d9',
42093
+ success: '#66800b',
42094
+ warning: '#ad8301',
42095
+ },
42096
+ 'material-lighter': {
42097
+ accent: '#39adb5',
42098
+ border: '#e7eaec',
42099
+ danger: '#e53935',
42100
+ focusBorder: '#39adb5',
42101
+ gitAdded: '#91b859',
42102
+ gitDeleted: '#e53935',
42103
+ gitModified: '#f6a434',
42104
+ info: '#6182b8',
42105
+ muted: '#90a4ae',
42106
+ selection: '#d3e1e8',
42107
+ success: '#91b859',
42108
+ warning: '#f6a434',
42109
+ },
42110
+ 'papercolor-light': {
42111
+ accent: '#0087af',
42112
+ border: '#d7d7d7',
42113
+ danger: '#af0000',
42114
+ focusBorder: '#005f87',
42115
+ gitAdded: '#008700',
42116
+ gitDeleted: '#af0000',
42117
+ gitModified: '#d75f00',
42118
+ info: '#0087af',
42119
+ muted: '#878787',
42120
+ selection: '#d0d0d0',
42121
+ success: '#008700',
42122
+ warning: '#d75f00',
42123
+ },
42124
+ 'modus-operandi': {
42125
+ accent: '#0031a9',
42126
+ border: '#d7d7d7',
42127
+ danger: '#a60000',
42128
+ focusBorder: '#005e8b',
42129
+ gitAdded: '#006800',
42130
+ gitDeleted: '#a60000',
42131
+ gitModified: '#6f5500',
42132
+ info: '#0031a9',
42133
+ muted: '#595959',
42134
+ selection: '#c0deff',
42135
+ success: '#006800',
42136
+ warning: '#6f5500',
42137
+ },
42138
+ 'quiet-light': {
42139
+ accent: '#4b83cd',
42140
+ border: '#e0e0e0',
42141
+ danger: '#aa3731',
42142
+ focusBorder: '#4b83cd',
42143
+ gitAdded: '#448c27',
42144
+ gitDeleted: '#aa3731',
42145
+ gitModified: '#a67d00',
42146
+ info: '#4b83cd',
42147
+ muted: '#a3a6ad',
42148
+ selection: '#c9d0d9',
42149
+ success: '#448c27',
42150
+ warning: '#a67d00',
42151
+ },
41767
42152
  };
41768
42153
  function shouldUseAscii(term) {
41769
42154
  if (!term) {
@@ -41788,8 +42173,28 @@ function createLogInkTheme(options = {}) {
41788
42173
  ? {}
41789
42174
  : {
41790
42175
  ...THEME_PRESET_COLORS[preset],
42176
+ // Preserve the requested theme's selection background even when the
42177
+ // rest of the palette downgrades to `default`. The selection is a
42178
+ // single background color the terminal can approximate; without this,
42179
+ // a light theme inherits `default`'s dark selection (#1a3a4a) and the
42180
+ // selected row renders as a dark bar on a light background.
42181
+ ...(preset !== requestedPreset && THEME_PRESET_COLORS[requestedPreset]?.selection
42182
+ ? { selection: THEME_PRESET_COLORS[requestedPreset].selection }
42183
+ : {}),
41791
42184
  ...options.colors,
41792
42185
  };
42186
+ // Derive a contrasting foreground for the selected row from its own
42187
+ // selection background, unless the caller supplied one explicitly. coco
42188
+ // owns the selection background but not the terminal's default foreground,
42189
+ // so without this the selected row's text falls back to whatever the
42190
+ // user's terminal foreground is — which may not contrast with the bar at
42191
+ // all (the bug behind unreadable selected rows on many themes).
42192
+ if (!noColor && colors.selection && !colors.selectionForeground) {
42193
+ const selectionForeground = readableForegroundFor(colors.selection);
42194
+ if (selectionForeground) {
42195
+ colors.selectionForeground = selectionForeground;
42196
+ }
42197
+ }
41793
42198
  return {
41794
42199
  noColor,
41795
42200
  ascii,
@@ -43418,7 +43823,7 @@ const options$1 = {
43418
43823
  },
43419
43824
  theme: {
43420
43825
  description: 'TUI theme preset',
43421
- choices: ['default', 'monochrome', 'catppuccin', 'gruvbox', 'dracula', 'nord', 'solarized-dark', 'tokyo-night', 'one-dark', 'rose-pine', 'kanagawa', 'everforest', 'monokai', 'synthwave', 'ayu-dark', 'palenight', 'github-dark', 'horizon', 'nightfox', 'carbonfox', 'tokyonight-storm', 'catppuccin-latte', 'solarized-light', 'github-light', 'iceberg', 'material-ocean', 'moonlight', 'poimandres', 'vitesse-dark', 'vesper', 'flexoki', 'mellow'],
43826
+ choices: ['default', 'monochrome', 'catppuccin', 'gruvbox', 'dracula', 'nord', 'solarized-dark', 'tokyo-night', 'one-dark', 'rose-pine', 'kanagawa', 'everforest', 'monokai', 'synthwave', 'ayu-dark', 'palenight', 'github-dark', 'horizon', 'nightfox', 'carbonfox', 'tokyonight-storm', 'catppuccin-latte', 'solarized-light', 'github-light', 'iceberg', 'material-ocean', 'moonlight', 'poimandres', 'vitesse-dark', 'vesper', 'flexoki', 'mellow', 'night-owl', 'cobalt2', 'oceanic-next', 'catppuccin-macchiato', 'gruvbox-light', 'tokyo-night-day', 'one-light', 'ayu-light', 'rose-pine-dawn', 'everforest-light', 'vitesse-light', 'dayfox', 'night-owl-light', 'flexoki-light', 'material-lighter', 'papercolor-light', 'modus-operandi', 'quiet-light'],
43422
43827
  },
43423
43828
  };
43424
43829
  const builder$1 = (yargs) => {
@@ -43446,7 +43851,7 @@ const options = {
43446
43851
  },
43447
43852
  theme: {
43448
43853
  description: 'TUI theme preset',
43449
- choices: ['default', 'monochrome', 'catppuccin', 'gruvbox', 'dracula', 'nord', 'solarized-dark', 'tokyo-night', 'one-dark', 'rose-pine', 'kanagawa', 'everforest', 'monokai', 'synthwave', 'ayu-dark', 'palenight', 'github-dark', 'horizon', 'nightfox', 'carbonfox', 'tokyonight-storm', 'catppuccin-latte', 'solarized-light', 'github-light', 'iceberg', 'material-ocean', 'moonlight', 'poimandres', 'vitesse-dark', 'vesper', 'flexoki', 'mellow'],
43854
+ choices: ['default', 'monochrome', 'catppuccin', 'gruvbox', 'dracula', 'nord', 'solarized-dark', 'tokyo-night', 'one-dark', 'rose-pine', 'kanagawa', 'everforest', 'monokai', 'synthwave', 'ayu-dark', 'palenight', 'github-dark', 'horizon', 'nightfox', 'carbonfox', 'tokyonight-storm', 'catppuccin-latte', 'solarized-light', 'github-light', 'iceberg', 'material-ocean', 'moonlight', 'poimandres', 'vitesse-dark', 'vesper', 'flexoki', 'mellow', 'night-owl', 'cobalt2', 'oceanic-next', 'catppuccin-macchiato', 'gruvbox-light', 'tokyo-night-day', 'one-light', 'ayu-light', 'rose-pine-dawn', 'everforest-light', 'vitesse-light', 'dayfox', 'night-owl-light', 'flexoki-light', 'material-lighter', 'papercolor-light', 'modus-operandi', 'quiet-light'],
43450
43855
  },
43451
43856
  };
43452
43857
  const builder = (yargs) => {
@@ -44730,33 +45135,48 @@ function buildWorkspaceHeaderChips(state, options = { focusLabel: 'List' }) {
44730
45135
  });
44731
45136
  return chips;
44732
45137
  }
44733
- // Footer hints prioritize discoverable / forgettable actions and
44734
- // drop the bindings users can find on their own (arrow keys, Enter
44735
- // for "open", Tab for "switch panels"). The full keymap lives behind
44736
- // `?` so nothing is hidden, just decluttered.
44737
- const LIST_HINT = 's sort · / filter · r/R refresh · a add · d remove · ? help · q quit';
44738
- const SIDEBAR_HINT = '? help · q quit';
44739
- const FILTER_HINT = 'type to filter · enter apply · esc cancel';
44740
- const ADD_REPO_HINT = 'type path · tab to complete · enter to add · esc to cancel';
44741
- const CONFIRM_DELETE_HINT = 'press y to remove · any other key to cancel';
44742
- function hintFor(focus) {
45138
+ // Footer hints are split into two slots — same pattern as `coco ui`:
45139
+ // - contextual : per-mode actions (sort, filter, refresh, add/remove)
45140
+ // - global : always-on essentials (help, quit) never crowded out
45141
+ //
45142
+ // The contextual slot drops bindings users can find via the help
45143
+ // overlay (arrow keys, tab); the global slot is the safety net so
45144
+ // `? help` and `q quit` never disappear.
45145
+ const LIST_CONTEXTUAL = ['s sort', '/ filter', 'r/R refresh', 'a add', 'd remove'];
45146
+ const SIDEBAR_CONTEXTUAL = ['↑/↓ cycle tab', 'enter open'];
45147
+ const FILTER_CONTEXTUAL = ['type to filter', 'enter apply', 'esc cancel'];
45148
+ const ADD_REPO_CONTEXTUAL = ['type path', 'tab to complete', 'enter to add', 'esc to cancel'];
45149
+ const CONFIRM_DELETE_CONTEXTUAL = ['y confirm', 'any other key cancels'];
45150
+ const GLOBAL_HINTS = ['? help', 'q quit'];
45151
+ function contextualHintsFor(focus) {
44743
45152
  switch (focus) {
44744
45153
  case 'sidebar':
44745
- return SIDEBAR_HINT;
45154
+ return SIDEBAR_CONTEXTUAL;
44746
45155
  case 'filter':
44747
- return FILTER_HINT;
45156
+ return FILTER_CONTEXTUAL;
44748
45157
  case 'add-repo':
44749
- return ADD_REPO_HINT;
45158
+ return ADD_REPO_CONTEXTUAL;
44750
45159
  case 'confirm-delete':
44751
- return CONFIRM_DELETE_HINT;
45160
+ return CONFIRM_DELETE_CONTEXTUAL;
44752
45161
  case 'list':
44753
45162
  default:
44754
- return LIST_HINT;
45163
+ return LIST_CONTEXTUAL;
44755
45164
  }
44756
45165
  }
44757
45166
  function buildWorkspaceFooter(state) {
45167
+ const contextual = contextualHintsFor(state.focus);
45168
+ // Modal modes (filter / add-repo / confirm-delete) suppress the
45169
+ // global hints — those bindings are not reachable while a prompt
45170
+ // is open and showing them would be misleading.
45171
+ const isModal = state.focus === 'filter' ||
45172
+ state.focus === 'add-repo' ||
45173
+ state.focus === 'confirm-delete';
45174
+ const global = isModal ? [] : GLOBAL_HINTS;
45175
+ const allHints = [...contextual, ...global];
44758
45176
  return {
44759
- hint: hintFor(state.focus),
45177
+ hint: allHints.join(' · '),
45178
+ contextual,
45179
+ global,
44760
45180
  status: state.status,
44761
45181
  filterMode: state.focus === 'filter',
44762
45182
  };
@@ -44766,11 +45186,25 @@ function buildWorkspaceFooter(state) {
44766
45186
  * sections so users can scan by intent ("how do I navigate?" "how
44767
45187
  * do I act?") rather than reading a flat alphabetized list.
44768
45188
  *
45189
+ * Section order mirrors `coco ui`'s help convention — Essentials
45190
+ * first so newcomers see `?`/`esc`/`q` immediately, then move outward
45191
+ * to navigation, modification, and the destructive verbs last.
45192
+ *
44769
45193
  * The view layer composes these into a panel with section titles,
44770
45194
  * a leading app/title bar, and a closing hint at the bottom.
44771
45195
  */
44772
45196
  function buildWorkspaceHelpSections() {
44773
45197
  return [
45198
+ {
45199
+ title: 'Essentials',
45200
+ subtitle: 'The keys you reach for most often.',
45201
+ rows: [
45202
+ { glyph: '?', keys: '?', description: 'Toggle this help overlay' },
45203
+ { glyph: '⎋', keys: 'esc', description: 'Clear filter / close overlay / cancel prompt' },
45204
+ { glyph: '◴', keys: 'q · ctrl+c', description: 'Quit the workspace surface' },
45205
+ { glyph: '↵', keys: 'enter', description: 'Drill into the cursored repo (coco ui)' },
45206
+ ],
45207
+ },
44774
45208
  {
44775
45209
  title: 'Navigate',
44776
45210
  subtitle: 'Move the cursor and switch focus between panels.',
@@ -44780,7 +45214,6 @@ function buildWorkspaceHelpSections() {
44780
45214
  { glyph: '←', keys: 'h', description: 'Jump focus to the sidebar' },
44781
45215
  { glyph: '→', keys: 'l', description: 'Jump focus to the list' },
44782
45216
  { glyph: '⤒', keys: 'g / G', description: 'Jump to top / bottom of the list' },
44783
- { glyph: '↵', keys: 'enter', description: 'Drill into the cursored repo (coco ui)' },
44784
45217
  ],
44785
45218
  },
44786
45219
  {
@@ -44800,14 +45233,6 @@ function buildWorkspaceHelpSections() {
44800
45233
  { glyph: '✕', keys: 'd', description: 'Remove the cursored repo from the known-repos store' },
44801
45234
  ],
44802
45235
  },
44803
- {
44804
- title: 'General',
44805
- rows: [
44806
- { glyph: '?', keys: '?', description: 'Toggle this help overlay' },
44807
- { glyph: '⎋', keys: 'esc', description: 'Clear filter / close overlay / cancel prompt' },
44808
- { glyph: '◴', keys: 'q · ctrl+c', description: 'Quit the workspace surface' },
44809
- ],
44810
- },
44811
45236
  ];
44812
45237
  }
44813
45238
  function buildWorkspaceOnboarding(state) {
@@ -45254,13 +45679,19 @@ function renderFooter(deps) {
45254
45679
  // height shifted by a row every time a status banner came and went,
45255
45680
  // forcing the panel chrome to reflow.
45256
45681
  const statusContent = model.status ?? '';
45682
+ const contextualText = model.contextual.join(' ');
45683
+ const globalText = model.global.join(' · ');
45257
45684
  return React.createElement(Box, {
45258
45685
  borderColor: focusBorderColor(theme, false),
45259
45686
  borderStyle: theme.borderStyle,
45260
45687
  paddingX: 1,
45261
45688
  flexDirection: 'column',
45262
45689
  height: FOOTER_HEIGHT,
45263
- }, React.createElement(Text, { dimColor: true }, model.hint), React.createElement(Text, {
45690
+ },
45691
+ // Row 1: contextual ↔ global hints. justifyContent pushes them
45692
+ // to opposite edges so the eye scans each cluster as one block —
45693
+ // same shape as `coco ui`'s footer post-0.54.2 redesign.
45694
+ React.createElement(Box, { flexDirection: 'row', justifyContent: 'space-between' }, React.createElement(Text, { dimColor: true }, contextualText), React.createElement(Text, { dimColor: true }, globalText)), React.createElement(Text, {
45264
45695
  color: model.status ? toneColor('warn', theme) : undefined,
45265
45696
  dimColor: !model.status,
45266
45697
  }, statusContent || ' '));
@@ -46563,6 +46994,148 @@ var workspace = {
46563
46994
  options,
46564
46995
  };
46565
46996
 
46997
+ /**
46998
+ * Default-command router for `coco` invoked with no positional
46999
+ * arguments. Decides where to send the user based on the state of
47000
+ * their machine:
47001
+ *
47002
+ * ┌─────────────────────────┬─────────────────────┬──────────────┐
47003
+ * │ Config present? │ In a git repo? │ Action │
47004
+ * ├─────────────────────────┼─────────────────────┼──────────────┤
47005
+ * │ No (default-only) │ — │ run `init` │
47006
+ * │ Yes │ Yes (worktree) │ run `ui` │
47007
+ * │ Yes │ No │ run `ws` │
47008
+ * └─────────────────────────┴─────────────────────┴──────────────┘
47009
+ *
47010
+ * The pre-existing default — fall through to `commit` — was hostile
47011
+ * to first-time users: a fresh install with no config landed
47012
+ * straight in the API-key error path, with no hint that `coco init`
47013
+ * was the right next step. Routing fresh installs to `init` and
47014
+ * configured users to the workstation/UI matches what every other
47015
+ * git-aware CLI does (lazygit, tig, gitui all open their TUI on bare
47016
+ * invocation).
47017
+ *
47018
+ * `coco commit` keeps its dedicated subcommand entry so existing
47019
+ * scripts (`git aliases`, hook integrations, CI jobs) that call
47020
+ * `coco commit` continue to work unchanged.
47021
+ *
47022
+ * The router is a thin shim — it forwards to the existing handlers
47023
+ * via their public exports rather than re-implementing the logic.
47024
+ */
47025
+ /**
47026
+ * Pure decision function — given probed signals (whether config
47027
+ * exists, whether the current directory is a git repo, whether the
47028
+ * user opted into legacy commit-by-default), decides which command
47029
+ * to invoke. Kept pure so unit tests can cover every quadrant of
47030
+ * the router table without spawning processes.
47031
+ *
47032
+ * "Config exists" is defined as: the loader detected at least one
47033
+ * source beyond `default` — i.e., the user has either a project
47034
+ * config, a git config `[coco]` section, an env var, or an XDG
47035
+ * config. A pure-defaults run is treated as "never been configured"
47036
+ * because `coco init` is the only way to populate any of those
47037
+ * sources.
47038
+ */
47039
+ function decideDefaultRoute(input) {
47040
+ if (input.envOverride === 'commit' || input.explicitCommit) {
47041
+ return {
47042
+ kind: 'commit',
47043
+ reason: input.envOverride === 'commit' ? 'env-override' : 'explicit-flag',
47044
+ };
47045
+ }
47046
+ if (!input.hasConfigSource) {
47047
+ return { kind: 'init', reason: 'no-config' };
47048
+ }
47049
+ if (input.isGitRepo) {
47050
+ return { kind: 'ui', reason: 'config-and-repo' };
47051
+ }
47052
+ return { kind: 'workspace', reason: 'config-no-repo' };
47053
+ }
47054
+ /**
47055
+ * Probe whether the cwd (after `--repo` is honored) is inside a git
47056
+ * worktree. Tolerant of every error class — a thrown simple-git
47057
+ * call should never block the router; it should fall back to
47058
+ * "not a repo" so the user lands somewhere sensible (workspace
47059
+ * surface) rather than crashing on an empty machine.
47060
+ */
47061
+ async function probeIsGitRepo() {
47062
+ try {
47063
+ // Lazy-import simple-git so the cold-start path stays fast for
47064
+ // users running `coco --help` / `coco doctor` etc.
47065
+ const { default: simpleGit } = await import('simple-git');
47066
+ const git = simpleGit();
47067
+ return await git.checkIsRepo();
47068
+ }
47069
+ catch {
47070
+ return false;
47071
+ }
47072
+ }
47073
+ /**
47074
+ * Build a synthetic argv for one of the targeted handlers. Each
47075
+ * handler reads its own typed argv contract (`CommitArgv`,
47076
+ * `InitArgv`, `UiArgv`, `WorkspaceArgv`) so we can't just spread the
47077
+ * raw default argv — we have to project the shared fields and let
47078
+ * the handler fill in command-specific defaults.
47079
+ */
47080
+ function buildSyntheticArgv(argv) {
47081
+ return {
47082
+ _: ['$0'],
47083
+ $0: argv.$0,
47084
+ repo: argv.repo,
47085
+ cwd: argv.cwd,
47086
+ verbose: argv.verbose ?? false,
47087
+ interactive: true,
47088
+ version: false,
47089
+ help: false,
47090
+ };
47091
+ }
47092
+ /**
47093
+ * Default-command handler installed under yargs's `$0` slot. Probes
47094
+ * the environment, computes the right route, and forwards to the
47095
+ * matching command handler. Falls through to commit if any
47096
+ * unexpected error blocks routing — preserves backwards-compat
47097
+ * for users on weird setups while still giving newcomers the
47098
+ * onboarding path they deserve.
47099
+ */
47100
+ const defaultRouteHandler = async (argv, logger) => {
47101
+ // The `--repo` flag has to land before any probe runs — otherwise
47102
+ // we'd sniff the launcher's cwd instead of the targeted repo.
47103
+ applyRepoCwd(argv);
47104
+ // Trigger a config load so `getConfigSources()` returns the active
47105
+ // source list. We discard the config object — the decision only
47106
+ // cares about which sources contributed.
47107
+ void loadConfig(argv);
47108
+ const sources = getConfigSources();
47109
+ const hasConfigSource = sources.some((source) => source.source !== 'default');
47110
+ const isGitRepo = await probeIsGitRepo();
47111
+ const decision = decideDefaultRoute({
47112
+ hasConfigSource,
47113
+ isGitRepo,
47114
+ explicitCommit: Boolean(argv.commit),
47115
+ envOverride: process.env.COCO_DEFAULT,
47116
+ });
47117
+ switch (decision.kind) {
47118
+ case 'init':
47119
+ // Friendly hint before the wizard kicks in — sets expectations
47120
+ // that the user is being walked through setup, not silently
47121
+ // routed to a different command.
47122
+ logger.log('No coco config detected — running `coco init` to set up your provider + key.', { color: 'cyan' });
47123
+ logger.log('');
47124
+ await handler$7(buildSyntheticArgv(argv), logger);
47125
+ return;
47126
+ case 'ui':
47127
+ await handler$5(buildSyntheticArgv(argv));
47128
+ return;
47129
+ case 'workspace':
47130
+ await handler(buildSyntheticArgv(argv));
47131
+ return;
47132
+ case 'commit':
47133
+ default:
47134
+ await handler$9(buildSyntheticArgv(argv), logger);
47135
+ return;
47136
+ }
47137
+ };
47138
+
46566
47139
  var types = /*#__PURE__*/Object.freeze({
46567
47140
  __proto__: null
46568
47141
  });
@@ -46581,7 +47154,37 @@ y.option('repo', {
46581
47154
  description: 'Target a specific repository directory instead of the current working directory.',
46582
47155
  global: true,
46583
47156
  });
46584
- y.command([commit.command, '$0'], commit.desc, commit.builder, commit.handler);
47157
+ // Global `--verbose` (alias `-v`) every subcommand inherits it.
47158
+ // Flips `argv.verbose: true` so `commandExecutor` and `Logger` print
47159
+ // stack traces / debug spans. Previously only settable via the
47160
+ // `COCO_VERBOSE=true` env var or `coco.verbose` git/json config —
47161
+ // `BaseArgvOptions.verbose` was typed but never declared as a yargs
47162
+ // option, so passing `--verbose` from the CLI was a silent no-op.
47163
+ y.option('verbose', {
47164
+ type: 'boolean',
47165
+ alias: 'v',
47166
+ description: 'Print verbose diagnostic output (stack traces on errors, debug spans).',
47167
+ default: false,
47168
+ global: true,
47169
+ });
47170
+ // `$0` (no positional args) routes through the smart default router
47171
+ // rather than aliasing directly to `coco commit`. The router probes
47172
+ // the user's environment (config presence, git-repo presence) and
47173
+ // forwards to `init` / `ui` / `workspace` / `commit` based on which
47174
+ // of those is most likely to be helpful. Mirrors what other modern
47175
+ // git-aware CLIs do (lazygit / tig / gitui) — fresh installs land in
47176
+ // a setup wizard, configured users land in the TUI, scripts that
47177
+ // rely on `coco commit` keep their dedicated subcommand entry.
47178
+ y.command('$0', 'Smart entry point — routes to init / ui / workspace / commit based on your environment.', (yargs) => yargs.option('commit', {
47179
+ type: 'boolean',
47180
+ description: 'Force the legacy default — run `coco commit` regardless of routing.',
47181
+ default: false,
47182
+ }),
47183
+ // `commandExecutor` wraps every command with config loading, error
47184
+ // formatting, and exit-code handling. The router is a regular
47185
+ // command so it lights up the same plumbing for free.
47186
+ commandExecutor(defaultRouteHandler));
47187
+ y.command(commit.command, commit.desc, commit.builder, commit.handler);
46585
47188
  y.command(changelog.command, changelog.desc, changelog.builder, changelog.handler);
46586
47189
  y.command(recap.command, recap.desc, recap.builder, recap.handler);
46587
47190
  y.command(review.command, review.desc, review.builder, review.handler);