git-coco 0.62.4 → 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.4";
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
  }
@@ -26387,6 +27319,8 @@ function applyLogInkAction(state, action) {
26387
27319
  };
26388
27320
  case 'setWorktreeCheckoutConflict':
26389
27321
  return { ...state, worktreeCheckoutConflict: action.value, pendingKey: undefined };
27322
+ case 'setPendingChoice':
27323
+ return { ...state, pendingChoice: action.value, pendingKey: undefined };
26390
27324
  case 'setPendingMutationConfirmation':
26391
27325
  return {
26392
27326
  ...state,
@@ -27758,46 +28692,48 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
27758
28692
  }
27759
28693
  return [];
27760
28694
  }
27761
- if (state.pendingConfirmationId) {
27762
- // Worktree-conflict removal options (#1175): alongside the y-switch,
27763
- // `r` removes the conflicting worktree and checks the branch out
27764
- // here, `x` removes the worktree AND deletes the branch. Both defer
27765
- // to the runtime (it owns the git ops + the conflict context); the
27766
- // runtime clears the conflict state once it resolves.
27767
- if (state.pendingConfirmationId === 'switch-to-conflicting-worktree' && state.worktreeCheckoutConflict) {
27768
- if (inputValue === 'r') {
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;
27769
28707
  return [
27770
- { type: 'runWorkflowAction', id: 'conflict-remove-worktree-checkout' },
27771
- action({ type: 'setPendingConfirmation', value: undefined }),
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 }),
27772
28712
  ];
27773
28713
  }
27774
- if (inputValue === 'x') {
28714
+ if (option.workflowId) {
28715
+ // The workflow runner owns the live context + clears any
28716
+ // conflict state once it resolves.
27775
28717
  return [
27776
- { type: 'runWorkflowAction', id: 'conflict-remove-worktree-branch' },
27777
- action({ type: 'setPendingConfirmation', value: undefined }),
28718
+ { type: 'runWorkflowAction', id: option.workflowId },
28719
+ action({ type: 'setPendingChoice', value: undefined }),
27778
28720
  ];
27779
28721
  }
28722
+ return [action({ type: 'setPendingChoice', value: undefined })];
27780
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
+ }
28735
+ if (state.pendingConfirmationId) {
27781
28736
  if (inputValue === 'y') {
27782
- // Worktree-conflict switch (#1175): the branch is already checked
27783
- // out elsewhere, so "switch" just opens that worktree as a nested
27784
- // repo frame (same mechanism as drilling into a submodule) — no
27785
- // git mutation, hence handled here rather than via the runtime.
27786
- if (state.pendingConfirmationId === 'switch-to-conflicting-worktree') {
27787
- const conflict = state.worktreeCheckoutConflict;
27788
- if (conflict) {
27789
- return [
27790
- action({ type: 'pushRepoFrame', label: conflict.branch, workdir: conflict.worktreePath }),
27791
- action({ type: 'setStatus', value: `Switched to worktree ${conflict.worktreePath} (${conflict.branch})` }),
27792
- action({ type: 'setPendingConfirmation', value: undefined }),
27793
- action({ type: 'setWorktreeCheckoutConflict', value: undefined }),
27794
- ];
27795
- }
27796
- return [
27797
- action({ type: 'setPendingConfirmation', value: undefined }),
27798
- action({ type: 'setWorktreeCheckoutConflict', value: undefined }),
27799
- ];
27800
- }
27801
28737
  const workflowAction = getLogInkWorkflowActionById(state.pendingConfirmationId);
27802
28738
  if (workflowAction?.id === 'ai-commit-summary') {
27803
28739
  return [
@@ -27823,11 +28759,6 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
27823
28759
  if (inputValue === 'n' || key.escape) {
27824
28760
  return [
27825
28761
  action({ type: 'setPendingConfirmation', value: undefined }),
27826
- // Drop any worktree-conflict context so the prompt doesn't
27827
- // linger after the user declines to switch.
27828
- ...(state.worktreeCheckoutConflict
27829
- ? [action({ type: 'setWorktreeCheckoutConflict', value: undefined })]
27830
- : []),
27831
28762
  action({ type: 'setStatus', value: 'workflow action cancelled' }),
27832
28763
  ];
27833
28764
  }
@@ -28674,22 +29605,16 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28674
29605
  fileCount: context.worktreeFileCount,
28675
29606
  })];
28676
29607
  }
28677
- // Worktree (staging) diff: ↑/↓ move between hunks the hunk is the
28678
- // unit you stage, so the cursor walks hunks (auto-scrolling to the
28679
- // selected one). Single-hunk files fall through to line-scroll so a
28680
- // long lone hunk stays readable; `[`/`]` remain hunk-jump aliases.
28681
- if (state.activeView === 'diff' && (context.worktreeHunkOffsets?.length ?? 0) > 1) {
28682
- return [action({
28683
- type: 'jumpWorktreeHunk',
28684
- delta: -1,
28685
- hunkOffsets: context.worktreeHunkOffsets,
28686
- })];
28687
- }
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.
28688
29612
  if (state.activeView === 'diff' && context.worktreeDiffLineCount) {
28689
29613
  return [action({
28690
29614
  type: 'pageWorktreeDiff',
28691
29615
  delta: -1,
28692
29616
  lineCount: context.worktreeDiffLineCount,
29617
+ hunkOffsets: context.worktreeHunkOffsets,
28693
29618
  })];
28694
29619
  }
28695
29620
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -28799,20 +29724,14 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28799
29724
  fileCount: context.worktreeFileCount,
28800
29725
  })];
28801
29726
  }
28802
- // Worktree (staging) diff: ↓ walks to the next hunk (see the ↑
28803
- // handler). Multi-hunk only; single-hunk files line-scroll.
28804
- if (state.activeView === 'diff' && (context.worktreeHunkOffsets?.length ?? 0) > 1) {
28805
- return [action({
28806
- type: 'jumpWorktreeHunk',
28807
- delta: 1,
28808
- hunkOffsets: context.worktreeHunkOffsets,
28809
- })];
28810
- }
29727
+ // Worktree (staging) diff: ↓ scrolls lines (see the ↑ handler) —
29728
+ // `[`/`]` jump hunks (#1185).
28811
29729
  if (state.activeView === 'diff' && context.worktreeDiffLineCount) {
28812
29730
  return [action({
28813
29731
  type: 'pageWorktreeDiff',
28814
29732
  delta: 1,
28815
29733
  lineCount: context.worktreeDiffLineCount,
29734
+ hunkOffsets: context.worktreeHunkOffsets,
28816
29735
  })];
28817
29736
  }
28818
29737
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -28872,6 +29791,7 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28872
29791
  type: 'pageWorktreeDiff',
28873
29792
  delta: -8,
28874
29793
  lineCount: context.worktreeDiffLineCount,
29794
+ hunkOffsets: context.worktreeHunkOffsets,
28875
29795
  })];
28876
29796
  }
28877
29797
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -28896,6 +29816,7 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28896
29816
  type: 'pageWorktreeDiff',
28897
29817
  delta: 8,
28898
29818
  lineCount: context.worktreeDiffLineCount,
29819
+ hunkOffsets: context.worktreeHunkOffsets,
28899
29820
  })];
28900
29821
  }
28901
29822
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -33419,6 +34340,7 @@ function renderFooter$1(h, components, state, context, theme, idleTip, spinnerFr
33419
34340
  state.gitignorePicker ||
33420
34341
  state.inputPrompt ||
33421
34342
  state.pendingConfirmationId ||
34343
+ state.pendingChoice ||
33422
34344
  state.pendingMutationConfirmation ||
33423
34345
  state.pendingKey ||
33424
34346
  state.filterMode);
@@ -36226,7 +37148,7 @@ function renderDiffSurface(ctx, diff) {
36226
37148
  ? []
36227
37149
  : splitActive
36228
37150
  ? renderSplitDiffBody(h, components, previewHunks, state.diffPreviewOffset, visibleRows, width, theme, 'commit-diff-split', syntaxSpans)
36229
- : 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}`));
36230
37152
  return h(Box, {
36231
37153
  borderColor: focusBorderColor(theme, focused),
36232
37154
  borderStyle: theme.borderStyle,
@@ -36237,25 +37159,38 @@ function renderDiffSurface(ctx, diff) {
36237
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, {
36238
37160
  key: `diff-surface-header-${index}`,
36239
37161
  dimColor: index > 0,
36240
- }, truncateCells(line, 140))), ...commitBodyNodes);
37162
+ }, truncateCells(line, Math.max(20, width - 4)))), ...commitBodyNodes);
36241
37163
  }
36242
37164
  const diffLines = worktreeDiff?.lines || [];
36243
- const selectedHunk = worktreeHunks?.hunks[state.selectedWorktreeHunkIndex];
36244
37165
  const totalHunks = worktreeHunks?.hunks.length ?? 0;
36245
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 ?? []);
36246
37172
  const visibleDiffLines = diffLines.slice(state.worktreeDiffOffset, state.worktreeDiffOffset + visibleRows);
36247
- // Hunk-position line: badge + selected hunk's state + a staged/total
36248
- // progress count, so the user always sees how far through staging they
36249
- // are. Untracked/new files have no hunks point them at whole-file
36250
- // 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('');
36251
37188
  const hunkHeaderLine = worktreeHunksLoading
36252
37189
  ? 'Hunks loading…'
36253
37190
  : worktreeDiff?.untracked
36254
37191
  ? (theme.ascii ? 'New file — press space to stage it whole.' : '✚ New file — press space to stage it whole.')
36255
37192
  : totalHunks
36256
- ? `Hunk ${state.selectedWorktreeHunkIndex + 1}/${totalHunks} · ${selectedHunk?.state === 'staged'
36257
- ? (theme.ascii ? '[x] staged' : '● staged')
36258
- : (theme.ascii ? '[ ] unstaged' : '○ unstaged')} · ${stagedHunks}/${totalHunks} staged`
37193
+ ? `Hunk ${currentHunkIndex + 1}/${totalHunks} ${hunkRail} ${stagedHunks}/${totalHunks} staged`
36259
37194
  : 'No stageable hunks for this file.';
36260
37195
  const headerLines = isLogInkContextKeyLoading(contextStatus, 'worktree')
36261
37196
  ? ['Loading file context...']
@@ -36287,7 +37222,7 @@ function renderDiffSurface(ctx, diff) {
36287
37222
  h(Text, { dimColor: true }, worktreeDiff?.filePath || worktreeFile?.path || 'no file')), ...headerLines.map((line, index) => h(Text, {
36288
37223
  key: `diff-surface-header-${index}`,
36289
37224
  dimColor: index > 0,
36290
- }, truncateCells(line, 140))), ...(showDiffLines
37225
+ }, truncateCells(line, Math.max(20, width - 4)))), ...(showDiffLines
36291
37226
  ? renderWorktreeDiffBody(h, components, {
36292
37227
  lines: diffLines,
36293
37228
  offset: state.worktreeDiffOffset,
@@ -36297,7 +37232,7 @@ function renderDiffSurface(ctx, diff) {
36297
37232
  syntaxSpans,
36298
37233
  hunkOffsets: worktreeDiff?.hunkOffsets || [],
36299
37234
  hunks: worktreeHunks?.hunks || [],
36300
- selectedIndex: state.selectedWorktreeHunkIndex,
37235
+ selectedIndex: currentHunkIndex,
36301
37236
  keyPrefix: 'diff-surface-line',
36302
37237
  })
36303
37238
  : []));
@@ -37702,36 +38637,45 @@ function renderConfirmationPanel(h, components, state, width, theme, focused) {
37702
38637
  : state.pendingMutationConfirmation === 'discard-draft'
37703
38638
  ? 'Quit and discard the in-progress commit draft'
37704
38639
  : undefined;
37705
- // Worktree-conflict switch (#1175): a checkout was rejected because
37706
- // the branch is checked out elsewhere — name the branch + worktree so
37707
- // the prompt explains what "y" does (jump into that worktree).
37708
- const conflict = state.worktreeCheckoutConflict;
37709
- const isWorktreeConflict = state.pendingConfirmationId === 'switch-to-conflicting-worktree';
37710
- const label = isWorktreeConflict && conflict
37711
- ? `Switch to the worktree where '${conflict.branch}' is checked out?`
37712
- : action?.label || mutationLabel || 'Workflow action';
38640
+ const label = action?.label || mutationLabel || 'Workflow action';
37713
38641
  const warning = state.pendingMutationConfirmation === 'discard-draft'
37714
38642
  ? 'You have an unsaved commit draft. Press y to discard it and quit.'
37715
38643
  : state.pendingMutationConfirmation
37716
38644
  ? 'This discards local changes and cannot be undone by Coco.'
37717
- : isWorktreeConflict && conflict
37718
- ? `'${conflict.branch}' is checked out at ${conflict.worktreePath}.${conflict.dirty ? ' That worktree has uncommitted changes — removal will be refused until it is clean or stashed.' : ''}`
37719
- // Second-stage confirm raised when a safe delete hit an unmerged
37720
- // branch name the reason so the force isn't a blind "y again".
37721
- : state.pendingConfirmationId === 'force-delete-branch'
37722
- ? 'Not fully merged. Force-delete (git branch -D) is irreversible.'
37723
- : action?.kind === 'ai'
37724
- ? `AI action requires confirmation. Estimated ${action.estimatedTokens || '<unknown>'} tokens.`
37725
- : 'Destructive Git action requires confirmation.';
38645
+ // Second-stage confirm raised when a safe delete hit an unmerged
38646
+ // branch name the reason so the force isn't a blind "y again".
38647
+ : state.pendingConfirmationId === 'force-delete-branch'
38648
+ ? 'Not fully merged. Force-delete (git branch -D) is irreversible.'
38649
+ : action?.kind === 'ai'
38650
+ ? `AI action requires confirmation. Estimated ${action.estimatedTokens || '<unknown>'} tokens.`
38651
+ : 'Destructive Git action requires confirmation.';
37726
38652
  return h(Box, {
37727
38653
  borderColor: focusBorderColor(theme, focused),
37728
38654
  borderStyle: theme.borderStyle,
37729
38655
  flexDirection: 'column',
37730
38656
  width,
37731
38657
  paddingX: 1,
37732
- }, 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, truncateCells(isWorktreeConflict
37733
- ? 'y switch · r remove worktree & check out here · x remove worktree & delete branch · n/Esc cancel'
37734
- : 'Press y to confirm or n/Esc to cancel.', width - 4)));
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.'));
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'));
37735
38679
  }
37736
38680
  /**
37737
38681
  * First-launch onboarding overlay (P1.3). Shown once per machine, gated
@@ -38124,9 +39068,20 @@ function renderSplitPlanOverlay(h, components, state, width, bodyRows, theme, fo
38124
39068
  // overlay rather than crashing.
38125
39069
  return h(Box, { width }, h(Text, { dimColor: true }, 'No plan data available.'));
38126
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);
38127
39075
  const lines = [];
38128
- plan.groups.forEach((group, index) => {
38129
- 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
+ }
38130
39085
  if (group.body) {
38131
39086
  group.body.split('\n').forEach((bodyLine) => lines.push(` ${bodyLine}`));
38132
39087
  }
@@ -38151,9 +39106,10 @@ function renderSplitPlanOverlay(h, components, state, width, bodyRows, theme, fo
38151
39106
  const totalLines = lines.length;
38152
39107
  const scrollOffset = Math.min(overlay.scrollOffset, Math.max(0, totalLines - 1));
38153
39108
  const visible = lines.slice(scrollOffset, scrollOffset + listRows);
39109
+ const unclaimedCount = plan.groups.length - committedGroups.length;
38154
39110
  const headerRight = overlay.status === 'applying'
38155
39111
  ? `${spinner} applying…`
38156
- : `${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}`;
38157
39113
  // Apply errors get the full available width — long validator
38158
39114
  // messages (the failure path that surfaced in PR #916 testing was
38159
39115
  // "unknown hunks: src/widgets/button.ts::hunk-1, ...") frequently
@@ -40627,6 +41583,9 @@ function renderDetailPanel(h, components, state, context, contextStatus, detail,
40627
41583
  if (state.inputPrompt) {
40628
41584
  return renderInputPromptPanel(h, components, state, width, theme, focused);
40629
41585
  }
41586
+ if (state.pendingChoice) {
41587
+ return renderChoicePanel(h, components, state.pendingChoice, width, theme, focused);
41588
+ }
40630
41589
  if (state.pendingConfirmationId || state.pendingMutationConfirmation) {
40631
41590
  return renderConfirmationPanel(h, components, state, width, theme, focused);
40632
41591
  }
@@ -41480,7 +42439,7 @@ function LogInkApp(deps) {
41480
42439
  React.useEffect(() => {
41481
42440
  if (!state.statusMessage)
41482
42441
  return;
41483
- if (state.inputPrompt || state.pendingConfirmationId || state.pendingMutationConfirmation || state.showCommandPalette) {
42442
+ if (state.inputPrompt || state.pendingConfirmationId || state.pendingChoice || state.pendingMutationConfirmation || state.showCommandPalette) {
41484
42443
  return;
41485
42444
  }
41486
42445
  // The `setTimeout` callback is a literal arrow function (not a
@@ -41497,6 +42456,7 @@ function LogInkApp(deps) {
41497
42456
  dispatch,
41498
42457
  state.inputPrompt,
41499
42458
  state.pendingConfirmationId,
42459
+ state.pendingChoice,
41500
42460
  state.pendingMutationConfirmation,
41501
42461
  state.showCommandPalette,
41502
42462
  state.statusMessage,
@@ -42360,7 +43320,9 @@ function LogInkApp(deps) {
42360
43320
  setWorktreeHunks(undefined);
42361
43321
  }, [dispatch, git, refreshWorktreeContext, selectedWorktreeFile]);
42362
43322
  const toggleSelectedHunkStage = React.useCallback(async () => {
42363
- 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 ?? [])];
42364
43326
  if (!selectedHunk) {
42365
43327
  dispatch({ type: 'setStatus', value: 'no hunk selected', kind: 'warning' });
42366
43328
  return;
@@ -42389,7 +43351,7 @@ function LogInkApp(deps) {
42389
43351
  kind: 'error',
42390
43352
  });
42391
43353
  }
42392
- }, [dispatch, git, refreshWorktreeContext, state.selectedWorktreeHunkIndex, worktreeHunks]);
43354
+ }, [dispatch, git, refreshWorktreeContext, state.worktreeDiffOffset, worktreeDiff, worktreeHunks]);
42393
43355
  const revertSelectedFile = React.useCallback(async () => {
42394
43356
  if (!selectedWorktreeFile) {
42395
43357
  dispatch({ type: 'setStatus', value: 'no worktree file selected', kind: 'warning' });
@@ -42403,7 +43365,7 @@ function LogInkApp(deps) {
42403
43365
  setWorktreeHunks(undefined);
42404
43366
  }, [dispatch, git, refreshWorktreeContext, selectedWorktreeFile]);
42405
43367
  const revertSelectedHunk = React.useCallback(async () => {
42406
- const selectedHunk = worktreeHunks?.hunks[state.selectedWorktreeHunkIndex];
43368
+ const selectedHunk = worktreeHunks?.hunks[hunkIndexAtOffset(state.worktreeDiffOffset, worktreeDiff?.hunkOffsets ?? [])];
42407
43369
  if (!selectedHunk) {
42408
43370
  dispatch({ type: 'setStatus', value: 'no hunk selected', kind: 'warning' });
42409
43371
  return;
@@ -42423,7 +43385,7 @@ function LogInkApp(deps) {
42423
43385
  kind: 'error',
42424
43386
  });
42425
43387
  }
42426
- }, [dispatch, git, refreshWorktreeContext, state.selectedWorktreeHunkIndex, worktreeHunks]);
43388
+ }, [dispatch, git, refreshWorktreeContext, state.worktreeDiffOffset, worktreeDiff, worktreeHunks]);
42427
43389
  const createCommitFromCompose = React.useCallback(async () => {
42428
43390
  const stagedCount = context.worktree?.stagedCount || 0;
42429
43391
  if (!stagedCount) {
@@ -43305,9 +44267,19 @@ function LogInkApp(deps) {
43305
44267
  // navigateHome nukes the rest of the stack so `<` after apply
43306
44268
  // doesn't walk back into the now-empty compose / status state
43307
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;
43308
44277
  dispatch({ type: 'clearSplitPlan' });
43309
44278
  dispatch({ type: 'commitCompose', action: { type: 'reset' } });
43310
44279
  dispatch({ type: 'navigateHome' });
44280
+ if (unclaimedFileCount > 0) {
44281
+ dispatch({ type: 'pushView', value: 'status' });
44282
+ }
43311
44283
  // Refresh BEFORE setting the final status so we can peek at the
43312
44284
  // post-apply worktree state and craft a directive next-step hint
43313
44285
  // ("X unstaged + Y untracked remaining — press gs to stage / I
@@ -43357,12 +44329,17 @@ function LogInkApp(deps) {
43357
44329
  return;
43358
44330
  }
43359
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
+ : '';
43360
44337
  // Fallback path uses 'info' kind — apply technically succeeded
43361
44338
  // but the user should know it landed as a single combined commit
43362
44339
  // rather than a real LLM-driven multi-group split.
43363
44340
  dispatch({
43364
44341
  type: 'setStatus',
43365
- value: successMessage,
44342
+ value: `${successMessage}${unclaimedNote}`,
43366
44343
  kind: result.fallback ? 'info' : 'success',
43367
44344
  });
43368
44345
  }, [dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext, state.splitPlan]);
@@ -44346,18 +45323,31 @@ function LogInkApp(deps) {
44346
45323
  // Checking out a branch that's already checked out in another
44347
45324
  // worktree is rejected by git ("already checked out at <path>").
44348
45325
  // Rather than dead-end on that, capture the conflict and raise a
44349
- // y-confirm offering to switch into that worktree the branch IS
44350
- // checked out, just elsewhere (#1175).
45326
+ // multi-option prompt: switch into that worktree, remove it and
45327
+ // check out here, or remove it and delete the branch (#1175, #1181).
44351
45328
  if (id === 'checkout-branch' && !result?.ok && isBranchCheckedOutElsewhereError(result?.message)) {
44352
45329
  const worktreePath = parseCheckedOutWorktreePath(result?.message);
44353
45330
  const branchName = pendingItemAction?.id;
44354
45331
  if (worktreePath && branchName) {
44355
45332
  const worktree = context.worktreeList?.worktrees?.find((w) => w.path === worktreePath);
45333
+ const dirty = worktree?.dirty ?? false;
44356
45334
  dispatch({
44357
45335
  type: 'setWorktreeCheckoutConflict',
44358
- value: { branch: branchName, worktreePath, dirty: worktree?.dirty ?? false },
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
+ },
44359
45350
  });
44360
- dispatch({ type: 'setPendingConfirmation', value: 'switch-to-conflicting-worktree' });
44361
45351
  }
44362
45352
  else {
44363
45353
  dispatch({
@@ -45293,6 +46283,7 @@ function LogInkApp(deps) {
45293
46283
  state.gitignorePicker ||
45294
46284
  state.inputPrompt ||
45295
46285
  state.pendingConfirmationId ||
46286
+ state.pendingChoice ||
45296
46287
  state.pendingMutationConfirmation ||
45297
46288
  state.pendingKey
45298
46289
  ? 'inspector'
@@ -47000,7 +47991,9 @@ const options$1 = {
47000
47991
  },
47001
47992
  theme: {
47002
47993
  description: 'TUI theme preset',
47003
- 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(),
47004
47997
  },
47005
47998
  };
47006
47999
  const builder$1 = (yargs) => {
@@ -47028,7 +48021,9 @@ const options = {
47028
48021
  },
47029
48022
  theme: {
47030
48023
  description: 'TUI theme preset',
47031
- 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(),
47032
48027
  },
47033
48028
  };
47034
48029
  const builder = (yargs) => {
@@ -47796,20 +48791,55 @@ function createWorkspaceState(init) {
47796
48791
  }
47797
48792
  return { ...base, selectedIndex: idx };
47798
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;
47799
48810
  /**
47800
48811
  * Recompute the visible repo list — sort → tab filter → text filter.
47801
48812
  * The renderer consumes this; the reducer also uses it to rectify the
47802
48813
  * cursor after a filter/sort change so the selection stays in range.
48814
+ * Memoized on its five inputs (see {@link visibleReposCache}).
47803
48815
  */
47804
48816
  function selectVisibleRepos(state) {
47805
- 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);
47806
48828
  const tabFiltered = filterWorkspaceRepos(sorted, state.tab, {
47807
48829
  pullRequestCounts: state.pullRequestCounts,
47808
48830
  });
47809
- if (!state.filter) {
47810
- return tabFiltered;
47811
- }
47812
- 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;
47813
48843
  }
47814
48844
  function clampCursor(index, length) {
47815
48845
  if (length <= 0) {