git-coco 0.41.1 → 0.42.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.esm.mjs +258 -7
- package/dist/index.js +259 -8
- package/package.json +2 -1
package/dist/index.esm.mjs
CHANGED
|
@@ -17,6 +17,8 @@ import prettyMilliseconds from 'pretty-ms';
|
|
|
17
17
|
import { ChatAnthropic } from '@langchain/anthropic';
|
|
18
18
|
import { ChatOllama } from '@langchain/ollama';
|
|
19
19
|
import { ChatOpenAI } from '@langchain/openai';
|
|
20
|
+
import * as fs$1 from 'node:fs';
|
|
21
|
+
import * as path$1 from 'node:path';
|
|
20
22
|
import { StructuredOutputParser, BaseOutputParser, StringOutputParser } from '@langchain/core/output_parsers';
|
|
21
23
|
import { minimatch } from 'minimatch';
|
|
22
24
|
import { simpleGit, GitError } from 'simple-git';
|
|
@@ -38,9 +40,7 @@ import '@langchain/core/utils/async_caller';
|
|
|
38
40
|
import { encoding_for_model } from 'tiktoken';
|
|
39
41
|
import { spawn, exec, execFile } from 'child_process';
|
|
40
42
|
import { spawnSync } from 'node:child_process';
|
|
41
|
-
import * as fs$1 from 'node:fs';
|
|
42
43
|
import * as os$1 from 'node:os';
|
|
43
|
-
import * as path$1 from 'node:path';
|
|
44
44
|
import * as crypto from 'node:crypto';
|
|
45
45
|
import * as readline from 'readline';
|
|
46
46
|
import readline__default from 'readline';
|
|
@@ -53,7 +53,7 @@ import { pathToFileURL } from 'url';
|
|
|
53
53
|
/**
|
|
54
54
|
* Current build version from package.json
|
|
55
55
|
*/
|
|
56
|
-
const BUILD_VERSION = "0.
|
|
56
|
+
const BUILD_VERSION = "0.42.0";
|
|
57
57
|
|
|
58
58
|
const isInteractive = (config) => {
|
|
59
59
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -6343,6 +6343,7 @@ function resolveDynamicService(config, task) {
|
|
|
6343
6343
|
};
|
|
6344
6344
|
}
|
|
6345
6345
|
|
|
6346
|
+
const benchCalls = [];
|
|
6346
6347
|
const telemetryByCommand = new Map();
|
|
6347
6348
|
function estimatePromptTokens(tokenizer, renderedPrompt) {
|
|
6348
6349
|
if (!tokenizer)
|
|
@@ -6354,10 +6355,28 @@ function estimatePromptTokens(tokenizer, renderedPrompt) {
|
|
|
6354
6355
|
return undefined;
|
|
6355
6356
|
}
|
|
6356
6357
|
}
|
|
6358
|
+
function isBenchModeActive() {
|
|
6359
|
+
return Boolean(process.env.COCO_BENCH && process.env.COCO_BENCH !== '0');
|
|
6360
|
+
}
|
|
6361
|
+
function recordBenchCall(metadata) {
|
|
6362
|
+
if (!isBenchModeActive())
|
|
6363
|
+
return;
|
|
6364
|
+
benchCalls.push({
|
|
6365
|
+
task: metadata.task,
|
|
6366
|
+
command: metadata.command,
|
|
6367
|
+
provider: metadata.provider,
|
|
6368
|
+
model: metadata.model,
|
|
6369
|
+
promptTokens: metadata.promptTokens,
|
|
6370
|
+
elapsedMs: metadata.elapsedMs,
|
|
6371
|
+
inputDocuments: metadata.inputDocuments,
|
|
6372
|
+
inputChunks: metadata.inputChunks,
|
|
6373
|
+
});
|
|
6374
|
+
}
|
|
6357
6375
|
function logLlmCall(logger, metadata) {
|
|
6358
6376
|
if (!logger)
|
|
6359
6377
|
return;
|
|
6360
6378
|
recordLlmTelemetry(metadata);
|
|
6379
|
+
recordBenchCall(metadata);
|
|
6361
6380
|
const fields = [
|
|
6362
6381
|
`task=${metadata.task}`,
|
|
6363
6382
|
metadata.command ? `command=${metadata.command}` : undefined,
|
|
@@ -6750,13 +6769,35 @@ async function getCommitLogRangeDetails(from, to, { noMerges, git }) {
|
|
|
6750
6769
|
}
|
|
6751
6770
|
|
|
6752
6771
|
/**
|
|
6753
|
-
*
|
|
6772
|
+
* Retrieve the name of the current branch.
|
|
6773
|
+
*
|
|
6774
|
+
* The first-choice path uses `git rev-parse --abbrev-ref HEAD`, which
|
|
6775
|
+
* returns the active branch on a normal repo. On an initial-commit
|
|
6776
|
+
* repo (fresh `git init` with no commits yet) HEAD does not resolve
|
|
6777
|
+
* and rev-parse fails fatally — but `git symbolic-ref --short HEAD`
|
|
6778
|
+
* still reports the configured initial branch name, so we fall
|
|
6779
|
+
* through to that. Final fallback is an empty string for genuinely
|
|
6780
|
+
* detached / corrupt states; every caller treats that as "no branch
|
|
6781
|
+
* context", which is the right semantics for a no-HEAD repo.
|
|
6754
6782
|
*
|
|
6755
|
-
*
|
|
6756
|
-
*
|
|
6783
|
+
* Without this resilience, every command that depends on the branch
|
|
6784
|
+
* name (e.g. the post-summary step in `coco commit`) would crash
|
|
6785
|
+
* with `fatal: ambiguous argument 'HEAD'` after the entire diff
|
|
6786
|
+
* pipeline already ran (#844).
|
|
6757
6787
|
*/
|
|
6758
6788
|
async function getCurrentBranchName({ git }) {
|
|
6759
|
-
|
|
6789
|
+
try {
|
|
6790
|
+
return await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
6791
|
+
}
|
|
6792
|
+
catch {
|
|
6793
|
+
try {
|
|
6794
|
+
const ref = await git.raw(['symbolic-ref', '--short', 'HEAD']);
|
|
6795
|
+
return ref.trim();
|
|
6796
|
+
}
|
|
6797
|
+
catch {
|
|
6798
|
+
return '';
|
|
6799
|
+
}
|
|
6800
|
+
}
|
|
6760
6801
|
}
|
|
6761
6802
|
|
|
6762
6803
|
/**
|
|
@@ -15347,6 +15388,13 @@ const LOG_INK_KEY_BINDINGS = [
|
|
|
15347
15388
|
description: 'Push the dedicated pull-request action panel for the current branch.',
|
|
15348
15389
|
contexts: ['normal'],
|
|
15349
15390
|
},
|
|
15391
|
+
{
|
|
15392
|
+
id: 'navigateConflicts',
|
|
15393
|
+
keys: ['gx'],
|
|
15394
|
+
label: 'conflicts',
|
|
15395
|
+
description: 'Push the conflict resolution helper view (available during merge/rebase/cherry-pick/revert).',
|
|
15396
|
+
contexts: ['normal'],
|
|
15397
|
+
},
|
|
15350
15398
|
{
|
|
15351
15399
|
id: 'navigateBack',
|
|
15352
15400
|
keys: ['<', 'esc'],
|
|
@@ -15476,6 +15524,7 @@ const GLOBAL_BINDING_IDS = [
|
|
|
15476
15524
|
'navigateStash',
|
|
15477
15525
|
'navigateWorktrees',
|
|
15478
15526
|
'navigatePullRequest',
|
|
15527
|
+
'navigateConflicts',
|
|
15479
15528
|
'navigateBack',
|
|
15480
15529
|
];
|
|
15481
15530
|
const NORMAL_GLOBAL_HINTS = ['g jump', '< back', '? help', ': cmds', 'q quit'];
|
|
@@ -15648,6 +15697,12 @@ function getLogInkFooterHints(options) {
|
|
|
15648
15697
|
global: NORMAL_GLOBAL_HINTS,
|
|
15649
15698
|
};
|
|
15650
15699
|
}
|
|
15700
|
+
if (options.activeView === 'conflicts') {
|
|
15701
|
+
return {
|
|
15702
|
+
contextual: ['↑/↓ files', 'enter diff', 's stage', 'u theirs', 'U ours', 'o edit', 'C continue*', 'esc back'],
|
|
15703
|
+
global: NORMAL_GLOBAL_HINTS,
|
|
15704
|
+
};
|
|
15705
|
+
}
|
|
15651
15706
|
return {
|
|
15652
15707
|
// History view default hints. Mutating ops (`c` cherry-pick, `R`
|
|
15653
15708
|
// revert, `Z` reset, `i` interactive-rebase) all route through a
|
|
@@ -16394,6 +16449,7 @@ function createLogInkState(rows, options = {}) {
|
|
|
16394
16449
|
selectedTagIndex: 0,
|
|
16395
16450
|
selectedStashIndex: 0,
|
|
16396
16451
|
selectedWorktreeListIndex: 0,
|
|
16452
|
+
selectedConflictFileIndex: 0,
|
|
16397
16453
|
branchSort: DEFAULT_BRANCH_SORT_MODE,
|
|
16398
16454
|
tagSort: DEFAULT_TAG_SORT_MODE,
|
|
16399
16455
|
paletteFilter: '',
|
|
@@ -16647,6 +16703,12 @@ function applyLogInkAction(state, action) {
|
|
|
16647
16703
|
selectedWorktreeListIndex: clampIndex$1(state.selectedWorktreeListIndex + action.delta, action.count),
|
|
16648
16704
|
pendingKey: undefined,
|
|
16649
16705
|
};
|
|
16706
|
+
case 'moveConflictFile':
|
|
16707
|
+
return {
|
|
16708
|
+
...state,
|
|
16709
|
+
selectedConflictFileIndex: clampIndex$1(state.selectedConflictFileIndex + action.delta, action.count),
|
|
16710
|
+
pendingKey: undefined,
|
|
16711
|
+
};
|
|
16650
16712
|
case 'cycleBranchSort':
|
|
16651
16713
|
return {
|
|
16652
16714
|
...state,
|
|
@@ -17315,6 +17377,8 @@ function getLogInkPaletteExecuteEvents(command, state) {
|
|
|
17315
17377
|
return [action({ type: 'pushView', value: 'worktrees' })];
|
|
17316
17378
|
case 'navigatePullRequest':
|
|
17317
17379
|
return [action({ type: 'pushView', value: 'pull-request' })];
|
|
17380
|
+
case 'navigateConflicts':
|
|
17381
|
+
return [action({ type: 'pushView', value: 'conflicts' })];
|
|
17318
17382
|
case 'navigateBack':
|
|
17319
17383
|
return [action({ type: 'popView' })];
|
|
17320
17384
|
case 'openSelected': {
|
|
@@ -17786,6 +17850,12 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
17786
17850
|
action({ type: 'setStatus', value: 'jumped to pull request' }),
|
|
17787
17851
|
];
|
|
17788
17852
|
}
|
|
17853
|
+
if (state.pendingKey === 'g' && inputValue === 'x') {
|
|
17854
|
+
return [
|
|
17855
|
+
action({ type: 'pushView', value: 'conflicts' }),
|
|
17856
|
+
action({ type: 'setStatus', value: 'jumped to conflicts' }),
|
|
17857
|
+
];
|
|
17858
|
+
}
|
|
17789
17859
|
// `gH` chord: apply the cursored hunk to the index (`git apply
|
|
17790
17860
|
// --cached`). Sibling of bare `H` which targets the worktree.
|
|
17791
17861
|
// Discoverable via the footer hint on diff views and the help
|
|
@@ -18095,6 +18165,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18095
18165
|
if (isWorktreeActionTarget(state) && context.worktreeListCount) {
|
|
18096
18166
|
return [action({ type: 'moveWorktreeListEntry', delta: -1, count: context.worktreeListCount })];
|
|
18097
18167
|
}
|
|
18168
|
+
if (state.activeView === 'conflicts' && context.conflictFileCount) {
|
|
18169
|
+
return [action({ type: 'moveConflictFile', delta: -1, count: context.conflictFileCount })];
|
|
18170
|
+
}
|
|
18098
18171
|
if (state.activeView === 'history' &&
|
|
18099
18172
|
state.focus === 'commits' &&
|
|
18100
18173
|
state.selectedIndex === 0 &&
|
|
@@ -18173,6 +18246,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18173
18246
|
if (isWorktreeActionTarget(state) && context.worktreeListCount) {
|
|
18174
18247
|
return [action({ type: 'moveWorktreeListEntry', delta: 1, count: context.worktreeListCount })];
|
|
18175
18248
|
}
|
|
18249
|
+
if (state.activeView === 'conflicts' && context.conflictFileCount) {
|
|
18250
|
+
return [action({ type: 'moveConflictFile', delta: 1, count: context.conflictFileCount })];
|
|
18251
|
+
}
|
|
18176
18252
|
return [
|
|
18177
18253
|
action(state.focus === 'sidebar'
|
|
18178
18254
|
? { type: 'nextSidebarTab' }
|
|
@@ -18358,6 +18434,11 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18358
18434
|
fileIndex: state.selectedWorktreeFileIndex,
|
|
18359
18435
|
})];
|
|
18360
18436
|
}
|
|
18437
|
+
// Enter on a conflict file opens the worktree diff for that file so
|
|
18438
|
+
// the user can inspect the conflict markers in context.
|
|
18439
|
+
if (key.return && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
|
|
18440
|
+
return [{ type: 'runWorkflowAction', id: 'resolve-conflict-open-diff', payload: context.conflictSelectedPath }];
|
|
18441
|
+
}
|
|
18361
18442
|
// Enter on a branch row checks the branch out. Non-destructive workflow
|
|
18362
18443
|
// action — no confirmation prompt. Fires from either the dedicated
|
|
18363
18444
|
// branches view or from the sidebar when the branches tab is focused
|
|
@@ -18499,6 +18580,32 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18499
18580
|
if (inputValue === 'o' && state.activeView === 'diff' && state.diffSource === 'stash' && context.stashDiffSelectedPath) {
|
|
18500
18581
|
return [{ type: 'openFileInEditor', path: context.stashDiffSelectedPath }];
|
|
18501
18582
|
}
|
|
18583
|
+
// --- Conflicts view per-row handlers ---
|
|
18584
|
+
// `o` opens the conflicted file in $EDITOR for manual resolution.
|
|
18585
|
+
if (inputValue === 'o' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
|
|
18586
|
+
return [{ type: 'openFileInEditor', path: context.conflictSelectedPath }];
|
|
18587
|
+
}
|
|
18588
|
+
// `s` stages the conflicted file (marks it resolved).
|
|
18589
|
+
if (inputValue === 's' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
|
|
18590
|
+
return [{ type: 'runWorkflowAction', id: 'resolve-conflict-stage', payload: context.conflictSelectedPath }];
|
|
18591
|
+
}
|
|
18592
|
+
// `u` resolves by keeping theirs (incoming changes).
|
|
18593
|
+
if (inputValue === 'u' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
|
|
18594
|
+
return [{ type: 'runWorkflowAction', id: 'resolve-conflict-theirs', payload: context.conflictSelectedPath }];
|
|
18595
|
+
}
|
|
18596
|
+
// `U` resolves by keeping ours (current branch).
|
|
18597
|
+
if (inputValue === 'U' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
|
|
18598
|
+
return [{ type: 'runWorkflowAction', id: 'resolve-conflict-ours', payload: context.conflictSelectedPath }];
|
|
18599
|
+
}
|
|
18600
|
+
// `C` continues the in-progress operation (available when no conflicts remain).
|
|
18601
|
+
if (inputValue === 'C' && state.activeView === 'conflicts' && context.conflictFileCount === 0) {
|
|
18602
|
+
return [{ type: 'runWorkflowAction', id: 'continue-operation' }];
|
|
18603
|
+
}
|
|
18604
|
+
// Always intercept `C` on the conflicts view to prevent fallthrough to
|
|
18605
|
+
// the global `C` (Create PR) binding when conflicts remain.
|
|
18606
|
+
if (inputValue === 'C' && state.activeView === 'conflicts') {
|
|
18607
|
+
return [action({ type: 'setStatus', value: 'Resolve all conflicts before continuing' })];
|
|
18608
|
+
}
|
|
18502
18609
|
// `c` on a stash diff cherry-picks the file under the cursor —
|
|
18503
18610
|
// materializes that single path from the stash into the working tree
|
|
18504
18611
|
// (`git checkout <stashRef> -- <path>`). Routed through the y-confirm
|
|
@@ -21795,6 +21902,21 @@ function skipOperation(git, operation) {
|
|
|
21795
21902
|
}
|
|
21796
21903
|
return runAction(() => git.raw(command.args), command.successMessage);
|
|
21797
21904
|
}
|
|
21905
|
+
function resolveConflictOurs(git, path) {
|
|
21906
|
+
return runAction(async () => {
|
|
21907
|
+
await git.raw(['checkout', '--ours', '--', path]);
|
|
21908
|
+
await git.raw(['add', '--', path]);
|
|
21909
|
+
}, `Resolved ${path} (kept ours)`);
|
|
21910
|
+
}
|
|
21911
|
+
function resolveConflictTheirs(git, path) {
|
|
21912
|
+
return runAction(async () => {
|
|
21913
|
+
await git.raw(['checkout', '--theirs', '--', path]);
|
|
21914
|
+
await git.raw(['add', '--', path]);
|
|
21915
|
+
}, `Resolved ${path} (kept theirs)`);
|
|
21916
|
+
}
|
|
21917
|
+
function stageConflictResolved(git, path) {
|
|
21918
|
+
return runAction(() => git.raw(['add', '--', path]), `Staged ${path} (marked resolved)`);
|
|
21919
|
+
}
|
|
21798
21920
|
|
|
21799
21921
|
function openProviderUrl(repository, target, openUrl = defaultOpenUrlRunner) {
|
|
21800
21922
|
const url = repository ? buildProviderUrl(repository, target) : undefined;
|
|
@@ -25177,6 +25299,51 @@ function LogInkApp(deps) {
|
|
|
25177
25299
|
}
|
|
25178
25300
|
return abortOperation(git, operation);
|
|
25179
25301
|
},
|
|
25302
|
+
'resolve-conflict-ours': async () => {
|
|
25303
|
+
const path = payload?.trim();
|
|
25304
|
+
if (!path)
|
|
25305
|
+
return { ok: false, message: 'No conflict file selected' };
|
|
25306
|
+
return resolveConflictOurs(git, path);
|
|
25307
|
+
},
|
|
25308
|
+
'resolve-conflict-theirs': async () => {
|
|
25309
|
+
const path = payload?.trim();
|
|
25310
|
+
if (!path)
|
|
25311
|
+
return { ok: false, message: 'No conflict file selected' };
|
|
25312
|
+
return resolveConflictTheirs(git, path);
|
|
25313
|
+
},
|
|
25314
|
+
'resolve-conflict-stage': async () => {
|
|
25315
|
+
const path = payload?.trim();
|
|
25316
|
+
if (!path)
|
|
25317
|
+
return { ok: false, message: 'No conflict file selected' };
|
|
25318
|
+
return stageConflictResolved(git, path);
|
|
25319
|
+
},
|
|
25320
|
+
'resolve-conflict-open-diff': async () => {
|
|
25321
|
+
// Push the diff view for the conflicted file so the user can
|
|
25322
|
+
// inspect conflict markers in context. We find the file's index
|
|
25323
|
+
// in the worktree file list and navigate to its diff.
|
|
25324
|
+
const path = payload?.trim();
|
|
25325
|
+
if (!path)
|
|
25326
|
+
return { ok: false, message: 'No conflict file selected' };
|
|
25327
|
+
const worktreeFiles = context.worktree?.files || [];
|
|
25328
|
+
const fileIndex = worktreeFiles.findIndex((f) => f.path === path);
|
|
25329
|
+
if (fileIndex >= 0) {
|
|
25330
|
+
dispatch({ type: 'navigateOpenDiffForWorktreeFile', fileIndex });
|
|
25331
|
+
return { ok: true, message: `Viewing diff for ${path}` };
|
|
25332
|
+
}
|
|
25333
|
+
// File not in worktree list (e.g. deleted-by-us) — open in
|
|
25334
|
+
// editor as fallback so the user can still inspect it.
|
|
25335
|
+
return { ok: true, message: `${path} not in worktree diff list` };
|
|
25336
|
+
},
|
|
25337
|
+
'continue-operation': async () => {
|
|
25338
|
+
const operation = context.operation?.operation;
|
|
25339
|
+
if (!operation || operation === 'none') {
|
|
25340
|
+
return { ok: false, message: 'No git operation in progress' };
|
|
25341
|
+
}
|
|
25342
|
+
if ((context.operation?.conflictedFiles.length ?? 0) > 0) {
|
|
25343
|
+
return { ok: false, message: 'Resolve all conflicts before continuing' };
|
|
25344
|
+
}
|
|
25345
|
+
return continueOperation(git, operation);
|
|
25346
|
+
},
|
|
25180
25347
|
'open-pr': async () => {
|
|
25181
25348
|
const repo = context.provider?.repository;
|
|
25182
25349
|
if (!repo || repo.provider !== 'github' || !repo.owner || !repo.name) {
|
|
@@ -25700,6 +25867,14 @@ function LogInkApp(deps) {
|
|
|
25700
25867
|
? selected?.hash
|
|
25701
25868
|
: undefined,
|
|
25702
25869
|
worktreeDirty,
|
|
25870
|
+
conflictFileCount: context.operation?.conflictedFiles.length,
|
|
25871
|
+
conflictSelectedPath: (() => {
|
|
25872
|
+
const files = context.operation?.conflictedFiles;
|
|
25873
|
+
if (!files || files.length === 0)
|
|
25874
|
+
return undefined;
|
|
25875
|
+
const clamped = Math.min(state.selectedConflictFileIndex, files.length - 1);
|
|
25876
|
+
return files[clamped]?.path;
|
|
25877
|
+
})(),
|
|
25703
25878
|
// H / gH need the actual diff text (not just hunk offsets) to
|
|
25704
25879
|
// slice the cursored hunk into a `git apply` patch. Stash uses
|
|
25705
25880
|
// the full `git stash show -p` output; commit-diff uses the
|
|
@@ -26064,6 +26239,9 @@ function renderMainPanel(h, components, state, context, contextStatus, worktreeD
|
|
|
26064
26239
|
if (state.activeView === 'pull-request') {
|
|
26065
26240
|
return renderPullRequestSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
26066
26241
|
}
|
|
26242
|
+
if (state.activeView === 'conflicts') {
|
|
26243
|
+
return renderConflictsSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
26244
|
+
}
|
|
26067
26245
|
return renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits);
|
|
26068
26246
|
}
|
|
26069
26247
|
function renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
|
|
@@ -26247,6 +26425,79 @@ function buildStatusSurfaceRows(groups) {
|
|
|
26247
26425
|
}
|
|
26248
26426
|
return rows;
|
|
26249
26427
|
}
|
|
26428
|
+
function renderConflictsSurface(h, components, state, context, contextStatus, bodyRows, width, theme) {
|
|
26429
|
+
const { Box, Text } = components;
|
|
26430
|
+
const focused = state.focus === 'commits';
|
|
26431
|
+
const loading = isLogInkContextKeyLoading(contextStatus, 'operation');
|
|
26432
|
+
const operation = context.operation;
|
|
26433
|
+
const conflictedFiles = operation?.conflictedFiles || [];
|
|
26434
|
+
const operationType = operation?.operation || 'none';
|
|
26435
|
+
// If no operation is in progress, show a fallback message.
|
|
26436
|
+
if (!loading && operationType === 'none') {
|
|
26437
|
+
return h(Box, {
|
|
26438
|
+
borderColor: focusBorderColor(theme, focused),
|
|
26439
|
+
borderStyle: theme.borderStyle,
|
|
26440
|
+
flexDirection: 'column',
|
|
26441
|
+
flexShrink: 0,
|
|
26442
|
+
paddingX: 1,
|
|
26443
|
+
width,
|
|
26444
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Conflicts', focused)), h(Text, { dimColor: true }, 'no operation in progress')), h(Text, { key: 'conflicts-empty', dimColor: true }, 'No merge, rebase, cherry-pick, or revert in progress.'));
|
|
26445
|
+
}
|
|
26446
|
+
// All conflicts resolved — show the "continue" hint.
|
|
26447
|
+
if (!loading && conflictedFiles.length === 0 && operationType !== 'none') {
|
|
26448
|
+
return h(Box, {
|
|
26449
|
+
borderColor: focusBorderColor(theme, focused),
|
|
26450
|
+
borderStyle: theme.borderStyle,
|
|
26451
|
+
flexDirection: 'column',
|
|
26452
|
+
flexShrink: 0,
|
|
26453
|
+
paddingX: 1,
|
|
26454
|
+
width,
|
|
26455
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Conflicts', focused)), h(Text, { dimColor: true }, `${operationType} — all conflicts resolved`)), h(Text, { key: 'conflicts-hint', dimColor: true }, `All conflicts resolved. Press C to continue the ${operationType}, or < to go back.`));
|
|
26456
|
+
}
|
|
26457
|
+
const selected = Math.max(0, Math.min(state.selectedConflictFileIndex, Math.max(0, conflictedFiles.length - 1)));
|
|
26458
|
+
const listRows = Math.max(4, bodyRows - 4);
|
|
26459
|
+
const startIndex = Math.max(0, selected - Math.floor(listRows / 2));
|
|
26460
|
+
const visible = conflictedFiles.slice(startIndex, startIndex + listRows);
|
|
26461
|
+
const remaining = conflictedFiles.length;
|
|
26462
|
+
const headerRight = loading
|
|
26463
|
+
? 'loading conflicts'
|
|
26464
|
+
: `${operationType} — ${remaining} ${remaining === 1 ? 'conflict' : 'conflicts'} remaining`;
|
|
26465
|
+
const statusLabel = (file) => {
|
|
26466
|
+
const code = `${file.indexStatus}${file.worktreeStatus}`;
|
|
26467
|
+
switch (code) {
|
|
26468
|
+
case 'UU': return 'both modified';
|
|
26469
|
+
case 'AA': return 'added by both';
|
|
26470
|
+
case 'DD': return 'both deleted';
|
|
26471
|
+
case 'AU':
|
|
26472
|
+
case 'UA': return 'added by one';
|
|
26473
|
+
case 'DU': return 'deleted by us';
|
|
26474
|
+
case 'UD': return 'deleted by them';
|
|
26475
|
+
default: return code;
|
|
26476
|
+
}
|
|
26477
|
+
};
|
|
26478
|
+
const lines = loading
|
|
26479
|
+
? [h(Text, { key: 'conflicts-loading', dimColor: true }, formatLogInkLoading({ resource: 'conflicts' }))]
|
|
26480
|
+
: visible.map((file, offset) => {
|
|
26481
|
+
const index = startIndex + offset;
|
|
26482
|
+
const isSelected = index === selected;
|
|
26483
|
+
const cursor = isSelected ? '>' : ' ';
|
|
26484
|
+
const code = `${file.indexStatus}${file.worktreeStatus}`;
|
|
26485
|
+
const label = statusLabel(file);
|
|
26486
|
+
return h(Text, {
|
|
26487
|
+
key: `conflict-${index}`,
|
|
26488
|
+
bold: isSelected,
|
|
26489
|
+
dimColor: !isSelected,
|
|
26490
|
+
}, truncate$1(`${cursor} ${code} ${file.path} (${label})`, width - 4));
|
|
26491
|
+
});
|
|
26492
|
+
return h(Box, {
|
|
26493
|
+
borderColor: focusBorderColor(theme, focused),
|
|
26494
|
+
borderStyle: theme.borderStyle,
|
|
26495
|
+
flexDirection: 'column',
|
|
26496
|
+
flexShrink: 0,
|
|
26497
|
+
paddingX: 1,
|
|
26498
|
+
width,
|
|
26499
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Conflicts', focused)), h(Text, { dimColor: true }, headerRight)), ...lines);
|
|
26500
|
+
}
|
|
26250
26501
|
function renderStatusSurface(h, components, state, context, contextStatus, bodyRows, width, theme) {
|
|
26251
26502
|
const { Box, Text } = components;
|
|
26252
26503
|
const focused = state.focus === 'commits';
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,8 @@ var prettyMilliseconds = require('pretty-ms');
|
|
|
16
16
|
var anthropic = require('@langchain/anthropic');
|
|
17
17
|
var ollama = require('@langchain/ollama');
|
|
18
18
|
var openai = require('@langchain/openai');
|
|
19
|
+
var fs$1 = require('node:fs');
|
|
20
|
+
var path$1 = require('node:path');
|
|
19
21
|
var output_parsers = require('@langchain/core/output_parsers');
|
|
20
22
|
var minimatch = require('minimatch');
|
|
21
23
|
var simpleGit = require('simple-git');
|
|
@@ -37,9 +39,7 @@ require('@langchain/core/utils/async_caller');
|
|
|
37
39
|
var tiktoken = require('tiktoken');
|
|
38
40
|
var child_process = require('child_process');
|
|
39
41
|
var node_child_process = require('node:child_process');
|
|
40
|
-
var fs$1 = require('node:fs');
|
|
41
42
|
var os$1 = require('node:os');
|
|
42
|
-
var path$1 = require('node:path');
|
|
43
43
|
var crypto = require('node:crypto');
|
|
44
44
|
var readline = require('readline');
|
|
45
45
|
var util$1 = require('util');
|
|
@@ -68,8 +68,8 @@ var ini__namespace = /*#__PURE__*/_interopNamespaceDefault(ini);
|
|
|
68
68
|
var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
|
|
69
69
|
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
70
70
|
var fs__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(fs$1);
|
|
71
|
-
var os__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(os$1);
|
|
72
71
|
var path__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(path$1);
|
|
72
|
+
var os__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(os$1);
|
|
73
73
|
var crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto);
|
|
74
74
|
var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
|
|
75
75
|
|
|
@@ -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.
|
|
81
|
+
const BUILD_VERSION = "0.42.0";
|
|
82
82
|
|
|
83
83
|
const isInteractive = (config) => {
|
|
84
84
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -6368,6 +6368,7 @@ function resolveDynamicService(config, task) {
|
|
|
6368
6368
|
};
|
|
6369
6369
|
}
|
|
6370
6370
|
|
|
6371
|
+
const benchCalls = [];
|
|
6371
6372
|
const telemetryByCommand = new Map();
|
|
6372
6373
|
function estimatePromptTokens(tokenizer, renderedPrompt) {
|
|
6373
6374
|
if (!tokenizer)
|
|
@@ -6379,10 +6380,28 @@ function estimatePromptTokens(tokenizer, renderedPrompt) {
|
|
|
6379
6380
|
return undefined;
|
|
6380
6381
|
}
|
|
6381
6382
|
}
|
|
6383
|
+
function isBenchModeActive() {
|
|
6384
|
+
return Boolean(process.env.COCO_BENCH && process.env.COCO_BENCH !== '0');
|
|
6385
|
+
}
|
|
6386
|
+
function recordBenchCall(metadata) {
|
|
6387
|
+
if (!isBenchModeActive())
|
|
6388
|
+
return;
|
|
6389
|
+
benchCalls.push({
|
|
6390
|
+
task: metadata.task,
|
|
6391
|
+
command: metadata.command,
|
|
6392
|
+
provider: metadata.provider,
|
|
6393
|
+
model: metadata.model,
|
|
6394
|
+
promptTokens: metadata.promptTokens,
|
|
6395
|
+
elapsedMs: metadata.elapsedMs,
|
|
6396
|
+
inputDocuments: metadata.inputDocuments,
|
|
6397
|
+
inputChunks: metadata.inputChunks,
|
|
6398
|
+
});
|
|
6399
|
+
}
|
|
6382
6400
|
function logLlmCall(logger, metadata) {
|
|
6383
6401
|
if (!logger)
|
|
6384
6402
|
return;
|
|
6385
6403
|
recordLlmTelemetry(metadata);
|
|
6404
|
+
recordBenchCall(metadata);
|
|
6386
6405
|
const fields = [
|
|
6387
6406
|
`task=${metadata.task}`,
|
|
6388
6407
|
metadata.command ? `command=${metadata.command}` : undefined,
|
|
@@ -6775,13 +6794,35 @@ async function getCommitLogRangeDetails(from, to, { noMerges, git }) {
|
|
|
6775
6794
|
}
|
|
6776
6795
|
|
|
6777
6796
|
/**
|
|
6778
|
-
*
|
|
6797
|
+
* Retrieve the name of the current branch.
|
|
6779
6798
|
*
|
|
6780
|
-
*
|
|
6781
|
-
*
|
|
6799
|
+
* The first-choice path uses `git rev-parse --abbrev-ref HEAD`, which
|
|
6800
|
+
* returns the active branch on a normal repo. On an initial-commit
|
|
6801
|
+
* repo (fresh `git init` with no commits yet) HEAD does not resolve
|
|
6802
|
+
* and rev-parse fails fatally — but `git symbolic-ref --short HEAD`
|
|
6803
|
+
* still reports the configured initial branch name, so we fall
|
|
6804
|
+
* through to that. Final fallback is an empty string for genuinely
|
|
6805
|
+
* detached / corrupt states; every caller treats that as "no branch
|
|
6806
|
+
* context", which is the right semantics for a no-HEAD repo.
|
|
6807
|
+
*
|
|
6808
|
+
* Without this resilience, every command that depends on the branch
|
|
6809
|
+
* name (e.g. the post-summary step in `coco commit`) would crash
|
|
6810
|
+
* with `fatal: ambiguous argument 'HEAD'` after the entire diff
|
|
6811
|
+
* pipeline already ran (#844).
|
|
6782
6812
|
*/
|
|
6783
6813
|
async function getCurrentBranchName({ git }) {
|
|
6784
|
-
|
|
6814
|
+
try {
|
|
6815
|
+
return await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
6816
|
+
}
|
|
6817
|
+
catch {
|
|
6818
|
+
try {
|
|
6819
|
+
const ref = await git.raw(['symbolic-ref', '--short', 'HEAD']);
|
|
6820
|
+
return ref.trim();
|
|
6821
|
+
}
|
|
6822
|
+
catch {
|
|
6823
|
+
return '';
|
|
6824
|
+
}
|
|
6825
|
+
}
|
|
6785
6826
|
}
|
|
6786
6827
|
|
|
6787
6828
|
/**
|
|
@@ -15372,6 +15413,13 @@ const LOG_INK_KEY_BINDINGS = [
|
|
|
15372
15413
|
description: 'Push the dedicated pull-request action panel for the current branch.',
|
|
15373
15414
|
contexts: ['normal'],
|
|
15374
15415
|
},
|
|
15416
|
+
{
|
|
15417
|
+
id: 'navigateConflicts',
|
|
15418
|
+
keys: ['gx'],
|
|
15419
|
+
label: 'conflicts',
|
|
15420
|
+
description: 'Push the conflict resolution helper view (available during merge/rebase/cherry-pick/revert).',
|
|
15421
|
+
contexts: ['normal'],
|
|
15422
|
+
},
|
|
15375
15423
|
{
|
|
15376
15424
|
id: 'navigateBack',
|
|
15377
15425
|
keys: ['<', 'esc'],
|
|
@@ -15501,6 +15549,7 @@ const GLOBAL_BINDING_IDS = [
|
|
|
15501
15549
|
'navigateStash',
|
|
15502
15550
|
'navigateWorktrees',
|
|
15503
15551
|
'navigatePullRequest',
|
|
15552
|
+
'navigateConflicts',
|
|
15504
15553
|
'navigateBack',
|
|
15505
15554
|
];
|
|
15506
15555
|
const NORMAL_GLOBAL_HINTS = ['g jump', '< back', '? help', ': cmds', 'q quit'];
|
|
@@ -15673,6 +15722,12 @@ function getLogInkFooterHints(options) {
|
|
|
15673
15722
|
global: NORMAL_GLOBAL_HINTS,
|
|
15674
15723
|
};
|
|
15675
15724
|
}
|
|
15725
|
+
if (options.activeView === 'conflicts') {
|
|
15726
|
+
return {
|
|
15727
|
+
contextual: ['↑/↓ files', 'enter diff', 's stage', 'u theirs', 'U ours', 'o edit', 'C continue*', 'esc back'],
|
|
15728
|
+
global: NORMAL_GLOBAL_HINTS,
|
|
15729
|
+
};
|
|
15730
|
+
}
|
|
15676
15731
|
return {
|
|
15677
15732
|
// History view default hints. Mutating ops (`c` cherry-pick, `R`
|
|
15678
15733
|
// revert, `Z` reset, `i` interactive-rebase) all route through a
|
|
@@ -16419,6 +16474,7 @@ function createLogInkState(rows, options = {}) {
|
|
|
16419
16474
|
selectedTagIndex: 0,
|
|
16420
16475
|
selectedStashIndex: 0,
|
|
16421
16476
|
selectedWorktreeListIndex: 0,
|
|
16477
|
+
selectedConflictFileIndex: 0,
|
|
16422
16478
|
branchSort: DEFAULT_BRANCH_SORT_MODE,
|
|
16423
16479
|
tagSort: DEFAULT_TAG_SORT_MODE,
|
|
16424
16480
|
paletteFilter: '',
|
|
@@ -16672,6 +16728,12 @@ function applyLogInkAction(state, action) {
|
|
|
16672
16728
|
selectedWorktreeListIndex: clampIndex$1(state.selectedWorktreeListIndex + action.delta, action.count),
|
|
16673
16729
|
pendingKey: undefined,
|
|
16674
16730
|
};
|
|
16731
|
+
case 'moveConflictFile':
|
|
16732
|
+
return {
|
|
16733
|
+
...state,
|
|
16734
|
+
selectedConflictFileIndex: clampIndex$1(state.selectedConflictFileIndex + action.delta, action.count),
|
|
16735
|
+
pendingKey: undefined,
|
|
16736
|
+
};
|
|
16675
16737
|
case 'cycleBranchSort':
|
|
16676
16738
|
return {
|
|
16677
16739
|
...state,
|
|
@@ -17340,6 +17402,8 @@ function getLogInkPaletteExecuteEvents(command, state) {
|
|
|
17340
17402
|
return [action({ type: 'pushView', value: 'worktrees' })];
|
|
17341
17403
|
case 'navigatePullRequest':
|
|
17342
17404
|
return [action({ type: 'pushView', value: 'pull-request' })];
|
|
17405
|
+
case 'navigateConflicts':
|
|
17406
|
+
return [action({ type: 'pushView', value: 'conflicts' })];
|
|
17343
17407
|
case 'navigateBack':
|
|
17344
17408
|
return [action({ type: 'popView' })];
|
|
17345
17409
|
case 'openSelected': {
|
|
@@ -17811,6 +17875,12 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
17811
17875
|
action({ type: 'setStatus', value: 'jumped to pull request' }),
|
|
17812
17876
|
];
|
|
17813
17877
|
}
|
|
17878
|
+
if (state.pendingKey === 'g' && inputValue === 'x') {
|
|
17879
|
+
return [
|
|
17880
|
+
action({ type: 'pushView', value: 'conflicts' }),
|
|
17881
|
+
action({ type: 'setStatus', value: 'jumped to conflicts' }),
|
|
17882
|
+
];
|
|
17883
|
+
}
|
|
17814
17884
|
// `gH` chord: apply the cursored hunk to the index (`git apply
|
|
17815
17885
|
// --cached`). Sibling of bare `H` which targets the worktree.
|
|
17816
17886
|
// Discoverable via the footer hint on diff views and the help
|
|
@@ -18120,6 +18190,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18120
18190
|
if (isWorktreeActionTarget(state) && context.worktreeListCount) {
|
|
18121
18191
|
return [action({ type: 'moveWorktreeListEntry', delta: -1, count: context.worktreeListCount })];
|
|
18122
18192
|
}
|
|
18193
|
+
if (state.activeView === 'conflicts' && context.conflictFileCount) {
|
|
18194
|
+
return [action({ type: 'moveConflictFile', delta: -1, count: context.conflictFileCount })];
|
|
18195
|
+
}
|
|
18123
18196
|
if (state.activeView === 'history' &&
|
|
18124
18197
|
state.focus === 'commits' &&
|
|
18125
18198
|
state.selectedIndex === 0 &&
|
|
@@ -18198,6 +18271,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18198
18271
|
if (isWorktreeActionTarget(state) && context.worktreeListCount) {
|
|
18199
18272
|
return [action({ type: 'moveWorktreeListEntry', delta: 1, count: context.worktreeListCount })];
|
|
18200
18273
|
}
|
|
18274
|
+
if (state.activeView === 'conflicts' && context.conflictFileCount) {
|
|
18275
|
+
return [action({ type: 'moveConflictFile', delta: 1, count: context.conflictFileCount })];
|
|
18276
|
+
}
|
|
18201
18277
|
return [
|
|
18202
18278
|
action(state.focus === 'sidebar'
|
|
18203
18279
|
? { type: 'nextSidebarTab' }
|
|
@@ -18383,6 +18459,11 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18383
18459
|
fileIndex: state.selectedWorktreeFileIndex,
|
|
18384
18460
|
})];
|
|
18385
18461
|
}
|
|
18462
|
+
// Enter on a conflict file opens the worktree diff for that file so
|
|
18463
|
+
// the user can inspect the conflict markers in context.
|
|
18464
|
+
if (key.return && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
|
|
18465
|
+
return [{ type: 'runWorkflowAction', id: 'resolve-conflict-open-diff', payload: context.conflictSelectedPath }];
|
|
18466
|
+
}
|
|
18386
18467
|
// Enter on a branch row checks the branch out. Non-destructive workflow
|
|
18387
18468
|
// action — no confirmation prompt. Fires from either the dedicated
|
|
18388
18469
|
// branches view or from the sidebar when the branches tab is focused
|
|
@@ -18524,6 +18605,32 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18524
18605
|
if (inputValue === 'o' && state.activeView === 'diff' && state.diffSource === 'stash' && context.stashDiffSelectedPath) {
|
|
18525
18606
|
return [{ type: 'openFileInEditor', path: context.stashDiffSelectedPath }];
|
|
18526
18607
|
}
|
|
18608
|
+
// --- Conflicts view per-row handlers ---
|
|
18609
|
+
// `o` opens the conflicted file in $EDITOR for manual resolution.
|
|
18610
|
+
if (inputValue === 'o' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
|
|
18611
|
+
return [{ type: 'openFileInEditor', path: context.conflictSelectedPath }];
|
|
18612
|
+
}
|
|
18613
|
+
// `s` stages the conflicted file (marks it resolved).
|
|
18614
|
+
if (inputValue === 's' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
|
|
18615
|
+
return [{ type: 'runWorkflowAction', id: 'resolve-conflict-stage', payload: context.conflictSelectedPath }];
|
|
18616
|
+
}
|
|
18617
|
+
// `u` resolves by keeping theirs (incoming changes).
|
|
18618
|
+
if (inputValue === 'u' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
|
|
18619
|
+
return [{ type: 'runWorkflowAction', id: 'resolve-conflict-theirs', payload: context.conflictSelectedPath }];
|
|
18620
|
+
}
|
|
18621
|
+
// `U` resolves by keeping ours (current branch).
|
|
18622
|
+
if (inputValue === 'U' && state.activeView === 'conflicts' && context.conflictFileCount && context.conflictSelectedPath) {
|
|
18623
|
+
return [{ type: 'runWorkflowAction', id: 'resolve-conflict-ours', payload: context.conflictSelectedPath }];
|
|
18624
|
+
}
|
|
18625
|
+
// `C` continues the in-progress operation (available when no conflicts remain).
|
|
18626
|
+
if (inputValue === 'C' && state.activeView === 'conflicts' && context.conflictFileCount === 0) {
|
|
18627
|
+
return [{ type: 'runWorkflowAction', id: 'continue-operation' }];
|
|
18628
|
+
}
|
|
18629
|
+
// Always intercept `C` on the conflicts view to prevent fallthrough to
|
|
18630
|
+
// the global `C` (Create PR) binding when conflicts remain.
|
|
18631
|
+
if (inputValue === 'C' && state.activeView === 'conflicts') {
|
|
18632
|
+
return [action({ type: 'setStatus', value: 'Resolve all conflicts before continuing' })];
|
|
18633
|
+
}
|
|
18527
18634
|
// `c` on a stash diff cherry-picks the file under the cursor —
|
|
18528
18635
|
// materializes that single path from the stash into the working tree
|
|
18529
18636
|
// (`git checkout <stashRef> -- <path>`). Routed through the y-confirm
|
|
@@ -21820,6 +21927,21 @@ function skipOperation(git, operation) {
|
|
|
21820
21927
|
}
|
|
21821
21928
|
return runAction(() => git.raw(command.args), command.successMessage);
|
|
21822
21929
|
}
|
|
21930
|
+
function resolveConflictOurs(git, path) {
|
|
21931
|
+
return runAction(async () => {
|
|
21932
|
+
await git.raw(['checkout', '--ours', '--', path]);
|
|
21933
|
+
await git.raw(['add', '--', path]);
|
|
21934
|
+
}, `Resolved ${path} (kept ours)`);
|
|
21935
|
+
}
|
|
21936
|
+
function resolveConflictTheirs(git, path) {
|
|
21937
|
+
return runAction(async () => {
|
|
21938
|
+
await git.raw(['checkout', '--theirs', '--', path]);
|
|
21939
|
+
await git.raw(['add', '--', path]);
|
|
21940
|
+
}, `Resolved ${path} (kept theirs)`);
|
|
21941
|
+
}
|
|
21942
|
+
function stageConflictResolved(git, path) {
|
|
21943
|
+
return runAction(() => git.raw(['add', '--', path]), `Staged ${path} (marked resolved)`);
|
|
21944
|
+
}
|
|
21823
21945
|
|
|
21824
21946
|
function openProviderUrl(repository, target, openUrl = defaultOpenUrlRunner) {
|
|
21825
21947
|
const url = repository ? buildProviderUrl(repository, target) : undefined;
|
|
@@ -25202,6 +25324,51 @@ function LogInkApp(deps) {
|
|
|
25202
25324
|
}
|
|
25203
25325
|
return abortOperation(git, operation);
|
|
25204
25326
|
},
|
|
25327
|
+
'resolve-conflict-ours': async () => {
|
|
25328
|
+
const path = payload?.trim();
|
|
25329
|
+
if (!path)
|
|
25330
|
+
return { ok: false, message: 'No conflict file selected' };
|
|
25331
|
+
return resolveConflictOurs(git, path);
|
|
25332
|
+
},
|
|
25333
|
+
'resolve-conflict-theirs': async () => {
|
|
25334
|
+
const path = payload?.trim();
|
|
25335
|
+
if (!path)
|
|
25336
|
+
return { ok: false, message: 'No conflict file selected' };
|
|
25337
|
+
return resolveConflictTheirs(git, path);
|
|
25338
|
+
},
|
|
25339
|
+
'resolve-conflict-stage': async () => {
|
|
25340
|
+
const path = payload?.trim();
|
|
25341
|
+
if (!path)
|
|
25342
|
+
return { ok: false, message: 'No conflict file selected' };
|
|
25343
|
+
return stageConflictResolved(git, path);
|
|
25344
|
+
},
|
|
25345
|
+
'resolve-conflict-open-diff': async () => {
|
|
25346
|
+
// Push the diff view for the conflicted file so the user can
|
|
25347
|
+
// inspect conflict markers in context. We find the file's index
|
|
25348
|
+
// in the worktree file list and navigate to its diff.
|
|
25349
|
+
const path = payload?.trim();
|
|
25350
|
+
if (!path)
|
|
25351
|
+
return { ok: false, message: 'No conflict file selected' };
|
|
25352
|
+
const worktreeFiles = context.worktree?.files || [];
|
|
25353
|
+
const fileIndex = worktreeFiles.findIndex((f) => f.path === path);
|
|
25354
|
+
if (fileIndex >= 0) {
|
|
25355
|
+
dispatch({ type: 'navigateOpenDiffForWorktreeFile', fileIndex });
|
|
25356
|
+
return { ok: true, message: `Viewing diff for ${path}` };
|
|
25357
|
+
}
|
|
25358
|
+
// File not in worktree list (e.g. deleted-by-us) — open in
|
|
25359
|
+
// editor as fallback so the user can still inspect it.
|
|
25360
|
+
return { ok: true, message: `${path} not in worktree diff list` };
|
|
25361
|
+
},
|
|
25362
|
+
'continue-operation': async () => {
|
|
25363
|
+
const operation = context.operation?.operation;
|
|
25364
|
+
if (!operation || operation === 'none') {
|
|
25365
|
+
return { ok: false, message: 'No git operation in progress' };
|
|
25366
|
+
}
|
|
25367
|
+
if ((context.operation?.conflictedFiles.length ?? 0) > 0) {
|
|
25368
|
+
return { ok: false, message: 'Resolve all conflicts before continuing' };
|
|
25369
|
+
}
|
|
25370
|
+
return continueOperation(git, operation);
|
|
25371
|
+
},
|
|
25205
25372
|
'open-pr': async () => {
|
|
25206
25373
|
const repo = context.provider?.repository;
|
|
25207
25374
|
if (!repo || repo.provider !== 'github' || !repo.owner || !repo.name) {
|
|
@@ -25725,6 +25892,14 @@ function LogInkApp(deps) {
|
|
|
25725
25892
|
? selected?.hash
|
|
25726
25893
|
: undefined,
|
|
25727
25894
|
worktreeDirty,
|
|
25895
|
+
conflictFileCount: context.operation?.conflictedFiles.length,
|
|
25896
|
+
conflictSelectedPath: (() => {
|
|
25897
|
+
const files = context.operation?.conflictedFiles;
|
|
25898
|
+
if (!files || files.length === 0)
|
|
25899
|
+
return undefined;
|
|
25900
|
+
const clamped = Math.min(state.selectedConflictFileIndex, files.length - 1);
|
|
25901
|
+
return files[clamped]?.path;
|
|
25902
|
+
})(),
|
|
25728
25903
|
// H / gH need the actual diff text (not just hunk offsets) to
|
|
25729
25904
|
// slice the cursored hunk into a `git apply` patch. Stash uses
|
|
25730
25905
|
// the full `git stash show -p` output; commit-diff uses the
|
|
@@ -26089,6 +26264,9 @@ function renderMainPanel(h, components, state, context, contextStatus, worktreeD
|
|
|
26089
26264
|
if (state.activeView === 'pull-request') {
|
|
26090
26265
|
return renderPullRequestSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
26091
26266
|
}
|
|
26267
|
+
if (state.activeView === 'conflicts') {
|
|
26268
|
+
return renderConflictsSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
26269
|
+
}
|
|
26092
26270
|
return renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits);
|
|
26093
26271
|
}
|
|
26094
26272
|
function renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
|
|
@@ -26272,6 +26450,79 @@ function buildStatusSurfaceRows(groups) {
|
|
|
26272
26450
|
}
|
|
26273
26451
|
return rows;
|
|
26274
26452
|
}
|
|
26453
|
+
function renderConflictsSurface(h, components, state, context, contextStatus, bodyRows, width, theme) {
|
|
26454
|
+
const { Box, Text } = components;
|
|
26455
|
+
const focused = state.focus === 'commits';
|
|
26456
|
+
const loading = isLogInkContextKeyLoading(contextStatus, 'operation');
|
|
26457
|
+
const operation = context.operation;
|
|
26458
|
+
const conflictedFiles = operation?.conflictedFiles || [];
|
|
26459
|
+
const operationType = operation?.operation || 'none';
|
|
26460
|
+
// If no operation is in progress, show a fallback message.
|
|
26461
|
+
if (!loading && operationType === 'none') {
|
|
26462
|
+
return h(Box, {
|
|
26463
|
+
borderColor: focusBorderColor(theme, focused),
|
|
26464
|
+
borderStyle: theme.borderStyle,
|
|
26465
|
+
flexDirection: 'column',
|
|
26466
|
+
flexShrink: 0,
|
|
26467
|
+
paddingX: 1,
|
|
26468
|
+
width,
|
|
26469
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Conflicts', focused)), h(Text, { dimColor: true }, 'no operation in progress')), h(Text, { key: 'conflicts-empty', dimColor: true }, 'No merge, rebase, cherry-pick, or revert in progress.'));
|
|
26470
|
+
}
|
|
26471
|
+
// All conflicts resolved — show the "continue" hint.
|
|
26472
|
+
if (!loading && conflictedFiles.length === 0 && operationType !== 'none') {
|
|
26473
|
+
return h(Box, {
|
|
26474
|
+
borderColor: focusBorderColor(theme, focused),
|
|
26475
|
+
borderStyle: theme.borderStyle,
|
|
26476
|
+
flexDirection: 'column',
|
|
26477
|
+
flexShrink: 0,
|
|
26478
|
+
paddingX: 1,
|
|
26479
|
+
width,
|
|
26480
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Conflicts', focused)), h(Text, { dimColor: true }, `${operationType} — all conflicts resolved`)), h(Text, { key: 'conflicts-hint', dimColor: true }, `All conflicts resolved. Press C to continue the ${operationType}, or < to go back.`));
|
|
26481
|
+
}
|
|
26482
|
+
const selected = Math.max(0, Math.min(state.selectedConflictFileIndex, Math.max(0, conflictedFiles.length - 1)));
|
|
26483
|
+
const listRows = Math.max(4, bodyRows - 4);
|
|
26484
|
+
const startIndex = Math.max(0, selected - Math.floor(listRows / 2));
|
|
26485
|
+
const visible = conflictedFiles.slice(startIndex, startIndex + listRows);
|
|
26486
|
+
const remaining = conflictedFiles.length;
|
|
26487
|
+
const headerRight = loading
|
|
26488
|
+
? 'loading conflicts'
|
|
26489
|
+
: `${operationType} — ${remaining} ${remaining === 1 ? 'conflict' : 'conflicts'} remaining`;
|
|
26490
|
+
const statusLabel = (file) => {
|
|
26491
|
+
const code = `${file.indexStatus}${file.worktreeStatus}`;
|
|
26492
|
+
switch (code) {
|
|
26493
|
+
case 'UU': return 'both modified';
|
|
26494
|
+
case 'AA': return 'added by both';
|
|
26495
|
+
case 'DD': return 'both deleted';
|
|
26496
|
+
case 'AU':
|
|
26497
|
+
case 'UA': return 'added by one';
|
|
26498
|
+
case 'DU': return 'deleted by us';
|
|
26499
|
+
case 'UD': return 'deleted by them';
|
|
26500
|
+
default: return code;
|
|
26501
|
+
}
|
|
26502
|
+
};
|
|
26503
|
+
const lines = loading
|
|
26504
|
+
? [h(Text, { key: 'conflicts-loading', dimColor: true }, formatLogInkLoading({ resource: 'conflicts' }))]
|
|
26505
|
+
: visible.map((file, offset) => {
|
|
26506
|
+
const index = startIndex + offset;
|
|
26507
|
+
const isSelected = index === selected;
|
|
26508
|
+
const cursor = isSelected ? '>' : ' ';
|
|
26509
|
+
const code = `${file.indexStatus}${file.worktreeStatus}`;
|
|
26510
|
+
const label = statusLabel(file);
|
|
26511
|
+
return h(Text, {
|
|
26512
|
+
key: `conflict-${index}`,
|
|
26513
|
+
bold: isSelected,
|
|
26514
|
+
dimColor: !isSelected,
|
|
26515
|
+
}, truncate$1(`${cursor} ${code} ${file.path} (${label})`, width - 4));
|
|
26516
|
+
});
|
|
26517
|
+
return h(Box, {
|
|
26518
|
+
borderColor: focusBorderColor(theme, focused),
|
|
26519
|
+
borderStyle: theme.borderStyle,
|
|
26520
|
+
flexDirection: 'column',
|
|
26521
|
+
flexShrink: 0,
|
|
26522
|
+
paddingX: 1,
|
|
26523
|
+
width,
|
|
26524
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Conflicts', focused)), h(Text, { dimColor: true }, headerRight)), ...lines);
|
|
26525
|
+
}
|
|
26275
26526
|
function renderStatusSurface(h, components, state, context, contextStatus, bodyRows, width, theme) {
|
|
26276
26527
|
const { Box, Text } = components;
|
|
26277
26528
|
const focused = state.focus === 'commits';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-coco",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "zero-effort git commits with coco.",
|
|
5
5
|
"author": "gfargo <ghfargo@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"test": "npm run test:jest && npm run test:publish",
|
|
40
40
|
"test:publish": "npm run lint && npm run build && npm run test:cli && npm pack --dry-run",
|
|
41
41
|
"test:cli": "tsx bin/smokeCli.ts",
|
|
42
|
+
"bench": "tsx bin/benchmark.ts",
|
|
42
43
|
"pretest:jest": "npm run build:info",
|
|
43
44
|
"test:jest": "jest",
|
|
44
45
|
"test:jest:watch": "jest --watch",
|