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.
@@ -61,7 +61,7 @@ import { pathToFileURL } from 'url';
61
61
  /**
62
62
  * Current build version from package.json
63
63
  */
64
- const BUILD_VERSION = "0.62.4";
64
+ const BUILD_VERSION = "0.63.0";
65
65
 
66
66
  const isInteractive = (config) => {
67
67
  return config?.mode === 'interactive' || !!config?.interactive;
@@ -2246,7 +2246,66 @@ const schema$1 = {
2246
2246
  "material-lighter",
2247
2247
  "papercolor-light",
2248
2248
  "modus-operandi",
2249
- "quiet-light"
2249
+ "quiet-light",
2250
+ "catppuccin-frappe",
2251
+ "rose-pine-moon",
2252
+ "kanagawa-dragon",
2253
+ "kanagawa-lotus",
2254
+ "nordfox",
2255
+ "duskfox",
2256
+ "terafox",
2257
+ "dawnfox",
2258
+ "ayu-mirage",
2259
+ "material-darker",
2260
+ "tokyo-night-moon",
2261
+ "gruvbox-material",
2262
+ "gruvbox-material-light",
2263
+ "modus-vivendi",
2264
+ "zenburn",
2265
+ "oxocarbon",
2266
+ "tomorrow-night",
2267
+ "monokai-pro",
2268
+ "sonokai",
2269
+ "doom-one",
2270
+ "andromeda",
2271
+ "aura",
2272
+ "cyberdream",
2273
+ "nightfly",
2274
+ "panda",
2275
+ "hyper-snazzy",
2276
+ "apprentice",
2277
+ "melange",
2278
+ "melange-light",
2279
+ "spaceduck",
2280
+ "embark",
2281
+ "bluloco-dark",
2282
+ "bluloco-light",
2283
+ "papercolor-dark",
2284
+ "base16-ocean",
2285
+ "base16-eighties",
2286
+ "everblush",
2287
+ "darcula",
2288
+ "eldritch",
2289
+ "edge-light",
2290
+ "zenbones",
2291
+ "iceberg-light",
2292
+ "github-dark-dimmed",
2293
+ "edge-dark",
2294
+ "selenized-dark",
2295
+ "selenized-black",
2296
+ "selenized-light",
2297
+ "monokai-pro-machine",
2298
+ "monokai-pro-octagon",
2299
+ "monokai-pro-ristretto",
2300
+ "monokai-pro-spectrum",
2301
+ "base16-default-dark",
2302
+ "base16-default-light",
2303
+ "tomorrow",
2304
+ "tokyodark",
2305
+ "spacemacs-dark",
2306
+ "bamboo",
2307
+ "citylights",
2308
+ "oxocarbon-light"
2250
2309
  ]
2251
2310
  }
2252
2311
  }
@@ -7475,6 +7534,7 @@ var ZodFirstPartyTypeKind;
7475
7534
  ZodFirstPartyTypeKind["ZodReadonly"] = "ZodReadonly";
7476
7535
  })(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {}));
7477
7536
  const stringType = ZodString.create;
7537
+ const booleanType = ZodBoolean.create;
7478
7538
  ZodNever.create;
7479
7539
  const arrayType = ZodArray.create;
7480
7540
  const objectType = ZodObject.create;
@@ -15432,6 +15492,14 @@ const CommitSplitPlanSchema = objectType({
15432
15492
  // that.)
15433
15493
  files: arrayType(stringType()).optional(),
15434
15494
  hunks: arrayType(stringType()).optional(),
15495
+ // Internal flag (not emitted by the model). Set by
15496
+ // `rescueMissingFiles` on the catch-all group of files the
15497
+ // plan didn't confidently place: the apply step skips
15498
+ // committing these, leaving them in the worktree for the user
15499
+ // to handle, and the review overlay renders them as a "will
15500
+ // stay — not committed" note rather than a numbered commit
15501
+ // (#1180).
15502
+ unclaimed: booleanType().optional(),
15435
15503
  })
15436
15504
  .refine((group) => (group.files?.length ?? 0) > 0 || (group.hunks?.length ?? 0) > 0, {
15437
15505
  message: 'Each group must include at least one file or hunk',
@@ -15749,11 +15817,16 @@ function rescueMissingFiles(plan, staged, hunkInventory) {
15749
15817
  groups: [
15750
15818
  ...plan.groups,
15751
15819
  {
15752
- title: 'chore: misc unclaimed changes',
15753
- body: 'Files the split plan did not assign to any other commit. Review and re-roll (`r`) if these belong in a specific commit.',
15754
- rationale: 'Recovered by validator rescuemodel omitted these from every group.',
15820
+ // Tagged `unclaimed`: the apply step skips committing this group
15821
+ // (the files are left in the worktree), and the review overlay
15822
+ // renders it as a "will stay not committed" note rather than a
15823
+ // numbered commit. The confident commits land; these come back
15824
+ // to you on the status screen to handle (#1180).
15825
+ title: 'Left for you — not committed',
15826
+ body: `${missing.length} file${missing.length === 1 ? '' : 's'} the split couldn't confidently place. They stay in your worktree (uncommitted) — you'll land on the status screen to handle them. Re-roll (\`r\`) if they belong in a specific commit.`,
15755
15827
  files: missing,
15756
15828
  hunks: [],
15829
+ unclaimed: true,
15757
15830
  },
15758
15831
  ],
15759
15832
  };
@@ -16195,6 +16268,15 @@ async function applyCommitSplitPlan({ plan, changes, hunkInventory, git, logger,
16195
16268
  // "git commit with nothing staged" failure mode mid-loop after
16196
16269
  // the up-front `git reset` has already wiped the index.
16197
16270
  const applicableGroups = plan.groups.filter((group) => {
16271
+ // `unclaimed` groups are intentionally NOT committed (#1180): they
16272
+ // hold the files the plan couldn't confidently place. The up-front
16273
+ // `git reset` below unstages everything, and since these never get
16274
+ // re-added they simply stay in the worktree for the user to handle.
16275
+ // They still count as "claimed" for validatePlanForStagedFiles, so
16276
+ // that check (run above) passes.
16277
+ if (group.unclaimed) {
16278
+ return false;
16279
+ }
16198
16280
  const fileCount = (group.files || []).length;
16199
16281
  const hunkCount = (group.hunks || []).length;
16200
16282
  return fileCount + hunkCount > 0;
@@ -23807,11 +23889,12 @@ function computeLogInkFooterHints(options) {
23807
23889
  global: NORMAL_GLOBAL_HINTS,
23808
23890
  };
23809
23891
  }
23810
- // Worktree (staging) diff. The hunk is the unit of action: ↑/↓ walk
23811
- // hunks, space stages/unstages the selected one, a stages the whole
23812
- // file, z discards the hunk.
23892
+ // Worktree (staging) diff. Consistent with the commit/stash diffs
23893
+ // (#1185): j/k scroll lines, [/] jump between hunks. space stages /
23894
+ // unstages the hunk under the viewport, a stages the whole file, z
23895
+ // discards the current hunk.
23813
23896
  return {
23814
- contextual: ['↑/↓ hunk', 'space stage', 'a stage file', 'z discard', 'o edit', 'esc back'],
23897
+ contextual: ['j/k lines', '[/] hunk', 'space stage', 'a stage file', 'z discard', 'o edit', 'esc back'],
23815
23898
  global: NORMAL_GLOBAL_HINTS,
23816
23899
  };
23817
23900
  }
@@ -24184,14 +24267,23 @@ function getColorLevel(env = process.env) {
24184
24267
  return '256';
24185
24268
  return '16';
24186
24269
  }
24187
- const TRUECOLOR_PRESETS = new Set(['catppuccin', 'gruvbox', 'dracula', 'nord', 'solarized-dark', 'tokyo-night', 'one-dark', 'rose-pine', 'kanagawa', 'everforest', 'monokai', 'synthwave', 'ayu-dark', 'palenight', 'github-dark', 'horizon', 'nightfox', 'carbonfox', 'tokyonight-storm', 'catppuccin-latte', 'solarized-light', 'github-light', 'iceberg', 'material-ocean', 'moonlight', 'poimandres', 'vitesse-dark', 'vesper', 'flexoki', 'mellow', 'night-owl', 'cobalt2', 'oceanic-next', 'catppuccin-macchiato', 'gruvbox-light', 'tokyo-night-day', 'one-light', 'ayu-light', 'rose-pine-dawn', 'everforest-light', 'vitesse-light', 'dayfox', 'night-owl-light', 'flexoki-light', 'material-lighter', 'papercolor-light', 'modus-operandi', 'quiet-light']);
24270
+ /**
24271
+ * The only presets whose palettes are ANSI-named (not hex): `default`
24272
+ * renders faithfully on 16-color terminals, and `monochrome` carries no
24273
+ * color at all. Every other preset in `THEME_PRESET_COLORS` is hand-authored
24274
+ * in hex, so it's treated as truecolor by definition — no list to keep in
24275
+ * sync as themes are added.
24276
+ */
24277
+ const ANSI_NATIVE_PRESETS = new Set(['default', 'monochrome']);
24188
24278
  /**
24189
24279
  * `true` when the named preset relies on hex colors that look best under
24190
24280
  * 24-bit RGB. Used by `createLogInkTheme` to decide whether to downgrade
24191
- * to the ANSI-named `default` palette on lower-capability terminals.
24281
+ * to the ANSI-named `default` palette on lower-capability terminals. Every
24282
+ * preset except the two ANSI-native baselines uses hex, so this is derived
24283
+ * rather than enumerated — new themes are covered automatically.
24192
24284
  */
24193
24285
  function presetUsesTrueColor(preset) {
24194
- return preset !== undefined && TRUECOLOR_PRESETS.has(preset);
24286
+ return preset !== undefined && !ANSI_NATIVE_PRESETS.has(preset);
24195
24287
  }
24196
24288
  /**
24197
24289
  * WCAG 2.x relative luminance of a `#rrggbb` color, 0 (black) … 1 (white).
@@ -24920,6 +25012,832 @@ const THEME_PRESET_COLORS = {
24920
25012
  success: '#448c27',
24921
25013
  warning: '#a67d00',
24922
25014
  },
25015
+ 'catppuccin-frappe': {
25016
+ accent: '#8caaee',
25017
+ border: '#51576d',
25018
+ danger: '#e78284',
25019
+ focusBorder: '#81c8be',
25020
+ gitAdded: '#a6d189',
25021
+ gitDeleted: '#e78284',
25022
+ gitModified: '#e5c890',
25023
+ info: '#8caaee',
25024
+ muted: '#737994',
25025
+ selection: '#414559',
25026
+ success: '#a6d189',
25027
+ warning: '#e5c890',
25028
+ },
25029
+ 'rose-pine-moon': {
25030
+ accent: '#c4a7e7',
25031
+ border: '#393552',
25032
+ danger: '#eb6f92',
25033
+ focusBorder: '#9ccfd8',
25034
+ gitAdded: '#3e8fb0',
25035
+ gitDeleted: '#eb6f92',
25036
+ gitModified: '#f6c177',
25037
+ info: '#9ccfd8',
25038
+ muted: '#6e6a86',
25039
+ selection: '#44415a',
25040
+ success: '#3e8fb0',
25041
+ warning: '#f6c177',
25042
+ },
25043
+ 'kanagawa-dragon': {
25044
+ accent: '#8ba4b0',
25045
+ border: '#282727',
25046
+ danger: '#c4746e',
25047
+ focusBorder: '#8ea4a2',
25048
+ gitAdded: '#87a987',
25049
+ gitDeleted: '#c4746e',
25050
+ gitModified: '#c4b28a',
25051
+ info: '#8ba4b0',
25052
+ muted: '#737c73',
25053
+ selection: '#2d4f67',
25054
+ success: '#87a987',
25055
+ warning: '#c4b28a',
25056
+ },
25057
+ 'kanagawa-lotus': {
25058
+ accent: '#4d699b',
25059
+ border: '#e5ddb0',
25060
+ danger: '#c84053',
25061
+ focusBorder: '#597b75',
25062
+ gitAdded: '#6f894e',
25063
+ gitDeleted: '#c84053',
25064
+ gitModified: '#77713f',
25065
+ info: '#4d699b',
25066
+ muted: '#8a8980',
25067
+ selection: '#dcd5ac',
25068
+ success: '#6f894e',
25069
+ warning: '#77713f',
25070
+ },
25071
+ nordfox: {
25072
+ accent: '#81a1c1',
25073
+ border: '#39404f',
25074
+ danger: '#bf616a',
25075
+ focusBorder: '#88c0d0',
25076
+ gitAdded: '#a3be8c',
25077
+ gitDeleted: '#bf616a',
25078
+ gitModified: '#ebcb8b',
25079
+ info: '#81a1c1',
25080
+ muted: '#60728a',
25081
+ selection: '#3e4a5b',
25082
+ success: '#a3be8c',
25083
+ warning: '#ebcb8b',
25084
+ },
25085
+ duskfox: {
25086
+ accent: '#569fba',
25087
+ border: '#2d2a45',
25088
+ danger: '#eb6f92',
25089
+ focusBorder: '#9ccfd8',
25090
+ gitAdded: '#a3be8c',
25091
+ gitDeleted: '#eb6f92',
25092
+ gitModified: '#f6c177',
25093
+ info: '#569fba',
25094
+ muted: '#817c9c',
25095
+ selection: '#433c59',
25096
+ success: '#a3be8c',
25097
+ warning: '#f6c177',
25098
+ },
25099
+ terafox: {
25100
+ accent: '#5a93aa',
25101
+ border: '#1d3337',
25102
+ danger: '#e85c51',
25103
+ focusBorder: '#a1cdd8',
25104
+ gitAdded: '#7aa4a1',
25105
+ gitDeleted: '#e85c51',
25106
+ gitModified: '#fda47f',
25107
+ info: '#5a93aa',
25108
+ muted: '#6d7f8b',
25109
+ selection: '#293e40',
25110
+ success: '#7aa4a1',
25111
+ warning: '#fda47f',
25112
+ },
25113
+ dawnfox: {
25114
+ accent: '#286983',
25115
+ border: '#ebe0df',
25116
+ danger: '#b4637a',
25117
+ focusBorder: '#56949f',
25118
+ gitAdded: '#618774',
25119
+ gitDeleted: '#b4637a',
25120
+ gitModified: '#ea9d34',
25121
+ info: '#286983',
25122
+ muted: '#9893a5',
25123
+ selection: '#eadcd8',
25124
+ success: '#618774',
25125
+ warning: '#ea9d34',
25126
+ },
25127
+ 'ayu-mirage': {
25128
+ accent: '#ffcc66',
25129
+ border: '#323843',
25130
+ danger: '#f28779',
25131
+ focusBorder: '#95e6cb',
25132
+ gitAdded: '#d5ff80',
25133
+ gitDeleted: '#f28779',
25134
+ gitModified: '#ffd173',
25135
+ info: '#73d0ff',
25136
+ muted: '#5c6773',
25137
+ selection: '#33415e',
25138
+ success: '#d5ff80',
25139
+ warning: '#ffd173',
25140
+ },
25141
+ 'material-darker': {
25142
+ accent: '#82aaff',
25143
+ border: '#343434',
25144
+ danger: '#f07178',
25145
+ focusBorder: '#89ddff',
25146
+ gitAdded: '#c3e88d',
25147
+ gitDeleted: '#f07178',
25148
+ gitModified: '#ffcb6b',
25149
+ info: '#82aaff',
25150
+ muted: '#545454',
25151
+ selection: '#404040',
25152
+ success: '#c3e88d',
25153
+ warning: '#ffcb6b',
25154
+ },
25155
+ 'tokyo-night-moon': {
25156
+ accent: '#82aaff',
25157
+ border: '#2f334d',
25158
+ danger: '#ff757f',
25159
+ focusBorder: '#86e1fc',
25160
+ gitAdded: '#c3e88d',
25161
+ gitDeleted: '#ff757f',
25162
+ gitModified: '#ffc777',
25163
+ info: '#82aaff',
25164
+ muted: '#636da6',
25165
+ selection: '#2d3f76',
25166
+ success: '#c3e88d',
25167
+ warning: '#ffc777',
25168
+ },
25169
+ 'gruvbox-material': {
25170
+ accent: '#7daea3',
25171
+ border: '#504945',
25172
+ danger: '#ea6962',
25173
+ focusBorder: '#89b482',
25174
+ gitAdded: '#a9b665',
25175
+ gitDeleted: '#ea6962',
25176
+ gitModified: '#d8a657',
25177
+ info: '#7daea3',
25178
+ muted: '#928374',
25179
+ selection: '#3c3836',
25180
+ success: '#a9b665',
25181
+ warning: '#d8a657',
25182
+ },
25183
+ 'gruvbox-material-light': {
25184
+ accent: '#45707a',
25185
+ border: '#ddccab',
25186
+ danger: '#c14a4a',
25187
+ focusBorder: '#4c7a5d',
25188
+ gitAdded: '#6c782e',
25189
+ gitDeleted: '#c14a4a',
25190
+ gitModified: '#b47109',
25191
+ info: '#45707a',
25192
+ muted: '#928374',
25193
+ selection: '#eee0b7',
25194
+ success: '#6c782e',
25195
+ warning: '#b47109',
25196
+ },
25197
+ 'modus-vivendi': {
25198
+ accent: '#2fafff',
25199
+ border: '#646464',
25200
+ danger: '#ff5f59',
25201
+ focusBorder: '#00d3d0',
25202
+ gitAdded: '#44bc44',
25203
+ gitDeleted: '#ff5f59',
25204
+ gitModified: '#d0bc00',
25205
+ info: '#2fafff',
25206
+ muted: '#989898',
25207
+ selection: '#5a5a5a',
25208
+ success: '#44bc44',
25209
+ warning: '#d0bc00',
25210
+ },
25211
+ zenburn: {
25212
+ accent: '#8cd0d3',
25213
+ border: '#4f4f4f',
25214
+ danger: '#cc9393',
25215
+ focusBorder: '#93e0e3',
25216
+ gitAdded: '#7f9f7f',
25217
+ gitDeleted: '#cc9393',
25218
+ gitModified: '#f0dfaf',
25219
+ info: '#8cd0d3',
25220
+ muted: '#9f9f8f',
25221
+ selection: '#5f5f5f',
25222
+ success: '#7f9f7f',
25223
+ warning: '#f0dfaf',
25224
+ },
25225
+ oxocarbon: {
25226
+ accent: '#33b1ff',
25227
+ border: '#525252',
25228
+ danger: '#ee5396',
25229
+ focusBorder: '#3ddbd9',
25230
+ gitAdded: '#42be65',
25231
+ gitDeleted: '#ee5396',
25232
+ gitModified: '#ab8e34',
25233
+ info: '#33b1ff',
25234
+ muted: '#6f6f6f',
25235
+ selection: '#2a2a2a',
25236
+ success: '#42be65',
25237
+ warning: '#ab8e34',
25238
+ },
25239
+ 'tomorrow-night': {
25240
+ accent: '#81a2be',
25241
+ border: '#373b41',
25242
+ danger: '#cc6666',
25243
+ focusBorder: '#8abeb7',
25244
+ gitAdded: '#b5bd68',
25245
+ gitDeleted: '#cc6666',
25246
+ gitModified: '#f0c674',
25247
+ info: '#81a2be',
25248
+ muted: '#969896',
25249
+ selection: '#373b41',
25250
+ success: '#b5bd68',
25251
+ warning: '#f0c674',
25252
+ },
25253
+ 'monokai-pro': {
25254
+ accent: '#78dce8',
25255
+ border: '#403e41',
25256
+ danger: '#ff6188',
25257
+ focusBorder: '#a9dc76',
25258
+ gitAdded: '#a9dc76',
25259
+ gitDeleted: '#ff6188',
25260
+ gitModified: '#ffd866',
25261
+ info: '#78dce8',
25262
+ muted: '#727072',
25263
+ selection: '#5b595c',
25264
+ success: '#a9dc76',
25265
+ warning: '#ffd866',
25266
+ },
25267
+ sonokai: {
25268
+ accent: '#76cce0',
25269
+ border: '#33353f',
25270
+ danger: '#fc5d7c',
25271
+ focusBorder: '#9ed072',
25272
+ gitAdded: '#9ed072',
25273
+ gitDeleted: '#fc5d7c',
25274
+ gitModified: '#e7c664',
25275
+ info: '#76cce0',
25276
+ muted: '#7f8490',
25277
+ selection: '#414550',
25278
+ success: '#9ed072',
25279
+ warning: '#e7c664',
25280
+ },
25281
+ 'doom-one': {
25282
+ accent: '#51afef',
25283
+ border: '#3f444a',
25284
+ danger: '#ff6c6b',
25285
+ focusBorder: '#46d9ff',
25286
+ gitAdded: '#98be65',
25287
+ gitDeleted: '#ff6c6b',
25288
+ gitModified: '#ecbe7b',
25289
+ info: '#51afef',
25290
+ muted: '#5b6268',
25291
+ selection: '#42444a',
25292
+ success: '#98be65',
25293
+ warning: '#ecbe7b',
25294
+ },
25295
+ andromeda: {
25296
+ accent: '#00e8c6',
25297
+ border: '#2b2e36',
25298
+ danger: '#ee5d43',
25299
+ focusBorder: '#00e8c6',
25300
+ gitAdded: '#96e072',
25301
+ gitDeleted: '#ee5d43',
25302
+ gitModified: '#ffe66d',
25303
+ info: '#7cb7ff',
25304
+ muted: '#a0a1a7',
25305
+ selection: '#3d4352',
25306
+ success: '#96e072',
25307
+ warning: '#ffe66d',
25308
+ },
25309
+ aura: {
25310
+ accent: '#a277ff',
25311
+ border: '#363c49',
25312
+ danger: '#ff6767',
25313
+ focusBorder: '#61ffca',
25314
+ gitAdded: '#61ffca',
25315
+ gitDeleted: '#ff6767',
25316
+ gitModified: '#ffca85',
25317
+ info: '#82e2ff',
25318
+ muted: '#6d6d6d',
25319
+ selection: '#3d375e',
25320
+ success: '#61ffca',
25321
+ warning: '#ffca85',
25322
+ },
25323
+ cyberdream: {
25324
+ accent: '#5ea1ff',
25325
+ border: '#1e2124',
25326
+ danger: '#ff6e5e',
25327
+ focusBorder: '#5ef1ff',
25328
+ gitAdded: '#5eff6c',
25329
+ gitDeleted: '#ff6e5e',
25330
+ gitModified: '#f1ff5e',
25331
+ info: '#5ea1ff',
25332
+ muted: '#7b8496',
25333
+ selection: '#3c4048',
25334
+ success: '#5eff6c',
25335
+ warning: '#f1ff5e',
25336
+ },
25337
+ nightfly: {
25338
+ accent: '#82aaff',
25339
+ border: '#1d3b53',
25340
+ danger: '#fc514e',
25341
+ focusBorder: '#7fdbca',
25342
+ gitAdded: '#a1cd5e',
25343
+ gitDeleted: '#fc514e',
25344
+ gitModified: '#e3d18a',
25345
+ info: '#82aaff',
25346
+ muted: '#7c8f8f',
25347
+ selection: '#1d3b53',
25348
+ success: '#a1cd5e',
25349
+ warning: '#e3d18a',
25350
+ },
25351
+ panda: {
25352
+ accent: '#ff75b5',
25353
+ border: '#404954',
25354
+ danger: '#ff4b82',
25355
+ focusBorder: '#19f9d8',
25356
+ gitAdded: '#19f9d8',
25357
+ gitDeleted: '#ff4b82',
25358
+ gitModified: '#ffb86c',
25359
+ info: '#45a9f9',
25360
+ muted: '#676b79',
25361
+ selection: '#373841',
25362
+ success: '#19f9d8',
25363
+ warning: '#ffb86c',
25364
+ },
25365
+ 'hyper-snazzy': {
25366
+ accent: '#57c7ff',
25367
+ border: '#43454f',
25368
+ danger: '#ff5c57',
25369
+ focusBorder: '#9aedfe',
25370
+ gitAdded: '#5af78e',
25371
+ gitDeleted: '#ff5c57',
25372
+ gitModified: '#f3f99d',
25373
+ info: '#57c7ff',
25374
+ muted: '#686868',
25375
+ selection: '#3a3d4d',
25376
+ success: '#5af78e',
25377
+ warning: '#f3f99d',
25378
+ },
25379
+ apprentice: {
25380
+ accent: '#5f87af',
25381
+ border: '#444444',
25382
+ danger: '#af5f5f',
25383
+ focusBorder: '#5f8787',
25384
+ gitAdded: '#5f875f',
25385
+ gitDeleted: '#af5f5f',
25386
+ gitModified: '#ffffaf',
25387
+ info: '#5f87af',
25388
+ muted: '#6c6c6c',
25389
+ selection: '#444444',
25390
+ success: '#5f875f',
25391
+ warning: '#ffffaf',
25392
+ },
25393
+ melange: {
25394
+ accent: '#a3a9ce',
25395
+ border: '#34302c',
25396
+ danger: '#d47766',
25397
+ focusBorder: '#89b3b6',
25398
+ gitAdded: '#85b695',
25399
+ gitDeleted: '#d47766',
25400
+ gitModified: '#ebc06d',
25401
+ info: '#a3a9ce',
25402
+ muted: '#867462',
25403
+ selection: '#403a36',
25404
+ success: '#85b695',
25405
+ warning: '#ebc06d',
25406
+ },
25407
+ 'melange-light': {
25408
+ accent: '#465aa4',
25409
+ border: '#e9e1db',
25410
+ danger: '#bf0021',
25411
+ focusBorder: '#3d6568',
25412
+ gitAdded: '#3a684a',
25413
+ gitDeleted: '#bf0021',
25414
+ gitModified: '#a06d00',
25415
+ info: '#465aa4',
25416
+ muted: '#7d6658',
25417
+ selection: '#d9d3ce',
25418
+ success: '#3a684a',
25419
+ warning: '#a06d00',
25420
+ },
25421
+ spaceduck: {
25422
+ accent: '#00a3cc',
25423
+ border: '#30365f',
25424
+ danger: '#e33400',
25425
+ focusBorder: '#ce6f8f',
25426
+ gitAdded: '#5ccc96',
25427
+ gitDeleted: '#e33400',
25428
+ gitModified: '#f2ce00',
25429
+ info: '#7a5ccc',
25430
+ muted: '#686f9a',
25431
+ selection: '#30365f',
25432
+ success: '#5ccc96',
25433
+ warning: '#f2ce00',
25434
+ },
25435
+ embark: {
25436
+ accent: '#d4bfff',
25437
+ border: '#585273',
25438
+ danger: '#f48fb1',
25439
+ focusBorder: '#abf8f7',
25440
+ gitAdded: '#a1efd3',
25441
+ gitDeleted: '#f48fb1',
25442
+ gitModified: '#ffe6b3',
25443
+ info: '#91ddff',
25444
+ muted: '#8a889d',
25445
+ selection: '#3e3859',
25446
+ success: '#a1efd3',
25447
+ warning: '#ffe6b3',
25448
+ },
25449
+ 'bluloco-dark': {
25450
+ accent: '#3691ff',
25451
+ border: '#3d434f',
25452
+ danger: '#ff2e3f',
25453
+ focusBorder: '#4483aa',
25454
+ gitAdded: '#3fc56b',
25455
+ gitDeleted: '#ff2e3f',
25456
+ gitModified: '#f9c859',
25457
+ info: '#3691ff',
25458
+ muted: '#636d83',
25459
+ selection: '#2f343e',
25460
+ success: '#3fc56b',
25461
+ warning: '#f9c859',
25462
+ },
25463
+ 'bluloco-light': {
25464
+ accent: '#275fe4',
25465
+ border: '#d5d7d8',
25466
+ danger: '#d52753',
25467
+ focusBorder: '#40b8c5',
25468
+ gitAdded: '#23974a',
25469
+ gitDeleted: '#d52753',
25470
+ gitModified: '#c5a332',
25471
+ info: '#275fe4',
25472
+ muted: '#a0a1a7',
25473
+ selection: '#d2ecff',
25474
+ success: '#23974a',
25475
+ warning: '#c5a332',
25476
+ },
25477
+ 'papercolor-dark': {
25478
+ accent: '#5fafd7',
25479
+ border: '#444444',
25480
+ danger: '#af005f',
25481
+ focusBorder: '#00afaf',
25482
+ gitAdded: '#5faf00',
25483
+ gitDeleted: '#af005f',
25484
+ gitModified: '#d7af5f',
25485
+ info: '#5fafd7',
25486
+ muted: '#808080',
25487
+ selection: '#303030',
25488
+ success: '#5faf00',
25489
+ warning: '#d7af5f',
25490
+ },
25491
+ 'base16-ocean': {
25492
+ accent: '#8fa1b3',
25493
+ border: '#343d46',
25494
+ danger: '#bf616a',
25495
+ focusBorder: '#96b5b4',
25496
+ gitAdded: '#a3be8c',
25497
+ gitDeleted: '#bf616a',
25498
+ gitModified: '#ebcb8b',
25499
+ info: '#8fa1b3',
25500
+ muted: '#65737e',
25501
+ selection: '#4f5b66',
25502
+ success: '#a3be8c',
25503
+ warning: '#ebcb8b',
25504
+ },
25505
+ 'base16-eighties': {
25506
+ accent: '#6699cc',
25507
+ border: '#393939',
25508
+ danger: '#f2777a',
25509
+ focusBorder: '#66cccc',
25510
+ gitAdded: '#99cc99',
25511
+ gitDeleted: '#f2777a',
25512
+ gitModified: '#ffcc66',
25513
+ info: '#6699cc',
25514
+ muted: '#747369',
25515
+ selection: '#515151',
25516
+ success: '#99cc99',
25517
+ warning: '#ffcc66',
25518
+ },
25519
+ everblush: {
25520
+ accent: '#67b0e8',
25521
+ border: '#232a2d',
25522
+ danger: '#e57474',
25523
+ focusBorder: '#6cbfbf',
25524
+ gitAdded: '#8ccf7e',
25525
+ gitDeleted: '#e57474',
25526
+ gitModified: '#e5c76b',
25527
+ info: '#67b0e8',
25528
+ muted: '#5e6164',
25529
+ selection: '#2d3437',
25530
+ success: '#8ccf7e',
25531
+ warning: '#e5c76b',
25532
+ },
25533
+ darcula: {
25534
+ accent: '#cc7832',
25535
+ border: '#3c3f41',
25536
+ danger: '#ff6b68',
25537
+ focusBorder: '#629755',
25538
+ gitAdded: '#6a8759',
25539
+ gitDeleted: '#ff6b68',
25540
+ gitModified: '#ffc66d',
25541
+ info: '#6897bb',
25542
+ muted: '#808080',
25543
+ selection: '#214283',
25544
+ success: '#6a8759',
25545
+ warning: '#ffc66d',
25546
+ },
25547
+ eldritch: {
25548
+ accent: '#a48cf2',
25549
+ border: '#292e42',
25550
+ danger: '#f16c75',
25551
+ focusBorder: '#04d1f9',
25552
+ gitAdded: '#37f499',
25553
+ gitDeleted: '#f16c75',
25554
+ gitModified: '#f1fc79',
25555
+ info: '#04d1f9',
25556
+ muted: '#7081d0',
25557
+ selection: '#2d3052',
25558
+ success: '#37f499',
25559
+ warning: '#f1fc79',
25560
+ },
25561
+ 'edge-light': {
25562
+ accent: '#5079be',
25563
+ border: '#dde2e7',
25564
+ danger: '#d05858',
25565
+ focusBorder: '#3a8b84',
25566
+ gitAdded: '#608e32',
25567
+ gitDeleted: '#d05858',
25568
+ gitModified: '#be7e05',
25569
+ info: '#5079be',
25570
+ muted: '#a0a1a7',
25571
+ selection: '#e3e6eb',
25572
+ success: '#608e32',
25573
+ warning: '#be7e05',
25574
+ },
25575
+ zenbones: {
25576
+ accent: '#286486',
25577
+ border: '#cfd1d0',
25578
+ danger: '#a8334c',
25579
+ focusBorder: '#3b8992',
25580
+ gitAdded: '#4f6c31',
25581
+ gitDeleted: '#a8334c',
25582
+ gitModified: '#944927',
25583
+ info: '#286486',
25584
+ muted: '#a8a29e',
25585
+ selection: '#cbd9e3',
25586
+ success: '#4f6c31',
25587
+ warning: '#944927',
25588
+ },
25589
+ 'iceberg-light': {
25590
+ accent: '#2d539e',
25591
+ border: '#cad0de',
25592
+ danger: '#cc517a',
25593
+ focusBorder: '#3f83a6',
25594
+ gitAdded: '#668e3d',
25595
+ gitDeleted: '#cc517a',
25596
+ gitModified: '#c57339',
25597
+ info: '#2d539e',
25598
+ muted: '#8389a3',
25599
+ selection: '#c9cdd7',
25600
+ success: '#668e3d',
25601
+ warning: '#c57339',
25602
+ },
25603
+ 'github-dark-dimmed': {
25604
+ accent: '#539bf5',
25605
+ border: '#444c56',
25606
+ danger: '#f47067',
25607
+ focusBorder: '#39c5cf',
25608
+ gitAdded: '#57ab5a',
25609
+ gitDeleted: '#f47067',
25610
+ gitModified: '#c69026',
25611
+ info: '#539bf5',
25612
+ muted: '#636e7b',
25613
+ selection: '#2d333b',
25614
+ success: '#57ab5a',
25615
+ warning: '#c69026',
25616
+ },
25617
+ 'edge-dark': {
25618
+ accent: '#6cb6eb',
25619
+ border: '#414550',
25620
+ danger: '#ec7279',
25621
+ focusBorder: '#5dbbc1',
25622
+ gitAdded: '#a0c980',
25623
+ gitDeleted: '#ec7279',
25624
+ gitModified: '#deb974',
25625
+ info: '#6cb6eb',
25626
+ muted: '#758094',
25627
+ selection: '#3b3e48',
25628
+ success: '#a0c980',
25629
+ warning: '#deb974',
25630
+ },
25631
+ 'selenized-dark': {
25632
+ accent: '#4695f7',
25633
+ border: '#2d5b69',
25634
+ danger: '#fa5750',
25635
+ focusBorder: '#41c7b9',
25636
+ gitAdded: '#75b938',
25637
+ gitDeleted: '#fa5750',
25638
+ gitModified: '#dbb32d',
25639
+ info: '#4695f7',
25640
+ muted: '#72898f',
25641
+ selection: '#184956',
25642
+ success: '#75b938',
25643
+ warning: '#dbb32d',
25644
+ },
25645
+ 'selenized-black': {
25646
+ accent: '#368aeb',
25647
+ border: '#3b3b3b',
25648
+ danger: '#ed4a46',
25649
+ focusBorder: '#3fc5b7',
25650
+ gitAdded: '#70b433',
25651
+ gitDeleted: '#ed4a46',
25652
+ gitModified: '#dbb32d',
25653
+ info: '#368aeb',
25654
+ muted: '#777777',
25655
+ selection: '#252525',
25656
+ success: '#70b433',
25657
+ warning: '#dbb32d',
25658
+ },
25659
+ 'selenized-light': {
25660
+ accent: '#0072d4',
25661
+ border: '#d5cdb6',
25662
+ danger: '#d2212d',
25663
+ focusBorder: '#009c8f',
25664
+ gitAdded: '#489100',
25665
+ gitDeleted: '#d2212d',
25666
+ gitModified: '#ad8900',
25667
+ info: '#0072d4',
25668
+ muted: '#909995',
25669
+ selection: '#ece3cc',
25670
+ success: '#489100',
25671
+ warning: '#ad8900',
25672
+ },
25673
+ 'monokai-pro-machine': {
25674
+ accent: '#7cd5f1',
25675
+ border: '#1d2528',
25676
+ danger: '#ff6d7e',
25677
+ focusBorder: '#a2e57b',
25678
+ gitAdded: '#a2e57b',
25679
+ gitDeleted: '#ff6d7e',
25680
+ gitModified: '#ffed72',
25681
+ info: '#7cd5f1',
25682
+ muted: '#6b7678',
25683
+ selection: '#3a4449',
25684
+ success: '#a2e57b',
25685
+ warning: '#ffed72',
25686
+ },
25687
+ 'monokai-pro-octagon': {
25688
+ accent: '#9cd1bb',
25689
+ border: '#1e1f2b',
25690
+ danger: '#ff657a',
25691
+ focusBorder: '#bad761',
25692
+ gitAdded: '#bad761',
25693
+ gitDeleted: '#ff657a',
25694
+ gitModified: '#ffd76d',
25695
+ info: '#9cd1bb',
25696
+ muted: '#696d77',
25697
+ selection: '#3a3d4b',
25698
+ success: '#bad761',
25699
+ warning: '#ffd76d',
25700
+ },
25701
+ 'monokai-pro-ristretto': {
25702
+ accent: '#85dacc',
25703
+ border: '#211c1c',
25704
+ danger: '#fd6883',
25705
+ focusBorder: '#adda78',
25706
+ gitAdded: '#adda78',
25707
+ gitDeleted: '#fd6883',
25708
+ gitModified: '#f9cc6c',
25709
+ info: '#85dacc',
25710
+ muted: '#72696a',
25711
+ selection: '#403838',
25712
+ success: '#adda78',
25713
+ warning: '#f9cc6c',
25714
+ },
25715
+ 'monokai-pro-spectrum': {
25716
+ accent: '#5ad4e6',
25717
+ border: '#191919',
25718
+ danger: '#fc618d',
25719
+ focusBorder: '#7bd88f',
25720
+ gitAdded: '#7bd88f',
25721
+ gitDeleted: '#fc618d',
25722
+ gitModified: '#fce566',
25723
+ info: '#5ad4e6',
25724
+ muted: '#69676c',
25725
+ selection: '#363537',
25726
+ success: '#7bd88f',
25727
+ warning: '#fce566',
25728
+ },
25729
+ 'base16-default-dark': {
25730
+ accent: '#7cafc2',
25731
+ border: '#282828',
25732
+ danger: '#ab4642',
25733
+ focusBorder: '#86c1b9',
25734
+ gitAdded: '#a1b56c',
25735
+ gitDeleted: '#ab4642',
25736
+ gitModified: '#f7ca88',
25737
+ info: '#7cafc2',
25738
+ muted: '#585858',
25739
+ selection: '#383838',
25740
+ success: '#a1b56c',
25741
+ warning: '#f7ca88',
25742
+ },
25743
+ 'base16-default-light': {
25744
+ accent: '#7cafc2',
25745
+ border: '#e8e8e8',
25746
+ danger: '#ab4642',
25747
+ focusBorder: '#86c1b9',
25748
+ gitAdded: '#a1b56c',
25749
+ gitDeleted: '#ab4642',
25750
+ gitModified: '#dc9656',
25751
+ info: '#7cafc2',
25752
+ muted: '#b8b8b8',
25753
+ selection: '#d8d8d8',
25754
+ success: '#a1b56c',
25755
+ warning: '#dc9656',
25756
+ },
25757
+ tomorrow: {
25758
+ accent: '#4271ae',
25759
+ border: '#efefef',
25760
+ danger: '#c82829',
25761
+ focusBorder: '#3e999f',
25762
+ gitAdded: '#718c00',
25763
+ gitDeleted: '#c82829',
25764
+ gitModified: '#eab700',
25765
+ info: '#4271ae',
25766
+ muted: '#8e908c',
25767
+ selection: '#d6d6d6',
25768
+ success: '#718c00',
25769
+ warning: '#eab700',
25770
+ },
25771
+ tokyodark: {
25772
+ accent: '#a485dd',
25773
+ border: '#2a2c41',
25774
+ danger: '#ee6d85',
25775
+ focusBorder: '#38a89d',
25776
+ gitAdded: '#95c561',
25777
+ gitDeleted: '#ee6d85',
25778
+ gitModified: '#d7a65f',
25779
+ info: '#7199ee',
25780
+ muted: '#4a5057',
25781
+ selection: '#212234',
25782
+ success: '#95c561',
25783
+ warning: '#d7a65f',
25784
+ },
25785
+ 'spacemacs-dark': {
25786
+ accent: '#bc6ec5',
25787
+ border: '#5d4d7a',
25788
+ danger: '#f2241f',
25789
+ focusBorder: '#2d9574',
25790
+ gitAdded: '#67b11d',
25791
+ gitDeleted: '#f2241f',
25792
+ gitModified: '#b1951d',
25793
+ info: '#4f97d7',
25794
+ muted: '#6c6783',
25795
+ selection: '#444155',
25796
+ success: '#67b11d',
25797
+ warning: '#b1951d',
25798
+ },
25799
+ bamboo: {
25800
+ accent: '#8fb573',
25801
+ border: '#3a3d37',
25802
+ danger: '#e75a7c',
25803
+ focusBorder: '#70c2be',
25804
+ gitAdded: '#8fb573',
25805
+ gitDeleted: '#e75a7c',
25806
+ gitModified: '#dbb651',
25807
+ info: '#57a5e5',
25808
+ muted: '#838781',
25809
+ selection: '#383b35',
25810
+ success: '#8fb573',
25811
+ warning: '#dbb651',
25812
+ },
25813
+ citylights: {
25814
+ accent: '#5ec4ff',
25815
+ border: '#2f3a42',
25816
+ danger: '#e27e8d',
25817
+ focusBorder: '#70e1e8',
25818
+ gitAdded: '#54af83',
25819
+ gitDeleted: '#e27e8d',
25820
+ gitModified: '#ebda65',
25821
+ info: '#68a1f0',
25822
+ muted: '#41505e',
25823
+ selection: '#363c43',
25824
+ success: '#54af83',
25825
+ warning: '#ebda65',
25826
+ },
25827
+ 'oxocarbon-light': {
25828
+ accent: '#0f62fe',
25829
+ border: '#e0e0e0',
25830
+ danger: '#ee5396',
25831
+ focusBorder: '#08bdba',
25832
+ gitAdded: '#42be65',
25833
+ gitDeleted: '#ee5396',
25834
+ gitModified: '#ff6f00',
25835
+ info: '#0f62fe',
25836
+ muted: '#525252',
25837
+ selection: '#dde1e6',
25838
+ success: '#42be65',
25839
+ warning: '#ff6f00',
25840
+ },
24923
25841
  };
24924
25842
  /**
24925
25843
  * Ordered list of every selectable theme preset, for the `coco ui` theme
@@ -25308,7 +26226,6 @@ function withPushedView(state, value) {
25308
26226
  // persistence and pop-view restores the previous tab.
25309
26227
  sidebarTab: value === 'compose' || value === 'status' ? 'branches' : state.sidebarTab,
25310
26228
  worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
25311
- selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
25312
26229
  diffSource: value === 'diff' ? state.diffSource : undefined,
25313
26230
  stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
25314
26231
  compareHead: value === 'diff' ? state.compareHead : undefined,
@@ -25342,7 +26259,6 @@ function withPoppedView(state) {
25342
26259
  // returns the user to whatever they actually had open before.
25343
26260
  sidebarTab: state.userSidebarTab,
25344
26261
  worktreeDiffOffset: next === 'diff' ? state.worktreeDiffOffset : 0,
25345
- selectedWorktreeHunkIndex: next === 'diff' ? state.selectedWorktreeHunkIndex : 0,
25346
26262
  diffSource: next === 'diff' ? state.diffSource : undefined,
25347
26263
  stashDiffRef: next === 'diff' ? state.stashDiffRef : undefined,
25348
26264
  compareBase: wasOnDiff ? undefined : state.compareBase,
@@ -25471,7 +26387,6 @@ function withReplacedView(state, value) {
25471
26387
  activeView: value,
25472
26388
  viewStack,
25473
26389
  worktreeDiffOffset: value === 'diff' ? state.worktreeDiffOffset : 0,
25474
- selectedWorktreeHunkIndex: value === 'diff' ? state.selectedWorktreeHunkIndex : 0,
25475
26390
  diffSource: value === 'diff' ? state.diffSource : undefined,
25476
26391
  stashDiffRef: value === 'diff' ? state.stashDiffRef : undefined,
25477
26392
  compareHead: value === 'diff' ? state.compareHead : undefined,
@@ -25609,9 +26524,29 @@ function nextHunkOffset(currentOffset, hunkOffsets, delta) {
25609
26524
  const previousOffset = [...hunkOffsets].reverse().find((offset) => offset < currentOffset);
25610
26525
  return previousOffset === undefined ? currentOffset : previousOffset;
25611
26526
  }
25612
- function nextHunkIndex(currentOffset, hunkOffsets, delta) {
25613
- const offset = nextHunkOffset(currentOffset, hunkOffsets, delta);
25614
- return Math.max(0, hunkOffsets.indexOf(offset));
26527
+ /**
26528
+ * Which hunk the viewport is currently showing — the index of the last
26529
+ * hunk whose `@@` header offset is at or above the viewport top
26530
+ * (`offset`). This is the single source of truth for the worktree
26531
+ * staging diff's "current hunk" (#1179): deriving it from the scroll
26532
+ * position keeps the header, the in-body highlight, and `space`/`z`
26533
+ * (stage / revert) all pointed at the hunk you're actually looking at,
26534
+ * whether you got there by hunk-jump (↑/↓) or page-scroll (PgUp/PgDn).
26535
+ * The old `indexOf(landedOffset)` approach reset to hunk 0 whenever the
26536
+ * offset wasn't exactly on a boundary, and page-scroll never updated it
26537
+ * at all — so the indicator stuck at "1/N".
26538
+ */
26539
+ function hunkIndexAtOffset(offset, hunkOffsets) {
26540
+ let index = 0;
26541
+ for (let i = 0; i < hunkOffsets.length; i += 1) {
26542
+ if (hunkOffsets[i] <= offset) {
26543
+ index = i;
26544
+ }
26545
+ else {
26546
+ break;
26547
+ }
26548
+ }
26549
+ return index;
25615
26550
  }
25616
26551
  function getLogInkSidebarTabs() {
25617
26552
  return [...SIDEBAR_TABS];
@@ -25628,7 +26563,6 @@ function createLogInkState(rows, options = {}) {
25628
26563
  selectedIndex: 0,
25629
26564
  selectedFileIndex: 0,
25630
26565
  selectedWorktreeFileIndex: 0,
25631
- selectedWorktreeHunkIndex: 0,
25632
26566
  selectedBranchIndex: 0,
25633
26567
  selectedTagIndex: 0,
25634
26568
  selectedStashIndex: 0,
@@ -25829,7 +26763,6 @@ function applyLogInkAction(state, action) {
25829
26763
  return {
25830
26764
  ...next,
25831
26765
  selectedWorktreeFileIndex: clampIndex(state.selectedWorktreeFileIndex + action.delta, action.fileCount),
25832
- selectedWorktreeHunkIndex: 0,
25833
26766
  worktreeDiffOffset: 0,
25834
26767
  // Cursor moved to a real file row — drop header focus so the
25835
26768
  // file Enter handler (open diff) is what fires next.
@@ -25873,7 +26806,6 @@ function applyLogInkAction(state, action) {
25873
26806
  return {
25874
26807
  ...state,
25875
26808
  selectedWorktreeFileIndex: Math.max(0, action.targetIndex),
25876
- selectedWorktreeHunkIndex: 0,
25877
26809
  worktreeDiffOffset: 0,
25878
26810
  statusGroupHeaderFocused: false,
25879
26811
  pendingKey: undefined,
@@ -26113,16 +27045,19 @@ function applyLogInkAction(state, action) {
26113
27045
  pendingKey: undefined,
26114
27046
  };
26115
27047
  case 'pageWorktreeDiff':
27048
+ // The current staging hunk is derived from the scroll offset at
27049
+ // the read sites (#1185), so paging only moves the offset.
26116
27050
  return {
26117
27051
  ...state,
26118
27052
  worktreeDiffOffset: clampIndex(state.worktreeDiffOffset + action.delta, action.lineCount),
26119
27053
  pendingKey: undefined,
26120
27054
  };
26121
27055
  case 'jumpWorktreeHunk':
27056
+ // `[`/`]` move the offset onto the next/previous hunk header; the
27057
+ // current hunk is derived from that offset at the read sites.
26122
27058
  return {
26123
27059
  ...state,
26124
27060
  worktreeDiffOffset: nextHunkOffset(state.worktreeDiffOffset, action.hunkOffsets, action.delta),
26125
- selectedWorktreeHunkIndex: nextHunkIndex(state.worktreeDiffOffset, action.hunkOffsets, action.delta),
26126
27061
  pendingKey: undefined,
26127
27062
  };
26128
27063
  case 'jumpCommitDiffHunk':
@@ -26167,7 +27102,6 @@ function applyLogInkAction(state, action) {
26167
27102
  activeView: HOME_VIEW,
26168
27103
  viewStack: [HOME_VIEW],
26169
27104
  worktreeDiffOffset: 0,
26170
- selectedWorktreeHunkIndex: 0,
26171
27105
  pendingCommitFocused: false,
26172
27106
  pendingKey: undefined,
26173
27107
  };
@@ -26206,7 +27140,6 @@ function applyLogInkAction(state, action) {
26206
27140
  return {
26207
27141
  ...next,
26208
27142
  selectedWorktreeFileIndex: Math.max(0, action.fileIndex),
26209
- selectedWorktreeHunkIndex: 0,
26210
27143
  worktreeDiffOffset: 0,
26211
27144
  diffSource: 'worktree',
26212
27145
  };
@@ -26256,7 +27189,6 @@ function applyLogInkAction(state, action) {
26256
27189
  return {
26257
27190
  ...next,
26258
27191
  selectedWorktreeFileIndex: Math.max(0, action.fileIndex),
26259
- selectedWorktreeHunkIndex: 0,
26260
27192
  worktreeDiffOffset: 0,
26261
27193
  };
26262
27194
  }
@@ -26370,6 +27302,8 @@ function applyLogInkAction(state, action) {
26370
27302
  };
26371
27303
  case 'setWorktreeCheckoutConflict':
26372
27304
  return { ...state, worktreeCheckoutConflict: action.value, pendingKey: undefined };
27305
+ case 'setPendingChoice':
27306
+ return { ...state, pendingChoice: action.value, pendingKey: undefined };
26373
27307
  case 'setPendingMutationConfirmation':
26374
27308
  return {
26375
27309
  ...state,
@@ -27741,46 +28675,48 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
27741
28675
  }
27742
28676
  return [];
27743
28677
  }
27744
- if (state.pendingConfirmationId) {
27745
- // Worktree-conflict removal options (#1175): alongside the y-switch,
27746
- // `r` removes the conflicting worktree and checks the branch out
27747
- // here, `x` removes the worktree AND deletes the branch. Both defer
27748
- // to the runtime (it owns the git ops + the conflict context); the
27749
- // runtime clears the conflict state once it resolves.
27750
- if (state.pendingConfirmationId === 'switch-to-conflicting-worktree' && state.worktreeCheckoutConflict) {
27751
- if (inputValue === 'r') {
28678
+ // Multi-option prompt (#1181) — the n-way generalization of the y/n
28679
+ // confirmation. Match the keypress against the prompt's options; each
28680
+ // either runs a workflow or fires a built-in navigation intent.
28681
+ if (state.pendingChoice) {
28682
+ const option = state.pendingChoice.options.find((opt) => opt.key === inputValue);
28683
+ if (option) {
28684
+ // `switch-worktree` is pure navigation — open the worktree as a
28685
+ // nested repo frame. Handled here rather than via the workflow
28686
+ // runner, whose post-action context refresh would mis-target the
28687
+ // frame we just pushed.
28688
+ if (option.intent === 'switch-worktree' && state.worktreeCheckoutConflict) {
28689
+ const conflict = state.worktreeCheckoutConflict;
27752
28690
  return [
27753
- { type: 'runWorkflowAction', id: 'conflict-remove-worktree-checkout' },
27754
- action({ type: 'setPendingConfirmation', value: undefined }),
28691
+ action({ type: 'pushRepoFrame', label: conflict.branch, workdir: conflict.worktreePath }),
28692
+ action({ type: 'setStatus', value: `Switched to worktree ${conflict.worktreePath} (${conflict.branch})` }),
28693
+ action({ type: 'setPendingChoice', value: undefined }),
28694
+ action({ type: 'setWorktreeCheckoutConflict', value: undefined }),
27755
28695
  ];
27756
28696
  }
27757
- if (inputValue === 'x') {
28697
+ if (option.workflowId) {
28698
+ // The workflow runner owns the live context + clears any
28699
+ // conflict state once it resolves.
27758
28700
  return [
27759
- { type: 'runWorkflowAction', id: 'conflict-remove-worktree-branch' },
27760
- action({ type: 'setPendingConfirmation', value: undefined }),
28701
+ { type: 'runWorkflowAction', id: option.workflowId },
28702
+ action({ type: 'setPendingChoice', value: undefined }),
27761
28703
  ];
27762
28704
  }
28705
+ return [action({ type: 'setPendingChoice', value: undefined })];
27763
28706
  }
28707
+ if (inputValue === 'n' || key.escape) {
28708
+ return [
28709
+ action({ type: 'setPendingChoice', value: undefined }),
28710
+ ...(state.worktreeCheckoutConflict
28711
+ ? [action({ type: 'setWorktreeCheckoutConflict', value: undefined })]
28712
+ : []),
28713
+ action({ type: 'setStatus', value: 'cancelled' }),
28714
+ ];
28715
+ }
28716
+ return [];
28717
+ }
28718
+ if (state.pendingConfirmationId) {
27764
28719
  if (inputValue === 'y') {
27765
- // Worktree-conflict switch (#1175): the branch is already checked
27766
- // out elsewhere, so "switch" just opens that worktree as a nested
27767
- // repo frame (same mechanism as drilling into a submodule) — no
27768
- // git mutation, hence handled here rather than via the runtime.
27769
- if (state.pendingConfirmationId === 'switch-to-conflicting-worktree') {
27770
- const conflict = state.worktreeCheckoutConflict;
27771
- if (conflict) {
27772
- return [
27773
- action({ type: 'pushRepoFrame', label: conflict.branch, workdir: conflict.worktreePath }),
27774
- action({ type: 'setStatus', value: `Switched to worktree ${conflict.worktreePath} (${conflict.branch})` }),
27775
- action({ type: 'setPendingConfirmation', value: undefined }),
27776
- action({ type: 'setWorktreeCheckoutConflict', value: undefined }),
27777
- ];
27778
- }
27779
- return [
27780
- action({ type: 'setPendingConfirmation', value: undefined }),
27781
- action({ type: 'setWorktreeCheckoutConflict', value: undefined }),
27782
- ];
27783
- }
27784
28720
  const workflowAction = getLogInkWorkflowActionById(state.pendingConfirmationId);
27785
28721
  if (workflowAction?.id === 'ai-commit-summary') {
27786
28722
  return [
@@ -27806,11 +28742,6 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
27806
28742
  if (inputValue === 'n' || key.escape) {
27807
28743
  return [
27808
28744
  action({ type: 'setPendingConfirmation', value: undefined }),
27809
- // Drop any worktree-conflict context so the prompt doesn't
27810
- // linger after the user declines to switch.
27811
- ...(state.worktreeCheckoutConflict
27812
- ? [action({ type: 'setWorktreeCheckoutConflict', value: undefined })]
27813
- : []),
27814
28745
  action({ type: 'setStatus', value: 'workflow action cancelled' }),
27815
28746
  ];
27816
28747
  }
@@ -28657,22 +29588,16 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28657
29588
  fileCount: context.worktreeFileCount,
28658
29589
  })];
28659
29590
  }
28660
- // Worktree (staging) diff: ↑/↓ move between hunks the hunk is the
28661
- // unit you stage, so the cursor walks hunks (auto-scrolling to the
28662
- // selected one). Single-hunk files fall through to line-scroll so a
28663
- // long lone hunk stays readable; `[`/`]` remain hunk-jump aliases.
28664
- if (state.activeView === 'diff' && (context.worktreeHunkOffsets?.length ?? 0) > 1) {
28665
- return [action({
28666
- type: 'jumpWorktreeHunk',
28667
- delta: -1,
28668
- hunkOffsets: context.worktreeHunkOffsets,
28669
- })];
28670
- }
29591
+ // Worktree (staging) diff: ↑/↓ scroll linesconsistent with the
29592
+ // commit / stash diffs (#1185). `[`/`]` jump between hunks (the
29593
+ // staging unit), and the current hunk is derived from the scroll
29594
+ // position, so line-scrolling still walks the staging target.
28671
29595
  if (state.activeView === 'diff' && context.worktreeDiffLineCount) {
28672
29596
  return [action({
28673
29597
  type: 'pageWorktreeDiff',
28674
29598
  delta: -1,
28675
29599
  lineCount: context.worktreeDiffLineCount,
29600
+ hunkOffsets: context.worktreeHunkOffsets,
28676
29601
  })];
28677
29602
  }
28678
29603
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -28782,20 +29707,14 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28782
29707
  fileCount: context.worktreeFileCount,
28783
29708
  })];
28784
29709
  }
28785
- // Worktree (staging) diff: ↓ walks to the next hunk (see the ↑
28786
- // handler). Multi-hunk only; single-hunk files line-scroll.
28787
- if (state.activeView === 'diff' && (context.worktreeHunkOffsets?.length ?? 0) > 1) {
28788
- return [action({
28789
- type: 'jumpWorktreeHunk',
28790
- delta: 1,
28791
- hunkOffsets: context.worktreeHunkOffsets,
28792
- })];
28793
- }
29710
+ // Worktree (staging) diff: ↓ scrolls lines (see the ↑ handler) —
29711
+ // `[`/`]` jump hunks (#1185).
28794
29712
  if (state.activeView === 'diff' && context.worktreeDiffLineCount) {
28795
29713
  return [action({
28796
29714
  type: 'pageWorktreeDiff',
28797
29715
  delta: 1,
28798
29716
  lineCount: context.worktreeDiffLineCount,
29717
+ hunkOffsets: context.worktreeHunkOffsets,
28799
29718
  })];
28800
29719
  }
28801
29720
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -28855,6 +29774,7 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28855
29774
  type: 'pageWorktreeDiff',
28856
29775
  delta: -8,
28857
29776
  lineCount: context.worktreeDiffLineCount,
29777
+ hunkOffsets: context.worktreeHunkOffsets,
28858
29778
  })];
28859
29779
  }
28860
29780
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -28879,6 +29799,7 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
28879
29799
  type: 'pageWorktreeDiff',
28880
29800
  delta: 8,
28881
29801
  lineCount: context.worktreeDiffLineCount,
29802
+ hunkOffsets: context.worktreeHunkOffsets,
28882
29803
  })];
28883
29804
  }
28884
29805
  if (state.activeView === 'diff' && context.previewLineCount) {
@@ -33402,6 +34323,7 @@ function renderFooter$1(h, components, state, context, theme, idleTip, spinnerFr
33402
34323
  state.gitignorePicker ||
33403
34324
  state.inputPrompt ||
33404
34325
  state.pendingConfirmationId ||
34326
+ state.pendingChoice ||
33405
34327
  state.pendingMutationConfirmation ||
33406
34328
  state.pendingKey ||
33407
34329
  state.filterMode);
@@ -36209,7 +37131,7 @@ function renderDiffSurface(ctx, diff) {
36209
37131
  ? []
36210
37132
  : splitActive
36211
37133
  ? renderSplitDiffBody(h, components, previewHunks, state.diffPreviewOffset, visibleRows, width, theme, 'commit-diff-split', syntaxSpans)
36212
- : visiblePreviewHunks.map((line, index) => renderDiffLine(h, Text, line, theme, syntaxSpans, 140, `diff-surface-line-${state.diffPreviewOffset + index}`));
37134
+ : visiblePreviewHunks.map((line, index) => renderDiffLine(h, Text, line, theme, syntaxSpans, Math.max(8, width - 5), `diff-surface-line-${state.diffPreviewOffset + index}`));
36213
37135
  return h(Box, {
36214
37136
  borderColor: focusBorderColor(theme, focused),
36215
37137
  borderStyle: theme.borderStyle,
@@ -36220,25 +37142,38 @@ function renderDiffSurface(ctx, diff) {
36220
37142
  }, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle(splitActive ? 'Diff (split)' : 'Diff', focused)), h(Text, { dimColor: true }, selectedDetailFile?.path || 'no file')), ...headerLines.map((line, index) => h(Text, {
36221
37143
  key: `diff-surface-header-${index}`,
36222
37144
  dimColor: index > 0,
36223
- }, truncateCells(line, 140))), ...commitBodyNodes);
37145
+ }, truncateCells(line, Math.max(20, width - 4)))), ...commitBodyNodes);
36224
37146
  }
36225
37147
  const diffLines = worktreeDiff?.lines || [];
36226
- const selectedHunk = worktreeHunks?.hunks[state.selectedWorktreeHunkIndex];
36227
37148
  const totalHunks = worktreeHunks?.hunks.length ?? 0;
36228
37149
  const stagedHunks = worktreeHunks?.hunks.filter((hunk) => hunk.state === 'staged').length ?? 0;
37150
+ // The "current" hunk is derived from the scroll position (#1185) —
37151
+ // the single source of truth is `worktreeDiffOffset`. ↑/↓ scroll
37152
+ // lines and `[`/`]` jump hunks; either way the header, rail, and
37153
+ // stage/revert target all follow what's on screen.
37154
+ const currentHunkIndex = hunkIndexAtOffset(state.worktreeDiffOffset, worktreeDiff?.hunkOffsets ?? []);
36229
37155
  const visibleDiffLines = diffLines.slice(state.worktreeDiffOffset, state.worktreeDiffOffset + visibleRows);
36230
- // Hunk-position line: badge + selected hunk's state + a staged/total
36231
- // progress count, so the user always sees how far through staging they
36232
- // are. Untracked/new files have no hunks point them at whole-file
36233
- // staging instead of a dead-end "no hunks" message.
37156
+ // Hunk-position line: `Hunk n/N` + an at-a-glance staging rail + a
37157
+ // staged/total count, so the user sees how far through staging they
37158
+ // are without reading each hunk. The rail shows one marker per hunk —
37159
+ // filled = staged, hollow = unstaged with the current hunk bracketed
37160
+ // (which also conveys whether the current hunk is staged, replacing
37161
+ // the old standalone "● staged / ○ unstaged" badge). Untracked/new
37162
+ // files have no hunks — point them at whole-file staging instead of a
37163
+ // dead-end "no hunks" message.
37164
+ const railMarker = (staged) => staged ? (theme.ascii ? 'x' : '●') : (theme.ascii ? '.' : '○');
37165
+ const hunkRail = (worktreeHunks?.hunks ?? [])
37166
+ .map((hunk, index) => {
37167
+ const marker = railMarker(hunk.state === 'staged');
37168
+ return index === currentHunkIndex ? `[${marker}]` : marker;
37169
+ })
37170
+ .join('');
36234
37171
  const hunkHeaderLine = worktreeHunksLoading
36235
37172
  ? 'Hunks loading…'
36236
37173
  : worktreeDiff?.untracked
36237
37174
  ? (theme.ascii ? 'New file — press space to stage it whole.' : '✚ New file — press space to stage it whole.')
36238
37175
  : totalHunks
36239
- ? `Hunk ${state.selectedWorktreeHunkIndex + 1}/${totalHunks} · ${selectedHunk?.state === 'staged'
36240
- ? (theme.ascii ? '[x] staged' : '● staged')
36241
- : (theme.ascii ? '[ ] unstaged' : '○ unstaged')} · ${stagedHunks}/${totalHunks} staged`
37176
+ ? `Hunk ${currentHunkIndex + 1}/${totalHunks} ${hunkRail} ${stagedHunks}/${totalHunks} staged`
36242
37177
  : 'No stageable hunks for this file.';
36243
37178
  const headerLines = isLogInkContextKeyLoading(contextStatus, 'worktree')
36244
37179
  ? ['Loading file context...']
@@ -36270,7 +37205,7 @@ function renderDiffSurface(ctx, diff) {
36270
37205
  h(Text, { dimColor: true }, worktreeDiff?.filePath || worktreeFile?.path || 'no file')), ...headerLines.map((line, index) => h(Text, {
36271
37206
  key: `diff-surface-header-${index}`,
36272
37207
  dimColor: index > 0,
36273
- }, truncateCells(line, 140))), ...(showDiffLines
37208
+ }, truncateCells(line, Math.max(20, width - 4)))), ...(showDiffLines
36274
37209
  ? renderWorktreeDiffBody(h, components, {
36275
37210
  lines: diffLines,
36276
37211
  offset: state.worktreeDiffOffset,
@@ -36280,7 +37215,7 @@ function renderDiffSurface(ctx, diff) {
36280
37215
  syntaxSpans,
36281
37216
  hunkOffsets: worktreeDiff?.hunkOffsets || [],
36282
37217
  hunks: worktreeHunks?.hunks || [],
36283
- selectedIndex: state.selectedWorktreeHunkIndex,
37218
+ selectedIndex: currentHunkIndex,
36284
37219
  keyPrefix: 'diff-surface-line',
36285
37220
  })
36286
37221
  : []));
@@ -37685,36 +38620,45 @@ function renderConfirmationPanel(h, components, state, width, theme, focused) {
37685
38620
  : state.pendingMutationConfirmation === 'discard-draft'
37686
38621
  ? 'Quit and discard the in-progress commit draft'
37687
38622
  : undefined;
37688
- // Worktree-conflict switch (#1175): a checkout was rejected because
37689
- // the branch is checked out elsewhere — name the branch + worktree so
37690
- // the prompt explains what "y" does (jump into that worktree).
37691
- const conflict = state.worktreeCheckoutConflict;
37692
- const isWorktreeConflict = state.pendingConfirmationId === 'switch-to-conflicting-worktree';
37693
- const label = isWorktreeConflict && conflict
37694
- ? `Switch to the worktree where '${conflict.branch}' is checked out?`
37695
- : action?.label || mutationLabel || 'Workflow action';
38623
+ const label = action?.label || mutationLabel || 'Workflow action';
37696
38624
  const warning = state.pendingMutationConfirmation === 'discard-draft'
37697
38625
  ? 'You have an unsaved commit draft. Press y to discard it and quit.'
37698
38626
  : state.pendingMutationConfirmation
37699
38627
  ? 'This discards local changes and cannot be undone by Coco.'
37700
- : isWorktreeConflict && conflict
37701
- ? `'${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.' : ''}`
37702
- // Second-stage confirm raised when a safe delete hit an unmerged
37703
- // branch name the reason so the force isn't a blind "y again".
37704
- : state.pendingConfirmationId === 'force-delete-branch'
37705
- ? 'Not fully merged. Force-delete (git branch -D) is irreversible.'
37706
- : action?.kind === 'ai'
37707
- ? `AI action requires confirmation. Estimated ${action.estimatedTokens || '<unknown>'} tokens.`
37708
- : 'Destructive Git action requires confirmation.';
38628
+ // Second-stage confirm raised when a safe delete hit an unmerged
38629
+ // branch name the reason so the force isn't a blind "y again".
38630
+ : state.pendingConfirmationId === 'force-delete-branch'
38631
+ ? 'Not fully merged. Force-delete (git branch -D) is irreversible.'
38632
+ : action?.kind === 'ai'
38633
+ ? `AI action requires confirmation. Estimated ${action.estimatedTokens || '<unknown>'} tokens.`
38634
+ : 'Destructive Git action requires confirmation.';
37709
38635
  return h(Box, {
37710
38636
  borderColor: focusBorderColor(theme, focused),
37711
38637
  borderStyle: theme.borderStyle,
37712
38638
  flexDirection: 'column',
37713
38639
  width,
37714
38640
  paddingX: 1,
37715
- }, 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
37716
- ? 'y switch · r remove worktree & check out here · x remove worktree & delete branch · n/Esc cancel'
37717
- : 'Press y to confirm or n/Esc to cancel.', width - 4)));
38641
+ }, h(Text, { bold: true }, panelTitle('Confirm', focused)), h(Text, undefined, truncateCells(label, width - 4)), h(Text, { dimColor: true }, truncateCells(warning, width - 4)), h(Text, undefined, ''), h(Text, undefined, 'Press y to confirm or n/Esc to cancel.'));
38642
+ }
38643
+ /**
38644
+ * Multi-option prompt panel (#1181) — the n-way generalization of the
38645
+ * confirmation panel. Renders the prompt title, an optional warning, and
38646
+ * one row per option (`<key> <label>`, destructive options in the
38647
+ * danger colour), plus a cancel hint. Resolution happens in the input
38648
+ * layer by matching a keypress against the option keys.
38649
+ */
38650
+ function renderChoicePanel(h, components, prompt, width, theme, focused) {
38651
+ const { Box, Text } = components;
38652
+ return h(Box, {
38653
+ borderColor: focusBorderColor(theme, focused),
38654
+ borderStyle: theme.borderStyle,
38655
+ flexDirection: 'column',
38656
+ width,
38657
+ paddingX: 1,
38658
+ }, h(Text, { bold: true }, panelTitle('Choose', focused)), h(Text, undefined, truncateCells(prompt.title, width - 4)), ...(prompt.warning ? [h(Text, { dimColor: true }, truncateCells(prompt.warning, width - 4))] : []), h(Text, undefined, ''), ...prompt.options.map((option) => h(Text, {
38659
+ key: `choice-${prompt.id}-${option.key}`,
38660
+ color: option.destructive && !theme.noColor ? theme.colors.danger : undefined,
38661
+ }, truncateCells(` ${option.key} ${option.label}`, width - 4))), h(Text, { dimColor: true }, ' n/Esc cancel'));
37718
38662
  }
37719
38663
  /**
37720
38664
  * First-launch onboarding overlay (P1.3). Shown once per machine, gated
@@ -38107,9 +39051,20 @@ function renderSplitPlanOverlay(h, components, state, width, bodyRows, theme, fo
38107
39051
  // overlay rather than crashing.
38108
39052
  return h(Box, { width }, h(Text, { dimColor: true }, 'No plan data available.'));
38109
39053
  }
39054
+ // Committed groups are numbered 1..N; an `unclaimed` group (the files
39055
+ // the split couldn't place) renders as a distinct "will stay" note
39056
+ // rather than a phantom commit (#1180).
39057
+ const committedGroups = plan.groups.filter((group) => !group.unclaimed);
38110
39058
  const lines = [];
38111
- plan.groups.forEach((group, index) => {
38112
- lines.push(`▎ ${index + 1}. ${group.title}`);
39059
+ let commitNumber = 0;
39060
+ plan.groups.forEach((group) => {
39061
+ if (group.unclaimed) {
39062
+ lines.push(`⚠ ${group.title} (stays in your worktree — not committed)`);
39063
+ }
39064
+ else {
39065
+ commitNumber += 1;
39066
+ lines.push(`▎ ${commitNumber}. ${group.title}`);
39067
+ }
38113
39068
  if (group.body) {
38114
39069
  group.body.split('\n').forEach((bodyLine) => lines.push(` ${bodyLine}`));
38115
39070
  }
@@ -38134,9 +39089,10 @@ function renderSplitPlanOverlay(h, components, state, width, bodyRows, theme, fo
38134
39089
  const totalLines = lines.length;
38135
39090
  const scrollOffset = Math.min(overlay.scrollOffset, Math.max(0, totalLines - 1));
38136
39091
  const visible = lines.slice(scrollOffset, scrollOffset + listRows);
39092
+ const unclaimedCount = plan.groups.length - committedGroups.length;
38137
39093
  const headerRight = overlay.status === 'applying'
38138
39094
  ? `${spinner} applying…`
38139
- : `${plan.groups.length} commit(s) · ${scrollOffset + 1}–${Math.min(totalLines, scrollOffset + listRows)} / ${totalLines}`;
39095
+ : `${committedGroups.length} commit(s)${unclaimedCount ? ' · 1 set stays staged' : ''} · ${scrollOffset + 1}–${Math.min(totalLines, scrollOffset + listRows)} / ${totalLines}`;
38140
39096
  // Apply errors get the full available width — long validator
38141
39097
  // messages (the failure path that surfaced in PR #916 testing was
38142
39098
  // "unknown hunks: src/widgets/button.ts::hunk-1, ...") frequently
@@ -40610,6 +41566,9 @@ function renderDetailPanel(h, components, state, context, contextStatus, detail,
40610
41566
  if (state.inputPrompt) {
40611
41567
  return renderInputPromptPanel(h, components, state, width, theme, focused);
40612
41568
  }
41569
+ if (state.pendingChoice) {
41570
+ return renderChoicePanel(h, components, state.pendingChoice, width, theme, focused);
41571
+ }
40613
41572
  if (state.pendingConfirmationId || state.pendingMutationConfirmation) {
40614
41573
  return renderConfirmationPanel(h, components, state, width, theme, focused);
40615
41574
  }
@@ -41463,7 +42422,7 @@ function LogInkApp(deps) {
41463
42422
  React.useEffect(() => {
41464
42423
  if (!state.statusMessage)
41465
42424
  return;
41466
- if (state.inputPrompt || state.pendingConfirmationId || state.pendingMutationConfirmation || state.showCommandPalette) {
42425
+ if (state.inputPrompt || state.pendingConfirmationId || state.pendingChoice || state.pendingMutationConfirmation || state.showCommandPalette) {
41467
42426
  return;
41468
42427
  }
41469
42428
  // The `setTimeout` callback is a literal arrow function (not a
@@ -41480,6 +42439,7 @@ function LogInkApp(deps) {
41480
42439
  dispatch,
41481
42440
  state.inputPrompt,
41482
42441
  state.pendingConfirmationId,
42442
+ state.pendingChoice,
41483
42443
  state.pendingMutationConfirmation,
41484
42444
  state.showCommandPalette,
41485
42445
  state.statusMessage,
@@ -42343,7 +43303,9 @@ function LogInkApp(deps) {
42343
43303
  setWorktreeHunks(undefined);
42344
43304
  }, [dispatch, git, refreshWorktreeContext, selectedWorktreeFile]);
42345
43305
  const toggleSelectedHunkStage = React.useCallback(async () => {
42346
- const selectedHunk = worktreeHunks?.hunks[state.selectedWorktreeHunkIndex];
43306
+ // The staging target is the hunk under the viewport (#1185) —
43307
+ // derived from the scroll offset, the single source of truth.
43308
+ const selectedHunk = worktreeHunks?.hunks[hunkIndexAtOffset(state.worktreeDiffOffset, worktreeDiff?.hunkOffsets ?? [])];
42347
43309
  if (!selectedHunk) {
42348
43310
  dispatch({ type: 'setStatus', value: 'no hunk selected', kind: 'warning' });
42349
43311
  return;
@@ -42372,7 +43334,7 @@ function LogInkApp(deps) {
42372
43334
  kind: 'error',
42373
43335
  });
42374
43336
  }
42375
- }, [dispatch, git, refreshWorktreeContext, state.selectedWorktreeHunkIndex, worktreeHunks]);
43337
+ }, [dispatch, git, refreshWorktreeContext, state.worktreeDiffOffset, worktreeDiff, worktreeHunks]);
42376
43338
  const revertSelectedFile = React.useCallback(async () => {
42377
43339
  if (!selectedWorktreeFile) {
42378
43340
  dispatch({ type: 'setStatus', value: 'no worktree file selected', kind: 'warning' });
@@ -42386,7 +43348,7 @@ function LogInkApp(deps) {
42386
43348
  setWorktreeHunks(undefined);
42387
43349
  }, [dispatch, git, refreshWorktreeContext, selectedWorktreeFile]);
42388
43350
  const revertSelectedHunk = React.useCallback(async () => {
42389
- const selectedHunk = worktreeHunks?.hunks[state.selectedWorktreeHunkIndex];
43351
+ const selectedHunk = worktreeHunks?.hunks[hunkIndexAtOffset(state.worktreeDiffOffset, worktreeDiff?.hunkOffsets ?? [])];
42390
43352
  if (!selectedHunk) {
42391
43353
  dispatch({ type: 'setStatus', value: 'no hunk selected', kind: 'warning' });
42392
43354
  return;
@@ -42406,7 +43368,7 @@ function LogInkApp(deps) {
42406
43368
  kind: 'error',
42407
43369
  });
42408
43370
  }
42409
- }, [dispatch, git, refreshWorktreeContext, state.selectedWorktreeHunkIndex, worktreeHunks]);
43371
+ }, [dispatch, git, refreshWorktreeContext, state.worktreeDiffOffset, worktreeDiff, worktreeHunks]);
42410
43372
  const createCommitFromCompose = React.useCallback(async () => {
42411
43373
  const stagedCount = context.worktree?.stagedCount || 0;
42412
43374
  if (!stagedCount) {
@@ -43288,9 +44250,19 @@ function LogInkApp(deps) {
43288
44250
  // navigateHome nukes the rest of the stack so `<` after apply
43289
44251
  // doesn't walk back into the now-empty compose / status state
43290
44252
  // the user just left behind.
44253
+ // Did the plan leave files for the user (the `unclaimed` group the
44254
+ // split couldn't confidently place)? They're now sitting unstaged in
44255
+ // the worktree, so land on status — not history — so the user sees
44256
+ // and handles them, rather than dropping them on a clean-looking
44257
+ // history view (#1180).
44258
+ const unclaimedGroup = splitPlan.plan.groups.find((group) => group.unclaimed);
44259
+ const unclaimedFileCount = unclaimedGroup?.files?.length ?? 0;
43291
44260
  dispatch({ type: 'clearSplitPlan' });
43292
44261
  dispatch({ type: 'commitCompose', action: { type: 'reset' } });
43293
44262
  dispatch({ type: 'navigateHome' });
44263
+ if (unclaimedFileCount > 0) {
44264
+ dispatch({ type: 'pushView', value: 'status' });
44265
+ }
43294
44266
  // Refresh BEFORE setting the final status so we can peek at the
43295
44267
  // post-apply worktree state and craft a directive next-step hint
43296
44268
  // ("X unstaged + Y untracked remaining — press gs to stage / I
@@ -43340,12 +44312,17 @@ function LogInkApp(deps) {
43340
44312
  return;
43341
44313
  }
43342
44314
  const successMessage = formatSplitApplySuccess(commitHashes.length, unstaged, untracked, result.fallback ? { reason: result.fallback.reason } : undefined);
44315
+ // Name the files the split deliberately left behind so the jump to
44316
+ // status reads as intentional, not a surprise (#1180).
44317
+ const unclaimedNote = unclaimedFileCount > 0
44318
+ ? ` · ${unclaimedFileCount} file${unclaimedFileCount === 1 ? '' : 's'} left for you on status`
44319
+ : '';
43343
44320
  // Fallback path uses 'info' kind — apply technically succeeded
43344
44321
  // but the user should know it landed as a single combined commit
43345
44322
  // rather than a real LLM-driven multi-group split.
43346
44323
  dispatch({
43347
44324
  type: 'setStatus',
43348
- value: successMessage,
44325
+ value: `${successMessage}${unclaimedNote}`,
43349
44326
  kind: result.fallback ? 'info' : 'success',
43350
44327
  });
43351
44328
  }, [dispatch, git, refreshContext, refreshHistoryRows, refreshWorktreeContext, state.splitPlan]);
@@ -44329,18 +45306,31 @@ function LogInkApp(deps) {
44329
45306
  // Checking out a branch that's already checked out in another
44330
45307
  // worktree is rejected by git ("already checked out at <path>").
44331
45308
  // Rather than dead-end on that, capture the conflict and raise a
44332
- // y-confirm offering to switch into that worktree the branch IS
44333
- // checked out, just elsewhere (#1175).
45309
+ // multi-option prompt: switch into that worktree, remove it and
45310
+ // check out here, or remove it and delete the branch (#1175, #1181).
44334
45311
  if (id === 'checkout-branch' && !result?.ok && isBranchCheckedOutElsewhereError(result?.message)) {
44335
45312
  const worktreePath = parseCheckedOutWorktreePath(result?.message);
44336
45313
  const branchName = pendingItemAction?.id;
44337
45314
  if (worktreePath && branchName) {
44338
45315
  const worktree = context.worktreeList?.worktrees?.find((w) => w.path === worktreePath);
45316
+ const dirty = worktree?.dirty ?? false;
44339
45317
  dispatch({
44340
45318
  type: 'setWorktreeCheckoutConflict',
44341
- value: { branch: branchName, worktreePath, dirty: worktree?.dirty ?? false },
45319
+ value: { branch: branchName, worktreePath, dirty },
45320
+ });
45321
+ dispatch({
45322
+ type: 'setPendingChoice',
45323
+ value: {
45324
+ id: 'worktree-checkout-conflict',
45325
+ title: `'${branchName}' is checked out in another worktree`,
45326
+ warning: `Checked out at ${worktreePath}.${dirty ? ' That worktree has uncommitted changes — removal will be refused until it is clean or stashed.' : ''}`,
45327
+ options: [
45328
+ { key: 'y', label: 'Switch to that worktree', intent: 'switch-worktree' },
45329
+ { key: 'r', label: 'Remove worktree & check out here', workflowId: 'conflict-remove-worktree-checkout', destructive: true },
45330
+ { key: 'x', label: 'Remove worktree & delete branch', workflowId: 'conflict-remove-worktree-branch', destructive: true },
45331
+ ],
45332
+ },
44342
45333
  });
44343
- dispatch({ type: 'setPendingConfirmation', value: 'switch-to-conflicting-worktree' });
44344
45334
  }
44345
45335
  else {
44346
45336
  dispatch({
@@ -45276,6 +46266,7 @@ function LogInkApp(deps) {
45276
46266
  state.gitignorePicker ||
45277
46267
  state.inputPrompt ||
45278
46268
  state.pendingConfirmationId ||
46269
+ state.pendingChoice ||
45279
46270
  state.pendingMutationConfirmation ||
45280
46271
  state.pendingKey
45281
46272
  ? 'inspector'
@@ -46983,7 +47974,9 @@ const options$1 = {
46983
47974
  },
46984
47975
  theme: {
46985
47976
  description: 'TUI theme preset',
46986
- choices: ['default', 'monochrome', 'catppuccin', 'gruvbox', 'dracula', 'nord', 'solarized-dark', 'tokyo-night', 'one-dark', 'rose-pine', 'kanagawa', 'everforest', 'monokai', 'synthwave', 'ayu-dark', 'palenight', 'github-dark', 'horizon', 'nightfox', 'carbonfox', 'tokyonight-storm', 'catppuccin-latte', 'solarized-light', 'github-light', 'iceberg', 'material-ocean', 'moonlight', 'poimandres', 'vitesse-dark', 'vesper', 'flexoki', 'mellow', 'night-owl', 'cobalt2', 'oceanic-next', 'catppuccin-macchiato', 'gruvbox-light', 'tokyo-night-day', 'one-light', 'ayu-light', 'rose-pine-dawn', 'everforest-light', 'vitesse-light', 'dayfox', 'night-owl-light', 'flexoki-light', 'material-lighter', 'papercolor-light', 'modus-operandi', 'quiet-light'],
47977
+ // Derived from the single source of truth (`THEME_PRESET_COLORS`) so the
47978
+ // CLI choices can never drift from the themes the picker actually offers.
47979
+ choices: getLogInkThemePresets(),
46987
47980
  },
46988
47981
  };
46989
47982
  const builder$1 = (yargs) => {
@@ -47011,7 +48004,9 @@ const options = {
47011
48004
  },
47012
48005
  theme: {
47013
48006
  description: 'TUI theme preset',
47014
- choices: ['default', 'monochrome', 'catppuccin', 'gruvbox', 'dracula', 'nord', 'solarized-dark', 'tokyo-night', 'one-dark', 'rose-pine', 'kanagawa', 'everforest', 'monokai', 'synthwave', 'ayu-dark', 'palenight', 'github-dark', 'horizon', 'nightfox', 'carbonfox', 'tokyonight-storm', 'catppuccin-latte', 'solarized-light', 'github-light', 'iceberg', 'material-ocean', 'moonlight', 'poimandres', 'vitesse-dark', 'vesper', 'flexoki', 'mellow', 'night-owl', 'cobalt2', 'oceanic-next', 'catppuccin-macchiato', 'gruvbox-light', 'tokyo-night-day', 'one-light', 'ayu-light', 'rose-pine-dawn', 'everforest-light', 'vitesse-light', 'dayfox', 'night-owl-light', 'flexoki-light', 'material-lighter', 'papercolor-light', 'modus-operandi', 'quiet-light'],
48007
+ // Derived from the single source of truth (`THEME_PRESET_COLORS`) so the
48008
+ // workspace CLI choices stay in sync with the themes the picker offers.
48009
+ choices: getLogInkThemePresets(),
47015
48010
  },
47016
48011
  };
47017
48012
  const builder = (yargs) => {
@@ -47779,20 +48774,55 @@ function createWorkspaceState(init) {
47779
48774
  }
47780
48775
  return { ...base, selectedIndex: idx };
47781
48776
  }
48777
+ /**
48778
+ * Single-entry memo for {@link selectVisibleRepos}. The visible list
48779
+ * is a pure function of five referentially-stable state slices, and
48780
+ * the hot path — cursor movement (`j`/`k`) — leaves all five untouched
48781
+ * (`move-cursor` only swaps `selectedIndex`). Without the memo the
48782
+ * renderer recomputes the full sort + tab-filter + text-filter three
48783
+ * times per render (list window, header chips, and the direct call in
48784
+ * `renderListBody`), plus once more in the reducer, on every keystroke.
48785
+ *
48786
+ * Keyed on object/string identity so a reducer transition that swaps
48787
+ * any of `overview.repos`, `sortMode`, `tab`, `filter`, or
48788
+ * `pullRequestCounts` is a guaranteed cache miss. Callers never mutate
48789
+ * the returned array, so handing back the cached reference is safe and
48790
+ * also stabilises the array identity for any future `React.memo`.
48791
+ */
48792
+ let visibleReposCache = null;
47782
48793
  /**
47783
48794
  * Recompute the visible repo list — sort → tab filter → text filter.
47784
48795
  * The renderer consumes this; the reducer also uses it to rectify the
47785
48796
  * cursor after a filter/sort change so the selection stays in range.
48797
+ * Memoized on its five inputs (see {@link visibleReposCache}).
47786
48798
  */
47787
48799
  function selectVisibleRepos(state) {
47788
- const sorted = sortWorkspaceRepos(state.overview.repos, state.sortMode);
48800
+ const repos = state.overview.repos;
48801
+ const cache = visibleReposCache;
48802
+ if (cache !== null &&
48803
+ cache.repos === repos &&
48804
+ cache.sortMode === state.sortMode &&
48805
+ cache.tab === state.tab &&
48806
+ cache.filter === state.filter &&
48807
+ cache.pullRequestCounts === state.pullRequestCounts) {
48808
+ return cache.result;
48809
+ }
48810
+ const sorted = sortWorkspaceRepos(repos, state.sortMode);
47789
48811
  const tabFiltered = filterWorkspaceRepos(sorted, state.tab, {
47790
48812
  pullRequestCounts: state.pullRequestCounts,
47791
48813
  });
47792
- if (!state.filter) {
47793
- return tabFiltered;
47794
- }
47795
- return tabFiltered.filter((entry) => matchesWorkspaceText(entry, state.filter));
48814
+ const result = state.filter
48815
+ ? tabFiltered.filter((entry) => matchesWorkspaceText(entry, state.filter))
48816
+ : tabFiltered;
48817
+ visibleReposCache = {
48818
+ repos,
48819
+ sortMode: state.sortMode,
48820
+ tab: state.tab,
48821
+ filter: state.filter,
48822
+ pullRequestCounts: state.pullRequestCounts,
48823
+ result,
48824
+ };
48825
+ return result;
47796
48826
  }
47797
48827
  function clampCursor(index, length) {
47798
48828
  if (length <= 0) {