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.
@@ -22,7 +22,7 @@ import ora from 'ora';
22
22
  import now from 'performance-now';
23
23
  import prettyMilliseconds from 'pretty-ms';
24
24
  import * as fs$1 from 'node:fs';
25
- import { existsSync, mkdirSync, unlinkSync, statSync, writeFileSync, renameSync, mkdtempSync, rmSync, readFileSync as readFileSync$1 } from 'node:fs';
25
+ import { existsSync, unlinkSync, statSync, mkdirSync, writeFileSync, renameSync, mkdtempSync, rmSync, readFileSync as readFileSync$1 } from 'node:fs';
26
26
  import * as os$1 from 'node:os';
27
27
  import { platform, homedir, tmpdir as tmpdir$1 } from 'node:os';
28
28
  import * as path$1 from 'node:path';
@@ -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.56.0";
64
+ const BUILD_VERSION = "0.57.0";
65
65
 
66
66
  const isInteractive = (config) => {
67
67
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -2139,6 +2139,10 @@ const schema$1 = {
2139
2139
  "selection": {
2140
2140
  "type": "string"
2141
2141
  },
2142
+ "selectionForeground": {
2143
+ "type": "string",
2144
+ "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`."
2145
+ },
2142
2146
  "success": {
2143
2147
  "type": "string"
2144
2148
  },
@@ -2182,7 +2186,25 @@ const schema$1 = {
2182
2186
  "vitesse-dark",
2183
2187
  "vesper",
2184
2188
  "flexoki",
2185
- "mellow"
2189
+ "mellow",
2190
+ "night-owl",
2191
+ "cobalt2",
2192
+ "oceanic-next",
2193
+ "catppuccin-macchiato",
2194
+ "gruvbox-light",
2195
+ "tokyo-night-day",
2196
+ "one-light",
2197
+ "ayu-light",
2198
+ "rose-pine-dawn",
2199
+ "everforest-light",
2200
+ "vitesse-light",
2201
+ "dayfox",
2202
+ "night-owl-light",
2203
+ "flexoki-light",
2204
+ "material-lighter",
2205
+ "papercolor-light",
2206
+ "modus-operandi",
2207
+ "quiet-light"
2186
2208
  ]
2187
2209
  }
2188
2210
  }
@@ -15212,10 +15234,19 @@ const CommitSplitPlanSchema = objectType({
15212
15234
  title: stringType().min(1),
15213
15235
  body: stringType().optional(),
15214
15236
  rationale: stringType().optional(),
15215
- files: arrayType(stringType()),
15216
- hunks: arrayType(stringType()),
15237
+ // Both optional: the model legitimately emits a group with *either*
15238
+ // `files` or `hunks` (a file-level vs hunk-level grouping), not always
15239
+ // both. Requiring both made Zod throw "Required" and the whole split
15240
+ // chain failed to parse before the refine could run. The refine below
15241
+ // still enforces "at least one", and every downstream consumer already
15242
+ // reads these as `group.files || []`. (Kept `.optional()` rather than
15243
+ // `.default([])` so the schema's input and output types stay identical
15244
+ // — `executeChainWithSchema` takes a `z.ZodSchema<T>`, which requires
15245
+ // that.)
15246
+ files: arrayType(stringType()).optional(),
15247
+ hunks: arrayType(stringType()).optional(),
15217
15248
  })
15218
- .refine((group) => group.files.length > 0 || group.hunks.length > 0, {
15249
+ .refine((group) => (group.files?.length ?? 0) > 0 || (group.hunks?.length ?? 0) > 0, {
15219
15250
  message: 'Each group must include at least one file or hunk',
15220
15251
  }))
15221
15252
  .min(1),
@@ -32180,6 +32211,10 @@ function renderBranchesSurface(h, components, state, context, contextStatus, bod
32180
32211
  // row's dim and read as quiet chrome.
32181
32212
  h(Text, { color: markerColor, dimColor: markerColor ? false : undefined }, marker.glyph), trailingName, h(Text, { dimColor: true }, timestampPadded), trailingDivergence);
32182
32213
  });
32214
+ // Scroll indicators — same "N more above/below" pattern as the
32215
+ // sidebar and help overlay so the user knows the list continues.
32216
+ const branchesHasMoreAbove = startIndex > 0 && localBranches.length > 0;
32217
+ const branchesHasMoreBelow = startIndex + listRows < localBranches.length;
32183
32218
  return h(Box, {
32184
32219
  borderColor: focusBorderColor(theme, focused),
32185
32220
  borderStyle: theme.borderStyle,
@@ -32187,7 +32222,11 @@ function renderBranchesSurface(h, components, state, context, contextStatus, bod
32187
32222
  flexShrink: 0,
32188
32223
  paddingX: 1,
32189
32224
  width,
32190
- }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Branches', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...lines);
32225
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Branches', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...(branchesHasMoreAbove
32226
+ ? [h(Text, { key: 'branches-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
32227
+ : []), ...lines, ...(branchesHasMoreBelow
32228
+ ? [h(Text, { key: 'branches-more-below', dimColor: true }, ` ↓ ${localBranches.length - (startIndex + listRows)} more below`)]
32229
+ : []));
32191
32230
  }
32192
32231
 
32193
32232
  /**
@@ -32960,12 +32999,19 @@ function renderDiffSurface(h, components, state, context, contextStatus, worktre
32960
32999
  // sees at a glance which file the cursor is inside.
32961
33000
  const isActive = absoluteIndex === activeStartLine;
32962
33001
  const arrow = theme.ascii ? '> ' : '▾ ';
33002
+ const activeHeader = isActive && focused && !theme.noColor;
32963
33003
  return h(Text, {
32964
33004
  key: `stash-diff-line-${absoluteIndex}`,
32965
33005
  bold: true,
32966
- color: theme.noColor ? undefined : theme.colors.accent,
32967
- backgroundColor: isActive && focused && !theme.noColor ? theme.colors.selection : undefined,
32968
- inverse: isActive && focused,
33006
+ // Active header sits on the selection bar with a
33007
+ // contrast-guaranteed foreground (matches history/status).
33008
+ // The old `inverse` swap turned the accent into the bar and
33009
+ // left the path in the selection color — low-contrast on
33010
+ // light themes (e.g. accent blue bar + light-gray text).
33011
+ color: activeHeader
33012
+ ? theme.colors.selectionForeground
33013
+ : (theme.noColor ? undefined : theme.colors.accent),
33014
+ backgroundColor: activeHeader ? theme.colors.selection : undefined,
32969
33015
  }, (() => {
32970
33016
  // Smart path truncation for the diff file header: keep
32971
33017
  // the leading arrow glyph and elide middle path
@@ -34043,7 +34089,7 @@ function formatHistoryFetchArgs(args) {
34043
34089
  * Returns the spans flat so the caller can splat them into the row's
34044
34090
  * outer Text alongside other segments without an extra wrapper.
34045
34091
  */
34046
- function renderTypedSubject(h, Text, text, theme, key) {
34092
+ function renderTypedSubject(h, Text, text, theme, key, suppressColor = false) {
34047
34093
  const parsed = parseConventionalCommitPrefix(text);
34048
34094
  if (!parsed) {
34049
34095
  return [h(Text, { key: `${key}-msg` }, text)];
@@ -34051,7 +34097,9 @@ function renderTypedSubject(h, Text, text, theme, key) {
34051
34097
  if (text.length < parsed.prefix.length) {
34052
34098
  return [h(Text, { key: `${key}-msg` }, text)];
34053
34099
  }
34054
- const color = getConventionalCommitColor(parsed, theme);
34100
+ // When the row is selected (inverted), suppress the type color so
34101
+ // text inherits the dark inverted foreground and stays readable.
34102
+ const color = suppressColor ? undefined : getConventionalCommitColor(parsed, theme);
34055
34103
  return [
34056
34104
  h(Text, { key: `${key}-type`, color, bold: parsed.breaking }, parsed.prefix),
34057
34105
  h(Text, { key: `${key}-rest` }, text.slice(parsed.prefix.length)),
@@ -34072,15 +34120,10 @@ function renderLaneSegmentSpans(h, Text, segments, theme, padTo, keyPrefix, opti
34072
34120
  const elements = [];
34073
34121
  let totalLen = 0;
34074
34122
  segments.forEach((seg, idx) => {
34075
- const laneColor = getLaneColor(seg.laneId, theme);
34123
+ const laneColor = options.suppressColor ? undefined : (getLaneColor(seg.laneId, theme) ?? muted);
34076
34124
  elements.push(h(Text, {
34077
34125
  key: `${keyPrefix}-${idx}`,
34078
- color: laneColor ?? muted,
34079
- // Ink does not cascade dimColor from a parent Text to children,
34080
- // so the caller's "this whole row should fade" intent has to
34081
- // travel here as an explicit flag (#831). Used for graph-only
34082
- // lane-closure rows, where the lane colors otherwise compete
34083
- // for attention with the commits they connect.
34126
+ color: laneColor,
34084
34127
  dimColor: options.forceDim || (theme.noColor && seg.laneId === undefined),
34085
34128
  }, seg.text));
34086
34129
  totalLen += seg.text.length;
@@ -34131,18 +34174,26 @@ function renderCommitHistoryRow(h, Text, commit, graph, graphWidth, selected, th
34131
34174
  const messageRoom = Math.max(8, totalWidth - fixedWidth - cellWidth(refsTrunc));
34132
34175
  const message = truncateCells(commit.message, messageRoom);
34133
34176
  const selectedBg = selected && !theme.noColor ? theme.colors.selection : undefined;
34134
- const accent = theme.noColor ? undefined : theme.colors.accent;
34135
- const muted = theme.noColor ? undefined : theme.colors.muted;
34177
+ // Don't use inverse it makes child colors unreadable. Instead, set a
34178
+ // background on the row AND an explicit, contrast-guaranteed foreground
34179
+ // (`selectionForeground`, derived from the selection bg) on the outer
34180
+ // span. Suppressing each child's own color to `undefined` then lets it
34181
+ // inherit that readable foreground — so the whole selected row stays
34182
+ // legible regardless of the user's terminal default foreground, which
34183
+ // is what the old "rely on the default fg" approach got wrong.
34184
+ const selectedFg = selected && !theme.noColor ? theme.colors.selectionForeground : undefined;
34185
+ const accent = selected ? undefined : (theme.noColor ? undefined : theme.colors.accent);
34186
+ const muted = selected ? undefined : (theme.noColor ? undefined : theme.colors.muted);
34136
34187
  // Lane-colored graph spans when full graph mode + non-ASCII rendering
34137
34188
  // is in play; otherwise fall back to the legacy single-muted span so
34138
34189
  // compact mode and legacy terminals stay visually unchanged.
34139
34190
  const graphChildren = laneSegments && !theme.ascii
34140
- ? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `c${index}`)
34141
- : [h(Text, { color: muted, dimColor: theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
34191
+ ? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `c${index}`, { suppressColor: selected })
34192
+ : [h(Text, { color: muted, dimColor: !selected && theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
34142
34193
  return h(Text, {
34143
34194
  key: `${commit.hash}-${index}`,
34144
34195
  backgroundColor: selectedBg,
34145
- inverse: selected,
34196
+ color: selectedFg,
34146
34197
  }, ...graphChildren, ' ',
34147
34198
  // "Just landed" marker — a single thick vertical bar in the
34148
34199
  // accent color before the short hash. Fades when the runtime
@@ -34164,11 +34215,11 @@ function renderCommitHistoryRow(h, Text, commit, graph, graphWidth, selected, th
34164
34215
  // Date column drops out entirely at `tight` density — no spacer
34165
34216
  // either, so the message column slides left into the freed cells.
34166
34217
  dateText
34167
- ? h(Text, { key: `${commit.hash}-${index}-date`, dimColor: true }, dateText, ' ')
34218
+ ? h(Text, { key: `${commit.hash}-${index}-date`, dimColor: !selected }, dateText, ' ')
34168
34219
  : null,
34169
34220
  // Branch chip prefix (full-graph mode only) lands right before the
34170
34221
  // message so the eye reads "branch · subject" as a unit.
34171
- chip.node, ...renderTypedSubject(h, Text, message, theme, `${commit.hash}-${index}-subj`), refsTrunc ? h(Text, { color: accent }, refsTrunc) : null);
34222
+ chip.node, ...renderTypedSubject(h, Text, message, theme, `${commit.hash}-${index}-subj`, selected), refsTrunc ? h(Text, { color: accent }, refsTrunc) : null);
34172
34223
  }
34173
34224
  /**
34174
34225
  * Stacked variant used at `rowMode='stacked'` (rail tier). Each
@@ -34183,9 +34234,13 @@ function renderCommitHistoryRow(h, Text, commit, graph, graphWidth, selected, th
34183
34234
  */
34184
34235
  function renderStackedCommitHistoryRow(h, Text, Box, commit, graph, graphWidth, selected, theme, index, panelWidth, fullGraph, now, laneSegments, isRecent = false, remoteNames) {
34185
34236
  const totalWidth = Math.max(20, panelWidth - 4);
34186
- const accent = theme.noColor ? undefined : theme.colors.accent;
34187
- const muted = theme.noColor ? undefined : theme.colors.muted;
34237
+ // Suppress child colors on selected rows so each span inherits the
34238
+ // contrast-guaranteed `selectionForeground` set on the line-1 span,
34239
+ // keeping the selected row readable against the selection bg.
34240
+ const accent = selected ? undefined : (theme.noColor ? undefined : theme.colors.accent);
34241
+ const muted = selected ? undefined : (theme.noColor ? undefined : theme.colors.muted);
34188
34242
  const selectedBg = selected && !theme.noColor ? theme.colors.selection : undefined;
34243
+ const selectedFg = selected && !theme.noColor ? theme.colors.selectionForeground : undefined;
34189
34244
  // Line 1 — subject row. Mostly mirrors the single-line layout but
34190
34245
  // skips the date and refs so the message has the whole tail to
34191
34246
  // itself. Branch chip rides between the hash and the subject the
@@ -34197,15 +34252,15 @@ function renderStackedCommitHistoryRow(h, Text, Box, commit, graph, graphWidth,
34197
34252
  const lineOneFixed = graphWidth + 1 + commit.shortHash.length + 1 + recentMarkerWidth + chip.width;
34198
34253
  const subject = truncateCells(commit.message, Math.max(8, totalWidth - lineOneFixed));
34199
34254
  const graphChildren = laneSegments && !theme.ascii
34200
- ? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `cs${index}`)
34201
- : [h(Text, { color: muted, dimColor: theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
34255
+ ? renderLaneSegmentSpans(h, Text, laneSegments, theme, graphWidth, `cs${index}`, { suppressColor: selected })
34256
+ : [h(Text, { color: muted, dimColor: !selected && theme.noColor }, substituteGraphChars(graph.padEnd(graphWidth), { ascii: theme.ascii }))];
34202
34257
  const lineOne = h(Text, {
34203
34258
  key: `${commit.hash}-${index}-l1`,
34204
34259
  backgroundColor: selectedBg,
34205
- inverse: selected,
34260
+ color: selectedFg,
34206
34261
  }, ...graphChildren, ' ', isRecent
34207
34262
  ? h(Text, { color: accent, bold: true }, theme.ascii ? '* ' : '▎ ')
34208
- : null, h(Text, { color: accent, bold: selected || isRecent }, commit.shortHash), ' ', chip.node, ...renderTypedSubject(h, Text, subject, theme, `${commit.hash}-${index}-stk-subj`));
34263
+ : null, h(Text, { color: accent, bold: selected || isRecent }, commit.shortHash), ' ', chip.node, ...renderTypedSubject(h, Text, subject, theme, `${commit.hash}-${index}-stk-subj`, selected));
34209
34264
  // Line 2 — metadata row, padded to align with the start of the
34210
34265
  // shortHash on line 1 so the eye still groups them as one commit.
34211
34266
  // Selection background does not extend here so we don't get a thick
@@ -34258,8 +34313,11 @@ function renderPendingCommitRow(h, Text, worktree, selected, theme) {
34258
34313
  return h(Text, {
34259
34314
  key: 'pending-commit-row',
34260
34315
  bold: true,
34261
- color: theme.noColor ? undefined : theme.colors.accent,
34262
- inverse: selected,
34316
+ // On selection, swap to the contrast-guaranteed foreground so the
34317
+ // accent label doesn't wash out against the selection bar.
34318
+ color: selected && !theme.noColor
34319
+ ? theme.colors.selectionForeground
34320
+ : (theme.noColor ? undefined : theme.colors.accent),
34263
34321
  backgroundColor: selected && !theme.noColor ? theme.colors.selection : undefined,
34264
34322
  }, truncateCells(label, 140));
34265
34323
  }
@@ -34673,6 +34731,10 @@ function renderCommandPalette(h, components, state, width, theme, focused) {
34673
34731
  dimColor: !isSelected,
34674
34732
  }, truncateCells(line, width - 4));
34675
34733
  });
34734
+ // Scroll indicators for the palette list — same pattern as the
34735
+ // sidebar and help overlay so the user knows there's more content.
34736
+ const paletteHasMoreAbove = startIndex > 0 && filtered.length > 0;
34737
+ const paletteHasMoreBelow = startIndex + listRows < filtered.length;
34676
34738
  return h(Box, {
34677
34739
  borderColor: focusBorderColor(theme, focused),
34678
34740
  borderStyle: theme.borderStyle,
@@ -34681,7 +34743,11 @@ function renderCommandPalette(h, components, state, width, theme, focused) {
34681
34743
  paddingX: 1,
34682
34744
  }, 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
34683
34745
  ? [h(Text, { key: 'palette-recent-hint', dimColor: true }, '· marks recently-used')]
34684
- : []), ...itemLines);
34746
+ : []), ...(paletteHasMoreAbove
34747
+ ? [h(Text, { key: 'palette-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
34748
+ : []), ...itemLines, ...(paletteHasMoreBelow
34749
+ ? [h(Text, { key: 'palette-more-below', dimColor: true }, ` ↓ ${filtered.length - (startIndex + listRows)} more below`)]
34750
+ : []));
34685
34751
  }
34686
34752
  /**
34687
34753
  * Split-plan overlay (#907) — renders the proposed commit groups for
@@ -35537,6 +35603,8 @@ function renderStashSurface(h, components, state, context, contextStatus, bodyRo
35537
35603
  dimColor: !isSelected,
35538
35604
  }, truncateCells(`${cursor} ${stash.ref.padEnd(12)} ${stash.message}`, 140));
35539
35605
  });
35606
+ const stashHasMoreAbove = startIndex > 0 && stashes.length > 0;
35607
+ const stashHasMoreBelow = startIndex + listRows < stashes.length;
35540
35608
  return h(Box, {
35541
35609
  borderColor: focusBorderColor(theme, focused),
35542
35610
  borderStyle: theme.borderStyle,
@@ -35544,7 +35612,11 @@ function renderStashSurface(h, components, state, context, contextStatus, bodyRo
35544
35612
  flexShrink: 0,
35545
35613
  paddingX: 1,
35546
35614
  width,
35547
- }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Stash', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...lines);
35615
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Stash', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...(stashHasMoreAbove
35616
+ ? [h(Text, { key: 'stash-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
35617
+ : []), ...lines, ...(stashHasMoreBelow
35618
+ ? [h(Text, { key: 'stash-more-below', dimColor: true }, ` ↓ ${stashes.length - (startIndex + listRows)} more below`)]
35619
+ : []));
35548
35620
  }
35549
35621
 
35550
35622
  /**
@@ -35640,7 +35712,7 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
35640
35712
  bold: true,
35641
35713
  dimColor: !headerSelected && rowIndex > cursorRowIndex,
35642
35714
  backgroundColor: headerSelected && !theme.noColor ? theme.colors.selection : undefined,
35643
- inverse: headerSelected,
35715
+ color: headerSelected && !theme.noColor ? theme.colors.selectionForeground : undefined,
35644
35716
  }, truncateCells(text, 140));
35645
35717
  }
35646
35718
  const isSelected = !headerFocused && row.flatIndex === selectedIndex;
@@ -35660,8 +35732,11 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
35660
35732
  key: `status-file-${row.flatIndex}-${rowIndex}`,
35661
35733
  dimColor: !isSelected && rowIndex > cursorRowIndex,
35662
35734
  backgroundColor: isSelected && focused && !theme.noColor ? theme.colors.selection : undefined,
35663
- inverse: isSelected && focused,
35664
- }, ` ${cursorPart}`, ...(useDot ? [h(Text, { color: dotColor }, STAGE_STATUS_DOT), ' '] : []), tailTrunc);
35735
+ color: isSelected && focused && !theme.noColor ? theme.colors.selectionForeground : undefined,
35736
+ }, ` ${cursorPart}`,
35737
+ // Suppress the dot's own color on selected rows so it inherits the
35738
+ // contrast-guaranteed selection foreground set on the row span.
35739
+ ...(useDot ? [h(Text, { color: (isSelected && focused) ? undefined : dotColor }, STAGE_STATUS_DOT), ' '] : []), tailTrunc);
35665
35740
  });
35666
35741
  // When the mask narrows the list to nothing but the underlying repo
35667
35742
  // is non-clean, surface why the panel looks empty so the user can
@@ -35676,6 +35751,10 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
35676
35751
  : cleanHint
35677
35752
  ? [cleanHint]
35678
35753
  : ['Worktree clean'];
35754
+ // Scroll indicators for the status file list — same pattern as
35755
+ // branches and the sidebar so the user knows there's more content.
35756
+ const statusHasMoreAbove = windowStart > 0 && surfaceRows.length > 0;
35757
+ const statusHasMoreBelow = windowStart + listRows < surfaceRows.length;
35679
35758
  return h(Box, {
35680
35759
  borderColor: focusBorderColor(theme, focused),
35681
35760
  borderStyle: theme.borderStyle,
@@ -35691,7 +35770,11 @@ function renderStatusSurface(h, components, state, context, contextStatus, bodyR
35691
35770
  // never touch the filter.
35692
35771
  ...(isStatusFilterMaskActive(state.statusFilterMask)
35693
35772
  ? [h(Text, { key: 'status-mask-indicator', dimColor: true }, `filter: ${formatStatusFilterMask(state.statusFilterMask)} (1/2/3 to toggle)`)]
35694
- : []), ...renderedRows, ...fallbackLines.map((line, index) => h(Text, {
35773
+ : []), ...(statusHasMoreAbove
35774
+ ? [h(Text, { key: 'status-more-above', dimColor: true }, ` ↑ ${windowStart} more above`)]
35775
+ : []), ...renderedRows, ...(statusHasMoreBelow
35776
+ ? [h(Text, { key: 'status-more-below', dimColor: true }, ` ↓ ${surfaceRows.length - (windowStart + listRows)} more below`)]
35777
+ : []), ...fallbackLines.map((line, index) => h(Text, {
35695
35778
  key: `status-surface-fallback-${index}`,
35696
35779
  dimColor: index > 0,
35697
35780
  }, truncateCells(line, 140))));
@@ -35921,6 +36004,8 @@ function renderTagsSurface(h, components, state, context, contextStatus, bodyRow
35921
36004
  dimColor: !isSelected,
35922
36005
  }, before, formatHyperlink(namePadded, url), after);
35923
36006
  });
36007
+ const tagsHasMoreAbove = startIndex > 0 && tags.length > 0;
36008
+ const tagsHasMoreBelow = startIndex + listRows < tags.length;
35924
36009
  return h(Box, {
35925
36010
  borderColor: focusBorderColor(theme, focused),
35926
36011
  borderStyle: theme.borderStyle,
@@ -35928,7 +36013,11 @@ function renderTagsSurface(h, components, state, context, contextStatus, bodyRow
35928
36013
  flexShrink: 0,
35929
36014
  paddingX: 1,
35930
36015
  width,
35931
- }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Tags', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...lines);
36016
+ }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Tags', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...(tagsHasMoreAbove
36017
+ ? [h(Text, { key: 'tags-more-above', dimColor: true }, ` ↑ ${startIndex} more above`)]
36018
+ : []), ...lines, ...(tagsHasMoreBelow
36019
+ ? [h(Text, { key: 'tags-more-below', dimColor: true }, ` ↓ ${tags.length - (startIndex + listRows)} more below`)]
36020
+ : []));
35932
36021
  }
35933
36022
 
35934
36023
  /**
@@ -36447,12 +36536,17 @@ function renderInspectorActionsSection(h, Text, context, width, theme, options =
36447
36536
  h(Text, { key: 'actions-title' }, cursorActive ? '[Actions]' : 'Actions:'),
36448
36537
  ...actions.map((action, index) => {
36449
36538
  const isSelected = cursorActive && index === cursorIndex;
36539
+ // On the selected row, swap every span to the contrast-guaranteed
36540
+ // selection foreground so the key glyph / destructive marker don't
36541
+ // wash out against the selection bar; the row is already highlighted,
36542
+ // and the label text still conveys which actions are destructive.
36543
+ const selectedFg = isSelected && !theme.noColor ? theme.colors.selectionForeground : undefined;
36450
36544
  const keyCell = action.key.padEnd(KEY_COLUMN);
36451
36545
  const label = truncateCells(action.label, labelBudget);
36452
36546
  const children = [
36453
36547
  h(Text, {
36454
36548
  key: `actions-${index}-key`,
36455
- color: action.destructive ? theme.colors.danger : theme.colors.accent,
36549
+ color: selectedFg ?? (action.destructive ? theme.colors.danger : theme.colors.accent),
36456
36550
  }, keyCell),
36457
36551
  GAP,
36458
36552
  label,
@@ -36460,14 +36554,14 @@ function renderInspectorActionsSection(h, Text, context, width, theme, options =
36460
36554
  if (action.destructive) {
36461
36555
  children.push(h(Text, {
36462
36556
  key: `actions-${index}-mark`,
36463
- color: theme.colors.danger,
36557
+ color: selectedFg ?? theme.colors.danger,
36464
36558
  dimColor: false,
36465
36559
  }, DESTRUCTIVE_SUFFIX));
36466
36560
  }
36467
36561
  return h(Text, {
36468
36562
  key: `actions-${index}`,
36469
36563
  backgroundColor: isSelected && !theme.noColor ? theme.colors.selection : undefined,
36470
- inverse: isSelected,
36564
+ color: selectedFg,
36471
36565
  }, ...children);
36472
36566
  }),
36473
36567
  ];
@@ -36544,7 +36638,6 @@ function renderCommitFileList(h, Text, files, selectedIndex, focused, maxRows, w
36544
36638
  return h(Text, {
36545
36639
  key: `commit-file-${index}`,
36546
36640
  color: statusCodeColor(file.status, theme),
36547
- inverse: isSelected && focused && !theme.noColor,
36548
36641
  bold: isSelected,
36549
36642
  }, label);
36550
36643
  });
@@ -41136,8 +41229,9 @@ function LogInkApp(deps) {
41136
41229
  if (group.rationale)
41137
41230
  lines += 2;
41138
41231
  lines += (group.files?.length || 0) + 1;
41139
- if ((group.hunks?.length || 0) > 0)
41140
- lines += group.hunks.length + 1;
41232
+ const hunkCount = group.hunks?.length || 0;
41233
+ if (hunkCount > 0)
41234
+ lines += hunkCount + 1;
41141
41235
  return sum + lines;
41142
41236
  }, 0)
41143
41237
  : undefined,
@@ -41302,7 +41396,7 @@ function getColorLevel(env = process.env) {
41302
41396
  return '256';
41303
41397
  return '16';
41304
41398
  }
41305
- 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']);
41399
+ 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']);
41306
41400
  /**
41307
41401
  * `true` when the named preset relies on hex colors that look best under
41308
41402
  * 24-bit RGB. Used by `createLogInkTheme` to decide whether to downgrade
@@ -41311,6 +41405,45 @@ const TRUECOLOR_PRESETS = new Set(['catppuccin', 'gruvbox', 'dracula', 'nord', '
41311
41405
  function presetUsesTrueColor(preset) {
41312
41406
  return preset !== undefined && TRUECOLOR_PRESETS.has(preset);
41313
41407
  }
41408
+ /**
41409
+ * WCAG 2.x relative luminance of a `#rrggbb` color, 0 (black) … 1 (white).
41410
+ * Returns `null` for anything that isn't a 6-digit hex (e.g. ANSI-named
41411
+ * colors), so callers can fall back rather than guess.
41412
+ */
41413
+ function relativeLuminance(hex) {
41414
+ const match = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
41415
+ if (!match)
41416
+ return null;
41417
+ const int = parseInt(match[1], 16);
41418
+ const channel = (c) => {
41419
+ const x = c / 255;
41420
+ return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
41421
+ };
41422
+ const r = channel((int >> 16) & 0xff);
41423
+ const g = channel((int >> 8) & 0xff);
41424
+ const b = channel(int & 0xff);
41425
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
41426
+ }
41427
+ /**
41428
+ * Pick a foreground guaranteed to stay readable on `bg` — black for light
41429
+ * backgrounds, white for dark ones. The 0.179 threshold is the luminance
41430
+ * crossover where black and white yield identical contrast, so the choice
41431
+ * always maximizes it; every background clears WCAG AA (≥ 4.5:1).
41432
+ *
41433
+ * This is how the selected-row text stays legible across every theme:
41434
+ * coco controls the selection *background* but not the user's terminal
41435
+ * default foreground, so it must supply its own contrasting foreground
41436
+ * instead of hoping the terminal's happens to contrast. Returns
41437
+ * `undefined` for non-hex backgrounds (let the caller leave color alone).
41438
+ */
41439
+ function readableForegroundFor(bg) {
41440
+ if (!bg)
41441
+ return undefined;
41442
+ const luminance = relativeLuminance(bg);
41443
+ if (luminance === null)
41444
+ return undefined;
41445
+ return luminance > 0.179 ? '#000000' : '#ffffff';
41446
+ }
41314
41447
 
41315
41448
  const THEME_PRESET_COLORS = {
41316
41449
  default: {
@@ -41323,7 +41456,7 @@ const THEME_PRESET_COLORS = {
41323
41456
  gitModified: 'yellow',
41324
41457
  info: 'blue',
41325
41458
  muted: 'gray',
41326
- selection: 'cyan',
41459
+ selection: '#1a3a4a',
41327
41460
  success: 'green',
41328
41461
  warning: 'yellow',
41329
41462
  },
@@ -41747,6 +41880,258 @@ const THEME_PRESET_COLORS = {
41747
41880
  success: '#a3d4a0',
41748
41881
  warning: '#f0c674',
41749
41882
  },
41883
+ 'night-owl': {
41884
+ accent: '#82aaff',
41885
+ border: '#1d3b53',
41886
+ danger: '#ef5350',
41887
+ focusBorder: '#7fdbca',
41888
+ gitAdded: '#addb67',
41889
+ gitDeleted: '#ef5350',
41890
+ gitModified: '#ecc48d',
41891
+ info: '#82aaff',
41892
+ muted: '#637777',
41893
+ selection: '#1d3b53',
41894
+ success: '#addb67',
41895
+ warning: '#ecc48d',
41896
+ },
41897
+ cobalt2: {
41898
+ accent: '#ffc600',
41899
+ border: '#234e6d',
41900
+ danger: '#ff628c',
41901
+ focusBorder: '#9effff',
41902
+ gitAdded: '#3ad900',
41903
+ gitDeleted: '#ff628c',
41904
+ gitModified: '#ffc600',
41905
+ info: '#9effff',
41906
+ muted: '#627e99',
41907
+ selection: '#0d3a58',
41908
+ success: '#3ad900',
41909
+ warning: '#ffc600',
41910
+ },
41911
+ 'oceanic-next': {
41912
+ accent: '#6699cc',
41913
+ border: '#343d46',
41914
+ danger: '#ec5f67',
41915
+ focusBorder: '#5fb3b3',
41916
+ gitAdded: '#99c794',
41917
+ gitDeleted: '#ec5f67',
41918
+ gitModified: '#fac863',
41919
+ info: '#6699cc',
41920
+ muted: '#65737e',
41921
+ selection: '#4f5b66',
41922
+ success: '#99c794',
41923
+ warning: '#fac863',
41924
+ },
41925
+ 'catppuccin-macchiato': {
41926
+ accent: '#8aadf4',
41927
+ border: '#494d64',
41928
+ danger: '#ed8796',
41929
+ focusBorder: '#91d7e3',
41930
+ gitAdded: '#a6da95',
41931
+ gitDeleted: '#ed8796',
41932
+ gitModified: '#eed49f',
41933
+ info: '#8aadf4',
41934
+ muted: '#6e738d',
41935
+ selection: '#363a4f',
41936
+ success: '#a6da95',
41937
+ warning: '#eed49f',
41938
+ },
41939
+ 'gruvbox-light': {
41940
+ accent: '#076678',
41941
+ border: '#bdae93',
41942
+ danger: '#9d0006',
41943
+ focusBorder: '#427b58',
41944
+ gitAdded: '#79740e',
41945
+ gitDeleted: '#9d0006',
41946
+ gitModified: '#b57614',
41947
+ info: '#076678',
41948
+ muted: '#7c6f64',
41949
+ selection: '#ebdbb2',
41950
+ success: '#79740e',
41951
+ warning: '#b57614',
41952
+ },
41953
+ 'tokyo-night-day': {
41954
+ accent: '#2e7de9',
41955
+ border: '#b7c1e3',
41956
+ danger: '#f52a65',
41957
+ focusBorder: '#007197',
41958
+ gitAdded: '#587539',
41959
+ gitDeleted: '#f52a65',
41960
+ gitModified: '#8c6c3e',
41961
+ info: '#2e7de9',
41962
+ muted: '#848cb5',
41963
+ selection: '#b7c1e3',
41964
+ success: '#587539',
41965
+ warning: '#8c6c3e',
41966
+ },
41967
+ 'one-light': {
41968
+ accent: '#4078f2',
41969
+ border: '#d4d4d4',
41970
+ danger: '#e45649',
41971
+ focusBorder: '#0184bc',
41972
+ gitAdded: '#50a14f',
41973
+ gitDeleted: '#e45649',
41974
+ gitModified: '#c18401',
41975
+ info: '#4078f2',
41976
+ muted: '#a0a1a7',
41977
+ selection: '#e5e5e6',
41978
+ success: '#50a14f',
41979
+ warning: '#c18401',
41980
+ },
41981
+ 'ayu-light': {
41982
+ accent: '#fa8d3e',
41983
+ border: '#e6e6e6',
41984
+ danger: '#e65050',
41985
+ focusBorder: '#4cbf99',
41986
+ gitAdded: '#6cbf43',
41987
+ gitDeleted: '#e65050',
41988
+ gitModified: '#f2ae49',
41989
+ info: '#399ee6',
41990
+ muted: '#abb0b6',
41991
+ selection: '#d1e4f4',
41992
+ success: '#6cbf43',
41993
+ warning: '#f2ae49',
41994
+ },
41995
+ 'rose-pine-dawn': {
41996
+ accent: '#907aa9',
41997
+ border: '#dfdad9',
41998
+ danger: '#b4637a',
41999
+ focusBorder: '#56949f',
42000
+ gitAdded: '#286983',
42001
+ gitDeleted: '#b4637a',
42002
+ gitModified: '#ea9d34',
42003
+ info: '#56949f',
42004
+ muted: '#9893a5',
42005
+ selection: '#dfdad9',
42006
+ success: '#286983',
42007
+ warning: '#ea9d34',
42008
+ },
42009
+ 'everforest-light': {
42010
+ accent: '#8da101',
42011
+ border: '#ddd8be',
42012
+ danger: '#f85552',
42013
+ focusBorder: '#35a77c',
42014
+ gitAdded: '#8da101',
42015
+ gitDeleted: '#f85552',
42016
+ gitModified: '#dfa000',
42017
+ info: '#3a94c5',
42018
+ muted: '#939f91',
42019
+ selection: '#edeada',
42020
+ success: '#8da101',
42021
+ warning: '#dfa000',
42022
+ },
42023
+ 'vitesse-light': {
42024
+ accent: '#1e754f',
42025
+ border: '#e0e0e0',
42026
+ danger: '#ab5959',
42027
+ focusBorder: '#2993a3',
42028
+ gitAdded: '#1e754f',
42029
+ gitDeleted: '#ab5959',
42030
+ gitModified: '#b07d48',
42031
+ info: '#296aa3',
42032
+ muted: '#999fa6',
42033
+ selection: '#eaeaeb',
42034
+ success: '#1e754f',
42035
+ warning: '#b07d48',
42036
+ },
42037
+ dayfox: {
42038
+ accent: '#2848a9',
42039
+ border: '#e4dcd4',
42040
+ danger: '#a5222f',
42041
+ focusBorder: '#287980',
42042
+ gitAdded: '#396847',
42043
+ gitDeleted: '#a5222f',
42044
+ gitModified: '#ac5402',
42045
+ info: '#2848a9',
42046
+ muted: '#908479',
42047
+ selection: '#e7d2be',
42048
+ success: '#396847',
42049
+ warning: '#ac5402',
42050
+ },
42051
+ 'night-owl-light': {
42052
+ accent: '#288ed7',
42053
+ border: '#d9d9d9',
42054
+ danger: '#d3423e',
42055
+ focusBorder: '#2aa298',
42056
+ gitAdded: '#08916a',
42057
+ gitDeleted: '#d3423e',
42058
+ gitModified: '#daaa01',
42059
+ info: '#288ed7',
42060
+ muted: '#989fb1',
42061
+ selection: '#e4e8f0',
42062
+ success: '#08916a',
42063
+ warning: '#daaa01',
42064
+ },
42065
+ 'flexoki-light': {
42066
+ accent: '#205ea6',
42067
+ border: '#cecdc3',
42068
+ danger: '#af3029',
42069
+ focusBorder: '#24837b',
42070
+ gitAdded: '#66800b',
42071
+ gitDeleted: '#af3029',
42072
+ gitModified: '#ad8301',
42073
+ info: '#205ea6',
42074
+ muted: '#6f6e69',
42075
+ selection: '#e6e4d9',
42076
+ success: '#66800b',
42077
+ warning: '#ad8301',
42078
+ },
42079
+ 'material-lighter': {
42080
+ accent: '#39adb5',
42081
+ border: '#e7eaec',
42082
+ danger: '#e53935',
42083
+ focusBorder: '#39adb5',
42084
+ gitAdded: '#91b859',
42085
+ gitDeleted: '#e53935',
42086
+ gitModified: '#f6a434',
42087
+ info: '#6182b8',
42088
+ muted: '#90a4ae',
42089
+ selection: '#d3e1e8',
42090
+ success: '#91b859',
42091
+ warning: '#f6a434',
42092
+ },
42093
+ 'papercolor-light': {
42094
+ accent: '#0087af',
42095
+ border: '#d7d7d7',
42096
+ danger: '#af0000',
42097
+ focusBorder: '#005f87',
42098
+ gitAdded: '#008700',
42099
+ gitDeleted: '#af0000',
42100
+ gitModified: '#d75f00',
42101
+ info: '#0087af',
42102
+ muted: '#878787',
42103
+ selection: '#d0d0d0',
42104
+ success: '#008700',
42105
+ warning: '#d75f00',
42106
+ },
42107
+ 'modus-operandi': {
42108
+ accent: '#0031a9',
42109
+ border: '#d7d7d7',
42110
+ danger: '#a60000',
42111
+ focusBorder: '#005e8b',
42112
+ gitAdded: '#006800',
42113
+ gitDeleted: '#a60000',
42114
+ gitModified: '#6f5500',
42115
+ info: '#0031a9',
42116
+ muted: '#595959',
42117
+ selection: '#c0deff',
42118
+ success: '#006800',
42119
+ warning: '#6f5500',
42120
+ },
42121
+ 'quiet-light': {
42122
+ accent: '#4b83cd',
42123
+ border: '#e0e0e0',
42124
+ danger: '#aa3731',
42125
+ focusBorder: '#4b83cd',
42126
+ gitAdded: '#448c27',
42127
+ gitDeleted: '#aa3731',
42128
+ gitModified: '#a67d00',
42129
+ info: '#4b83cd',
42130
+ muted: '#a3a6ad',
42131
+ selection: '#c9d0d9',
42132
+ success: '#448c27',
42133
+ warning: '#a67d00',
42134
+ },
41750
42135
  };
41751
42136
  function shouldUseAscii(term) {
41752
42137
  if (!term) {
@@ -41771,8 +42156,28 @@ function createLogInkTheme(options = {}) {
41771
42156
  ? {}
41772
42157
  : {
41773
42158
  ...THEME_PRESET_COLORS[preset],
42159
+ // Preserve the requested theme's selection background even when the
42160
+ // rest of the palette downgrades to `default`. The selection is a
42161
+ // single background color the terminal can approximate; without this,
42162
+ // a light theme inherits `default`'s dark selection (#1a3a4a) and the
42163
+ // selected row renders as a dark bar on a light background.
42164
+ ...(preset !== requestedPreset && THEME_PRESET_COLORS[requestedPreset]?.selection
42165
+ ? { selection: THEME_PRESET_COLORS[requestedPreset].selection }
42166
+ : {}),
41774
42167
  ...options.colors,
41775
42168
  };
42169
+ // Derive a contrasting foreground for the selected row from its own
42170
+ // selection background, unless the caller supplied one explicitly. coco
42171
+ // owns the selection background but not the terminal's default foreground,
42172
+ // so without this the selected row's text falls back to whatever the
42173
+ // user's terminal foreground is — which may not contrast with the bar at
42174
+ // all (the bug behind unreadable selected rows on many themes).
42175
+ if (!noColor && colors.selection && !colors.selectionForeground) {
42176
+ const selectionForeground = readableForegroundFor(colors.selection);
42177
+ if (selectionForeground) {
42178
+ colors.selectionForeground = selectionForeground;
42179
+ }
42180
+ }
41776
42181
  return {
41777
42182
  noColor,
41778
42183
  ascii,
@@ -43401,7 +43806,7 @@ const options$1 = {
43401
43806
  },
43402
43807
  theme: {
43403
43808
  description: 'TUI theme preset',
43404
- 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'],
43809
+ 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'],
43405
43810
  },
43406
43811
  };
43407
43812
  const builder$1 = (yargs) => {
@@ -43429,7 +43834,7 @@ const options = {
43429
43834
  },
43430
43835
  theme: {
43431
43836
  description: 'TUI theme preset',
43432
- 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'],
43837
+ 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'],
43433
43838
  },
43434
43839
  };
43435
43840
  const builder = (yargs) => {
@@ -44713,33 +45118,48 @@ function buildWorkspaceHeaderChips(state, options = { focusLabel: 'List' }) {
44713
45118
  });
44714
45119
  return chips;
44715
45120
  }
44716
- // Footer hints prioritize discoverable / forgettable actions and
44717
- // drop the bindings users can find on their own (arrow keys, Enter
44718
- // for "open", Tab for "switch panels"). The full keymap lives behind
44719
- // `?` so nothing is hidden, just decluttered.
44720
- const LIST_HINT = 's sort · / filter · r/R refresh · a add · d remove · ? help · q quit';
44721
- const SIDEBAR_HINT = '? help · q quit';
44722
- const FILTER_HINT = 'type to filter · enter apply · esc cancel';
44723
- const ADD_REPO_HINT = 'type path · tab to complete · enter to add · esc to cancel';
44724
- const CONFIRM_DELETE_HINT = 'press y to remove · any other key to cancel';
44725
- function hintFor(focus) {
45121
+ // Footer hints are split into two slots — same pattern as `coco ui`:
45122
+ // - contextual : per-mode actions (sort, filter, refresh, add/remove)
45123
+ // - global : always-on essentials (help, quit) never crowded out
45124
+ //
45125
+ // The contextual slot drops bindings users can find via the help
45126
+ // overlay (arrow keys, tab); the global slot is the safety net so
45127
+ // `? help` and `q quit` never disappear.
45128
+ const LIST_CONTEXTUAL = ['s sort', '/ filter', 'r/R refresh', 'a add', 'd remove'];
45129
+ const SIDEBAR_CONTEXTUAL = ['↑/↓ cycle tab', 'enter open'];
45130
+ const FILTER_CONTEXTUAL = ['type to filter', 'enter apply', 'esc cancel'];
45131
+ const ADD_REPO_CONTEXTUAL = ['type path', 'tab to complete', 'enter to add', 'esc to cancel'];
45132
+ const CONFIRM_DELETE_CONTEXTUAL = ['y confirm', 'any other key cancels'];
45133
+ const GLOBAL_HINTS = ['? help', 'q quit'];
45134
+ function contextualHintsFor(focus) {
44726
45135
  switch (focus) {
44727
45136
  case 'sidebar':
44728
- return SIDEBAR_HINT;
45137
+ return SIDEBAR_CONTEXTUAL;
44729
45138
  case 'filter':
44730
- return FILTER_HINT;
45139
+ return FILTER_CONTEXTUAL;
44731
45140
  case 'add-repo':
44732
- return ADD_REPO_HINT;
45141
+ return ADD_REPO_CONTEXTUAL;
44733
45142
  case 'confirm-delete':
44734
- return CONFIRM_DELETE_HINT;
45143
+ return CONFIRM_DELETE_CONTEXTUAL;
44735
45144
  case 'list':
44736
45145
  default:
44737
- return LIST_HINT;
45146
+ return LIST_CONTEXTUAL;
44738
45147
  }
44739
45148
  }
44740
45149
  function buildWorkspaceFooter(state) {
45150
+ const contextual = contextualHintsFor(state.focus);
45151
+ // Modal modes (filter / add-repo / confirm-delete) suppress the
45152
+ // global hints — those bindings are not reachable while a prompt
45153
+ // is open and showing them would be misleading.
45154
+ const isModal = state.focus === 'filter' ||
45155
+ state.focus === 'add-repo' ||
45156
+ state.focus === 'confirm-delete';
45157
+ const global = isModal ? [] : GLOBAL_HINTS;
45158
+ const allHints = [...contextual, ...global];
44741
45159
  return {
44742
- hint: hintFor(state.focus),
45160
+ hint: allHints.join(' · '),
45161
+ contextual,
45162
+ global,
44743
45163
  status: state.status,
44744
45164
  filterMode: state.focus === 'filter',
44745
45165
  };
@@ -44749,11 +45169,25 @@ function buildWorkspaceFooter(state) {
44749
45169
  * sections so users can scan by intent ("how do I navigate?" "how
44750
45170
  * do I act?") rather than reading a flat alphabetized list.
44751
45171
  *
45172
+ * Section order mirrors `coco ui`'s help convention — Essentials
45173
+ * first so newcomers see `?`/`esc`/`q` immediately, then move outward
45174
+ * to navigation, modification, and the destructive verbs last.
45175
+ *
44752
45176
  * The view layer composes these into a panel with section titles,
44753
45177
  * a leading app/title bar, and a closing hint at the bottom.
44754
45178
  */
44755
45179
  function buildWorkspaceHelpSections() {
44756
45180
  return [
45181
+ {
45182
+ title: 'Essentials',
45183
+ subtitle: 'The keys you reach for most often.',
45184
+ rows: [
45185
+ { glyph: '?', keys: '?', description: 'Toggle this help overlay' },
45186
+ { glyph: '⎋', keys: 'esc', description: 'Clear filter / close overlay / cancel prompt' },
45187
+ { glyph: '◴', keys: 'q · ctrl+c', description: 'Quit the workspace surface' },
45188
+ { glyph: '↵', keys: 'enter', description: 'Drill into the cursored repo (coco ui)' },
45189
+ ],
45190
+ },
44757
45191
  {
44758
45192
  title: 'Navigate',
44759
45193
  subtitle: 'Move the cursor and switch focus between panels.',
@@ -44763,7 +45197,6 @@ function buildWorkspaceHelpSections() {
44763
45197
  { glyph: '←', keys: 'h', description: 'Jump focus to the sidebar' },
44764
45198
  { glyph: '→', keys: 'l', description: 'Jump focus to the list' },
44765
45199
  { glyph: '⤒', keys: 'g / G', description: 'Jump to top / bottom of the list' },
44766
- { glyph: '↵', keys: 'enter', description: 'Drill into the cursored repo (coco ui)' },
44767
45200
  ],
44768
45201
  },
44769
45202
  {
@@ -44783,14 +45216,6 @@ function buildWorkspaceHelpSections() {
44783
45216
  { glyph: '✕', keys: 'd', description: 'Remove the cursored repo from the known-repos store' },
44784
45217
  ],
44785
45218
  },
44786
- {
44787
- title: 'General',
44788
- rows: [
44789
- { glyph: '?', keys: '?', description: 'Toggle this help overlay' },
44790
- { glyph: '⎋', keys: 'esc', description: 'Clear filter / close overlay / cancel prompt' },
44791
- { glyph: '◴', keys: 'q · ctrl+c', description: 'Quit the workspace surface' },
44792
- ],
44793
- },
44794
45219
  ];
44795
45220
  }
44796
45221
  function buildWorkspaceOnboarding(state) {
@@ -45237,13 +45662,19 @@ function renderFooter(deps) {
45237
45662
  // height shifted by a row every time a status banner came and went,
45238
45663
  // forcing the panel chrome to reflow.
45239
45664
  const statusContent = model.status ?? '';
45665
+ const contextualText = model.contextual.join(' ');
45666
+ const globalText = model.global.join(' · ');
45240
45667
  return React.createElement(Box, {
45241
45668
  borderColor: focusBorderColor(theme, false),
45242
45669
  borderStyle: theme.borderStyle,
45243
45670
  paddingX: 1,
45244
45671
  flexDirection: 'column',
45245
45672
  height: FOOTER_HEIGHT,
45246
- }, React.createElement(Text, { dimColor: true }, model.hint), React.createElement(Text, {
45673
+ },
45674
+ // Row 1: contextual ↔ global hints. justifyContent pushes them
45675
+ // to opposite edges so the eye scans each cluster as one block —
45676
+ // same shape as `coco ui`'s footer post-0.54.2 redesign.
45677
+ React.createElement(Box, { flexDirection: 'row', justifyContent: 'space-between' }, React.createElement(Text, { dimColor: true }, contextualText), React.createElement(Text, { dimColor: true }, globalText)), React.createElement(Text, {
45247
45678
  color: model.status ? toneColor('warn', theme) : undefined,
45248
45679
  dimColor: !model.status,
45249
45680
  }, statusContent || ' '));
@@ -46546,6 +46977,148 @@ var workspace = {
46546
46977
  options,
46547
46978
  };
46548
46979
 
46980
+ /**
46981
+ * Default-command router for `coco` invoked with no positional
46982
+ * arguments. Decides where to send the user based on the state of
46983
+ * their machine:
46984
+ *
46985
+ * ┌─────────────────────────┬─────────────────────┬──────────────┐
46986
+ * │ Config present? │ In a git repo? │ Action │
46987
+ * ├─────────────────────────┼─────────────────────┼──────────────┤
46988
+ * │ No (default-only) │ — │ run `init` │
46989
+ * │ Yes │ Yes (worktree) │ run `ui` │
46990
+ * │ Yes │ No │ run `ws` │
46991
+ * └─────────────────────────┴─────────────────────┴──────────────┘
46992
+ *
46993
+ * The pre-existing default — fall through to `commit` — was hostile
46994
+ * to first-time users: a fresh install with no config landed
46995
+ * straight in the API-key error path, with no hint that `coco init`
46996
+ * was the right next step. Routing fresh installs to `init` and
46997
+ * configured users to the workstation/UI matches what every other
46998
+ * git-aware CLI does (lazygit, tig, gitui all open their TUI on bare
46999
+ * invocation).
47000
+ *
47001
+ * `coco commit` keeps its dedicated subcommand entry so existing
47002
+ * scripts (`git aliases`, hook integrations, CI jobs) that call
47003
+ * `coco commit` continue to work unchanged.
47004
+ *
47005
+ * The router is a thin shim — it forwards to the existing handlers
47006
+ * via their public exports rather than re-implementing the logic.
47007
+ */
47008
+ /**
47009
+ * Pure decision function — given probed signals (whether config
47010
+ * exists, whether the current directory is a git repo, whether the
47011
+ * user opted into legacy commit-by-default), decides which command
47012
+ * to invoke. Kept pure so unit tests can cover every quadrant of
47013
+ * the router table without spawning processes.
47014
+ *
47015
+ * "Config exists" is defined as: the loader detected at least one
47016
+ * source beyond `default` — i.e., the user has either a project
47017
+ * config, a git config `[coco]` section, an env var, or an XDG
47018
+ * config. A pure-defaults run is treated as "never been configured"
47019
+ * because `coco init` is the only way to populate any of those
47020
+ * sources.
47021
+ */
47022
+ function decideDefaultRoute(input) {
47023
+ if (input.envOverride === 'commit' || input.explicitCommit) {
47024
+ return {
47025
+ kind: 'commit',
47026
+ reason: input.envOverride === 'commit' ? 'env-override' : 'explicit-flag',
47027
+ };
47028
+ }
47029
+ if (!input.hasConfigSource) {
47030
+ return { kind: 'init', reason: 'no-config' };
47031
+ }
47032
+ if (input.isGitRepo) {
47033
+ return { kind: 'ui', reason: 'config-and-repo' };
47034
+ }
47035
+ return { kind: 'workspace', reason: 'config-no-repo' };
47036
+ }
47037
+ /**
47038
+ * Probe whether the cwd (after `--repo` is honored) is inside a git
47039
+ * worktree. Tolerant of every error class — a thrown simple-git
47040
+ * call should never block the router; it should fall back to
47041
+ * "not a repo" so the user lands somewhere sensible (workspace
47042
+ * surface) rather than crashing on an empty machine.
47043
+ */
47044
+ async function probeIsGitRepo() {
47045
+ try {
47046
+ // Lazy-import simple-git so the cold-start path stays fast for
47047
+ // users running `coco --help` / `coco doctor` etc.
47048
+ const { default: simpleGit } = await import('simple-git');
47049
+ const git = simpleGit();
47050
+ return await git.checkIsRepo();
47051
+ }
47052
+ catch {
47053
+ return false;
47054
+ }
47055
+ }
47056
+ /**
47057
+ * Build a synthetic argv for one of the targeted handlers. Each
47058
+ * handler reads its own typed argv contract (`CommitArgv`,
47059
+ * `InitArgv`, `UiArgv`, `WorkspaceArgv`) so we can't just spread the
47060
+ * raw default argv — we have to project the shared fields and let
47061
+ * the handler fill in command-specific defaults.
47062
+ */
47063
+ function buildSyntheticArgv(argv) {
47064
+ return {
47065
+ _: ['$0'],
47066
+ $0: argv.$0,
47067
+ repo: argv.repo,
47068
+ cwd: argv.cwd,
47069
+ verbose: argv.verbose ?? false,
47070
+ interactive: true,
47071
+ version: false,
47072
+ help: false,
47073
+ };
47074
+ }
47075
+ /**
47076
+ * Default-command handler installed under yargs's `$0` slot. Probes
47077
+ * the environment, computes the right route, and forwards to the
47078
+ * matching command handler. Falls through to commit if any
47079
+ * unexpected error blocks routing — preserves backwards-compat
47080
+ * for users on weird setups while still giving newcomers the
47081
+ * onboarding path they deserve.
47082
+ */
47083
+ const defaultRouteHandler = async (argv, logger) => {
47084
+ // The `--repo` flag has to land before any probe runs — otherwise
47085
+ // we'd sniff the launcher's cwd instead of the targeted repo.
47086
+ applyRepoCwd(argv);
47087
+ // Trigger a config load so `getConfigSources()` returns the active
47088
+ // source list. We discard the config object — the decision only
47089
+ // cares about which sources contributed.
47090
+ void loadConfig(argv);
47091
+ const sources = getConfigSources();
47092
+ const hasConfigSource = sources.some((source) => source.source !== 'default');
47093
+ const isGitRepo = await probeIsGitRepo();
47094
+ const decision = decideDefaultRoute({
47095
+ hasConfigSource,
47096
+ isGitRepo,
47097
+ explicitCommit: Boolean(argv.commit),
47098
+ envOverride: process.env.COCO_DEFAULT,
47099
+ });
47100
+ switch (decision.kind) {
47101
+ case 'init':
47102
+ // Friendly hint before the wizard kicks in — sets expectations
47103
+ // that the user is being walked through setup, not silently
47104
+ // routed to a different command.
47105
+ logger.log('No coco config detected — running `coco init` to set up your provider + key.', { color: 'cyan' });
47106
+ logger.log('');
47107
+ await handler$7(buildSyntheticArgv(argv), logger);
47108
+ return;
47109
+ case 'ui':
47110
+ await handler$5(buildSyntheticArgv(argv));
47111
+ return;
47112
+ case 'workspace':
47113
+ await handler(buildSyntheticArgv(argv));
47114
+ return;
47115
+ case 'commit':
47116
+ default:
47117
+ await handler$9(buildSyntheticArgv(argv), logger);
47118
+ return;
47119
+ }
47120
+ };
47121
+
46549
47122
  var types = /*#__PURE__*/Object.freeze({
46550
47123
  __proto__: null
46551
47124
  });
@@ -46564,7 +47137,37 @@ y.option('repo', {
46564
47137
  description: 'Target a specific repository directory instead of the current working directory.',
46565
47138
  global: true,
46566
47139
  });
46567
- y.command([commit.command, '$0'], commit.desc, commit.builder, commit.handler);
47140
+ // Global `--verbose` (alias `-v`) every subcommand inherits it.
47141
+ // Flips `argv.verbose: true` so `commandExecutor` and `Logger` print
47142
+ // stack traces / debug spans. Previously only settable via the
47143
+ // `COCO_VERBOSE=true` env var or `coco.verbose` git/json config —
47144
+ // `BaseArgvOptions.verbose` was typed but never declared as a yargs
47145
+ // option, so passing `--verbose` from the CLI was a silent no-op.
47146
+ y.option('verbose', {
47147
+ type: 'boolean',
47148
+ alias: 'v',
47149
+ description: 'Print verbose diagnostic output (stack traces on errors, debug spans).',
47150
+ default: false,
47151
+ global: true,
47152
+ });
47153
+ // `$0` (no positional args) routes through the smart default router
47154
+ // rather than aliasing directly to `coco commit`. The router probes
47155
+ // the user's environment (config presence, git-repo presence) and
47156
+ // forwards to `init` / `ui` / `workspace` / `commit` based on which
47157
+ // of those is most likely to be helpful. Mirrors what other modern
47158
+ // git-aware CLIs do (lazygit / tig / gitui) — fresh installs land in
47159
+ // a setup wizard, configured users land in the TUI, scripts that
47160
+ // rely on `coco commit` keep their dedicated subcommand entry.
47161
+ y.command('$0', 'Smart entry point — routes to init / ui / workspace / commit based on your environment.', (yargs) => yargs.option('commit', {
47162
+ type: 'boolean',
47163
+ description: 'Force the legacy default — run `coco commit` regardless of routing.',
47164
+ default: false,
47165
+ }),
47166
+ // `commandExecutor` wraps every command with config loading, error
47167
+ // formatting, and exit-code handling. The router is a regular
47168
+ // command so it lights up the same plumbing for free.
47169
+ commandExecutor(defaultRouteHandler));
47170
+ y.command(commit.command, commit.desc, commit.builder, commit.handler);
46568
47171
  y.command(changelog.command, changelog.desc, changelog.builder, changelog.handler);
46569
47172
  y.command(recap.command, recap.desc, recap.builder, recap.handler);
46570
47173
  y.command(review.command, review.desc, review.builder, review.handler);