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.
package/dist/index.js CHANGED
@@ -78,7 +78,7 @@ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
78
78
  /**
79
79
  * Current build version from package.json
80
80
  */
81
- const BUILD_VERSION = "0.62.3";
81
+ const BUILD_VERSION = "0.63.0";
82
82
 
83
83
  const isInteractive = (config) => {
84
84
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -2263,7 +2263,66 @@ const schema$1 = {
2263
2263
  "material-lighter",
2264
2264
  "papercolor-light",
2265
2265
  "modus-operandi",
2266
- "quiet-light"
2266
+ "quiet-light",
2267
+ "catppuccin-frappe",
2268
+ "rose-pine-moon",
2269
+ "kanagawa-dragon",
2270
+ "kanagawa-lotus",
2271
+ "nordfox",
2272
+ "duskfox",
2273
+ "terafox",
2274
+ "dawnfox",
2275
+ "ayu-mirage",
2276
+ "material-darker",
2277
+ "tokyo-night-moon",
2278
+ "gruvbox-material",
2279
+ "gruvbox-material-light",
2280
+ "modus-vivendi",
2281
+ "zenburn",
2282
+ "oxocarbon",
2283
+ "tomorrow-night",
2284
+ "monokai-pro",
2285
+ "sonokai",
2286
+ "doom-one",
2287
+ "andromeda",
2288
+ "aura",
2289
+ "cyberdream",
2290
+ "nightfly",
2291
+ "panda",
2292
+ "hyper-snazzy",
2293
+ "apprentice",
2294
+ "melange",
2295
+ "melange-light",
2296
+ "spaceduck",
2297
+ "embark",
2298
+ "bluloco-dark",
2299
+ "bluloco-light",
2300
+ "papercolor-dark",
2301
+ "base16-ocean",
2302
+ "base16-eighties",
2303
+ "everblush",
2304
+ "darcula",
2305
+ "eldritch",
2306
+ "edge-light",
2307
+ "zenbones",
2308
+ "iceberg-light",
2309
+ "github-dark-dimmed",
2310
+ "edge-dark",
2311
+ "selenized-dark",
2312
+ "selenized-black",
2313
+ "selenized-light",
2314
+ "monokai-pro-machine",
2315
+ "monokai-pro-octagon",
2316
+ "monokai-pro-ristretto",
2317
+ "monokai-pro-spectrum",
2318
+ "base16-default-dark",
2319
+ "base16-default-light",
2320
+ "tomorrow",
2321
+ "tokyodark",
2322
+ "spacemacs-dark",
2323
+ "bamboo",
2324
+ "citylights",
2325
+ "oxocarbon-light"
2267
2326
  ]
2268
2327
  }
2269
2328
  }
@@ -7492,6 +7551,7 @@ var ZodFirstPartyTypeKind;
7492
7551
  ZodFirstPartyTypeKind["ZodReadonly"] = "ZodReadonly";
7493
7552
  })(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {}));
7494
7553
  const stringType = ZodString.create;
7554
+ const booleanType = ZodBoolean.create;
7495
7555
  ZodNever.create;
7496
7556
  const arrayType = ZodArray.create;
7497
7557
  const objectType = ZodObject.create;
@@ -15449,6 +15509,14 @@ const CommitSplitPlanSchema = objectType({
15449
15509
  // that.)
15450
15510
  files: arrayType(stringType()).optional(),
15451
15511
  hunks: arrayType(stringType()).optional(),
15512
+ // Internal flag (not emitted by the model). Set by
15513
+ // `rescueMissingFiles` on the catch-all group of files the
15514
+ // plan didn't confidently place: the apply step skips
15515
+ // committing these, leaving them in the worktree for the user
15516
+ // to handle, and the review overlay renders them as a "will
15517
+ // stay — not committed" note rather than a numbered commit
15518
+ // (#1180).
15519
+ unclaimed: booleanType().optional(),
15452
15520
  })
15453
15521
  .refine((group) => (group.files?.length ?? 0) > 0 || (group.hunks?.length ?? 0) > 0, {
15454
15522
  message: 'Each group must include at least one file or hunk',
@@ -15766,11 +15834,16 @@ function rescueMissingFiles(plan, staged, hunkInventory) {
15766
15834
  groups: [
15767
15835
  ...plan.groups,
15768
15836
  {
15769
- title: 'chore: misc unclaimed changes',
15770
- body: 'Files the split plan did not assign to any other commit. Review and re-roll (`r`) if these belong in a specific commit.',
15771
- rationale: 'Recovered by validator rescuemodel omitted these from every group.',
15837
+ // Tagged `unclaimed`: the apply step skips committing this group
15838
+ // (the files are left in the worktree), and the review overlay
15839
+ // renders it as a "will stay not committed" note rather than a
15840
+ // numbered commit. The confident commits land; these come back
15841
+ // to you on the status screen to handle (#1180).
15842
+ title: 'Left for you — not committed',
15843
+ 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.`,
15772
15844
  files: missing,
15773
15845
  hunks: [],
15846
+ unclaimed: true,
15774
15847
  },
15775
15848
  ],
15776
15849
  };
@@ -16212,6 +16285,15 @@ async function applyCommitSplitPlan({ plan, changes, hunkInventory, git, logger,
16212
16285
  // "git commit with nothing staged" failure mode mid-loop after
16213
16286
  // the up-front `git reset` has already wiped the index.
16214
16287
  const applicableGroups = plan.groups.filter((group) => {
16288
+ // `unclaimed` groups are intentionally NOT committed (#1180): they
16289
+ // hold the files the plan couldn't confidently place. The up-front
16290
+ // `git reset` below unstages everything, and since these never get
16291
+ // re-added they simply stay in the worktree for the user to handle.
16292
+ // They still count as "claimed" for validatePlanForStagedFiles, so
16293
+ // that check (run above) passes.
16294
+ if (group.unclaimed) {
16295
+ return false;
16296
+ }
16215
16297
  const fileCount = (group.files || []).length;
16216
16298
  const hunkCount = (group.hunks || []).length;
16217
16299
  return fileCount + hunkCount > 0;
@@ -23824,11 +23906,12 @@ function computeLogInkFooterHints(options) {
23824
23906
  global: NORMAL_GLOBAL_HINTS,
23825
23907
  };
23826
23908
  }
23827
- // Worktree (staging) diff. The hunk is the unit of action: ↑/↓ walk
23828
- // hunks, space stages/unstages the selected one, a stages the whole
23829
- // file, z discards the hunk.
23909
+ // Worktree (staging) diff. Consistent with the commit/stash diffs
23910
+ // (#1185): j/k scroll lines, [/] jump between hunks. space stages /
23911
+ // unstages the hunk under the viewport, a stages the whole file, z
23912
+ // discards the current hunk.
23830
23913
  return {
23831
- contextual: ['↑/↓ hunk', 'space stage', 'a stage file', 'z discard', 'o edit', 'esc back'],
23914
+ contextual: ['j/k lines', '[/] hunk', 'space stage', 'a stage file', 'z discard', 'o edit', 'esc back'],
23832
23915
  global: NORMAL_GLOBAL_HINTS,
23833
23916
  };
23834
23917
  }
@@ -24201,14 +24284,23 @@ function getColorLevel(env = process.env) {
24201
24284
  return '256';
24202
24285
  return '16';
24203
24286
  }
24204
- 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']);
24287
+ /**
24288
+ * The only presets whose palettes are ANSI-named (not hex): `default`
24289
+ * renders faithfully on 16-color terminals, and `monochrome` carries no
24290
+ * color at all. Every other preset in `THEME_PRESET_COLORS` is hand-authored
24291
+ * in hex, so it's treated as truecolor by definition — no list to keep in
24292
+ * sync as themes are added.
24293
+ */
24294
+ const ANSI_NATIVE_PRESETS = new Set(['default', 'monochrome']);
24205
24295
  /**
24206
24296
  * `true` when the named preset relies on hex colors that look best under
24207
24297
  * 24-bit RGB. Used by `createLogInkTheme` to decide whether to downgrade
24208
- * to the ANSI-named `default` palette on lower-capability terminals.
24298
+ * to the ANSI-named `default` palette on lower-capability terminals. Every
24299
+ * preset except the two ANSI-native baselines uses hex, so this is derived
24300
+ * rather than enumerated — new themes are covered automatically.
24209
24301
  */
24210
24302
  function presetUsesTrueColor(preset) {
24211
- return preset !== undefined && TRUECOLOR_PRESETS.has(preset);
24303
+ return preset !== undefined && !ANSI_NATIVE_PRESETS.has(preset);
24212
24304
  }
24213
24305
  /**
24214
24306
  * WCAG 2.x relative luminance of a `#rrggbb` color, 0 (black) … 1 (white).
@@ -24937,6 +25029,832 @@ const THEME_PRESET_COLORS = {
24937
25029
  success: '#448c27',
24938
25030
  warning: '#a67d00',
24939
25031
  },
25032
+ 'catppuccin-frappe': {
25033
+ accent: '#8caaee',
25034
+ border: '#51576d',
25035
+ danger: '#e78284',
25036
+ focusBorder: '#81c8be',
25037
+ gitAdded: '#a6d189',
25038
+ gitDeleted: '#e78284',
25039
+ gitModified: '#e5c890',
25040
+ info: '#8caaee',
25041
+ muted: '#737994',
25042
+ selection: '#414559',
25043
+ success: '#a6d189',
25044
+ warning: '#e5c890',
25045
+ },
25046
+ 'rose-pine-moon': {
25047
+ accent: '#c4a7e7',
25048
+ border: '#393552',
25049
+ danger: '#eb6f92',
25050
+ focusBorder: '#9ccfd8',
25051
+ gitAdded: '#3e8fb0',
25052
+ gitDeleted: '#eb6f92',
25053
+ gitModified: '#f6c177',
25054
+ info: '#9ccfd8',
25055
+ muted: '#6e6a86',
25056
+ selection: '#44415a',
25057
+ success: '#3e8fb0',
25058
+ warning: '#f6c177',
25059
+ },
25060
+ 'kanagawa-dragon': {
25061
+ accent: '#8ba4b0',
25062
+ border: '#282727',
25063
+ danger: '#c4746e',
25064
+ focusBorder: '#8ea4a2',
25065
+ gitAdded: '#87a987',
25066
+ gitDeleted: '#c4746e',
25067
+ gitModified: '#c4b28a',
25068
+ info: '#8ba4b0',
25069
+ muted: '#737c73',
25070
+ selection: '#2d4f67',
25071
+ success: '#87a987',
25072
+ warning: '#c4b28a',
25073
+ },
25074
+ 'kanagawa-lotus': {
25075
+ accent: '#4d699b',
25076
+ border: '#e5ddb0',
25077
+ danger: '#c84053',
25078
+ focusBorder: '#597b75',
25079
+ gitAdded: '#6f894e',
25080
+ gitDeleted: '#c84053',
25081
+ gitModified: '#77713f',
25082
+ info: '#4d699b',
25083
+ muted: '#8a8980',
25084
+ selection: '#dcd5ac',
25085
+ success: '#6f894e',
25086
+ warning: '#77713f',
25087
+ },
25088
+ nordfox: {
25089
+ accent: '#81a1c1',
25090
+ border: '#39404f',
25091
+ danger: '#bf616a',
25092
+ focusBorder: '#88c0d0',
25093
+ gitAdded: '#a3be8c',
25094
+ gitDeleted: '#bf616a',
25095
+ gitModified: '#ebcb8b',
25096
+ info: '#81a1c1',
25097
+ muted: '#60728a',
25098
+ selection: '#3e4a5b',
25099
+ success: '#a3be8c',
25100
+ warning: '#ebcb8b',
25101
+ },
25102
+ duskfox: {
25103
+ accent: '#569fba',
25104
+ border: '#2d2a45',
25105
+ danger: '#eb6f92',
25106
+ focusBorder: '#9ccfd8',
25107
+ gitAdded: '#a3be8c',
25108
+ gitDeleted: '#eb6f92',
25109
+ gitModified: '#f6c177',
25110
+ info: '#569fba',
25111
+ muted: '#817c9c',
25112
+ selection: '#433c59',
25113
+ success: '#a3be8c',
25114
+ warning: '#f6c177',
25115
+ },
25116
+ terafox: {
25117
+ accent: '#5a93aa',
25118
+ border: '#1d3337',
25119
+ danger: '#e85c51',
25120
+ focusBorder: '#a1cdd8',
25121
+ gitAdded: '#7aa4a1',
25122
+ gitDeleted: '#e85c51',
25123
+ gitModified: '#fda47f',
25124
+ info: '#5a93aa',
25125
+ muted: '#6d7f8b',
25126
+ selection: '#293e40',
25127
+ success: '#7aa4a1',
25128
+ warning: '#fda47f',
25129
+ },
25130
+ dawnfox: {
25131
+ accent: '#286983',
25132
+ border: '#ebe0df',
25133
+ danger: '#b4637a',
25134
+ focusBorder: '#56949f',
25135
+ gitAdded: '#618774',
25136
+ gitDeleted: '#b4637a',
25137
+ gitModified: '#ea9d34',
25138
+ info: '#286983',
25139
+ muted: '#9893a5',
25140
+ selection: '#eadcd8',
25141
+ success: '#618774',
25142
+ warning: '#ea9d34',
25143
+ },
25144
+ 'ayu-mirage': {
25145
+ accent: '#ffcc66',
25146
+ border: '#323843',
25147
+ danger: '#f28779',
25148
+ focusBorder: '#95e6cb',
25149
+ gitAdded: '#d5ff80',
25150
+ gitDeleted: '#f28779',
25151
+ gitModified: '#ffd173',
25152
+ info: '#73d0ff',
25153
+ muted: '#5c6773',
25154
+ selection: '#33415e',
25155
+ success: '#d5ff80',
25156
+ warning: '#ffd173',
25157
+ },
25158
+ 'material-darker': {
25159
+ accent: '#82aaff',
25160
+ border: '#343434',
25161
+ danger: '#f07178',
25162
+ focusBorder: '#89ddff',
25163
+ gitAdded: '#c3e88d',
25164
+ gitDeleted: '#f07178',
25165
+ gitModified: '#ffcb6b',
25166
+ info: '#82aaff',
25167
+ muted: '#545454',
25168
+ selection: '#404040',
25169
+ success: '#c3e88d',
25170
+ warning: '#ffcb6b',
25171
+ },
25172
+ 'tokyo-night-moon': {
25173
+ accent: '#82aaff',
25174
+ border: '#2f334d',
25175
+ danger: '#ff757f',
25176
+ focusBorder: '#86e1fc',
25177
+ gitAdded: '#c3e88d',
25178
+ gitDeleted: '#ff757f',
25179
+ gitModified: '#ffc777',
25180
+ info: '#82aaff',
25181
+ muted: '#636da6',
25182
+ selection: '#2d3f76',
25183
+ success: '#c3e88d',
25184
+ warning: '#ffc777',
25185
+ },
25186
+ 'gruvbox-material': {
25187
+ accent: '#7daea3',
25188
+ border: '#504945',
25189
+ danger: '#ea6962',
25190
+ focusBorder: '#89b482',
25191
+ gitAdded: '#a9b665',
25192
+ gitDeleted: '#ea6962',
25193
+ gitModified: '#d8a657',
25194
+ info: '#7daea3',
25195
+ muted: '#928374',
25196
+ selection: '#3c3836',
25197
+ success: '#a9b665',
25198
+ warning: '#d8a657',
25199
+ },
25200
+ 'gruvbox-material-light': {
25201
+ accent: '#45707a',
25202
+ border: '#ddccab',
25203
+ danger: '#c14a4a',
25204
+ focusBorder: '#4c7a5d',
25205
+ gitAdded: '#6c782e',
25206
+ gitDeleted: '#c14a4a',
25207
+ gitModified: '#b47109',
25208
+ info: '#45707a',
25209
+ muted: '#928374',
25210
+ selection: '#eee0b7',
25211
+ success: '#6c782e',
25212
+ warning: '#b47109',
25213
+ },
25214
+ 'modus-vivendi': {
25215
+ accent: '#2fafff',
25216
+ border: '#646464',
25217
+ danger: '#ff5f59',
25218
+ focusBorder: '#00d3d0',
25219
+ gitAdded: '#44bc44',
25220
+ gitDeleted: '#ff5f59',
25221
+ gitModified: '#d0bc00',
25222
+ info: '#2fafff',
25223
+ muted: '#989898',
25224
+ selection: '#5a5a5a',
25225
+ success: '#44bc44',
25226
+ warning: '#d0bc00',
25227
+ },
25228
+ zenburn: {
25229
+ accent: '#8cd0d3',
25230
+ border: '#4f4f4f',
25231
+ danger: '#cc9393',
25232
+ focusBorder: '#93e0e3',
25233
+ gitAdded: '#7f9f7f',
25234
+ gitDeleted: '#cc9393',
25235
+ gitModified: '#f0dfaf',
25236
+ info: '#8cd0d3',
25237
+ muted: '#9f9f8f',
25238
+ selection: '#5f5f5f',
25239
+ success: '#7f9f7f',
25240
+ warning: '#f0dfaf',
25241
+ },
25242
+ oxocarbon: {
25243
+ accent: '#33b1ff',
25244
+ border: '#525252',
25245
+ danger: '#ee5396',
25246
+ focusBorder: '#3ddbd9',
25247
+ gitAdded: '#42be65',
25248
+ gitDeleted: '#ee5396',
25249
+ gitModified: '#ab8e34',
25250
+ info: '#33b1ff',
25251
+ muted: '#6f6f6f',
25252
+ selection: '#2a2a2a',
25253
+ success: '#42be65',
25254
+ warning: '#ab8e34',
25255
+ },
25256
+ 'tomorrow-night': {
25257
+ accent: '#81a2be',
25258
+ border: '#373b41',
25259
+ danger: '#cc6666',
25260
+ focusBorder: '#8abeb7',
25261
+ gitAdded: '#b5bd68',
25262
+ gitDeleted: '#cc6666',
25263
+ gitModified: '#f0c674',
25264
+ info: '#81a2be',
25265
+ muted: '#969896',
25266
+ selection: '#373b41',
25267
+ success: '#b5bd68',
25268
+ warning: '#f0c674',
25269
+ },
25270
+ 'monokai-pro': {
25271
+ accent: '#78dce8',
25272
+ border: '#403e41',
25273
+ danger: '#ff6188',
25274
+ focusBorder: '#a9dc76',
25275
+ gitAdded: '#a9dc76',
25276
+ gitDeleted: '#ff6188',
25277
+ gitModified: '#ffd866',
25278
+ info: '#78dce8',
25279
+ muted: '#727072',
25280
+ selection: '#5b595c',
25281
+ success: '#a9dc76',
25282
+ warning: '#ffd866',
25283
+ },
25284
+ sonokai: {
25285
+ accent: '#76cce0',
25286
+ border: '#33353f',
25287
+ danger: '#fc5d7c',
25288
+ focusBorder: '#9ed072',
25289
+ gitAdded: '#9ed072',
25290
+ gitDeleted: '#fc5d7c',
25291
+ gitModified: '#e7c664',
25292
+ info: '#76cce0',
25293
+ muted: '#7f8490',
25294
+ selection: '#414550',
25295
+ success: '#9ed072',
25296
+ warning: '#e7c664',
25297
+ },
25298
+ 'doom-one': {
25299
+ accent: '#51afef',
25300
+ border: '#3f444a',
25301
+ danger: '#ff6c6b',
25302
+ focusBorder: '#46d9ff',
25303
+ gitAdded: '#98be65',
25304
+ gitDeleted: '#ff6c6b',
25305
+ gitModified: '#ecbe7b',
25306
+ info: '#51afef',
25307
+ muted: '#5b6268',
25308
+ selection: '#42444a',
25309
+ success: '#98be65',
25310
+ warning: '#ecbe7b',
25311
+ },
25312
+ andromeda: {
25313
+ accent: '#00e8c6',
25314
+ border: '#2b2e36',
25315
+ danger: '#ee5d43',
25316
+ focusBorder: '#00e8c6',
25317
+ gitAdded: '#96e072',
25318
+ gitDeleted: '#ee5d43',
25319
+ gitModified: '#ffe66d',
25320
+ info: '#7cb7ff',
25321
+ muted: '#a0a1a7',
25322
+ selection: '#3d4352',
25323
+ success: '#96e072',
25324
+ warning: '#ffe66d',
25325
+ },
25326
+ aura: {
25327
+ accent: '#a277ff',
25328
+ border: '#363c49',
25329
+ danger: '#ff6767',
25330
+ focusBorder: '#61ffca',
25331
+ gitAdded: '#61ffca',
25332
+ gitDeleted: '#ff6767',
25333
+ gitModified: '#ffca85',
25334
+ info: '#82e2ff',
25335
+ muted: '#6d6d6d',
25336
+ selection: '#3d375e',
25337
+ success: '#61ffca',
25338
+ warning: '#ffca85',
25339
+ },
25340
+ cyberdream: {
25341
+ accent: '#5ea1ff',
25342
+ border: '#1e2124',
25343
+ danger: '#ff6e5e',
25344
+ focusBorder: '#5ef1ff',
25345
+ gitAdded: '#5eff6c',
25346
+ gitDeleted: '#ff6e5e',
25347
+ gitModified: '#f1ff5e',
25348
+ info: '#5ea1ff',
25349
+ muted: '#7b8496',
25350
+ selection: '#3c4048',
25351
+ success: '#5eff6c',
25352
+ warning: '#f1ff5e',
25353
+ },
25354
+ nightfly: {
25355
+ accent: '#82aaff',
25356
+ border: '#1d3b53',
25357
+ danger: '#fc514e',
25358
+ focusBorder: '#7fdbca',
25359
+ gitAdded: '#a1cd5e',
25360
+ gitDeleted: '#fc514e',
25361
+ gitModified: '#e3d18a',
25362
+ info: '#82aaff',
25363
+ muted: '#7c8f8f',
25364
+ selection: '#1d3b53',
25365
+ success: '#a1cd5e',
25366
+ warning: '#e3d18a',
25367
+ },
25368
+ panda: {
25369
+ accent: '#ff75b5',
25370
+ border: '#404954',
25371
+ danger: '#ff4b82',
25372
+ focusBorder: '#19f9d8',
25373
+ gitAdded: '#19f9d8',
25374
+ gitDeleted: '#ff4b82',
25375
+ gitModified: '#ffb86c',
25376
+ info: '#45a9f9',
25377
+ muted: '#676b79',
25378
+ selection: '#373841',
25379
+ success: '#19f9d8',
25380
+ warning: '#ffb86c',
25381
+ },
25382
+ 'hyper-snazzy': {
25383
+ accent: '#57c7ff',
25384
+ border: '#43454f',
25385
+ danger: '#ff5c57',
25386
+ focusBorder: '#9aedfe',
25387
+ gitAdded: '#5af78e',
25388
+ gitDeleted: '#ff5c57',
25389
+ gitModified: '#f3f99d',
25390
+ info: '#57c7ff',
25391
+ muted: '#686868',
25392
+ selection: '#3a3d4d',
25393
+ success: '#5af78e',
25394
+ warning: '#f3f99d',
25395
+ },
25396
+ apprentice: {
25397
+ accent: '#5f87af',
25398
+ border: '#444444',
25399
+ danger: '#af5f5f',
25400
+ focusBorder: '#5f8787',
25401
+ gitAdded: '#5f875f',
25402
+ gitDeleted: '#af5f5f',
25403
+ gitModified: '#ffffaf',
25404
+ info: '#5f87af',
25405
+ muted: '#6c6c6c',
25406
+ selection: '#444444',
25407
+ success: '#5f875f',
25408
+ warning: '#ffffaf',
25409
+ },
25410
+ melange: {
25411
+ accent: '#a3a9ce',
25412
+ border: '#34302c',
25413
+ danger: '#d47766',
25414
+ focusBorder: '#89b3b6',
25415
+ gitAdded: '#85b695',
25416
+ gitDeleted: '#d47766',
25417
+ gitModified: '#ebc06d',
25418
+ info: '#a3a9ce',
25419
+ muted: '#867462',
25420
+ selection: '#403a36',
25421
+ success: '#85b695',
25422
+ warning: '#ebc06d',
25423
+ },
25424
+ 'melange-light': {
25425
+ accent: '#465aa4',
25426
+ border: '#e9e1db',
25427
+ danger: '#bf0021',
25428
+ focusBorder: '#3d6568',
25429
+ gitAdded: '#3a684a',
25430
+ gitDeleted: '#bf0021',
25431
+ gitModified: '#a06d00',
25432
+ info: '#465aa4',
25433
+ muted: '#7d6658',
25434
+ selection: '#d9d3ce',
25435
+ success: '#3a684a',
25436
+ warning: '#a06d00',
25437
+ },
25438
+ spaceduck: {
25439
+ accent: '#00a3cc',
25440
+ border: '#30365f',
25441
+ danger: '#e33400',
25442
+ focusBorder: '#ce6f8f',
25443
+ gitAdded: '#5ccc96',
25444
+ gitDeleted: '#e33400',
25445
+ gitModified: '#f2ce00',
25446
+ info: '#7a5ccc',
25447
+ muted: '#686f9a',
25448
+ selection: '#30365f',
25449
+ success: '#5ccc96',
25450
+ warning: '#f2ce00',
25451
+ },
25452
+ embark: {
25453
+ accent: '#d4bfff',
25454
+ border: '#585273',
25455
+ danger: '#f48fb1',
25456
+ focusBorder: '#abf8f7',
25457
+ gitAdded: '#a1efd3',
25458
+ gitDeleted: '#f48fb1',
25459
+ gitModified: '#ffe6b3',
25460
+ info: '#91ddff',
25461
+ muted: '#8a889d',
25462
+ selection: '#3e3859',
25463
+ success: '#a1efd3',
25464
+ warning: '#ffe6b3',
25465
+ },
25466
+ 'bluloco-dark': {
25467
+ accent: '#3691ff',
25468
+ border: '#3d434f',
25469
+ danger: '#ff2e3f',
25470
+ focusBorder: '#4483aa',
25471
+ gitAdded: '#3fc56b',
25472
+ gitDeleted: '#ff2e3f',
25473
+ gitModified: '#f9c859',
25474
+ info: '#3691ff',
25475
+ muted: '#636d83',
25476
+ selection: '#2f343e',
25477
+ success: '#3fc56b',
25478
+ warning: '#f9c859',
25479
+ },
25480
+ 'bluloco-light': {
25481
+ accent: '#275fe4',
25482
+ border: '#d5d7d8',
25483
+ danger: '#d52753',
25484
+ focusBorder: '#40b8c5',
25485
+ gitAdded: '#23974a',
25486
+ gitDeleted: '#d52753',
25487
+ gitModified: '#c5a332',
25488
+ info: '#275fe4',
25489
+ muted: '#a0a1a7',
25490
+ selection: '#d2ecff',
25491
+ success: '#23974a',
25492
+ warning: '#c5a332',
25493
+ },
25494
+ 'papercolor-dark': {
25495
+ accent: '#5fafd7',
25496
+ border: '#444444',
25497
+ danger: '#af005f',
25498
+ focusBorder: '#00afaf',
25499
+ gitAdded: '#5faf00',
25500
+ gitDeleted: '#af005f',
25501
+ gitModified: '#d7af5f',
25502
+ info: '#5fafd7',
25503
+ muted: '#808080',
25504
+ selection: '#303030',
25505
+ success: '#5faf00',
25506
+ warning: '#d7af5f',
25507
+ },
25508
+ 'base16-ocean': {
25509
+ accent: '#8fa1b3',
25510
+ border: '#343d46',
25511
+ danger: '#bf616a',
25512
+ focusBorder: '#96b5b4',
25513
+ gitAdded: '#a3be8c',
25514
+ gitDeleted: '#bf616a',
25515
+ gitModified: '#ebcb8b',
25516
+ info: '#8fa1b3',
25517
+ muted: '#65737e',
25518
+ selection: '#4f5b66',
25519
+ success: '#a3be8c',
25520
+ warning: '#ebcb8b',
25521
+ },
25522
+ 'base16-eighties': {
25523
+ accent: '#6699cc',
25524
+ border: '#393939',
25525
+ danger: '#f2777a',
25526
+ focusBorder: '#66cccc',
25527
+ gitAdded: '#99cc99',
25528
+ gitDeleted: '#f2777a',
25529
+ gitModified: '#ffcc66',
25530
+ info: '#6699cc',
25531
+ muted: '#747369',
25532
+ selection: '#515151',
25533
+ success: '#99cc99',
25534
+ warning: '#ffcc66',
25535
+ },
25536
+ everblush: {
25537
+ accent: '#67b0e8',
25538
+ border: '#232a2d',
25539
+ danger: '#e57474',
25540
+ focusBorder: '#6cbfbf',
25541
+ gitAdded: '#8ccf7e',
25542
+ gitDeleted: '#e57474',
25543
+ gitModified: '#e5c76b',
25544
+ info: '#67b0e8',
25545
+ muted: '#5e6164',
25546
+ selection: '#2d3437',
25547
+ success: '#8ccf7e',
25548
+ warning: '#e5c76b',
25549
+ },
25550
+ darcula: {
25551
+ accent: '#cc7832',
25552
+ border: '#3c3f41',
25553
+ danger: '#ff6b68',
25554
+ focusBorder: '#629755',
25555
+ gitAdded: '#6a8759',
25556
+ gitDeleted: '#ff6b68',
25557
+ gitModified: '#ffc66d',
25558
+ info: '#6897bb',
25559
+ muted: '#808080',
25560
+ selection: '#214283',
25561
+ success: '#6a8759',
25562
+ warning: '#ffc66d',
25563
+ },
25564
+ eldritch: {
25565
+ accent: '#a48cf2',
25566
+ border: '#292e42',
25567
+ danger: '#f16c75',
25568
+ focusBorder: '#04d1f9',
25569
+ gitAdded: '#37f499',
25570
+ gitDeleted: '#f16c75',
25571
+ gitModified: '#f1fc79',
25572
+ info: '#04d1f9',
25573
+ muted: '#7081d0',
25574
+ selection: '#2d3052',
25575
+ success: '#37f499',
25576
+ warning: '#f1fc79',
25577
+ },
25578
+ 'edge-light': {
25579
+ accent: '#5079be',
25580
+ border: '#dde2e7',
25581
+ danger: '#d05858',
25582
+ focusBorder: '#3a8b84',
25583
+ gitAdded: '#608e32',
25584
+ gitDeleted: '#d05858',
25585
+ gitModified: '#be7e05',
25586
+ info: '#5079be',
25587
+ muted: '#a0a1a7',
25588
+ selection: '#e3e6eb',
25589
+ success: '#608e32',
25590
+ warning: '#be7e05',
25591
+ },
25592
+ zenbones: {
25593
+ accent: '#286486',
25594
+ border: '#cfd1d0',
25595
+ danger: '#a8334c',
25596
+ focusBorder: '#3b8992',
25597
+ gitAdded: '#4f6c31',
25598
+ gitDeleted: '#a8334c',
25599
+ gitModified: '#944927',
25600
+ info: '#286486',
25601
+ muted: '#a8a29e',
25602
+ selection: '#cbd9e3',
25603
+ success: '#4f6c31',
25604
+ warning: '#944927',
25605
+ },
25606
+ 'iceberg-light': {
25607
+ accent: '#2d539e',
25608
+ border: '#cad0de',
25609
+ danger: '#cc517a',
25610
+ focusBorder: '#3f83a6',
25611
+ gitAdded: '#668e3d',
25612
+ gitDeleted: '#cc517a',
25613
+ gitModified: '#c57339',
25614
+ info: '#2d539e',
25615
+ muted: '#8389a3',
25616
+ selection: '#c9cdd7',
25617
+ success: '#668e3d',
25618
+ warning: '#c57339',
25619
+ },
25620
+ 'github-dark-dimmed': {
25621
+ accent: '#539bf5',
25622
+ border: '#444c56',
25623
+ danger: '#f47067',
25624
+ focusBorder: '#39c5cf',
25625
+ gitAdded: '#57ab5a',
25626
+ gitDeleted: '#f47067',
25627
+ gitModified: '#c69026',
25628
+ info: '#539bf5',
25629
+ muted: '#636e7b',
25630
+ selection: '#2d333b',
25631
+ success: '#57ab5a',
25632
+ warning: '#c69026',
25633
+ },
25634
+ 'edge-dark': {
25635
+ accent: '#6cb6eb',
25636
+ border: '#414550',
25637
+ danger: '#ec7279',
25638
+ focusBorder: '#5dbbc1',
25639
+ gitAdded: '#a0c980',
25640
+ gitDeleted: '#ec7279',
25641
+ gitModified: '#deb974',
25642
+ info: '#6cb6eb',
25643
+ muted: '#758094',
25644
+ selection: '#3b3e48',
25645
+ success: '#a0c980',
25646
+ warning: '#deb974',
25647
+ },
25648
+ 'selenized-dark': {
25649
+ accent: '#4695f7',
25650
+ border: '#2d5b69',
25651
+ danger: '#fa5750',
25652
+ focusBorder: '#41c7b9',
25653
+ gitAdded: '#75b938',
25654
+ gitDeleted: '#fa5750',
25655
+ gitModified: '#dbb32d',
25656
+ info: '#4695f7',
25657
+ muted: '#72898f',
25658
+ selection: '#184956',
25659
+ success: '#75b938',
25660
+ warning: '#dbb32d',
25661
+ },
25662
+ 'selenized-black': {
25663
+ accent: '#368aeb',
25664
+ border: '#3b3b3b',
25665
+ danger: '#ed4a46',
25666
+ focusBorder: '#3fc5b7',
25667
+ gitAdded: '#70b433',
25668
+ gitDeleted: '#ed4a46',
25669
+ gitModified: '#dbb32d',
25670
+ info: '#368aeb',
25671
+ muted: '#777777',
25672
+ selection: '#252525',
25673
+ success: '#70b433',
25674
+ warning: '#dbb32d',
25675
+ },
25676
+ 'selenized-light': {
25677
+ accent: '#0072d4',
25678
+ border: '#d5cdb6',
25679
+ danger: '#d2212d',
25680
+ focusBorder: '#009c8f',
25681
+ gitAdded: '#489100',
25682
+ gitDeleted: '#d2212d',
25683
+ gitModified: '#ad8900',
25684
+ info: '#0072d4',
25685
+ muted: '#909995',
25686
+ selection: '#ece3cc',
25687
+ success: '#489100',
25688
+ warning: '#ad8900',
25689
+ },
25690
+ 'monokai-pro-machine': {
25691
+ accent: '#7cd5f1',
25692
+ border: '#1d2528',
25693
+ danger: '#ff6d7e',
25694
+ focusBorder: '#a2e57b',
25695
+ gitAdded: '#a2e57b',
25696
+ gitDeleted: '#ff6d7e',
25697
+ gitModified: '#ffed72',
25698
+ info: '#7cd5f1',
25699
+ muted: '#6b7678',
25700
+ selection: '#3a4449',
25701
+ success: '#a2e57b',
25702
+ warning: '#ffed72',
25703
+ },
25704
+ 'monokai-pro-octagon': {
25705
+ accent: '#9cd1bb',
25706
+ border: '#1e1f2b',
25707
+ danger: '#ff657a',
25708
+ focusBorder: '#bad761',
25709
+ gitAdded: '#bad761',
25710
+ gitDeleted: '#ff657a',
25711
+ gitModified: '#ffd76d',
25712
+ info: '#9cd1bb',
25713
+ muted: '#696d77',
25714
+ selection: '#3a3d4b',
25715
+ success: '#bad761',
25716
+ warning: '#ffd76d',
25717
+ },
25718
+ 'monokai-pro-ristretto': {
25719
+ accent: '#85dacc',
25720
+ border: '#211c1c',
25721
+ danger: '#fd6883',
25722
+ focusBorder: '#adda78',
25723
+ gitAdded: '#adda78',
25724
+ gitDeleted: '#fd6883',
25725
+ gitModified: '#f9cc6c',
25726
+ info: '#85dacc',
25727
+ muted: '#72696a',
25728
+ selection: '#403838',
25729
+ success: '#adda78',
25730
+ warning: '#f9cc6c',
25731
+ },
25732
+ 'monokai-pro-spectrum': {
25733
+ accent: '#5ad4e6',
25734
+ border: '#191919',
25735
+ danger: '#fc618d',
25736
+ focusBorder: '#7bd88f',
25737
+ gitAdded: '#7bd88f',
25738
+ gitDeleted: '#fc618d',
25739
+ gitModified: '#fce566',
25740
+ info: '#5ad4e6',
25741
+ muted: '#69676c',
25742
+ selection: '#363537',
25743
+ success: '#7bd88f',
25744
+ warning: '#fce566',
25745
+ },
25746
+ 'base16-default-dark': {
25747
+ accent: '#7cafc2',
25748
+ border: '#282828',
25749
+ danger: '#ab4642',
25750
+ focusBorder: '#86c1b9',
25751
+ gitAdded: '#a1b56c',
25752
+ gitDeleted: '#ab4642',
25753
+ gitModified: '#f7ca88',
25754
+ info: '#7cafc2',
25755
+ muted: '#585858',
25756
+ selection: '#383838',
25757
+ success: '#a1b56c',
25758
+ warning: '#f7ca88',
25759
+ },
25760
+ 'base16-default-light': {
25761
+ accent: '#7cafc2',
25762
+ border: '#e8e8e8',
25763
+ danger: '#ab4642',
25764
+ focusBorder: '#86c1b9',
25765
+ gitAdded: '#a1b56c',
25766
+ gitDeleted: '#ab4642',
25767
+ gitModified: '#dc9656',
25768
+ info: '#7cafc2',
25769
+ muted: '#b8b8b8',
25770
+ selection: '#d8d8d8',
25771
+ success: '#a1b56c',
25772
+ warning: '#dc9656',
25773
+ },
25774
+ tomorrow: {
25775
+ accent: '#4271ae',
25776
+ border: '#efefef',
25777
+ danger: '#c82829',
25778
+ focusBorder: '#3e999f',
25779
+ gitAdded: '#718c00',
25780
+ gitDeleted: '#c82829',
25781
+ gitModified: '#eab700',
25782
+ info: '#4271ae',
25783
+ muted: '#8e908c',
25784
+ selection: '#d6d6d6',
25785
+ success: '#718c00',
25786
+ warning: '#eab700',
25787
+ },
25788
+ tokyodark: {
25789
+ accent: '#a485dd',
25790
+ border: '#2a2c41',
25791
+ danger: '#ee6d85',
25792
+ focusBorder: '#38a89d',
25793
+ gitAdded: '#95c561',
25794
+ gitDeleted: '#ee6d85',
25795
+ gitModified: '#d7a65f',
25796
+ info: '#7199ee',
25797
+ muted: '#4a5057',
25798
+ selection: '#212234',
25799
+ success: '#95c561',
25800
+ warning: '#d7a65f',
25801
+ },
25802
+ 'spacemacs-dark': {
25803
+ accent: '#bc6ec5',
25804
+ border: '#5d4d7a',
25805
+ danger: '#f2241f',
25806
+ focusBorder: '#2d9574',
25807
+ gitAdded: '#67b11d',
25808
+ gitDeleted: '#f2241f',
25809
+ gitModified: '#b1951d',
25810
+ info: '#4f97d7',
25811
+ muted: '#6c6783',
25812
+ selection: '#444155',
25813
+ success: '#67b11d',
25814
+ warning: '#b1951d',
25815
+ },
25816
+ bamboo: {
25817
+ accent: '#8fb573',
25818
+ border: '#3a3d37',
25819
+ danger: '#e75a7c',
25820
+ focusBorder: '#70c2be',
25821
+ gitAdded: '#8fb573',
25822
+ gitDeleted: '#e75a7c',
25823
+ gitModified: '#dbb651',
25824
+ info: '#57a5e5',
25825
+ muted: '#838781',
25826
+ selection: '#383b35',
25827
+ success: '#8fb573',
25828
+ warning: '#dbb651',
25829
+ },
25830
+ citylights: {
25831
+ accent: '#5ec4ff',
25832
+ border: '#2f3a42',
25833
+ danger: '#e27e8d',
25834
+ focusBorder: '#70e1e8',
25835
+ gitAdded: '#54af83',
25836
+ gitDeleted: '#e27e8d',
25837
+ gitModified: '#ebda65',
25838
+ info: '#68a1f0',
25839
+ muted: '#41505e',
25840
+ selection: '#363c43',
25841
+ success: '#54af83',
25842
+ warning: '#ebda65',
25843
+ },
25844
+ 'oxocarbon-light': {
25845
+ accent: '#0f62fe',
25846
+ border: '#e0e0e0',
25847
+ danger: '#ee5396',
25848
+ focusBorder: '#08bdba',
25849
+ gitAdded: '#42be65',
25850
+ gitDeleted: '#ee5396',
25851
+ gitModified: '#ff6f00',
25852
+ info: '#0f62fe',
25853
+ muted: '#525252',
25854
+ selection: '#dde1e6',
25855
+ success: '#42be65',
25856
+ warning: '#ff6f00',
25857
+ },
24940
25858
  };
24941
25859
  /**
24942
25860
  * Ordered list of every selectable theme preset, for the `coco ui` theme
@@ -25325,7 +26243,6 @@ function withPushedView(state, value) {
25325
26243
  // persistence and pop-view restores the previous tab.
25326
26244
  sidebarTab: value === 'compose' || value === 'status' ? 'branches' : state.sidebarTab,
25327
26245
  worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
25328
- selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
25329
26246
  diffSource: value === 'diff' ? state.diffSource : undefined,
25330
26247
  stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
25331
26248
  compareHead: value === 'diff' ? state.compareHead : undefined,
@@ -25359,7 +26276,6 @@ function withPoppedView(state) {
25359
26276
  // returns the user to whatever they actually had open before.
25360
26277
  sidebarTab: state.userSidebarTab,
25361
26278
  worktreeDiffOffset: next === 'diff' ? state.worktreeDiffOffset : 0,
25362
- selectedWorktreeHunkIndex: next === 'diff' ? state.selectedWorktreeHunkIndex : 0,
25363
26279
  diffSource: next === 'diff' ? state.diffSource : undefined,
25364
26280
  stashDiffRef: next === 'diff' ? state.stashDiffRef : undefined,
25365
26281
  compareBase: wasOnDiff ? undefined : state.compareBase,
@@ -25488,7 +26404,6 @@ function withReplacedView(state, value) {
25488
26404
  activeView: value,
25489
26405
  viewStack,
25490
26406
  worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
25491
- selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
25492
26407
  diffSource: value === 'diff' ? state.diffSource : undefined,
25493
26408
  stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
25494
26409
  compareHead: value === 'diff' ? state.compareHead : undefined,
@@ -25626,9 +26541,29 @@ function nextHunkOffset(currentOffset, hunkOffsets, delta) {
25626
26541
  const previousOffset = [...hunkOffsets].reverse().find((offset) => offset < currentOffset);
25627
26542
  return previousOffset === undefined ? currentOffset : previousOffset;
25628
26543
  }
25629
- function nextHunkIndex(currentOffset, hunkOffsets, delta) {
25630
- const offset = nextHunkOffset(currentOffset, hunkOffsets, delta);
25631
- return Math.max(0, hunkOffsets.indexOf(offset));
26544
+ /**
26545
+ * Which hunk the viewport is currently showing — the index of the last
26546
+ * hunk whose `@@` header offset is at or above the viewport top
26547
+ * (`offset`). This is the single source of truth for the worktree
26548
+ * staging diff's "current hunk" (#1179): deriving it from the scroll
26549
+ * position keeps the header, the in-body highlight, and `space`/`z`
26550
+ * (stage / revert) all pointed at the hunk you're actually looking at,
26551
+ * whether you got there by hunk-jump (↑/↓) or page-scroll (PgUp/PgDn).
26552
+ * The old `indexOf(landedOffset)` approach reset to hunk 0 whenever the
26553
+ * offset wasn't exactly on a boundary, and page-scroll never updated it
26554
+ * at all — so the indicator stuck at "1/N".
26555
+ */
26556
+ function hunkIndexAtOffset(offset, hunkOffsets) {
26557
+ let index = 0;
26558
+ for (let i = 0; i < hunkOffsets.length; i += 1) {
26559
+ if (hunkOffsets[i] <= offset) {
26560
+ index = i;
26561
+ }
26562
+ else {
26563
+ break;
26564
+ }
26565
+ }
26566
+ return index;
25632
26567
  }
25633
26568
  function getLogInkSidebarTabs() {
25634
26569
  return [...SIDEBAR_TABS];
@@ -25645,7 +26580,6 @@ function createLogInkState(rows, options = {}) {
25645
26580
  selectedIndex: 0,
25646
26581
  selectedFileIndex: 0,
25647
26582
  selectedWorktreeFileIndex: 0,
25648
- selectedWorktreeHunkIndex: 0,
25649
26583
  selectedBranchIndex: 0,
25650
26584
  selectedTagIndex: 0,
25651
26585
  selectedStashIndex: 0,
@@ -25846,7 +26780,6 @@ function applyLogInkAction(state, action) {
25846
26780
  return {
25847
26781
  ...next,
25848
26782
  selectedWorktreeFileIndex: clampIndex(state.selectedWorktreeFileIndex + action.delta, action.fileCount),
25849
- selectedWorktreeHunkIndex: 0,
25850
26783
  worktreeDiffOffset: 0,
25851
26784
  // Cursor moved to a real file row — drop header focus so the
25852
26785
  // file Enter handler (open diff) is what fires next.
@@ -25890,7 +26823,6 @@ function applyLogInkAction(state, action) {
25890
26823
  return {
25891
26824
  ...state,
25892
26825
  selectedWorktreeFileIndex: Math.max(0, action.targetIndex),
25893
- selectedWorktreeHunkIndex: 0,
25894
26826
  worktreeDiffOffset: 0,
25895
26827
  statusGroupHeaderFocused: false,
25896
26828
  pendingKey: undefined,
@@ -26130,16 +27062,19 @@ function applyLogInkAction(state, action) {
26130
27062
  pendingKey: undefined,
26131
27063
  };
26132
27064
  case 'pageWorktreeDiff':
27065
+ // The current staging hunk is derived from the scroll offset at
27066
+ // the read sites (#1185), so paging only moves the offset.
26133
27067
  return {
26134
27068
  ...state,
26135
27069
  worktreeDiffOffset: clampIndex(state.worktreeDiffOffset + action.delta, action.lineCount),
26136
27070
  pendingKey: undefined,
26137
27071
  };
26138
27072
  case 'jumpWorktreeHunk':
27073
+ // `[`/`]` move the offset onto the next/previous hunk header; the
27074
+ // current hunk is derived from that offset at the read sites.
26139
27075
  return {
26140
27076
  ...state,
26141
27077
  worktreeDiffOffset: nextHunkOffset(state.worktreeDiffOffset, action.hunkOffsets, action.delta),
26142
- selectedWorktreeHunkIndex: nextHunkIndex(state.worktreeDiffOffset, action.hunkOffsets, action.delta),
26143
27078
  pendingKey: undefined,
26144
27079
  };
26145
27080
  case 'jumpCommitDiffHunk':
@@ -26184,7 +27119,6 @@ function applyLogInkAction(state, action) {
26184
27119
  activeView: HOME_VIEW,
26185
27120
  viewStack: [HOME_VIEW],
26186
27121
  worktreeDiffOffset: 0,
26187
- selectedWorktreeHunkIndex: 0,
26188
27122
  pendingCommitFocused: false,
26189
27123
  pendingKey: undefined,
26190
27124
  };
@@ -26223,7 +27157,6 @@ function applyLogInkAction(state, action) {
26223
27157
  return {
26224
27158
  ...next,
26225
27159
  selectedWorktreeFileIndex: Math.max(0, action.fileIndex),
26226
- selectedWorktreeHunkIndex: 0,
26227
27160
  worktreeDiffOffset: 0,
26228
27161
  diffSource: 'worktree',
26229
27162
  };
@@ -26273,7 +27206,6 @@ function applyLogInkAction(state, action) {
26273
27206
  return {
26274
27207
  ...next,
26275
27208
  selectedWorktreeFileIndex: Math.max(0, action.fileIndex),
26276
- selectedWorktreeHunkIndex: 0,
26277
27209
  worktreeDiffOffset: 0,
26278
27210
  };
26279
27211
  }
@@ -26385,6 +27317,10 @@ function applyLogInkAction(state, action) {
26385
27317
  pendingMutationConfirmation: action.value ? undefined : state.pendingMutationConfirmation,
26386
27318
  pendingKey: undefined,
26387
27319
  };
27320
+ case 'setWorktreeCheckoutConflict':
27321
+ return { ...state, worktreeCheckoutConflict: action.value, pendingKey: undefined };
27322
+ case 'setPendingChoice':
27323
+ return { ...state, pendingChoice: action.value, pendingKey: undefined };
26388
27324
  case 'setPendingMutationConfirmation':
26389
27325
  return {
26390
27326
  ...state,
@@ -27756,6 +28692,46 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
27756
28692
  }
27757
28693
  return [];
27758
28694
  }
28695
+ // Multi-option prompt (#1181) — the n-way generalization of the y/n
28696
+ // confirmation. Match the keypress against the prompt's options; each
28697
+ // either runs a workflow or fires a built-in navigation intent.
28698
+ if (state.pendingChoice) {
28699
+ const option = state.pendingChoice.options.find((opt) => opt.key === inputValue);
28700
+ if (option) {
28701
+ // `switch-worktree` is pure navigation — open the worktree as a
28702
+ // nested repo frame. Handled here rather than via the workflow
28703
+ // runner, whose post-action context refresh would mis-target the
28704
+ // frame we just pushed.
28705
+ if (option.intent === 'switch-worktree' && state.worktreeCheckoutConflict) {
28706
+ const conflict = state.worktreeCheckoutConflict;
28707
+ return [
28708
+ action({ type: 'pushRepoFrame', label: conflict.branch, workdir: conflict.worktreePath }),
28709
+ action({ type: 'setStatus', value: `Switched to worktree ${conflict.worktreePath} (${conflict.branch})` }),
28710
+ action({ type: 'setPendingChoice', value: undefined }),
28711
+ action({ type: 'setWorktreeCheckoutConflict', value: undefined }),
28712
+ ];
28713
+ }
28714
+ if (option.workflowId) {
28715
+ // The workflow runner owns the live context + clears any
28716
+ // conflict state once it resolves.
28717
+ return [
28718
+ { type: 'runWorkflowAction', id: option.workflowId },
28719
+ action({ type: 'setPendingChoice', value: undefined }),
28720
+ ];
28721
+ }
28722
+ return [action({ type: 'setPendingChoice', value: undefined })];
28723
+ }
28724
+ if (inputValue === 'n' || key.escape) {
28725
+ return [
28726
+ action({ type: 'setPendingChoice', value: undefined }),
28727
+ ...(state.worktreeCheckoutConflict
28728
+ ? [action({ type: 'setWorktreeCheckoutConflict', value: undefined })]
28729
+ : []),
28730
+ action({ type: 'setStatus', value: 'cancelled' }),
28731
+ ];
28732
+ }
28733
+ return [];
28734
+ }
27759
28735
  if (state.pendingConfirmationId) {
27760
28736
  if (inputValue === 'y') {
27761
28737
  const workflowAction = getLogInkWorkflowActionById(state.pendingConfirmationId);
@@ -28629,22 +29605,16 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28629
29605
  fileCount: context.worktreeFileCount,
28630
29606
  })];
28631
29607
  }
28632
- // Worktree (staging) diff: ↑/↓ move between hunks the hunk is the
28633
- // unit you stage, so the cursor walks hunks (auto-scrolling to the
28634
- // selected one). Single-hunk files fall through to line-scroll so a
28635
- // long lone hunk stays readable; `[`/`]` remain hunk-jump aliases.
28636
- if (state.activeView === 'diff' && (context.worktreeHunkOffsets?.length ?? 0) > 1) {
28637
- return [action({
28638
- type: 'jumpWorktreeHunk',
28639
- delta: -1,
28640
- hunkOffsets: context.worktreeHunkOffsets,
28641
- })];
28642
- }
29608
+ // Worktree (staging) diff: ↑/↓ scroll linesconsistent with the
29609
+ // commit / stash diffs (#1185). `[`/`]` jump between hunks (the
29610
+ // staging unit), and the current hunk is derived from the scroll
29611
+ // position, so line-scrolling still walks the staging target.
28643
29612
  if (state.activeView === 'diff' && context.worktreeDiffLineCount) {
28644
29613
  return [action({
28645
29614
  type: 'pageWorktreeDiff',
28646
29615
  delta: -1,
28647
29616
  lineCount: context.worktreeDiffLineCount,
29617
+ hunkOffsets: context.worktreeHunkOffsets,
28648
29618
  })];
28649
29619
  }
28650
29620
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -28754,20 +29724,14 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28754
29724
  fileCount: context.worktreeFileCount,
28755
29725
  })];
28756
29726
  }
28757
- // Worktree (staging) diff: ↓ walks to the next hunk (see the ↑
28758
- // handler). Multi-hunk only; single-hunk files line-scroll.
28759
- if (state.activeView === 'diff' && (context.worktreeHunkOffsets?.length ?? 0) > 1) {
28760
- return [action({
28761
- type: 'jumpWorktreeHunk',
28762
- delta: 1,
28763
- hunkOffsets: context.worktreeHunkOffsets,
28764
- })];
28765
- }
29727
+ // Worktree (staging) diff: ↓ scrolls lines (see the ↑ handler) —
29728
+ // `[`/`]` jump hunks (#1185).
28766
29729
  if (state.activeView === 'diff' && context.worktreeDiffLineCount) {
28767
29730
  return [action({
28768
29731
  type: 'pageWorktreeDiff',
28769
29732
  delta: 1,
28770
29733
  lineCount: context.worktreeDiffLineCount,
29734
+ hunkOffsets: context.worktreeHunkOffsets,
28771
29735
  })];
28772
29736
  }
28773
29737
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -28827,6 +29791,7 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28827
29791
  type: 'pageWorktreeDiff',
28828
29792
  delta: -8,
28829
29793
  lineCount: context.worktreeDiffLineCount,
29794
+ hunkOffsets: context.worktreeHunkOffsets,
28830
29795
  })];
28831
29796
  }
28832
29797
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -28851,6 +29816,7 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28851
29816
  type: 'pageWorktreeDiff',
28852
29817
  delta: 8,
28853
29818
  lineCount: context.worktreeDiffLineCount,
29819
+ hunkOffsets: context.worktreeHunkOffsets,
28854
29820
  })];
28855
29821
  }
28856
29822
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -33374,6 +34340,7 @@ function renderFooter$1(h, components, state, context, theme, idleTip, spinnerFr
33374
34340
  state.gitignorePicker ||
33375
34341
  state.inputPrompt ||
33376
34342
  state.pendingConfirmationId ||
34343
+ state.pendingChoice ||
33377
34344
  state.pendingMutationConfirmation ||
33378
34345
  state.pendingKey ||
33379
34346
  state.filterMode);
@@ -36181,7 +37148,7 @@ function renderDiffSurface(ctx, diff) {
36181
37148
  ? []
36182
37149
  : splitActive
36183
37150
  ? renderSplitDiffBody(h, components, previewHunks, state.diffPreviewOffset, visibleRows, width, theme, 'commit-diff-split', syntaxSpans)
36184
- : visiblePreviewHunks.map((line, index) => renderDiffLine(h, Text, line, theme, syntaxSpans, 140, `diff-surface-line-${state.diffPreviewOffset + index}`));
37151
+ : visiblePreviewHunks.map((line, index) => renderDiffLine(h, Text, line, theme, syntaxSpans, Math.max(8, width - 5), `diff-surface-line-${state.diffPreviewOffset + index}`));
36185
37152
  return h(Box, {
36186
37153
  borderColor: focusBorderColor(theme, focused),
36187
37154
  borderStyle: theme.borderStyle,
@@ -36192,25 +37159,38 @@ function renderDiffSurface(ctx, diff) {
36192
37159
  }, 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, {
36193
37160
  key: `diff-surface-header-${index}`,
36194
37161
  dimColor: index > 0,
36195
- }, truncateCells(line, 140))), ...commitBodyNodes);
37162
+ }, truncateCells(line, Math.max(20, width - 4)))), ...commitBodyNodes);
36196
37163
  }
36197
37164
  const diffLines = worktreeDiff?.lines || [];
36198
- const selectedHunk = worktreeHunks?.hunks[state.selectedWorktreeHunkIndex];
36199
37165
  const totalHunks = worktreeHunks?.hunks.length ?? 0;
36200
37166
  const stagedHunks = worktreeHunks?.hunks.filter((hunk) => hunk.state === 'staged').length ?? 0;
37167
+ // The "current" hunk is derived from the scroll position (#1185) —
37168
+ // the single source of truth is `worktreeDiffOffset`. ↑/↓ scroll
37169
+ // lines and `[`/`]` jump hunks; either way the header, rail, and
37170
+ // stage/revert target all follow what's on screen.
37171
+ const currentHunkIndex = hunkIndexAtOffset(state.worktreeDiffOffset, worktreeDiff?.hunkOffsets ?? []);
36201
37172
  const visibleDiffLines = diffLines.slice(state.worktreeDiffOffset, state.worktreeDiffOffset + visibleRows);
36202
- // Hunk-position line: badge + selected hunk's state + a staged/total
36203
- // progress count, so the user always sees how far through staging they
36204
- // are. Untracked/new files have no hunks point them at whole-file
36205
- // staging instead of a dead-end "no hunks" message.
37173
+ // Hunk-position line: `Hunk n/N` + an at-a-glance staging rail + a
37174
+ // staged/total count, so the user sees how far through staging they
37175
+ // are without reading each hunk. The rail shows one marker per hunk —
37176
+ // filled = staged, hollow = unstaged with the current hunk bracketed
37177
+ // (which also conveys whether the current hunk is staged, replacing
37178
+ // the old standalone "● staged / ○ unstaged" badge). Untracked/new
37179
+ // files have no hunks — point them at whole-file staging instead of a
37180
+ // dead-end "no hunks" message.
37181
+ const railMarker = (staged) => staged ? (theme.ascii ? 'x' : '●') : (theme.ascii ? '.' : '○');
37182
+ const hunkRail = (worktreeHunks?.hunks ?? [])
37183
+ .map((hunk, index) => {
37184
+ const marker = railMarker(hunk.state === 'staged');
37185
+ return index === currentHunkIndex ? `[${marker}]` : marker;
37186
+ })
37187
+ .join('');
36206
37188
  const hunkHeaderLine = worktreeHunksLoading
36207
37189
  ? 'Hunks loading…'
36208
37190
  : worktreeDiff?.untracked
36209
37191
  ? (theme.ascii ? 'New file — press space to stage it whole.' : '✚ New file — press space to stage it whole.')
36210
37192
  : totalHunks
36211
- ? `Hunk ${state.selectedWorktreeHunkIndex + 1}/${totalHunks} · ${selectedHunk?.state === 'staged'
36212
- ? (theme.ascii ? '[x] staged' : '● staged')
36213
- : (theme.ascii ? '[ ] unstaged' : '○ unstaged')} · ${stagedHunks}/${totalHunks} staged`
37193
+ ? `Hunk ${currentHunkIndex + 1}/${totalHunks} ${hunkRail} ${stagedHunks}/${totalHunks} staged`
36214
37194
  : 'No stageable hunks for this file.';
36215
37195
  const headerLines = isLogInkContextKeyLoading(contextStatus, 'worktree')
36216
37196
  ? ['Loading file context...']
@@ -36242,7 +37222,7 @@ function renderDiffSurface(ctx, diff) {
36242
37222
  h(Text, { dimColor: true }, worktreeDiff?.filePath || worktreeFile?.path || 'no file')), ...headerLines.map((line, index) => h(Text, {
36243
37223
  key: `diff-surface-header-${index}`,
36244
37224
  dimColor: index > 0,
36245
- }, truncateCells(line, 140))), ...(showDiffLines
37225
+ }, truncateCells(line, Math.max(20, width - 4)))), ...(showDiffLines
36246
37226
  ? renderWorktreeDiffBody(h, components, {
36247
37227
  lines: diffLines,
36248
37228
  offset: state.worktreeDiffOffset,
@@ -36252,7 +37232,7 @@ function renderDiffSurface(ctx, diff) {
36252
37232
  syntaxSpans,
36253
37233
  hunkOffsets: worktreeDiff?.hunkOffsets || [],
36254
37234
  hunks: worktreeHunks?.hunks || [],
36255
- selectedIndex: state.selectedWorktreeHunkIndex,
37235
+ selectedIndex: currentHunkIndex,
36256
37236
  keyPrefix: 'diff-surface-line',
36257
37237
  })
36258
37238
  : []));
@@ -37677,6 +38657,26 @@ function renderConfirmationPanel(h, components, state, width, theme, focused) {
37677
38657
  paddingX: 1,
37678
38658
  }, 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.'));
37679
38659
  }
38660
+ /**
38661
+ * Multi-option prompt panel (#1181) — the n-way generalization of the
38662
+ * confirmation panel. Renders the prompt title, an optional warning, and
38663
+ * one row per option (`<key> <label>`, destructive options in the
38664
+ * danger colour), plus a cancel hint. Resolution happens in the input
38665
+ * layer by matching a keypress against the option keys.
38666
+ */
38667
+ function renderChoicePanel(h, components, prompt, width, theme, focused) {
38668
+ const { Box, Text } = components;
38669
+ return h(Box, {
38670
+ borderColor: focusBorderColor(theme, focused),
38671
+ borderStyle: theme.borderStyle,
38672
+ flexDirection: 'column',
38673
+ width,
38674
+ paddingX: 1,
38675
+ }, 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, {
38676
+ key: `choice-${prompt.id}-${option.key}`,
38677
+ color: option.destructive && !theme.noColor ? theme.colors.danger : undefined,
38678
+ }, truncateCells(` ${option.key} ${option.label}`, width - 4))), h(Text, { dimColor: true }, ' n/Esc cancel'));
38679
+ }
37680
38680
  /**
37681
38681
  * First-launch onboarding overlay (P1.3). Shown once per machine, gated
37682
38682
  * by an XDG-style cache marker so subsequent launches go straight to the
@@ -38068,9 +39068,20 @@ function renderSplitPlanOverlay(h, components, state, width, bodyRows, theme, fo
38068
39068
  // overlay rather than crashing.
38069
39069
  return h(Box, { width }, h(Text, { dimColor: true }, 'No plan data available.'));
38070
39070
  }
39071
+ // Committed groups are numbered 1..N; an `unclaimed` group (the files
39072
+ // the split couldn't place) renders as a distinct "will stay" note
39073
+ // rather than a phantom commit (#1180).
39074
+ const committedGroups = plan.groups.filter((group) => !group.unclaimed);
38071
39075
  const lines = [];
38072
- plan.groups.forEach((group, index) => {
38073
- lines.push(`▎ ${index + 1}. ${group.title}`);
39076
+ let commitNumber = 0;
39077
+ plan.groups.forEach((group) => {
39078
+ if (group.unclaimed) {
39079
+ lines.push(`⚠ ${group.title} (stays in your worktree — not committed)`);
39080
+ }
39081
+ else {
39082
+ commitNumber += 1;
39083
+ lines.push(`▎ ${commitNumber}. ${group.title}`);
39084
+ }
38074
39085
  if (group.body) {
38075
39086
  group.body.split('\n').forEach((bodyLine) => lines.push(` ${bodyLine}`));
38076
39087
  }
@@ -38095,9 +39106,10 @@ function renderSplitPlanOverlay(h, components, state, width, bodyRows, theme, fo
38095
39106
  const totalLines = lines.length;
38096
39107
  const scrollOffset = Math.min(overlay.scrollOffset, Math.max(0, totalLines - 1));
38097
39108
  const visible = lines.slice(scrollOffset, scrollOffset + listRows);
39109
+ const unclaimedCount = plan.groups.length - committedGroups.length;
38098
39110
  const headerRight = overlay.status === 'applying'
38099
39111
  ? `${spinner} applying…`
38100
- : `${plan.groups.length} commit(s) · ${scrollOffset + 1}–${Math.min(totalLines, scrollOffset + listRows)} / ${totalLines}`;
39112
+ : `${committedGroups.length} commit(s)${unclaimedCount ? ' · 1 set stays staged' : ''} · ${scrollOffset + 1}–${Math.min(totalLines, scrollOffset + listRows)} / ${totalLines}`;
38101
39113
  // Apply errors get the full available width — long validator
38102
39114
  // messages (the failure path that surfaced in PR #916 testing was
38103
39115
  // "unknown hunks: src/widgets/button.ts::hunk-1, ...") frequently
@@ -40571,6 +41583,9 @@ function renderDetailPanel(h, components, state, context, contextStatus, detail,
40571
41583
  if (state.inputPrompt) {
40572
41584
  return renderInputPromptPanel(h, components, state, width, theme, focused);
40573
41585
  }
41586
+ if (state.pendingChoice) {
41587
+ return renderChoicePanel(h, components, state.pendingChoice, width, theme, focused);
41588
+ }
40574
41589
  if (state.pendingConfirmationId || state.pendingMutationConfirmation) {
40575
41590
  return renderConfirmationPanel(h, components, state, width, theme, focused);
40576
41591
  }
@@ -41424,7 +42439,7 @@ function LogInkApp(deps) {
41424
42439
  React.useEffect(() => {
41425
42440
  if (!state.statusMessage)
41426
42441
  return;
41427
- if (state.inputPrompt || state.pendingConfirmationId || state.pendingMutationConfirmation || state.showCommandPalette) {
42442
+ if (state.inputPrompt || state.pendingConfirmationId || state.pendingChoice || state.pendingMutationConfirmation || state.showCommandPalette) {
41428
42443
  return;
41429
42444
  }
41430
42445
  // The `setTimeout` callback is a literal arrow function (not a
@@ -41441,6 +42456,7 @@ function LogInkApp(deps) {
41441
42456
  dispatch,
41442
42457
  state.inputPrompt,
41443
42458
  state.pendingConfirmationId,
42459
+ state.pendingChoice,
41444
42460
  state.pendingMutationConfirmation,
41445
42461
  state.showCommandPalette,
41446
42462
  state.statusMessage,
@@ -42304,7 +43320,9 @@ function LogInkApp(deps) {
42304
43320
  setWorktreeHunks(undefined);
42305
43321
  }, [dispatch, git, refreshWorktreeContext, selectedWorktreeFile]);
42306
43322
  const toggleSelectedHunkStage = React.useCallback(async () => {
42307
- const selectedHunk = worktreeHunks?.hunks[state.selectedWorktreeHunkIndex];
43323
+ // The staging target is the hunk under the viewport (#1185) —
43324
+ // derived from the scroll offset, the single source of truth.
43325
+ const selectedHunk = worktreeHunks?.hunks[hunkIndexAtOffset(state.worktreeDiffOffset, worktreeDiff?.hunkOffsets ?? [])];
42308
43326
  if (!selectedHunk) {
42309
43327
  dispatch({ type: 'setStatus', value: 'no hunk selected', kind: 'warning' });
42310
43328
  return;
@@ -42333,7 +43351,7 @@ function LogInkApp(deps) {
42333
43351
  kind: 'error',
42334
43352
  });
42335
43353
  }
42336
- }, [dispatch, git, refreshWorktreeContext, state.selectedWorktreeHunkIndex, worktreeHunks]);
43354
+ }, [dispatch, git, refreshWorktreeContext, state.worktreeDiffOffset, worktreeDiff, worktreeHunks]);
42337
43355
  const revertSelectedFile = React.useCallback(async () => {
42338
43356
  if (!selectedWorktreeFile) {
42339
43357
  dispatch({ type: 'setStatus', value: 'no worktree file selected', kind: 'warning' });
@@ -42347,7 +43365,7 @@ function LogInkApp(deps) {
42347
43365
  setWorktreeHunks(undefined);
42348
43366
  }, [dispatch, git, refreshWorktreeContext, selectedWorktreeFile]);
42349
43367
  const revertSelectedHunk = React.useCallback(async () => {
42350
- const selectedHunk = worktreeHunks?.hunks[state.selectedWorktreeHunkIndex];
43368
+ const selectedHunk = worktreeHunks?.hunks[hunkIndexAtOffset(state.worktreeDiffOffset, worktreeDiff?.hunkOffsets ?? [])];
42351
43369
  if (!selectedHunk) {
42352
43370
  dispatch({ type: 'setStatus', value: 'no hunk selected', kind: 'warning' });
42353
43371
  return;
@@ -42367,7 +43385,7 @@ function LogInkApp(deps) {
42367
43385
  kind: 'error',
42368
43386
  });
42369
43387
  }
42370
- }, [dispatch, git, refreshWorktreeContext, state.selectedWorktreeHunkIndex, worktreeHunks]);
43388
+ }, [dispatch, git, refreshWorktreeContext, state.worktreeDiffOffset, worktreeDiff, worktreeHunks]);
42371
43389
  const createCommitFromCompose = React.useCallback(async () => {
42372
43390
  const stagedCount = context.worktree?.stagedCount || 0;
42373
43391
  if (!stagedCount) {
@@ -43249,9 +44267,19 @@ function LogInkApp(deps) {
43249
44267
  // navigateHome nukes the rest of the stack so `<` after apply
43250
44268
  // doesn't walk back into the now-empty compose / status state
43251
44269
  // the user just left behind.
44270
+ // Did the plan leave files for the user (the `unclaimed` group the
44271
+ // split couldn't confidently place)? They're now sitting unstaged in
44272
+ // the worktree, so land on status — not history — so the user sees
44273
+ // and handles them, rather than dropping them on a clean-looking
44274
+ // history view (#1180).
44275
+ const unclaimedGroup = splitPlan.plan.groups.find((group) => group.unclaimed);
44276
+ const unclaimedFileCount = unclaimedGroup?.files?.length ?? 0;
43252
44277
  dispatch({ type: 'clearSplitPlan' });
43253
44278
  dispatch({ type: 'commitCompose', action: { type: 'reset' } });
43254
44279
  dispatch({ type: 'navigateHome' });
44280
+ if (unclaimedFileCount > 0) {
44281
+ dispatch({ type: 'pushView', value: 'status' });
44282
+ }
43255
44283
  // Refresh BEFORE setting the final status so we can peek at the
43256
44284
  // post-apply worktree state and craft a directive next-step hint
43257
44285
  // ("X unstaged + Y untracked remaining — press gs to stage / I
@@ -43301,12 +44329,17 @@ function LogInkApp(deps) {
43301
44329
  return;
43302
44330
  }
43303
44331
  const successMessage = formatSplitApplySuccess(commitHashes.length, unstaged, untracked, result.fallback ? { reason: result.fallback.reason } : undefined);
44332
+ // Name the files the split deliberately left behind so the jump to
44333
+ // status reads as intentional, not a surprise (#1180).
44334
+ const unclaimedNote = unclaimedFileCount > 0
44335
+ ? ` · ${unclaimedFileCount} file${unclaimedFileCount === 1 ? '' : 's'} left for you on status`
44336
+ : '';
43304
44337
  // Fallback path uses 'info' kind — apply technically succeeded
43305
44338
  // but the user should know it landed as a single combined commit
43306
44339
  // rather than a real LLM-driven multi-group split.
43307
44340
  dispatch({
43308
44341
  type: 'setStatus',
43309
- value: successMessage,
44342
+ value: `${successMessage}${unclaimedNote}`,
43310
44343
  kind: result.fallback ? 'info' : 'success',
43311
44344
  });
43312
44345
  }, [dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext, state.splitPlan]);
@@ -43789,6 +44822,42 @@ function LogInkApp(deps) {
43789
44822
  // path on the wrong target.
43790
44823
  return removeWorktreeAndBranch(git, cursorTarget, context.branches?.localBranches || []);
43791
44824
  },
44825
+ // Worktree-checkout-conflict resolutions (#1175). Unlike the
44826
+ // cursor-targeted handlers above, these act on the worktree
44827
+ // captured in `state.worktreeCheckoutConflict` (the one git named
44828
+ // when it refused the checkout), not the worktrees-view cursor.
44829
+ 'conflict-remove-worktree-checkout': async () => {
44830
+ const conflict = state.worktreeCheckoutConflict;
44831
+ dispatch({ type: 'setWorktreeCheckoutConflict', value: undefined });
44832
+ if (!conflict)
44833
+ return { ok: false, message: 'No worktree conflict to resolve.' };
44834
+ const worktree = context.worktreeList?.worktrees?.find((w) => w.path === conflict.worktreePath);
44835
+ if (!worktree)
44836
+ return { ok: false, message: `Worktree ${conflict.worktreePath} not found.` };
44837
+ // removeWorktree refuses a dirty / current worktree and returns
44838
+ // a clear message — surface it rather than forcing.
44839
+ const removed = await removeWorktree(git, worktree);
44840
+ if (!removed.ok)
44841
+ return removed;
44842
+ const branch = (context.branches?.localBranches || []).find((b) => b.type === 'local' && b.shortName === conflict.branch);
44843
+ if (!branch) {
44844
+ return { ok: true, message: `Removed worktree ${worktree.path}; branch ${conflict.branch} not found to check out.` };
44845
+ }
44846
+ const checkout = await checkoutBranch(git, branch);
44847
+ return checkout.ok
44848
+ ? { ok: true, message: `Removed worktree ${worktree.path} and checked out ${conflict.branch}` }
44849
+ : { ok: false, message: `Removed worktree ${worktree.path}, but checkout failed: ${checkout.message}` };
44850
+ },
44851
+ 'conflict-remove-worktree-branch': async () => {
44852
+ const conflict = state.worktreeCheckoutConflict;
44853
+ dispatch({ type: 'setWorktreeCheckoutConflict', value: undefined });
44854
+ if (!conflict)
44855
+ return { ok: false, message: 'No worktree conflict to resolve.' };
44856
+ const worktree = context.worktreeList?.worktrees?.find((w) => w.path === conflict.worktreePath);
44857
+ if (!worktree)
44858
+ return { ok: false, message: `Worktree ${conflict.worktreePath} not found.` };
44859
+ return removeWorktreeAndBranch(git, worktree, context.branches?.localBranches || []);
44860
+ },
43792
44861
  'abort-operation': async () => {
43793
44862
  const operation = context.operation?.operation;
43794
44863
  if (!operation) {
@@ -44251,6 +45320,43 @@ function LogInkApp(deps) {
44251
45320
  kind: 'warning',
44252
45321
  });
44253
45322
  }
45323
+ // Checking out a branch that's already checked out in another
45324
+ // worktree is rejected by git ("already checked out at <path>").
45325
+ // Rather than dead-end on that, capture the conflict and raise a
45326
+ // multi-option prompt: switch into that worktree, remove it and
45327
+ // check out here, or remove it and delete the branch (#1175, #1181).
45328
+ if (id === 'checkout-branch' && !result?.ok && isBranchCheckedOutElsewhereError(result?.message)) {
45329
+ const worktreePath = parseCheckedOutWorktreePath(result?.message);
45330
+ const branchName = pendingItemAction?.id;
45331
+ if (worktreePath && branchName) {
45332
+ const worktree = context.worktreeList?.worktrees?.find((w) => w.path === worktreePath);
45333
+ const dirty = worktree?.dirty ?? false;
45334
+ dispatch({
45335
+ type: 'setWorktreeCheckoutConflict',
45336
+ value: { branch: branchName, worktreePath, dirty },
45337
+ });
45338
+ dispatch({
45339
+ type: 'setPendingChoice',
45340
+ value: {
45341
+ id: 'worktree-checkout-conflict',
45342
+ title: `'${branchName}' is checked out in another worktree`,
45343
+ warning: `Checked out at ${worktreePath}.${dirty ? ' That worktree has uncommitted changes — removal will be refused until it is clean or stashed.' : ''}`,
45344
+ options: [
45345
+ { key: 'y', label: 'Switch to that worktree', intent: 'switch-worktree' },
45346
+ { key: 'r', label: 'Remove worktree & check out here', workflowId: 'conflict-remove-worktree-checkout', destructive: true },
45347
+ { key: 'x', label: 'Remove worktree & delete branch', workflowId: 'conflict-remove-worktree-branch', destructive: true },
45348
+ ],
45349
+ },
45350
+ });
45351
+ }
45352
+ else {
45353
+ dispatch({
45354
+ type: 'setStatus',
45355
+ value: `'${branchName ?? 'branch'}' is already checked out in another worktree.`,
45356
+ kind: 'warning',
45357
+ });
45358
+ }
45359
+ }
44254
45360
  // Refresh history rows AS WELL when the workflow could have
44255
45361
  // changed the commits the user sees (#945 follow-up). The
44256
45362
  // workflow IDs below all either create/rewrite local commits or
@@ -44260,6 +45366,10 @@ function LogInkApp(deps) {
44260
45366
  // metadata-only mutations (delete-tag, set-upstream, etc.).
44261
45367
  const historyMutatingIds = new Set([
44262
45368
  'checkout-branch',
45369
+ // Resolving a checkout conflict changes HEAD (checkout) and/or the
45370
+ // ref set (branch delete), so the graph needs a refresh.
45371
+ 'conflict-remove-worktree-checkout',
45372
+ 'conflict-remove-worktree-branch',
44263
45373
  'continue-operation',
44264
45374
  'pull-current-branch',
44265
45375
  // Fetch / pull / push bring in new commits and move
@@ -44295,7 +45405,7 @@ function LogInkApp(deps) {
44295
45405
  // (resolvePendingItemAction → action 'checkout'), so a silent
44296
45406
  // stale-while-revalidate swap keeps the list readable and just
44297
45407
  // repaints the current-branch marker once the new context lands.
44298
- if (id === 'checkout-branch' && result?.ok) {
45408
+ if ((id === 'checkout-branch' || id === 'conflict-remove-worktree-checkout') && result?.ok) {
44299
45409
  dispatch({ type: 'resetBranchSelection' });
44300
45410
  await refreshContext({ silent: true });
44301
45411
  }
@@ -44362,7 +45472,7 @@ function LogInkApp(deps) {
44362
45472
  }, [context, dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext,
44363
45473
  state.branchSort, state.filter, state.selectedBranchIndex,
44364
45474
  state.selectedStashIndex, state.selectedTagIndex, state.selectedWorktreeListIndex, state.stashDiffRef,
44365
- state.statusFilterMask, state.tagSort]);
45475
+ state.statusFilterMask, state.tagSort, state.worktreeCheckoutConflict]);
44366
45476
  // Resolve the active view's "yank target" (commit hash / branch /
44367
45477
  // tag / stash ref / file path) against the live filtered+sorted list,
44368
45478
  // copy it to the system clipboard, and surface the result on the
@@ -45173,6 +46283,7 @@ function LogInkApp(deps) {
45173
46283
  state.gitignorePicker ||
45174
46284
  state.inputPrompt ||
45175
46285
  state.pendingConfirmationId ||
46286
+ state.pendingChoice ||
45176
46287
  state.pendingMutationConfirmation ||
45177
46288
  state.pendingKey
45178
46289
  ? 'inspector'
@@ -46880,7 +47991,9 @@ const options$1 = {
46880
47991
  },
46881
47992
  theme: {
46882
47993
  description: 'TUI theme preset',
46883
- 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'],
47994
+ // Derived from the single source of truth (`THEME_PRESET_COLORS`) so the
47995
+ // CLI choices can never drift from the themes the picker actually offers.
47996
+ choices: getLogInkThemePresets(),
46884
47997
  },
46885
47998
  };
46886
47999
  const builder$1 = (yargs) => {
@@ -46908,7 +48021,9 @@ const options = {
46908
48021
  },
46909
48022
  theme: {
46910
48023
  description: 'TUI theme preset',
46911
- 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'],
48024
+ // Derived from the single source of truth (`THEME_PRESET_COLORS`) so the
48025
+ // workspace CLI choices stay in sync with the themes the picker offers.
48026
+ choices: getLogInkThemePresets(),
46912
48027
  },
46913
48028
  };
46914
48029
  const builder = (yargs) => {
@@ -47676,20 +48791,55 @@ function createWorkspaceState(init) {
47676
48791
  }
47677
48792
  return { ...base, selectedIndex: idx };
47678
48793
  }
48794
+ /**
48795
+ * Single-entry memo for {@link selectVisibleRepos}. The visible list
48796
+ * is a pure function of five referentially-stable state slices, and
48797
+ * the hot path — cursor movement (`j`/`k`) — leaves all five untouched
48798
+ * (`move-cursor` only swaps `selectedIndex`). Without the memo the
48799
+ * renderer recomputes the full sort + tab-filter + text-filter three
48800
+ * times per render (list window, header chips, and the direct call in
48801
+ * `renderListBody`), plus once more in the reducer, on every keystroke.
48802
+ *
48803
+ * Keyed on object/string identity so a reducer transition that swaps
48804
+ * any of `overview.repos`, `sortMode`, `tab`, `filter`, or
48805
+ * `pullRequestCounts` is a guaranteed cache miss. Callers never mutate
48806
+ * the returned array, so handing back the cached reference is safe and
48807
+ * also stabilises the array identity for any future `React.memo`.
48808
+ */
48809
+ let visibleReposCache = null;
47679
48810
  /**
47680
48811
  * Recompute the visible repo list — sort → tab filter → text filter.
47681
48812
  * The renderer consumes this; the reducer also uses it to rectify the
47682
48813
  * cursor after a filter/sort change so the selection stays in range.
48814
+ * Memoized on its five inputs (see {@link visibleReposCache}).
47683
48815
  */
47684
48816
  function selectVisibleRepos(state) {
47685
- const sorted = sortWorkspaceRepos(state.overview.repos, state.sortMode);
48817
+ const repos = state.overview.repos;
48818
+ const cache = visibleReposCache;
48819
+ if (cache !== null &&
48820
+ cache.repos === repos &&
48821
+ cache.sortMode === state.sortMode &&
48822
+ cache.tab === state.tab &&
48823
+ cache.filter === state.filter &&
48824
+ cache.pullRequestCounts === state.pullRequestCounts) {
48825
+ return cache.result;
48826
+ }
48827
+ const sorted = sortWorkspaceRepos(repos, state.sortMode);
47686
48828
  const tabFiltered = filterWorkspaceRepos(sorted, state.tab, {
47687
48829
  pullRequestCounts: state.pullRequestCounts,
47688
48830
  });
47689
- if (!state.filter) {
47690
- return tabFiltered;
47691
- }
47692
- return tabFiltered.filter((entry) => matchesWorkspaceText(entry, state.filter));
48831
+ const result = state.filter
48832
+ ? tabFiltered.filter((entry) => matchesWorkspaceText(entry, state.filter))
48833
+ : tabFiltered;
48834
+ visibleReposCache = {
48835
+ repos,
48836
+ sortMode: state.sortMode,
48837
+ tab: state.tab,
48838
+ filter: state.filter,
48839
+ pullRequestCounts: state.pullRequestCounts,
48840
+ result,
48841
+ };
48842
+ return result;
47693
48843
  }
47694
48844
  function clampCursor(index, length) {
47695
48845
  if (length <= 0) {