git-coco 0.62.3 → 0.63.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.
@@ -61,7 +61,7 @@ import { pathToFileURL } from 'url';
61
61
  /**
62
62
  * Current build version from package.json
63
63
  */
64
- const BUILD_VERSION = "0.62.3";
64
+ const BUILD_VERSION = "0.63.0";
65
65
 
66
66
  const isInteractive = (config) => {
67
67
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -2246,7 +2246,66 @@ const schema$1 = {
2246
2246
  "material-lighter",
2247
2247
  "papercolor-light",
2248
2248
  "modus-operandi",
2249
- "quiet-light"
2249
+ "quiet-light",
2250
+ "catppuccin-frappe",
2251
+ "rose-pine-moon",
2252
+ "kanagawa-dragon",
2253
+ "kanagawa-lotus",
2254
+ "nordfox",
2255
+ "duskfox",
2256
+ "terafox",
2257
+ "dawnfox",
2258
+ "ayu-mirage",
2259
+ "material-darker",
2260
+ "tokyo-night-moon",
2261
+ "gruvbox-material",
2262
+ "gruvbox-material-light",
2263
+ "modus-vivendi",
2264
+ "zenburn",
2265
+ "oxocarbon",
2266
+ "tomorrow-night",
2267
+ "monokai-pro",
2268
+ "sonokai",
2269
+ "doom-one",
2270
+ "andromeda",
2271
+ "aura",
2272
+ "cyberdream",
2273
+ "nightfly",
2274
+ "panda",
2275
+ "hyper-snazzy",
2276
+ "apprentice",
2277
+ "melange",
2278
+ "melange-light",
2279
+ "spaceduck",
2280
+ "embark",
2281
+ "bluloco-dark",
2282
+ "bluloco-light",
2283
+ "papercolor-dark",
2284
+ "base16-ocean",
2285
+ "base16-eighties",
2286
+ "everblush",
2287
+ "darcula",
2288
+ "eldritch",
2289
+ "edge-light",
2290
+ "zenbones",
2291
+ "iceberg-light",
2292
+ "github-dark-dimmed",
2293
+ "edge-dark",
2294
+ "selenized-dark",
2295
+ "selenized-black",
2296
+ "selenized-light",
2297
+ "monokai-pro-machine",
2298
+ "monokai-pro-octagon",
2299
+ "monokai-pro-ristretto",
2300
+ "monokai-pro-spectrum",
2301
+ "base16-default-dark",
2302
+ "base16-default-light",
2303
+ "tomorrow",
2304
+ "tokyodark",
2305
+ "spacemacs-dark",
2306
+ "bamboo",
2307
+ "citylights",
2308
+ "oxocarbon-light"
2250
2309
  ]
2251
2310
  }
2252
2311
  }
@@ -7475,6 +7534,7 @@ var ZodFirstPartyTypeKind;
7475
7534
  ZodFirstPartyTypeKind["ZodReadonly"] = "ZodReadonly";
7476
7535
  })(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {}));
7477
7536
  const stringType = ZodString.create;
7537
+ const booleanType = ZodBoolean.create;
7478
7538
  ZodNever.create;
7479
7539
  const arrayType = ZodArray.create;
7480
7540
  const objectType = ZodObject.create;
@@ -15432,6 +15492,14 @@ const CommitSplitPlanSchema = objectType({
15432
15492
  // that.)
15433
15493
  files: arrayType(stringType()).optional(),
15434
15494
  hunks: arrayType(stringType()).optional(),
15495
+ // Internal flag (not emitted by the model). Set by
15496
+ // `rescueMissingFiles` on the catch-all group of files the
15497
+ // plan didn't confidently place: the apply step skips
15498
+ // committing these, leaving them in the worktree for the user
15499
+ // to handle, and the review overlay renders them as a "will
15500
+ // stay — not committed" note rather than a numbered commit
15501
+ // (#1180).
15502
+ unclaimed: booleanType().optional(),
15435
15503
  })
15436
15504
  .refine((group) => (group.files?.length ?? 0) > 0 || (group.hunks?.length ?? 0) > 0, {
15437
15505
  message: 'Each group must include at least one file or hunk',
@@ -15749,11 +15817,16 @@ function rescueMissingFiles(plan, staged, hunkInventory) {
15749
15817
  groups: [
15750
15818
  ...plan.groups,
15751
15819
  {
15752
- title: 'chore: misc unclaimed changes',
15753
- body: 'Files the split plan did not assign to any other commit. Review and re-roll (`r`) if these belong in a specific commit.',
15754
- rationale: 'Recovered by validator rescuemodel omitted these from every group.',
15820
+ // Tagged `unclaimed`: the apply step skips committing this group
15821
+ // (the files are left in the worktree), and the review overlay
15822
+ // renders it as a "will stay not committed" note rather than a
15823
+ // numbered commit. The confident commits land; these come back
15824
+ // to you on the status screen to handle (#1180).
15825
+ title: 'Left for you — not committed',
15826
+ body: `${missing.length} file${missing.length === 1 ? '' : 's'} the split couldn't confidently place. They stay in your worktree (uncommitted) — you'll land on the status screen to handle them. Re-roll (\`r\`) if they belong in a specific commit.`,
15755
15827
  files: missing,
15756
15828
  hunks: [],
15829
+ unclaimed: true,
15757
15830
  },
15758
15831
  ],
15759
15832
  };
@@ -16195,6 +16268,15 @@ async function applyCommitSplitPlan({ plan, changes, hunkInventory, git, logger,
16195
16268
  // "git commit with nothing staged" failure mode mid-loop after
16196
16269
  // the up-front `git reset` has already wiped the index.
16197
16270
  const applicableGroups = plan.groups.filter((group) => {
16271
+ // `unclaimed` groups are intentionally NOT committed (#1180): they
16272
+ // hold the files the plan couldn't confidently place. The up-front
16273
+ // `git reset` below unstages everything, and since these never get
16274
+ // re-added they simply stay in the worktree for the user to handle.
16275
+ // They still count as "claimed" for validatePlanForStagedFiles, so
16276
+ // that check (run above) passes.
16277
+ if (group.unclaimed) {
16278
+ return false;
16279
+ }
16198
16280
  const fileCount = (group.files || []).length;
16199
16281
  const hunkCount = (group.hunks || []).length;
16200
16282
  return fileCount + hunkCount > 0;
@@ -23807,11 +23889,12 @@ function computeLogInkFooterHints(options) {
23807
23889
  global: NORMAL_GLOBAL_HINTS,
23808
23890
  };
23809
23891
  }
23810
- // Worktree (staging) diff. The hunk is the unit of action: ↑/↓ walk
23811
- // hunks, space stages/unstages the selected one, a stages the whole
23812
- // file, z discards the hunk.
23892
+ // Worktree (staging) diff. Consistent with the commit/stash diffs
23893
+ // (#1185): j/k scroll lines, [/] jump between hunks. space stages /
23894
+ // unstages the hunk under the viewport, a stages the whole file, z
23895
+ // discards the current hunk.
23813
23896
  return {
23814
- contextual: ['↑/↓ hunk', 'space stage', 'a stage file', 'z discard', 'o edit', 'esc back'],
23897
+ contextual: ['j/k lines', '[/] hunk', 'space stage', 'a stage file', 'z discard', 'o edit', 'esc back'],
23815
23898
  global: NORMAL_GLOBAL_HINTS,
23816
23899
  };
23817
23900
  }
@@ -24184,14 +24267,23 @@ function getColorLevel(env = process.env) {
24184
24267
  return '256';
24185
24268
  return '16';
24186
24269
  }
24187
- 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']);
24270
+ /**
24271
+ * The only presets whose palettes are ANSI-named (not hex): `default`
24272
+ * renders faithfully on 16-color terminals, and `monochrome` carries no
24273
+ * color at all. Every other preset in `THEME_PRESET_COLORS` is hand-authored
24274
+ * in hex, so it's treated as truecolor by definition — no list to keep in
24275
+ * sync as themes are added.
24276
+ */
24277
+ const ANSI_NATIVE_PRESETS = new Set(['default', 'monochrome']);
24188
24278
  /**
24189
24279
  * `true` when the named preset relies on hex colors that look best under
24190
24280
  * 24-bit RGB. Used by `createLogInkTheme` to decide whether to downgrade
24191
- * to the ANSI-named `default` palette on lower-capability terminals.
24281
+ * to the ANSI-named `default` palette on lower-capability terminals. Every
24282
+ * preset except the two ANSI-native baselines uses hex, so this is derived
24283
+ * rather than enumerated — new themes are covered automatically.
24192
24284
  */
24193
24285
  function presetUsesTrueColor(preset) {
24194
- return preset !== undefined && TRUECOLOR_PRESETS.has(preset);
24286
+ return preset !== undefined && !ANSI_NATIVE_PRESETS.has(preset);
24195
24287
  }
24196
24288
  /**
24197
24289
  * WCAG 2.x relative luminance of a `#rrggbb` color, 0 (black) … 1 (white).
@@ -24920,6 +25012,832 @@ const THEME_PRESET_COLORS = {
24920
25012
  success: '#448c27',
24921
25013
  warning: '#a67d00',
24922
25014
  },
25015
+ 'catppuccin-frappe': {
25016
+ accent: '#8caaee',
25017
+ border: '#51576d',
25018
+ danger: '#e78284',
25019
+ focusBorder: '#81c8be',
25020
+ gitAdded: '#a6d189',
25021
+ gitDeleted: '#e78284',
25022
+ gitModified: '#e5c890',
25023
+ info: '#8caaee',
25024
+ muted: '#737994',
25025
+ selection: '#414559',
25026
+ success: '#a6d189',
25027
+ warning: '#e5c890',
25028
+ },
25029
+ 'rose-pine-moon': {
25030
+ accent: '#c4a7e7',
25031
+ border: '#393552',
25032
+ danger: '#eb6f92',
25033
+ focusBorder: '#9ccfd8',
25034
+ gitAdded: '#3e8fb0',
25035
+ gitDeleted: '#eb6f92',
25036
+ gitModified: '#f6c177',
25037
+ info: '#9ccfd8',
25038
+ muted: '#6e6a86',
25039
+ selection: '#44415a',
25040
+ success: '#3e8fb0',
25041
+ warning: '#f6c177',
25042
+ },
25043
+ 'kanagawa-dragon': {
25044
+ accent: '#8ba4b0',
25045
+ border: '#282727',
25046
+ danger: '#c4746e',
25047
+ focusBorder: '#8ea4a2',
25048
+ gitAdded: '#87a987',
25049
+ gitDeleted: '#c4746e',
25050
+ gitModified: '#c4b28a',
25051
+ info: '#8ba4b0',
25052
+ muted: '#737c73',
25053
+ selection: '#2d4f67',
25054
+ success: '#87a987',
25055
+ warning: '#c4b28a',
25056
+ },
25057
+ 'kanagawa-lotus': {
25058
+ accent: '#4d699b',
25059
+ border: '#e5ddb0',
25060
+ danger: '#c84053',
25061
+ focusBorder: '#597b75',
25062
+ gitAdded: '#6f894e',
25063
+ gitDeleted: '#c84053',
25064
+ gitModified: '#77713f',
25065
+ info: '#4d699b',
25066
+ muted: '#8a8980',
25067
+ selection: '#dcd5ac',
25068
+ success: '#6f894e',
25069
+ warning: '#77713f',
25070
+ },
25071
+ nordfox: {
25072
+ accent: '#81a1c1',
25073
+ border: '#39404f',
25074
+ danger: '#bf616a',
25075
+ focusBorder: '#88c0d0',
25076
+ gitAdded: '#a3be8c',
25077
+ gitDeleted: '#bf616a',
25078
+ gitModified: '#ebcb8b',
25079
+ info: '#81a1c1',
25080
+ muted: '#60728a',
25081
+ selection: '#3e4a5b',
25082
+ success: '#a3be8c',
25083
+ warning: '#ebcb8b',
25084
+ },
25085
+ duskfox: {
25086
+ accent: '#569fba',
25087
+ border: '#2d2a45',
25088
+ danger: '#eb6f92',
25089
+ focusBorder: '#9ccfd8',
25090
+ gitAdded: '#a3be8c',
25091
+ gitDeleted: '#eb6f92',
25092
+ gitModified: '#f6c177',
25093
+ info: '#569fba',
25094
+ muted: '#817c9c',
25095
+ selection: '#433c59',
25096
+ success: '#a3be8c',
25097
+ warning: '#f6c177',
25098
+ },
25099
+ terafox: {
25100
+ accent: '#5a93aa',
25101
+ border: '#1d3337',
25102
+ danger: '#e85c51',
25103
+ focusBorder: '#a1cdd8',
25104
+ gitAdded: '#7aa4a1',
25105
+ gitDeleted: '#e85c51',
25106
+ gitModified: '#fda47f',
25107
+ info: '#5a93aa',
25108
+ muted: '#6d7f8b',
25109
+ selection: '#293e40',
25110
+ success: '#7aa4a1',
25111
+ warning: '#fda47f',
25112
+ },
25113
+ dawnfox: {
25114
+ accent: '#286983',
25115
+ border: '#ebe0df',
25116
+ danger: '#b4637a',
25117
+ focusBorder: '#56949f',
25118
+ gitAdded: '#618774',
25119
+ gitDeleted: '#b4637a',
25120
+ gitModified: '#ea9d34',
25121
+ info: '#286983',
25122
+ muted: '#9893a5',
25123
+ selection: '#eadcd8',
25124
+ success: '#618774',
25125
+ warning: '#ea9d34',
25126
+ },
25127
+ 'ayu-mirage': {
25128
+ accent: '#ffcc66',
25129
+ border: '#323843',
25130
+ danger: '#f28779',
25131
+ focusBorder: '#95e6cb',
25132
+ gitAdded: '#d5ff80',
25133
+ gitDeleted: '#f28779',
25134
+ gitModified: '#ffd173',
25135
+ info: '#73d0ff',
25136
+ muted: '#5c6773',
25137
+ selection: '#33415e',
25138
+ success: '#d5ff80',
25139
+ warning: '#ffd173',
25140
+ },
25141
+ 'material-darker': {
25142
+ accent: '#82aaff',
25143
+ border: '#343434',
25144
+ danger: '#f07178',
25145
+ focusBorder: '#89ddff',
25146
+ gitAdded: '#c3e88d',
25147
+ gitDeleted: '#f07178',
25148
+ gitModified: '#ffcb6b',
25149
+ info: '#82aaff',
25150
+ muted: '#545454',
25151
+ selection: '#404040',
25152
+ success: '#c3e88d',
25153
+ warning: '#ffcb6b',
25154
+ },
25155
+ 'tokyo-night-moon': {
25156
+ accent: '#82aaff',
25157
+ border: '#2f334d',
25158
+ danger: '#ff757f',
25159
+ focusBorder: '#86e1fc',
25160
+ gitAdded: '#c3e88d',
25161
+ gitDeleted: '#ff757f',
25162
+ gitModified: '#ffc777',
25163
+ info: '#82aaff',
25164
+ muted: '#636da6',
25165
+ selection: '#2d3f76',
25166
+ success: '#c3e88d',
25167
+ warning: '#ffc777',
25168
+ },
25169
+ 'gruvbox-material': {
25170
+ accent: '#7daea3',
25171
+ border: '#504945',
25172
+ danger: '#ea6962',
25173
+ focusBorder: '#89b482',
25174
+ gitAdded: '#a9b665',
25175
+ gitDeleted: '#ea6962',
25176
+ gitModified: '#d8a657',
25177
+ info: '#7daea3',
25178
+ muted: '#928374',
25179
+ selection: '#3c3836',
25180
+ success: '#a9b665',
25181
+ warning: '#d8a657',
25182
+ },
25183
+ 'gruvbox-material-light': {
25184
+ accent: '#45707a',
25185
+ border: '#ddccab',
25186
+ danger: '#c14a4a',
25187
+ focusBorder: '#4c7a5d',
25188
+ gitAdded: '#6c782e',
25189
+ gitDeleted: '#c14a4a',
25190
+ gitModified: '#b47109',
25191
+ info: '#45707a',
25192
+ muted: '#928374',
25193
+ selection: '#eee0b7',
25194
+ success: '#6c782e',
25195
+ warning: '#b47109',
25196
+ },
25197
+ 'modus-vivendi': {
25198
+ accent: '#2fafff',
25199
+ border: '#646464',
25200
+ danger: '#ff5f59',
25201
+ focusBorder: '#00d3d0',
25202
+ gitAdded: '#44bc44',
25203
+ gitDeleted: '#ff5f59',
25204
+ gitModified: '#d0bc00',
25205
+ info: '#2fafff',
25206
+ muted: '#989898',
25207
+ selection: '#5a5a5a',
25208
+ success: '#44bc44',
25209
+ warning: '#d0bc00',
25210
+ },
25211
+ zenburn: {
25212
+ accent: '#8cd0d3',
25213
+ border: '#4f4f4f',
25214
+ danger: '#cc9393',
25215
+ focusBorder: '#93e0e3',
25216
+ gitAdded: '#7f9f7f',
25217
+ gitDeleted: '#cc9393',
25218
+ gitModified: '#f0dfaf',
25219
+ info: '#8cd0d3',
25220
+ muted: '#9f9f8f',
25221
+ selection: '#5f5f5f',
25222
+ success: '#7f9f7f',
25223
+ warning: '#f0dfaf',
25224
+ },
25225
+ oxocarbon: {
25226
+ accent: '#33b1ff',
25227
+ border: '#525252',
25228
+ danger: '#ee5396',
25229
+ focusBorder: '#3ddbd9',
25230
+ gitAdded: '#42be65',
25231
+ gitDeleted: '#ee5396',
25232
+ gitModified: '#ab8e34',
25233
+ info: '#33b1ff',
25234
+ muted: '#6f6f6f',
25235
+ selection: '#2a2a2a',
25236
+ success: '#42be65',
25237
+ warning: '#ab8e34',
25238
+ },
25239
+ 'tomorrow-night': {
25240
+ accent: '#81a2be',
25241
+ border: '#373b41',
25242
+ danger: '#cc6666',
25243
+ focusBorder: '#8abeb7',
25244
+ gitAdded: '#b5bd68',
25245
+ gitDeleted: '#cc6666',
25246
+ gitModified: '#f0c674',
25247
+ info: '#81a2be',
25248
+ muted: '#969896',
25249
+ selection: '#373b41',
25250
+ success: '#b5bd68',
25251
+ warning: '#f0c674',
25252
+ },
25253
+ 'monokai-pro': {
25254
+ accent: '#78dce8',
25255
+ border: '#403e41',
25256
+ danger: '#ff6188',
25257
+ focusBorder: '#a9dc76',
25258
+ gitAdded: '#a9dc76',
25259
+ gitDeleted: '#ff6188',
25260
+ gitModified: '#ffd866',
25261
+ info: '#78dce8',
25262
+ muted: '#727072',
25263
+ selection: '#5b595c',
25264
+ success: '#a9dc76',
25265
+ warning: '#ffd866',
25266
+ },
25267
+ sonokai: {
25268
+ accent: '#76cce0',
25269
+ border: '#33353f',
25270
+ danger: '#fc5d7c',
25271
+ focusBorder: '#9ed072',
25272
+ gitAdded: '#9ed072',
25273
+ gitDeleted: '#fc5d7c',
25274
+ gitModified: '#e7c664',
25275
+ info: '#76cce0',
25276
+ muted: '#7f8490',
25277
+ selection: '#414550',
25278
+ success: '#9ed072',
25279
+ warning: '#e7c664',
25280
+ },
25281
+ 'doom-one': {
25282
+ accent: '#51afef',
25283
+ border: '#3f444a',
25284
+ danger: '#ff6c6b',
25285
+ focusBorder: '#46d9ff',
25286
+ gitAdded: '#98be65',
25287
+ gitDeleted: '#ff6c6b',
25288
+ gitModified: '#ecbe7b',
25289
+ info: '#51afef',
25290
+ muted: '#5b6268',
25291
+ selection: '#42444a',
25292
+ success: '#98be65',
25293
+ warning: '#ecbe7b',
25294
+ },
25295
+ andromeda: {
25296
+ accent: '#00e8c6',
25297
+ border: '#2b2e36',
25298
+ danger: '#ee5d43',
25299
+ focusBorder: '#00e8c6',
25300
+ gitAdded: '#96e072',
25301
+ gitDeleted: '#ee5d43',
25302
+ gitModified: '#ffe66d',
25303
+ info: '#7cb7ff',
25304
+ muted: '#a0a1a7',
25305
+ selection: '#3d4352',
25306
+ success: '#96e072',
25307
+ warning: '#ffe66d',
25308
+ },
25309
+ aura: {
25310
+ accent: '#a277ff',
25311
+ border: '#363c49',
25312
+ danger: '#ff6767',
25313
+ focusBorder: '#61ffca',
25314
+ gitAdded: '#61ffca',
25315
+ gitDeleted: '#ff6767',
25316
+ gitModified: '#ffca85',
25317
+ info: '#82e2ff',
25318
+ muted: '#6d6d6d',
25319
+ selection: '#3d375e',
25320
+ success: '#61ffca',
25321
+ warning: '#ffca85',
25322
+ },
25323
+ cyberdream: {
25324
+ accent: '#5ea1ff',
25325
+ border: '#1e2124',
25326
+ danger: '#ff6e5e',
25327
+ focusBorder: '#5ef1ff',
25328
+ gitAdded: '#5eff6c',
25329
+ gitDeleted: '#ff6e5e',
25330
+ gitModified: '#f1ff5e',
25331
+ info: '#5ea1ff',
25332
+ muted: '#7b8496',
25333
+ selection: '#3c4048',
25334
+ success: '#5eff6c',
25335
+ warning: '#f1ff5e',
25336
+ },
25337
+ nightfly: {
25338
+ accent: '#82aaff',
25339
+ border: '#1d3b53',
25340
+ danger: '#fc514e',
25341
+ focusBorder: '#7fdbca',
25342
+ gitAdded: '#a1cd5e',
25343
+ gitDeleted: '#fc514e',
25344
+ gitModified: '#e3d18a',
25345
+ info: '#82aaff',
25346
+ muted: '#7c8f8f',
25347
+ selection: '#1d3b53',
25348
+ success: '#a1cd5e',
25349
+ warning: '#e3d18a',
25350
+ },
25351
+ panda: {
25352
+ accent: '#ff75b5',
25353
+ border: '#404954',
25354
+ danger: '#ff4b82',
25355
+ focusBorder: '#19f9d8',
25356
+ gitAdded: '#19f9d8',
25357
+ gitDeleted: '#ff4b82',
25358
+ gitModified: '#ffb86c',
25359
+ info: '#45a9f9',
25360
+ muted: '#676b79',
25361
+ selection: '#373841',
25362
+ success: '#19f9d8',
25363
+ warning: '#ffb86c',
25364
+ },
25365
+ 'hyper-snazzy': {
25366
+ accent: '#57c7ff',
25367
+ border: '#43454f',
25368
+ danger: '#ff5c57',
25369
+ focusBorder: '#9aedfe',
25370
+ gitAdded: '#5af78e',
25371
+ gitDeleted: '#ff5c57',
25372
+ gitModified: '#f3f99d',
25373
+ info: '#57c7ff',
25374
+ muted: '#686868',
25375
+ selection: '#3a3d4d',
25376
+ success: '#5af78e',
25377
+ warning: '#f3f99d',
25378
+ },
25379
+ apprentice: {
25380
+ accent: '#5f87af',
25381
+ border: '#444444',
25382
+ danger: '#af5f5f',
25383
+ focusBorder: '#5f8787',
25384
+ gitAdded: '#5f875f',
25385
+ gitDeleted: '#af5f5f',
25386
+ gitModified: '#ffffaf',
25387
+ info: '#5f87af',
25388
+ muted: '#6c6c6c',
25389
+ selection: '#444444',
25390
+ success: '#5f875f',
25391
+ warning: '#ffffaf',
25392
+ },
25393
+ melange: {
25394
+ accent: '#a3a9ce',
25395
+ border: '#34302c',
25396
+ danger: '#d47766',
25397
+ focusBorder: '#89b3b6',
25398
+ gitAdded: '#85b695',
25399
+ gitDeleted: '#d47766',
25400
+ gitModified: '#ebc06d',
25401
+ info: '#a3a9ce',
25402
+ muted: '#867462',
25403
+ selection: '#403a36',
25404
+ success: '#85b695',
25405
+ warning: '#ebc06d',
25406
+ },
25407
+ 'melange-light': {
25408
+ accent: '#465aa4',
25409
+ border: '#e9e1db',
25410
+ danger: '#bf0021',
25411
+ focusBorder: '#3d6568',
25412
+ gitAdded: '#3a684a',
25413
+ gitDeleted: '#bf0021',
25414
+ gitModified: '#a06d00',
25415
+ info: '#465aa4',
25416
+ muted: '#7d6658',
25417
+ selection: '#d9d3ce',
25418
+ success: '#3a684a',
25419
+ warning: '#a06d00',
25420
+ },
25421
+ spaceduck: {
25422
+ accent: '#00a3cc',
25423
+ border: '#30365f',
25424
+ danger: '#e33400',
25425
+ focusBorder: '#ce6f8f',
25426
+ gitAdded: '#5ccc96',
25427
+ gitDeleted: '#e33400',
25428
+ gitModified: '#f2ce00',
25429
+ info: '#7a5ccc',
25430
+ muted: '#686f9a',
25431
+ selection: '#30365f',
25432
+ success: '#5ccc96',
25433
+ warning: '#f2ce00',
25434
+ },
25435
+ embark: {
25436
+ accent: '#d4bfff',
25437
+ border: '#585273',
25438
+ danger: '#f48fb1',
25439
+ focusBorder: '#abf8f7',
25440
+ gitAdded: '#a1efd3',
25441
+ gitDeleted: '#f48fb1',
25442
+ gitModified: '#ffe6b3',
25443
+ info: '#91ddff',
25444
+ muted: '#8a889d',
25445
+ selection: '#3e3859',
25446
+ success: '#a1efd3',
25447
+ warning: '#ffe6b3',
25448
+ },
25449
+ 'bluloco-dark': {
25450
+ accent: '#3691ff',
25451
+ border: '#3d434f',
25452
+ danger: '#ff2e3f',
25453
+ focusBorder: '#4483aa',
25454
+ gitAdded: '#3fc56b',
25455
+ gitDeleted: '#ff2e3f',
25456
+ gitModified: '#f9c859',
25457
+ info: '#3691ff',
25458
+ muted: '#636d83',
25459
+ selection: '#2f343e',
25460
+ success: '#3fc56b',
25461
+ warning: '#f9c859',
25462
+ },
25463
+ 'bluloco-light': {
25464
+ accent: '#275fe4',
25465
+ border: '#d5d7d8',
25466
+ danger: '#d52753',
25467
+ focusBorder: '#40b8c5',
25468
+ gitAdded: '#23974a',
25469
+ gitDeleted: '#d52753',
25470
+ gitModified: '#c5a332',
25471
+ info: '#275fe4',
25472
+ muted: '#a0a1a7',
25473
+ selection: '#d2ecff',
25474
+ success: '#23974a',
25475
+ warning: '#c5a332',
25476
+ },
25477
+ 'papercolor-dark': {
25478
+ accent: '#5fafd7',
25479
+ border: '#444444',
25480
+ danger: '#af005f',
25481
+ focusBorder: '#00afaf',
25482
+ gitAdded: '#5faf00',
25483
+ gitDeleted: '#af005f',
25484
+ gitModified: '#d7af5f',
25485
+ info: '#5fafd7',
25486
+ muted: '#808080',
25487
+ selection: '#303030',
25488
+ success: '#5faf00',
25489
+ warning: '#d7af5f',
25490
+ },
25491
+ 'base16-ocean': {
25492
+ accent: '#8fa1b3',
25493
+ border: '#343d46',
25494
+ danger: '#bf616a',
25495
+ focusBorder: '#96b5b4',
25496
+ gitAdded: '#a3be8c',
25497
+ gitDeleted: '#bf616a',
25498
+ gitModified: '#ebcb8b',
25499
+ info: '#8fa1b3',
25500
+ muted: '#65737e',
25501
+ selection: '#4f5b66',
25502
+ success: '#a3be8c',
25503
+ warning: '#ebcb8b',
25504
+ },
25505
+ 'base16-eighties': {
25506
+ accent: '#6699cc',
25507
+ border: '#393939',
25508
+ danger: '#f2777a',
25509
+ focusBorder: '#66cccc',
25510
+ gitAdded: '#99cc99',
25511
+ gitDeleted: '#f2777a',
25512
+ gitModified: '#ffcc66',
25513
+ info: '#6699cc',
25514
+ muted: '#747369',
25515
+ selection: '#515151',
25516
+ success: '#99cc99',
25517
+ warning: '#ffcc66',
25518
+ },
25519
+ everblush: {
25520
+ accent: '#67b0e8',
25521
+ border: '#232a2d',
25522
+ danger: '#e57474',
25523
+ focusBorder: '#6cbfbf',
25524
+ gitAdded: '#8ccf7e',
25525
+ gitDeleted: '#e57474',
25526
+ gitModified: '#e5c76b',
25527
+ info: '#67b0e8',
25528
+ muted: '#5e6164',
25529
+ selection: '#2d3437',
25530
+ success: '#8ccf7e',
25531
+ warning: '#e5c76b',
25532
+ },
25533
+ darcula: {
25534
+ accent: '#cc7832',
25535
+ border: '#3c3f41',
25536
+ danger: '#ff6b68',
25537
+ focusBorder: '#629755',
25538
+ gitAdded: '#6a8759',
25539
+ gitDeleted: '#ff6b68',
25540
+ gitModified: '#ffc66d',
25541
+ info: '#6897bb',
25542
+ muted: '#808080',
25543
+ selection: '#214283',
25544
+ success: '#6a8759',
25545
+ warning: '#ffc66d',
25546
+ },
25547
+ eldritch: {
25548
+ accent: '#a48cf2',
25549
+ border: '#292e42',
25550
+ danger: '#f16c75',
25551
+ focusBorder: '#04d1f9',
25552
+ gitAdded: '#37f499',
25553
+ gitDeleted: '#f16c75',
25554
+ gitModified: '#f1fc79',
25555
+ info: '#04d1f9',
25556
+ muted: '#7081d0',
25557
+ selection: '#2d3052',
25558
+ success: '#37f499',
25559
+ warning: '#f1fc79',
25560
+ },
25561
+ 'edge-light': {
25562
+ accent: '#5079be',
25563
+ border: '#dde2e7',
25564
+ danger: '#d05858',
25565
+ focusBorder: '#3a8b84',
25566
+ gitAdded: '#608e32',
25567
+ gitDeleted: '#d05858',
25568
+ gitModified: '#be7e05',
25569
+ info: '#5079be',
25570
+ muted: '#a0a1a7',
25571
+ selection: '#e3e6eb',
25572
+ success: '#608e32',
25573
+ warning: '#be7e05',
25574
+ },
25575
+ zenbones: {
25576
+ accent: '#286486',
25577
+ border: '#cfd1d0',
25578
+ danger: '#a8334c',
25579
+ focusBorder: '#3b8992',
25580
+ gitAdded: '#4f6c31',
25581
+ gitDeleted: '#a8334c',
25582
+ gitModified: '#944927',
25583
+ info: '#286486',
25584
+ muted: '#a8a29e',
25585
+ selection: '#cbd9e3',
25586
+ success: '#4f6c31',
25587
+ warning: '#944927',
25588
+ },
25589
+ 'iceberg-light': {
25590
+ accent: '#2d539e',
25591
+ border: '#cad0de',
25592
+ danger: '#cc517a',
25593
+ focusBorder: '#3f83a6',
25594
+ gitAdded: '#668e3d',
25595
+ gitDeleted: '#cc517a',
25596
+ gitModified: '#c57339',
25597
+ info: '#2d539e',
25598
+ muted: '#8389a3',
25599
+ selection: '#c9cdd7',
25600
+ success: '#668e3d',
25601
+ warning: '#c57339',
25602
+ },
25603
+ 'github-dark-dimmed': {
25604
+ accent: '#539bf5',
25605
+ border: '#444c56',
25606
+ danger: '#f47067',
25607
+ focusBorder: '#39c5cf',
25608
+ gitAdded: '#57ab5a',
25609
+ gitDeleted: '#f47067',
25610
+ gitModified: '#c69026',
25611
+ info: '#539bf5',
25612
+ muted: '#636e7b',
25613
+ selection: '#2d333b',
25614
+ success: '#57ab5a',
25615
+ warning: '#c69026',
25616
+ },
25617
+ 'edge-dark': {
25618
+ accent: '#6cb6eb',
25619
+ border: '#414550',
25620
+ danger: '#ec7279',
25621
+ focusBorder: '#5dbbc1',
25622
+ gitAdded: '#a0c980',
25623
+ gitDeleted: '#ec7279',
25624
+ gitModified: '#deb974',
25625
+ info: '#6cb6eb',
25626
+ muted: '#758094',
25627
+ selection: '#3b3e48',
25628
+ success: '#a0c980',
25629
+ warning: '#deb974',
25630
+ },
25631
+ 'selenized-dark': {
25632
+ accent: '#4695f7',
25633
+ border: '#2d5b69',
25634
+ danger: '#fa5750',
25635
+ focusBorder: '#41c7b9',
25636
+ gitAdded: '#75b938',
25637
+ gitDeleted: '#fa5750',
25638
+ gitModified: '#dbb32d',
25639
+ info: '#4695f7',
25640
+ muted: '#72898f',
25641
+ selection: '#184956',
25642
+ success: '#75b938',
25643
+ warning: '#dbb32d',
25644
+ },
25645
+ 'selenized-black': {
25646
+ accent: '#368aeb',
25647
+ border: '#3b3b3b',
25648
+ danger: '#ed4a46',
25649
+ focusBorder: '#3fc5b7',
25650
+ gitAdded: '#70b433',
25651
+ gitDeleted: '#ed4a46',
25652
+ gitModified: '#dbb32d',
25653
+ info: '#368aeb',
25654
+ muted: '#777777',
25655
+ selection: '#252525',
25656
+ success: '#70b433',
25657
+ warning: '#dbb32d',
25658
+ },
25659
+ 'selenized-light': {
25660
+ accent: '#0072d4',
25661
+ border: '#d5cdb6',
25662
+ danger: '#d2212d',
25663
+ focusBorder: '#009c8f',
25664
+ gitAdded: '#489100',
25665
+ gitDeleted: '#d2212d',
25666
+ gitModified: '#ad8900',
25667
+ info: '#0072d4',
25668
+ muted: '#909995',
25669
+ selection: '#ece3cc',
25670
+ success: '#489100',
25671
+ warning: '#ad8900',
25672
+ },
25673
+ 'monokai-pro-machine': {
25674
+ accent: '#7cd5f1',
25675
+ border: '#1d2528',
25676
+ danger: '#ff6d7e',
25677
+ focusBorder: '#a2e57b',
25678
+ gitAdded: '#a2e57b',
25679
+ gitDeleted: '#ff6d7e',
25680
+ gitModified: '#ffed72',
25681
+ info: '#7cd5f1',
25682
+ muted: '#6b7678',
25683
+ selection: '#3a4449',
25684
+ success: '#a2e57b',
25685
+ warning: '#ffed72',
25686
+ },
25687
+ 'monokai-pro-octagon': {
25688
+ accent: '#9cd1bb',
25689
+ border: '#1e1f2b',
25690
+ danger: '#ff657a',
25691
+ focusBorder: '#bad761',
25692
+ gitAdded: '#bad761',
25693
+ gitDeleted: '#ff657a',
25694
+ gitModified: '#ffd76d',
25695
+ info: '#9cd1bb',
25696
+ muted: '#696d77',
25697
+ selection: '#3a3d4b',
25698
+ success: '#bad761',
25699
+ warning: '#ffd76d',
25700
+ },
25701
+ 'monokai-pro-ristretto': {
25702
+ accent: '#85dacc',
25703
+ border: '#211c1c',
25704
+ danger: '#fd6883',
25705
+ focusBorder: '#adda78',
25706
+ gitAdded: '#adda78',
25707
+ gitDeleted: '#fd6883',
25708
+ gitModified: '#f9cc6c',
25709
+ info: '#85dacc',
25710
+ muted: '#72696a',
25711
+ selection: '#403838',
25712
+ success: '#adda78',
25713
+ warning: '#f9cc6c',
25714
+ },
25715
+ 'monokai-pro-spectrum': {
25716
+ accent: '#5ad4e6',
25717
+ border: '#191919',
25718
+ danger: '#fc618d',
25719
+ focusBorder: '#7bd88f',
25720
+ gitAdded: '#7bd88f',
25721
+ gitDeleted: '#fc618d',
25722
+ gitModified: '#fce566',
25723
+ info: '#5ad4e6',
25724
+ muted: '#69676c',
25725
+ selection: '#363537',
25726
+ success: '#7bd88f',
25727
+ warning: '#fce566',
25728
+ },
25729
+ 'base16-default-dark': {
25730
+ accent: '#7cafc2',
25731
+ border: '#282828',
25732
+ danger: '#ab4642',
25733
+ focusBorder: '#86c1b9',
25734
+ gitAdded: '#a1b56c',
25735
+ gitDeleted: '#ab4642',
25736
+ gitModified: '#f7ca88',
25737
+ info: '#7cafc2',
25738
+ muted: '#585858',
25739
+ selection: '#383838',
25740
+ success: '#a1b56c',
25741
+ warning: '#f7ca88',
25742
+ },
25743
+ 'base16-default-light': {
25744
+ accent: '#7cafc2',
25745
+ border: '#e8e8e8',
25746
+ danger: '#ab4642',
25747
+ focusBorder: '#86c1b9',
25748
+ gitAdded: '#a1b56c',
25749
+ gitDeleted: '#ab4642',
25750
+ gitModified: '#dc9656',
25751
+ info: '#7cafc2',
25752
+ muted: '#b8b8b8',
25753
+ selection: '#d8d8d8',
25754
+ success: '#a1b56c',
25755
+ warning: '#dc9656',
25756
+ },
25757
+ tomorrow: {
25758
+ accent: '#4271ae',
25759
+ border: '#efefef',
25760
+ danger: '#c82829',
25761
+ focusBorder: '#3e999f',
25762
+ gitAdded: '#718c00',
25763
+ gitDeleted: '#c82829',
25764
+ gitModified: '#eab700',
25765
+ info: '#4271ae',
25766
+ muted: '#8e908c',
25767
+ selection: '#d6d6d6',
25768
+ success: '#718c00',
25769
+ warning: '#eab700',
25770
+ },
25771
+ tokyodark: {
25772
+ accent: '#a485dd',
25773
+ border: '#2a2c41',
25774
+ danger: '#ee6d85',
25775
+ focusBorder: '#38a89d',
25776
+ gitAdded: '#95c561',
25777
+ gitDeleted: '#ee6d85',
25778
+ gitModified: '#d7a65f',
25779
+ info: '#7199ee',
25780
+ muted: '#4a5057',
25781
+ selection: '#212234',
25782
+ success: '#95c561',
25783
+ warning: '#d7a65f',
25784
+ },
25785
+ 'spacemacs-dark': {
25786
+ accent: '#bc6ec5',
25787
+ border: '#5d4d7a',
25788
+ danger: '#f2241f',
25789
+ focusBorder: '#2d9574',
25790
+ gitAdded: '#67b11d',
25791
+ gitDeleted: '#f2241f',
25792
+ gitModified: '#b1951d',
25793
+ info: '#4f97d7',
25794
+ muted: '#6c6783',
25795
+ selection: '#444155',
25796
+ success: '#67b11d',
25797
+ warning: '#b1951d',
25798
+ },
25799
+ bamboo: {
25800
+ accent: '#8fb573',
25801
+ border: '#3a3d37',
25802
+ danger: '#e75a7c',
25803
+ focusBorder: '#70c2be',
25804
+ gitAdded: '#8fb573',
25805
+ gitDeleted: '#e75a7c',
25806
+ gitModified: '#dbb651',
25807
+ info: '#57a5e5',
25808
+ muted: '#838781',
25809
+ selection: '#383b35',
25810
+ success: '#8fb573',
25811
+ warning: '#dbb651',
25812
+ },
25813
+ citylights: {
25814
+ accent: '#5ec4ff',
25815
+ border: '#2f3a42',
25816
+ danger: '#e27e8d',
25817
+ focusBorder: '#70e1e8',
25818
+ gitAdded: '#54af83',
25819
+ gitDeleted: '#e27e8d',
25820
+ gitModified: '#ebda65',
25821
+ info: '#68a1f0',
25822
+ muted: '#41505e',
25823
+ selection: '#363c43',
25824
+ success: '#54af83',
25825
+ warning: '#ebda65',
25826
+ },
25827
+ 'oxocarbon-light': {
25828
+ accent: '#0f62fe',
25829
+ border: '#e0e0e0',
25830
+ danger: '#ee5396',
25831
+ focusBorder: '#08bdba',
25832
+ gitAdded: '#42be65',
25833
+ gitDeleted: '#ee5396',
25834
+ gitModified: '#ff6f00',
25835
+ info: '#0f62fe',
25836
+ muted: '#525252',
25837
+ selection: '#dde1e6',
25838
+ success: '#42be65',
25839
+ warning: '#ff6f00',
25840
+ },
24923
25841
  };
24924
25842
  /**
24925
25843
  * Ordered list of every selectable theme preset, for the `coco ui` theme
@@ -25308,7 +26226,6 @@ function withPushedView(state, value) {
25308
26226
  // persistence and pop-view restores the previous tab.
25309
26227
  sidebarTab: value === 'compose' || value === 'status' ? 'branches' : state.sidebarTab,
25310
26228
  worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
25311
- selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
25312
26229
  diffSource: value === 'diff' ? state.diffSource : undefined,
25313
26230
  stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
25314
26231
  compareHead: value === 'diff' ? state.compareHead : undefined,
@@ -25342,7 +26259,6 @@ function withPoppedView(state) {
25342
26259
  // returns the user to whatever they actually had open before.
25343
26260
  sidebarTab: state.userSidebarTab,
25344
26261
  worktreeDiffOffset: next === 'diff' ? state.worktreeDiffOffset : 0,
25345
- selectedWorktreeHunkIndex: next === 'diff' ? state.selectedWorktreeHunkIndex : 0,
25346
26262
  diffSource: next === 'diff' ? state.diffSource : undefined,
25347
26263
  stashDiffRef: next === 'diff' ? state.stashDiffRef : undefined,
25348
26264
  compareBase: wasOnDiff ? undefined : state.compareBase,
@@ -25471,7 +26387,6 @@ function withReplacedView(state, value) {
25471
26387
  activeView: value,
25472
26388
  viewStack,
25473
26389
  worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
25474
- selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
25475
26390
  diffSource: value === 'diff' ? state.diffSource : undefined,
25476
26391
  stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
25477
26392
  compareHead: value === 'diff' ? state.compareHead : undefined,
@@ -25609,9 +26524,29 @@ function nextHunkOffset(currentOffset, hunkOffsets, delta) {
25609
26524
  const previousOffset = [...hunkOffsets].reverse().find((offset) => offset < currentOffset);
25610
26525
  return previousOffset === undefined ? currentOffset : previousOffset;
25611
26526
  }
25612
- function nextHunkIndex(currentOffset, hunkOffsets, delta) {
25613
- const offset = nextHunkOffset(currentOffset, hunkOffsets, delta);
25614
- return Math.max(0, hunkOffsets.indexOf(offset));
26527
+ /**
26528
+ * Which hunk the viewport is currently showing — the index of the last
26529
+ * hunk whose `@@` header offset is at or above the viewport top
26530
+ * (`offset`). This is the single source of truth for the worktree
26531
+ * staging diff's "current hunk" (#1179): deriving it from the scroll
26532
+ * position keeps the header, the in-body highlight, and `space`/`z`
26533
+ * (stage / revert) all pointed at the hunk you're actually looking at,
26534
+ * whether you got there by hunk-jump (↑/↓) or page-scroll (PgUp/PgDn).
26535
+ * The old `indexOf(landedOffset)` approach reset to hunk 0 whenever the
26536
+ * offset wasn't exactly on a boundary, and page-scroll never updated it
26537
+ * at all — so the indicator stuck at "1/N".
26538
+ */
26539
+ function hunkIndexAtOffset(offset, hunkOffsets) {
26540
+ let index = 0;
26541
+ for (let i = 0; i < hunkOffsets.length; i += 1) {
26542
+ if (hunkOffsets[i] <= offset) {
26543
+ index = i;
26544
+ }
26545
+ else {
26546
+ break;
26547
+ }
26548
+ }
26549
+ return index;
25615
26550
  }
25616
26551
  function getLogInkSidebarTabs() {
25617
26552
  return [...SIDEBAR_TABS];
@@ -25628,7 +26563,6 @@ function createLogInkState(rows, options = {}) {
25628
26563
  selectedIndex: 0,
25629
26564
  selectedFileIndex: 0,
25630
26565
  selectedWorktreeFileIndex: 0,
25631
- selectedWorktreeHunkIndex: 0,
25632
26566
  selectedBranchIndex: 0,
25633
26567
  selectedTagIndex: 0,
25634
26568
  selectedStashIndex: 0,
@@ -25829,7 +26763,6 @@ function applyLogInkAction(state, action) {
25829
26763
  return {
25830
26764
  ...next,
25831
26765
  selectedWorktreeFileIndex: clampIndex(state.selectedWorktreeFileIndex + action.delta, action.fileCount),
25832
- selectedWorktreeHunkIndex: 0,
25833
26766
  worktreeDiffOffset: 0,
25834
26767
  // Cursor moved to a real file row — drop header focus so the
25835
26768
  // file Enter handler (open diff) is what fires next.
@@ -25873,7 +26806,6 @@ function applyLogInkAction(state, action) {
25873
26806
  return {
25874
26807
  ...state,
25875
26808
  selectedWorktreeFileIndex: Math.max(0, action.targetIndex),
25876
- selectedWorktreeHunkIndex: 0,
25877
26809
  worktreeDiffOffset: 0,
25878
26810
  statusGroupHeaderFocused: false,
25879
26811
  pendingKey: undefined,
@@ -26113,16 +27045,19 @@ function applyLogInkAction(state, action) {
26113
27045
  pendingKey: undefined,
26114
27046
  };
26115
27047
  case 'pageWorktreeDiff':
27048
+ // The current staging hunk is derived from the scroll offset at
27049
+ // the read sites (#1185), so paging only moves the offset.
26116
27050
  return {
26117
27051
  ...state,
26118
27052
  worktreeDiffOffset: clampIndex(state.worktreeDiffOffset + action.delta, action.lineCount),
26119
27053
  pendingKey: undefined,
26120
27054
  };
26121
27055
  case 'jumpWorktreeHunk':
27056
+ // `[`/`]` move the offset onto the next/previous hunk header; the
27057
+ // current hunk is derived from that offset at the read sites.
26122
27058
  return {
26123
27059
  ...state,
26124
27060
  worktreeDiffOffset: nextHunkOffset(state.worktreeDiffOffset, action.hunkOffsets, action.delta),
26125
- selectedWorktreeHunkIndex: nextHunkIndex(state.worktreeDiffOffset, action.hunkOffsets, action.delta),
26126
27061
  pendingKey: undefined,
26127
27062
  };
26128
27063
  case 'jumpCommitDiffHunk':
@@ -26167,7 +27102,6 @@ function applyLogInkAction(state, action) {
26167
27102
  activeView: HOME_VIEW,
26168
27103
  viewStack: [HOME_VIEW],
26169
27104
  worktreeDiffOffset: 0,
26170
- selectedWorktreeHunkIndex: 0,
26171
27105
  pendingCommitFocused: false,
26172
27106
  pendingKey: undefined,
26173
27107
  };
@@ -26206,7 +27140,6 @@ function applyLogInkAction(state, action) {
26206
27140
  return {
26207
27141
  ...next,
26208
27142
  selectedWorktreeFileIndex: Math.max(0, action.fileIndex),
26209
- selectedWorktreeHunkIndex: 0,
26210
27143
  worktreeDiffOffset: 0,
26211
27144
  diffSource: 'worktree',
26212
27145
  };
@@ -26256,7 +27189,6 @@ function applyLogInkAction(state, action) {
26256
27189
  return {
26257
27190
  ...next,
26258
27191
  selectedWorktreeFileIndex: Math.max(0, action.fileIndex),
26259
- selectedWorktreeHunkIndex: 0,
26260
27192
  worktreeDiffOffset: 0,
26261
27193
  };
26262
27194
  }
@@ -26368,6 +27300,10 @@ function applyLogInkAction(state, action) {
26368
27300
  pendingMutationConfirmation: action.value ? undefined : state.pendingMutationConfirmation,
26369
27301
  pendingKey: undefined,
26370
27302
  };
27303
+ case 'setWorktreeCheckoutConflict':
27304
+ return { ...state, worktreeCheckoutConflict: action.value, pendingKey: undefined };
27305
+ case 'setPendingChoice':
27306
+ return { ...state, pendingChoice: action.value, pendingKey: undefined };
26371
27307
  case 'setPendingMutationConfirmation':
26372
27308
  return {
26373
27309
  ...state,
@@ -27739,6 +28675,46 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
27739
28675
  }
27740
28676
  return [];
27741
28677
  }
28678
+ // Multi-option prompt (#1181) — the n-way generalization of the y/n
28679
+ // confirmation. Match the keypress against the prompt's options; each
28680
+ // either runs a workflow or fires a built-in navigation intent.
28681
+ if (state.pendingChoice) {
28682
+ const option = state.pendingChoice.options.find((opt) => opt.key === inputValue);
28683
+ if (option) {
28684
+ // `switch-worktree` is pure navigation — open the worktree as a
28685
+ // nested repo frame. Handled here rather than via the workflow
28686
+ // runner, whose post-action context refresh would mis-target the
28687
+ // frame we just pushed.
28688
+ if (option.intent === 'switch-worktree' && state.worktreeCheckoutConflict) {
28689
+ const conflict = state.worktreeCheckoutConflict;
28690
+ return [
28691
+ action({ type: 'pushRepoFrame', label: conflict.branch, workdir: conflict.worktreePath }),
28692
+ action({ type: 'setStatus', value: `Switched to worktree ${conflict.worktreePath} (${conflict.branch})` }),
28693
+ action({ type: 'setPendingChoice', value: undefined }),
28694
+ action({ type: 'setWorktreeCheckoutConflict', value: undefined }),
28695
+ ];
28696
+ }
28697
+ if (option.workflowId) {
28698
+ // The workflow runner owns the live context + clears any
28699
+ // conflict state once it resolves.
28700
+ return [
28701
+ { type: 'runWorkflowAction', id: option.workflowId },
28702
+ action({ type: 'setPendingChoice', value: undefined }),
28703
+ ];
28704
+ }
28705
+ return [action({ type: 'setPendingChoice', value: undefined })];
28706
+ }
28707
+ if (inputValue === 'n' || key.escape) {
28708
+ return [
28709
+ action({ type: 'setPendingChoice', value: undefined }),
28710
+ ...(state.worktreeCheckoutConflict
28711
+ ? [action({ type: 'setWorktreeCheckoutConflict', value: undefined })]
28712
+ : []),
28713
+ action({ type: 'setStatus', value: 'cancelled' }),
28714
+ ];
28715
+ }
28716
+ return [];
28717
+ }
27742
28718
  if (state.pendingConfirmationId) {
27743
28719
  if (inputValue === 'y') {
27744
28720
  const workflowAction = getLogInkWorkflowActionById(state.pendingConfirmationId);
@@ -28612,22 +29588,16 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28612
29588
  fileCount: context.worktreeFileCount,
28613
29589
  })];
28614
29590
  }
28615
- // Worktree (staging) diff: ↑/↓ move between hunks the hunk is the
28616
- // unit you stage, so the cursor walks hunks (auto-scrolling to the
28617
- // selected one). Single-hunk files fall through to line-scroll so a
28618
- // long lone hunk stays readable; `[`/`]` remain hunk-jump aliases.
28619
- if (state.activeView === 'diff' && (context.worktreeHunkOffsets?.length ?? 0) > 1) {
28620
- return [action({
28621
- type: 'jumpWorktreeHunk',
28622
- delta: -1,
28623
- hunkOffsets: context.worktreeHunkOffsets,
28624
- })];
28625
- }
29591
+ // Worktree (staging) diff: ↑/↓ scroll linesconsistent with the
29592
+ // commit / stash diffs (#1185). `[`/`]` jump between hunks (the
29593
+ // staging unit), and the current hunk is derived from the scroll
29594
+ // position, so line-scrolling still walks the staging target.
28626
29595
  if (state.activeView === 'diff' && context.worktreeDiffLineCount) {
28627
29596
  return [action({
28628
29597
  type: 'pageWorktreeDiff',
28629
29598
  delta: -1,
28630
29599
  lineCount: context.worktreeDiffLineCount,
29600
+ hunkOffsets: context.worktreeHunkOffsets,
28631
29601
  })];
28632
29602
  }
28633
29603
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -28737,20 +29707,14 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28737
29707
  fileCount: context.worktreeFileCount,
28738
29708
  })];
28739
29709
  }
28740
- // Worktree (staging) diff: ↓ walks to the next hunk (see the ↑
28741
- // handler). Multi-hunk only; single-hunk files line-scroll.
28742
- if (state.activeView === 'diff' && (context.worktreeHunkOffsets?.length ?? 0) > 1) {
28743
- return [action({
28744
- type: 'jumpWorktreeHunk',
28745
- delta: 1,
28746
- hunkOffsets: context.worktreeHunkOffsets,
28747
- })];
28748
- }
29710
+ // Worktree (staging) diff: ↓ scrolls lines (see the ↑ handler) —
29711
+ // `[`/`]` jump hunks (#1185).
28749
29712
  if (state.activeView === 'diff' && context.worktreeDiffLineCount) {
28750
29713
  return [action({
28751
29714
  type: 'pageWorktreeDiff',
28752
29715
  delta: 1,
28753
29716
  lineCount: context.worktreeDiffLineCount,
29717
+ hunkOffsets: context.worktreeHunkOffsets,
28754
29718
  })];
28755
29719
  }
28756
29720
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -28810,6 +29774,7 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28810
29774
  type: 'pageWorktreeDiff',
28811
29775
  delta: -8,
28812
29776
  lineCount: context.worktreeDiffLineCount,
29777
+ hunkOffsets: context.worktreeHunkOffsets,
28813
29778
  })];
28814
29779
  }
28815
29780
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -28834,6 +29799,7 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28834
29799
  type: 'pageWorktreeDiff',
28835
29800
  delta: 8,
28836
29801
  lineCount: context.worktreeDiffLineCount,
29802
+ hunkOffsets: context.worktreeHunkOffsets,
28837
29803
  })];
28838
29804
  }
28839
29805
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -33357,6 +34323,7 @@ function renderFooter$1(h, components, state, context, theme, idleTip, spinnerFr
33357
34323
  state.gitignorePicker ||
33358
34324
  state.inputPrompt ||
33359
34325
  state.pendingConfirmationId ||
34326
+ state.pendingChoice ||
33360
34327
  state.pendingMutationConfirmation ||
33361
34328
  state.pendingKey ||
33362
34329
  state.filterMode);
@@ -36164,7 +37131,7 @@ function renderDiffSurface(ctx, diff) {
36164
37131
  ? []
36165
37132
  : splitActive
36166
37133
  ? renderSplitDiffBody(h, components, previewHunks, state.diffPreviewOffset, visibleRows, width, theme, 'commit-diff-split', syntaxSpans)
36167
- : visiblePreviewHunks.map((line, index) => renderDiffLine(h, Text, line, theme, syntaxSpans, 140, `diff-surface-line-${state.diffPreviewOffset + index}`));
37134
+ : visiblePreviewHunks.map((line, index) => renderDiffLine(h, Text, line, theme, syntaxSpans, Math.max(8, width - 5), `diff-surface-line-${state.diffPreviewOffset + index}`));
36168
37135
  return h(Box, {
36169
37136
  borderColor: focusBorderColor(theme, focused),
36170
37137
  borderStyle: theme.borderStyle,
@@ -36175,25 +37142,38 @@ function renderDiffSurface(ctx, diff) {
36175
37142
  }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle(splitActive ? 'Diff (split)' : 'Diff', focused)), h(Text, { dimColor: true }, selectedDetailFile?.path || 'no file')), ...headerLines.map((line, index) => h(Text, {
36176
37143
  key: `diff-surface-header-${index}`,
36177
37144
  dimColor: index > 0,
36178
- }, truncateCells(line, 140))), ...commitBodyNodes);
37145
+ }, truncateCells(line, Math.max(20, width - 4)))), ...commitBodyNodes);
36179
37146
  }
36180
37147
  const diffLines = worktreeDiff?.lines || [];
36181
- const selectedHunk = worktreeHunks?.hunks[state.selectedWorktreeHunkIndex];
36182
37148
  const totalHunks = worktreeHunks?.hunks.length ?? 0;
36183
37149
  const stagedHunks = worktreeHunks?.hunks.filter((hunk) => hunk.state === 'staged').length ?? 0;
37150
+ // The "current" hunk is derived from the scroll position (#1185) —
37151
+ // the single source of truth is `worktreeDiffOffset`. ↑/↓ scroll
37152
+ // lines and `[`/`]` jump hunks; either way the header, rail, and
37153
+ // stage/revert target all follow what's on screen.
37154
+ const currentHunkIndex = hunkIndexAtOffset(state.worktreeDiffOffset, worktreeDiff?.hunkOffsets ?? []);
36184
37155
  const visibleDiffLines = diffLines.slice(state.worktreeDiffOffset, state.worktreeDiffOffset + visibleRows);
36185
- // Hunk-position line: badge + selected hunk's state + a staged/total
36186
- // progress count, so the user always sees how far through staging they
36187
- // are. Untracked/new files have no hunks point them at whole-file
36188
- // staging instead of a dead-end "no hunks" message.
37156
+ // Hunk-position line: `Hunk n/N` + an at-a-glance staging rail + a
37157
+ // staged/total count, so the user sees how far through staging they
37158
+ // are without reading each hunk. The rail shows one marker per hunk —
37159
+ // filled = staged, hollow = unstaged with the current hunk bracketed
37160
+ // (which also conveys whether the current hunk is staged, replacing
37161
+ // the old standalone "● staged / ○ unstaged" badge). Untracked/new
37162
+ // files have no hunks — point them at whole-file staging instead of a
37163
+ // dead-end "no hunks" message.
37164
+ const railMarker = (staged) => staged ? (theme.ascii ? 'x' : '●') : (theme.ascii ? '.' : '○');
37165
+ const hunkRail = (worktreeHunks?.hunks ?? [])
37166
+ .map((hunk, index) => {
37167
+ const marker = railMarker(hunk.state === 'staged');
37168
+ return index === currentHunkIndex ? `[${marker}]` : marker;
37169
+ })
37170
+ .join('');
36189
37171
  const hunkHeaderLine = worktreeHunksLoading
36190
37172
  ? 'Hunks loading…'
36191
37173
  : worktreeDiff?.untracked
36192
37174
  ? (theme.ascii ? 'New file — press space to stage it whole.' : '✚ New file — press space to stage it whole.')
36193
37175
  : totalHunks
36194
- ? `Hunk ${state.selectedWorktreeHunkIndex + 1}/${totalHunks} · ${selectedHunk?.state === 'staged'
36195
- ? (theme.ascii ? '[x] staged' : '● staged')
36196
- : (theme.ascii ? '[ ] unstaged' : '○ unstaged')} · ${stagedHunks}/${totalHunks} staged`
37176
+ ? `Hunk ${currentHunkIndex + 1}/${totalHunks} ${hunkRail} ${stagedHunks}/${totalHunks} staged`
36197
37177
  : 'No stageable hunks for this file.';
36198
37178
  const headerLines = isLogInkContextKeyLoading(contextStatus, 'worktree')
36199
37179
  ? ['Loading file context...']
@@ -36225,7 +37205,7 @@ function renderDiffSurface(ctx, diff) {
36225
37205
  h(Text, { dimColor: true }, worktreeDiff?.filePath || worktreeFile?.path || 'no file')), ...headerLines.map((line, index) => h(Text, {
36226
37206
  key: `diff-surface-header-${index}`,
36227
37207
  dimColor: index > 0,
36228
- }, truncateCells(line, 140))), ...(showDiffLines
37208
+ }, truncateCells(line, Math.max(20, width - 4)))), ...(showDiffLines
36229
37209
  ? renderWorktreeDiffBody(h, components, {
36230
37210
  lines: diffLines,
36231
37211
  offset: state.worktreeDiffOffset,
@@ -36235,7 +37215,7 @@ function renderDiffSurface(ctx, diff) {
36235
37215
  syntaxSpans,
36236
37216
  hunkOffsets: worktreeDiff?.hunkOffsets || [],
36237
37217
  hunks: worktreeHunks?.hunks || [],
36238
- selectedIndex: state.selectedWorktreeHunkIndex,
37218
+ selectedIndex: currentHunkIndex,
36239
37219
  keyPrefix: 'diff-surface-line',
36240
37220
  })
36241
37221
  : []));
@@ -37660,6 +38640,26 @@ function renderConfirmationPanel(h, components, state, width, theme, focused) {
37660
38640
  paddingX: 1,
37661
38641
  }, h(Text, { bold: true }, panelTitle('Confirm', focused)), h(Text, undefined, truncateCells(label, width - 4)), h(Text, { dimColor: true }, truncateCells(warning, width - 4)), h(Text, undefined, ''), h(Text, undefined, 'Press y to confirm or n/Esc to cancel.'));
37662
38642
  }
38643
+ /**
38644
+ * Multi-option prompt panel (#1181) — the n-way generalization of the
38645
+ * confirmation panel. Renders the prompt title, an optional warning, and
38646
+ * one row per option (`<key> <label>`, destructive options in the
38647
+ * danger colour), plus a cancel hint. Resolution happens in the input
38648
+ * layer by matching a keypress against the option keys.
38649
+ */
38650
+ function renderChoicePanel(h, components, prompt, width, theme, focused) {
38651
+ const { Box, Text } = components;
38652
+ return h(Box, {
38653
+ borderColor: focusBorderColor(theme, focused),
38654
+ borderStyle: theme.borderStyle,
38655
+ flexDirection: 'column',
38656
+ width,
38657
+ paddingX: 1,
38658
+ }, h(Text, { bold: true }, panelTitle('Choose', focused)), h(Text, undefined, truncateCells(prompt.title, width - 4)), ...(prompt.warning ? [h(Text, { dimColor: true }, truncateCells(prompt.warning, width - 4))] : []), h(Text, undefined, ''), ...prompt.options.map((option) => h(Text, {
38659
+ key: `choice-${prompt.id}-${option.key}`,
38660
+ color: option.destructive && !theme.noColor ? theme.colors.danger : undefined,
38661
+ }, truncateCells(` ${option.key} ${option.label}`, width - 4))), h(Text, { dimColor: true }, ' n/Esc cancel'));
38662
+ }
37663
38663
  /**
37664
38664
  * First-launch onboarding overlay (P1.3). Shown once per machine, gated
37665
38665
  * by an XDG-style cache marker so subsequent launches go straight to the
@@ -38051,9 +39051,20 @@ function renderSplitPlanOverlay(h, components, state, width, bodyRows, theme, fo
38051
39051
  // overlay rather than crashing.
38052
39052
  return h(Box, { width }, h(Text, { dimColor: true }, 'No plan data available.'));
38053
39053
  }
39054
+ // Committed groups are numbered 1..N; an `unclaimed` group (the files
39055
+ // the split couldn't place) renders as a distinct "will stay" note
39056
+ // rather than a phantom commit (#1180).
39057
+ const committedGroups = plan.groups.filter((group) => !group.unclaimed);
38054
39058
  const lines = [];
38055
- plan.groups.forEach((group, index) => {
38056
- lines.push(`▎ ${index + 1}. ${group.title}`);
39059
+ let commitNumber = 0;
39060
+ plan.groups.forEach((group) => {
39061
+ if (group.unclaimed) {
39062
+ lines.push(`⚠ ${group.title} (stays in your worktree — not committed)`);
39063
+ }
39064
+ else {
39065
+ commitNumber += 1;
39066
+ lines.push(`▎ ${commitNumber}. ${group.title}`);
39067
+ }
38057
39068
  if (group.body) {
38058
39069
  group.body.split('\n').forEach((bodyLine) => lines.push(` ${bodyLine}`));
38059
39070
  }
@@ -38078,9 +39089,10 @@ function renderSplitPlanOverlay(h, components, state, width, bodyRows, theme, fo
38078
39089
  const totalLines = lines.length;
38079
39090
  const scrollOffset = Math.min(overlay.scrollOffset, Math.max(0, totalLines - 1));
38080
39091
  const visible = lines.slice(scrollOffset, scrollOffset + listRows);
39092
+ const unclaimedCount = plan.groups.length - committedGroups.length;
38081
39093
  const headerRight = overlay.status === 'applying'
38082
39094
  ? `${spinner} applying…`
38083
- : `${plan.groups.length} commit(s) · ${scrollOffset + 1}–${Math.min(totalLines, scrollOffset + listRows)} / ${totalLines}`;
39095
+ : `${committedGroups.length} commit(s)${unclaimedCount ? ' · 1 set stays staged' : ''} · ${scrollOffset + 1}–${Math.min(totalLines, scrollOffset + listRows)} / ${totalLines}`;
38084
39096
  // Apply errors get the full available width — long validator
38085
39097
  // messages (the failure path that surfaced in PR #916 testing was
38086
39098
  // "unknown hunks: src/widgets/button.ts::hunk-1, ...") frequently
@@ -40554,6 +41566,9 @@ function renderDetailPanel(h, components, state, context, contextStatus, detail,
40554
41566
  if (state.inputPrompt) {
40555
41567
  return renderInputPromptPanel(h, components, state, width, theme, focused);
40556
41568
  }
41569
+ if (state.pendingChoice) {
41570
+ return renderChoicePanel(h, components, state.pendingChoice, width, theme, focused);
41571
+ }
40557
41572
  if (state.pendingConfirmationId || state.pendingMutationConfirmation) {
40558
41573
  return renderConfirmationPanel(h, components, state, width, theme, focused);
40559
41574
  }
@@ -41407,7 +42422,7 @@ function LogInkApp(deps) {
41407
42422
  React.useEffect(() => {
41408
42423
  if (!state.statusMessage)
41409
42424
  return;
41410
- if (state.inputPrompt || state.pendingConfirmationId || state.pendingMutationConfirmation || state.showCommandPalette) {
42425
+ if (state.inputPrompt || state.pendingConfirmationId || state.pendingChoice || state.pendingMutationConfirmation || state.showCommandPalette) {
41411
42426
  return;
41412
42427
  }
41413
42428
  // The `setTimeout` callback is a literal arrow function (not a
@@ -41424,6 +42439,7 @@ function LogInkApp(deps) {
41424
42439
  dispatch,
41425
42440
  state.inputPrompt,
41426
42441
  state.pendingConfirmationId,
42442
+ state.pendingChoice,
41427
42443
  state.pendingMutationConfirmation,
41428
42444
  state.showCommandPalette,
41429
42445
  state.statusMessage,
@@ -42287,7 +43303,9 @@ function LogInkApp(deps) {
42287
43303
  setWorktreeHunks(undefined);
42288
43304
  }, [dispatch, git, refreshWorktreeContext, selectedWorktreeFile]);
42289
43305
  const toggleSelectedHunkStage = React.useCallback(async () => {
42290
- const selectedHunk = worktreeHunks?.hunks[state.selectedWorktreeHunkIndex];
43306
+ // The staging target is the hunk under the viewport (#1185) —
43307
+ // derived from the scroll offset, the single source of truth.
43308
+ const selectedHunk = worktreeHunks?.hunks[hunkIndexAtOffset(state.worktreeDiffOffset, worktreeDiff?.hunkOffsets ?? [])];
42291
43309
  if (!selectedHunk) {
42292
43310
  dispatch({ type: 'setStatus', value: 'no hunk selected', kind: 'warning' });
42293
43311
  return;
@@ -42316,7 +43334,7 @@ function LogInkApp(deps) {
42316
43334
  kind: 'error',
42317
43335
  });
42318
43336
  }
42319
- }, [dispatch, git, refreshWorktreeContext, state.selectedWorktreeHunkIndex, worktreeHunks]);
43337
+ }, [dispatch, git, refreshWorktreeContext, state.worktreeDiffOffset, worktreeDiff, worktreeHunks]);
42320
43338
  const revertSelectedFile = React.useCallback(async () => {
42321
43339
  if (!selectedWorktreeFile) {
42322
43340
  dispatch({ type: 'setStatus', value: 'no worktree file selected', kind: 'warning' });
@@ -42330,7 +43348,7 @@ function LogInkApp(deps) {
42330
43348
  setWorktreeHunks(undefined);
42331
43349
  }, [dispatch, git, refreshWorktreeContext, selectedWorktreeFile]);
42332
43350
  const revertSelectedHunk = React.useCallback(async () => {
42333
- const selectedHunk = worktreeHunks?.hunks[state.selectedWorktreeHunkIndex];
43351
+ const selectedHunk = worktreeHunks?.hunks[hunkIndexAtOffset(state.worktreeDiffOffset, worktreeDiff?.hunkOffsets ?? [])];
42334
43352
  if (!selectedHunk) {
42335
43353
  dispatch({ type: 'setStatus', value: 'no hunk selected', kind: 'warning' });
42336
43354
  return;
@@ -42350,7 +43368,7 @@ function LogInkApp(deps) {
42350
43368
  kind: 'error',
42351
43369
  });
42352
43370
  }
42353
- }, [dispatch, git, refreshWorktreeContext, state.selectedWorktreeHunkIndex, worktreeHunks]);
43371
+ }, [dispatch, git, refreshWorktreeContext, state.worktreeDiffOffset, worktreeDiff, worktreeHunks]);
42354
43372
  const createCommitFromCompose = React.useCallback(async () => {
42355
43373
  const stagedCount = context.worktree?.stagedCount || 0;
42356
43374
  if (!stagedCount) {
@@ -43232,9 +44250,19 @@ function LogInkApp(deps) {
43232
44250
  // navigateHome nukes the rest of the stack so `<` after apply
43233
44251
  // doesn't walk back into the now-empty compose / status state
43234
44252
  // the user just left behind.
44253
+ // Did the plan leave files for the user (the `unclaimed` group the
44254
+ // split couldn't confidently place)? They're now sitting unstaged in
44255
+ // the worktree, so land on status — not history — so the user sees
44256
+ // and handles them, rather than dropping them on a clean-looking
44257
+ // history view (#1180).
44258
+ const unclaimedGroup = splitPlan.plan.groups.find((group) => group.unclaimed);
44259
+ const unclaimedFileCount = unclaimedGroup?.files?.length ?? 0;
43235
44260
  dispatch({ type: 'clearSplitPlan' });
43236
44261
  dispatch({ type: 'commitCompose', action: { type: 'reset' } });
43237
44262
  dispatch({ type: 'navigateHome' });
44263
+ if (unclaimedFileCount > 0) {
44264
+ dispatch({ type: 'pushView', value: 'status' });
44265
+ }
43238
44266
  // Refresh BEFORE setting the final status so we can peek at the
43239
44267
  // post-apply worktree state and craft a directive next-step hint
43240
44268
  // ("X unstaged + Y untracked remaining — press gs to stage / I
@@ -43284,12 +44312,17 @@ function LogInkApp(deps) {
43284
44312
  return;
43285
44313
  }
43286
44314
  const successMessage = formatSplitApplySuccess(commitHashes.length, unstaged, untracked, result.fallback ? { reason: result.fallback.reason } : undefined);
44315
+ // Name the files the split deliberately left behind so the jump to
44316
+ // status reads as intentional, not a surprise (#1180).
44317
+ const unclaimedNote = unclaimedFileCount > 0
44318
+ ? ` · ${unclaimedFileCount} file${unclaimedFileCount === 1 ? '' : 's'} left for you on status`
44319
+ : '';
43287
44320
  // Fallback path uses 'info' kind — apply technically succeeded
43288
44321
  // but the user should know it landed as a single combined commit
43289
44322
  // rather than a real LLM-driven multi-group split.
43290
44323
  dispatch({
43291
44324
  type: 'setStatus',
43292
- value: successMessage,
44325
+ value: `${successMessage}${unclaimedNote}`,
43293
44326
  kind: result.fallback ? 'info' : 'success',
43294
44327
  });
43295
44328
  }, [dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext, state.splitPlan]);
@@ -43772,6 +44805,42 @@ function LogInkApp(deps) {
43772
44805
  // path on the wrong target.
43773
44806
  return removeWorktreeAndBranch(git, cursorTarget, context.branches?.localBranches || []);
43774
44807
  },
44808
+ // Worktree-checkout-conflict resolutions (#1175). Unlike the
44809
+ // cursor-targeted handlers above, these act on the worktree
44810
+ // captured in `state.worktreeCheckoutConflict` (the one git named
44811
+ // when it refused the checkout), not the worktrees-view cursor.
44812
+ 'conflict-remove-worktree-checkout': async () => {
44813
+ const conflict = state.worktreeCheckoutConflict;
44814
+ dispatch({ type: 'setWorktreeCheckoutConflict', value: undefined });
44815
+ if (!conflict)
44816
+ return { ok: false, message: 'No worktree conflict to resolve.' };
44817
+ const worktree = context.worktreeList?.worktrees?.find((w) => w.path === conflict.worktreePath);
44818
+ if (!worktree)
44819
+ return { ok: false, message: `Worktree ${conflict.worktreePath} not found.` };
44820
+ // removeWorktree refuses a dirty / current worktree and returns
44821
+ // a clear message — surface it rather than forcing.
44822
+ const removed = await removeWorktree(git, worktree);
44823
+ if (!removed.ok)
44824
+ return removed;
44825
+ const branch = (context.branches?.localBranches || []).find((b) => b.type === 'local' && b.shortName === conflict.branch);
44826
+ if (!branch) {
44827
+ return { ok: true, message: `Removed worktree ${worktree.path}; branch ${conflict.branch} not found to check out.` };
44828
+ }
44829
+ const checkout = await checkoutBranch(git, branch);
44830
+ return checkout.ok
44831
+ ? { ok: true, message: `Removed worktree ${worktree.path} and checked out ${conflict.branch}` }
44832
+ : { ok: false, message: `Removed worktree ${worktree.path}, but checkout failed: ${checkout.message}` };
44833
+ },
44834
+ 'conflict-remove-worktree-branch': async () => {
44835
+ const conflict = state.worktreeCheckoutConflict;
44836
+ dispatch({ type: 'setWorktreeCheckoutConflict', value: undefined });
44837
+ if (!conflict)
44838
+ return { ok: false, message: 'No worktree conflict to resolve.' };
44839
+ const worktree = context.worktreeList?.worktrees?.find((w) => w.path === conflict.worktreePath);
44840
+ if (!worktree)
44841
+ return { ok: false, message: `Worktree ${conflict.worktreePath} not found.` };
44842
+ return removeWorktreeAndBranch(git, worktree, context.branches?.localBranches || []);
44843
+ },
43775
44844
  'abort-operation': async () => {
43776
44845
  const operation = context.operation?.operation;
43777
44846
  if (!operation) {
@@ -44234,6 +45303,43 @@ function LogInkApp(deps) {
44234
45303
  kind: 'warning',
44235
45304
  });
44236
45305
  }
45306
+ // Checking out a branch that's already checked out in another
45307
+ // worktree is rejected by git ("already checked out at <path>").
45308
+ // Rather than dead-end on that, capture the conflict and raise a
45309
+ // multi-option prompt: switch into that worktree, remove it and
45310
+ // check out here, or remove it and delete the branch (#1175, #1181).
45311
+ if (id === 'checkout-branch' && !result?.ok && isBranchCheckedOutElsewhereError(result?.message)) {
45312
+ const worktreePath = parseCheckedOutWorktreePath(result?.message);
45313
+ const branchName = pendingItemAction?.id;
45314
+ if (worktreePath && branchName) {
45315
+ const worktree = context.worktreeList?.worktrees?.find((w) => w.path === worktreePath);
45316
+ const dirty = worktree?.dirty ?? false;
45317
+ dispatch({
45318
+ type: 'setWorktreeCheckoutConflict',
45319
+ value: { branch: branchName, worktreePath, dirty },
45320
+ });
45321
+ dispatch({
45322
+ type: 'setPendingChoice',
45323
+ value: {
45324
+ id: 'worktree-checkout-conflict',
45325
+ title: `'${branchName}' is checked out in another worktree`,
45326
+ warning: `Checked out at ${worktreePath}.${dirty ? ' That worktree has uncommitted changes — removal will be refused until it is clean or stashed.' : ''}`,
45327
+ options: [
45328
+ { key: 'y', label: 'Switch to that worktree', intent: 'switch-worktree' },
45329
+ { key: 'r', label: 'Remove worktree & check out here', workflowId: 'conflict-remove-worktree-checkout', destructive: true },
45330
+ { key: 'x', label: 'Remove worktree & delete branch', workflowId: 'conflict-remove-worktree-branch', destructive: true },
45331
+ ],
45332
+ },
45333
+ });
45334
+ }
45335
+ else {
45336
+ dispatch({
45337
+ type: 'setStatus',
45338
+ value: `'${branchName ?? 'branch'}' is already checked out in another worktree.`,
45339
+ kind: 'warning',
45340
+ });
45341
+ }
45342
+ }
44237
45343
  // Refresh history rows AS WELL when the workflow could have
44238
45344
  // changed the commits the user sees (#945 follow-up). The
44239
45345
  // workflow IDs below all either create/rewrite local commits or
@@ -44243,6 +45349,10 @@ function LogInkApp(deps) {
44243
45349
  // metadata-only mutations (delete-tag, set-upstream, etc.).
44244
45350
  const historyMutatingIds = new Set([
44245
45351
  'checkout-branch',
45352
+ // Resolving a checkout conflict changes HEAD (checkout) and/or the
45353
+ // ref set (branch delete), so the graph needs a refresh.
45354
+ 'conflict-remove-worktree-checkout',
45355
+ 'conflict-remove-worktree-branch',
44246
45356
  'continue-operation',
44247
45357
  'pull-current-branch',
44248
45358
  // Fetch / pull / push bring in new commits and move
@@ -44278,7 +45388,7 @@ function LogInkApp(deps) {
44278
45388
  // (resolvePendingItemAction → action 'checkout'), so a silent
44279
45389
  // stale-while-revalidate swap keeps the list readable and just
44280
45390
  // repaints the current-branch marker once the new context lands.
44281
- if (id === 'checkout-branch' && result?.ok) {
45391
+ if ((id === 'checkout-branch' || id === 'conflict-remove-worktree-checkout') && result?.ok) {
44282
45392
  dispatch({ type: 'resetBranchSelection' });
44283
45393
  await refreshContext({ silent: true });
44284
45394
  }
@@ -44345,7 +45455,7 @@ function LogInkApp(deps) {
44345
45455
  }, [context, dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext,
44346
45456
  state.branchSort, state.filter, state.selectedBranchIndex,
44347
45457
  state.selectedStashIndex, state.selectedTagIndex, state.selectedWorktreeListIndex, state.stashDiffRef,
44348
- state.statusFilterMask, state.tagSort]);
45458
+ state.statusFilterMask, state.tagSort, state.worktreeCheckoutConflict]);
44349
45459
  // Resolve the active view's "yank target" (commit hash / branch /
44350
45460
  // tag / stash ref / file path) against the live filtered+sorted list,
44351
45461
  // copy it to the system clipboard, and surface the result on the
@@ -45156,6 +46266,7 @@ function LogInkApp(deps) {
45156
46266
  state.gitignorePicker ||
45157
46267
  state.inputPrompt ||
45158
46268
  state.pendingConfirmationId ||
46269
+ state.pendingChoice ||
45159
46270
  state.pendingMutationConfirmation ||
45160
46271
  state.pendingKey
45161
46272
  ? 'inspector'
@@ -46863,7 +47974,9 @@ const options$1 = {
46863
47974
  },
46864
47975
  theme: {
46865
47976
  description: 'TUI theme preset',
46866
- 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'],
47977
+ // Derived from the single source of truth (`THEME_PRESET_COLORS`) so the
47978
+ // CLI choices can never drift from the themes the picker actually offers.
47979
+ choices: getLogInkThemePresets(),
46867
47980
  },
46868
47981
  };
46869
47982
  const builder$1 = (yargs) => {
@@ -46891,7 +48004,9 @@ const options = {
46891
48004
  },
46892
48005
  theme: {
46893
48006
  description: 'TUI theme preset',
46894
- 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'],
48007
+ // Derived from the single source of truth (`THEME_PRESET_COLORS`) so the
48008
+ // workspace CLI choices stay in sync with the themes the picker offers.
48009
+ choices: getLogInkThemePresets(),
46895
48010
  },
46896
48011
  };
46897
48012
  const builder = (yargs) => {
@@ -47659,20 +48774,55 @@ function createWorkspaceState(init) {
47659
48774
  }
47660
48775
  return { ...base, selectedIndex: idx };
47661
48776
  }
48777
+ /**
48778
+ * Single-entry memo for {@link selectVisibleRepos}. The visible list
48779
+ * is a pure function of five referentially-stable state slices, and
48780
+ * the hot path — cursor movement (`j`/`k`) — leaves all five untouched
48781
+ * (`move-cursor` only swaps `selectedIndex`). Without the memo the
48782
+ * renderer recomputes the full sort + tab-filter + text-filter three
48783
+ * times per render (list window, header chips, and the direct call in
48784
+ * `renderListBody`), plus once more in the reducer, on every keystroke.
48785
+ *
48786
+ * Keyed on object/string identity so a reducer transition that swaps
48787
+ * any of `overview.repos`, `sortMode`, `tab`, `filter`, or
48788
+ * `pullRequestCounts` is a guaranteed cache miss. Callers never mutate
48789
+ * the returned array, so handing back the cached reference is safe and
48790
+ * also stabilises the array identity for any future `React.memo`.
48791
+ */
48792
+ let visibleReposCache = null;
47662
48793
  /**
47663
48794
  * Recompute the visible repo list — sort → tab filter → text filter.
47664
48795
  * The renderer consumes this; the reducer also uses it to rectify the
47665
48796
  * cursor after a filter/sort change so the selection stays in range.
48797
+ * Memoized on its five inputs (see {@link visibleReposCache}).
47666
48798
  */
47667
48799
  function selectVisibleRepos(state) {
47668
- const sorted = sortWorkspaceRepos(state.overview.repos, state.sortMode);
48800
+ const repos = state.overview.repos;
48801
+ const cache = visibleReposCache;
48802
+ if (cache !== null &&
48803
+ cache.repos === repos &&
48804
+ cache.sortMode === state.sortMode &&
48805
+ cache.tab === state.tab &&
48806
+ cache.filter === state.filter &&
48807
+ cache.pullRequestCounts === state.pullRequestCounts) {
48808
+ return cache.result;
48809
+ }
48810
+ const sorted = sortWorkspaceRepos(repos, state.sortMode);
47669
48811
  const tabFiltered = filterWorkspaceRepos(sorted, state.tab, {
47670
48812
  pullRequestCounts: state.pullRequestCounts,
47671
48813
  });
47672
- if (!state.filter) {
47673
- return tabFiltered;
47674
- }
47675
- return tabFiltered.filter((entry) => matchesWorkspaceText(entry, state.filter));
48814
+ const result = state.filter
48815
+ ? tabFiltered.filter((entry) => matchesWorkspaceText(entry, state.filter))
48816
+ : tabFiltered;
48817
+ visibleReposCache = {
48818
+ repos,
48819
+ sortMode: state.sortMode,
48820
+ tab: state.tab,
48821
+ filter: state.filter,
48822
+ pullRequestCounts: state.pullRequestCounts,
48823
+ result,
48824
+ };
48825
+ return result;
47676
48826
  }
47677
48827
  function clampCursor(index, length) {
47678
48828
  if (length <= 0) {