git-coco 0.41.2 → 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 +232 -3
- package/dist/index.js +233 -4
- 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,
|
|
@@ -15369,6 +15388,13 @@ const LOG_INK_KEY_BINDINGS = [
|
|
|
15369
15388
|
description: 'Push the dedicated pull-request action panel for the current branch.',
|
|
15370
15389
|
contexts: ['normal'],
|
|
15371
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
|
+
},
|
|
15372
15398
|
{
|
|
15373
15399
|
id: 'navigateBack',
|
|
15374
15400
|
keys: ['<', 'esc'],
|
|
@@ -15498,6 +15524,7 @@ const GLOBAL_BINDING_IDS = [
|
|
|
15498
15524
|
'navigateStash',
|
|
15499
15525
|
'navigateWorktrees',
|
|
15500
15526
|
'navigatePullRequest',
|
|
15527
|
+
'navigateConflicts',
|
|
15501
15528
|
'navigateBack',
|
|
15502
15529
|
];
|
|
15503
15530
|
const NORMAL_GLOBAL_HINTS = ['g jump', '< back', '? help', ': cmds', 'q quit'];
|
|
@@ -15670,6 +15697,12 @@ function getLogInkFooterHints(options) {
|
|
|
15670
15697
|
global: NORMAL_GLOBAL_HINTS,
|
|
15671
15698
|
};
|
|
15672
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
|
+
}
|
|
15673
15706
|
return {
|
|
15674
15707
|
// History view default hints. Mutating ops (`c` cherry-pick, `R`
|
|
15675
15708
|
// revert, `Z` reset, `i` interactive-rebase) all route through a
|
|
@@ -16416,6 +16449,7 @@ function createLogInkState(rows, options = {}) {
|
|
|
16416
16449
|
selectedTagIndex: 0,
|
|
16417
16450
|
selectedStashIndex: 0,
|
|
16418
16451
|
selectedWorktreeListIndex: 0,
|
|
16452
|
+
selectedConflictFileIndex: 0,
|
|
16419
16453
|
branchSort: DEFAULT_BRANCH_SORT_MODE,
|
|
16420
16454
|
tagSort: DEFAULT_TAG_SORT_MODE,
|
|
16421
16455
|
paletteFilter: '',
|
|
@@ -16669,6 +16703,12 @@ function applyLogInkAction(state, action) {
|
|
|
16669
16703
|
selectedWorktreeListIndex: clampIndex$1(state.selectedWorktreeListIndex + action.delta, action.count),
|
|
16670
16704
|
pendingKey: undefined,
|
|
16671
16705
|
};
|
|
16706
|
+
case 'moveConflictFile':
|
|
16707
|
+
return {
|
|
16708
|
+
...state,
|
|
16709
|
+
selectedConflictFileIndex: clampIndex$1(state.selectedConflictFileIndex + action.delta, action.count),
|
|
16710
|
+
pendingKey: undefined,
|
|
16711
|
+
};
|
|
16672
16712
|
case 'cycleBranchSort':
|
|
16673
16713
|
return {
|
|
16674
16714
|
...state,
|
|
@@ -17337,6 +17377,8 @@ function getLogInkPaletteExecuteEvents(command, state) {
|
|
|
17337
17377
|
return [action({ type: 'pushView', value: 'worktrees' })];
|
|
17338
17378
|
case 'navigatePullRequest':
|
|
17339
17379
|
return [action({ type: 'pushView', value: 'pull-request' })];
|
|
17380
|
+
case 'navigateConflicts':
|
|
17381
|
+
return [action({ type: 'pushView', value: 'conflicts' })];
|
|
17340
17382
|
case 'navigateBack':
|
|
17341
17383
|
return [action({ type: 'popView' })];
|
|
17342
17384
|
case 'openSelected': {
|
|
@@ -17808,6 +17850,12 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
17808
17850
|
action({ type: 'setStatus', value: 'jumped to pull request' }),
|
|
17809
17851
|
];
|
|
17810
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
|
+
}
|
|
17811
17859
|
// `gH` chord: apply the cursored hunk to the index (`git apply
|
|
17812
17860
|
// --cached`). Sibling of bare `H` which targets the worktree.
|
|
17813
17861
|
// Discoverable via the footer hint on diff views and the help
|
|
@@ -18117,6 +18165,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18117
18165
|
if (isWorktreeActionTarget(state) && context.worktreeListCount) {
|
|
18118
18166
|
return [action({ type: 'moveWorktreeListEntry', delta: -1, count: context.worktreeListCount })];
|
|
18119
18167
|
}
|
|
18168
|
+
if (state.activeView === 'conflicts' && context.conflictFileCount) {
|
|
18169
|
+
return [action({ type: 'moveConflictFile', delta: -1, count: context.conflictFileCount })];
|
|
18170
|
+
}
|
|
18120
18171
|
if (state.activeView === 'history' &&
|
|
18121
18172
|
state.focus === 'commits' &&
|
|
18122
18173
|
state.selectedIndex === 0 &&
|
|
@@ -18195,6 +18246,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18195
18246
|
if (isWorktreeActionTarget(state) && context.worktreeListCount) {
|
|
18196
18247
|
return [action({ type: 'moveWorktreeListEntry', delta: 1, count: context.worktreeListCount })];
|
|
18197
18248
|
}
|
|
18249
|
+
if (state.activeView === 'conflicts' && context.conflictFileCount) {
|
|
18250
|
+
return [action({ type: 'moveConflictFile', delta: 1, count: context.conflictFileCount })];
|
|
18251
|
+
}
|
|
18198
18252
|
return [
|
|
18199
18253
|
action(state.focus === 'sidebar'
|
|
18200
18254
|
? { type: 'nextSidebarTab' }
|
|
@@ -18380,6 +18434,11 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18380
18434
|
fileIndex: state.selectedWorktreeFileIndex,
|
|
18381
18435
|
})];
|
|
18382
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
|
+
}
|
|
18383
18442
|
// Enter on a branch row checks the branch out. Non-destructive workflow
|
|
18384
18443
|
// action — no confirmation prompt. Fires from either the dedicated
|
|
18385
18444
|
// branches view or from the sidebar when the branches tab is focused
|
|
@@ -18521,6 +18580,32 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18521
18580
|
if (inputValue === 'o' && state.activeView === 'diff' && state.diffSource === 'stash' && context.stashDiffSelectedPath) {
|
|
18522
18581
|
return [{ type: 'openFileInEditor', path: context.stashDiffSelectedPath }];
|
|
18523
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
|
+
}
|
|
18524
18609
|
// `c` on a stash diff cherry-picks the file under the cursor —
|
|
18525
18610
|
// materializes that single path from the stash into the working tree
|
|
18526
18611
|
// (`git checkout <stashRef> -- <path>`). Routed through the y-confirm
|
|
@@ -21817,6 +21902,21 @@ function skipOperation(git, operation) {
|
|
|
21817
21902
|
}
|
|
21818
21903
|
return runAction(() => git.raw(command.args), command.successMessage);
|
|
21819
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
|
+
}
|
|
21820
21920
|
|
|
21821
21921
|
function openProviderUrl(repository, target, openUrl = defaultOpenUrlRunner) {
|
|
21822
21922
|
const url = repository ? buildProviderUrl(repository, target) : undefined;
|
|
@@ -25199,6 +25299,51 @@ function LogInkApp(deps) {
|
|
|
25199
25299
|
}
|
|
25200
25300
|
return abortOperation(git, operation);
|
|
25201
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
|
+
},
|
|
25202
25347
|
'open-pr': async () => {
|
|
25203
25348
|
const repo = context.provider?.repository;
|
|
25204
25349
|
if (!repo || repo.provider !== 'github' || !repo.owner || !repo.name) {
|
|
@@ -25722,6 +25867,14 @@ function LogInkApp(deps) {
|
|
|
25722
25867
|
? selected?.hash
|
|
25723
25868
|
: undefined,
|
|
25724
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
|
+
})(),
|
|
25725
25878
|
// H / gH need the actual diff text (not just hunk offsets) to
|
|
25726
25879
|
// slice the cursored hunk into a `git apply` patch. Stash uses
|
|
25727
25880
|
// the full `git stash show -p` output; commit-diff uses the
|
|
@@ -26086,6 +26239,9 @@ function renderMainPanel(h, components, state, context, contextStatus, worktreeD
|
|
|
26086
26239
|
if (state.activeView === 'pull-request') {
|
|
26087
26240
|
return renderPullRequestSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
26088
26241
|
}
|
|
26242
|
+
if (state.activeView === 'conflicts') {
|
|
26243
|
+
return renderConflictsSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
26244
|
+
}
|
|
26089
26245
|
return renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits);
|
|
26090
26246
|
}
|
|
26091
26247
|
function renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
|
|
@@ -26269,6 +26425,79 @@ function buildStatusSurfaceRows(groups) {
|
|
|
26269
26425
|
}
|
|
26270
26426
|
return rows;
|
|
26271
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
|
+
}
|
|
26272
26501
|
function renderStatusSurface(h, components, state, context, contextStatus, bodyRows, width, theme) {
|
|
26273
26502
|
const { Box, Text } = components;
|
|
26274
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,
|
|
@@ -15394,6 +15413,13 @@ const LOG_INK_KEY_BINDINGS = [
|
|
|
15394
15413
|
description: 'Push the dedicated pull-request action panel for the current branch.',
|
|
15395
15414
|
contexts: ['normal'],
|
|
15396
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
|
+
},
|
|
15397
15423
|
{
|
|
15398
15424
|
id: 'navigateBack',
|
|
15399
15425
|
keys: ['<', 'esc'],
|
|
@@ -15523,6 +15549,7 @@ const GLOBAL_BINDING_IDS = [
|
|
|
15523
15549
|
'navigateStash',
|
|
15524
15550
|
'navigateWorktrees',
|
|
15525
15551
|
'navigatePullRequest',
|
|
15552
|
+
'navigateConflicts',
|
|
15526
15553
|
'navigateBack',
|
|
15527
15554
|
];
|
|
15528
15555
|
const NORMAL_GLOBAL_HINTS = ['g jump', '< back', '? help', ': cmds', 'q quit'];
|
|
@@ -15695,6 +15722,12 @@ function getLogInkFooterHints(options) {
|
|
|
15695
15722
|
global: NORMAL_GLOBAL_HINTS,
|
|
15696
15723
|
};
|
|
15697
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
|
+
}
|
|
15698
15731
|
return {
|
|
15699
15732
|
// History view default hints. Mutating ops (`c` cherry-pick, `R`
|
|
15700
15733
|
// revert, `Z` reset, `i` interactive-rebase) all route through a
|
|
@@ -16441,6 +16474,7 @@ function createLogInkState(rows, options = {}) {
|
|
|
16441
16474
|
selectedTagIndex: 0,
|
|
16442
16475
|
selectedStashIndex: 0,
|
|
16443
16476
|
selectedWorktreeListIndex: 0,
|
|
16477
|
+
selectedConflictFileIndex: 0,
|
|
16444
16478
|
branchSort: DEFAULT_BRANCH_SORT_MODE,
|
|
16445
16479
|
tagSort: DEFAULT_TAG_SORT_MODE,
|
|
16446
16480
|
paletteFilter: '',
|
|
@@ -16694,6 +16728,12 @@ function applyLogInkAction(state, action) {
|
|
|
16694
16728
|
selectedWorktreeListIndex: clampIndex$1(state.selectedWorktreeListIndex + action.delta, action.count),
|
|
16695
16729
|
pendingKey: undefined,
|
|
16696
16730
|
};
|
|
16731
|
+
case 'moveConflictFile':
|
|
16732
|
+
return {
|
|
16733
|
+
...state,
|
|
16734
|
+
selectedConflictFileIndex: clampIndex$1(state.selectedConflictFileIndex + action.delta, action.count),
|
|
16735
|
+
pendingKey: undefined,
|
|
16736
|
+
};
|
|
16697
16737
|
case 'cycleBranchSort':
|
|
16698
16738
|
return {
|
|
16699
16739
|
...state,
|
|
@@ -17362,6 +17402,8 @@ function getLogInkPaletteExecuteEvents(command, state) {
|
|
|
17362
17402
|
return [action({ type: 'pushView', value: 'worktrees' })];
|
|
17363
17403
|
case 'navigatePullRequest':
|
|
17364
17404
|
return [action({ type: 'pushView', value: 'pull-request' })];
|
|
17405
|
+
case 'navigateConflicts':
|
|
17406
|
+
return [action({ type: 'pushView', value: 'conflicts' })];
|
|
17365
17407
|
case 'navigateBack':
|
|
17366
17408
|
return [action({ type: 'popView' })];
|
|
17367
17409
|
case 'openSelected': {
|
|
@@ -17833,6 +17875,12 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
17833
17875
|
action({ type: 'setStatus', value: 'jumped to pull request' }),
|
|
17834
17876
|
];
|
|
17835
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
|
+
}
|
|
17836
17884
|
// `gH` chord: apply the cursored hunk to the index (`git apply
|
|
17837
17885
|
// --cached`). Sibling of bare `H` which targets the worktree.
|
|
17838
17886
|
// Discoverable via the footer hint on diff views and the help
|
|
@@ -18142,6 +18190,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18142
18190
|
if (isWorktreeActionTarget(state) && context.worktreeListCount) {
|
|
18143
18191
|
return [action({ type: 'moveWorktreeListEntry', delta: -1, count: context.worktreeListCount })];
|
|
18144
18192
|
}
|
|
18193
|
+
if (state.activeView === 'conflicts' && context.conflictFileCount) {
|
|
18194
|
+
return [action({ type: 'moveConflictFile', delta: -1, count: context.conflictFileCount })];
|
|
18195
|
+
}
|
|
18145
18196
|
if (state.activeView === 'history' &&
|
|
18146
18197
|
state.focus === 'commits' &&
|
|
18147
18198
|
state.selectedIndex === 0 &&
|
|
@@ -18220,6 +18271,9 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18220
18271
|
if (isWorktreeActionTarget(state) && context.worktreeListCount) {
|
|
18221
18272
|
return [action({ type: 'moveWorktreeListEntry', delta: 1, count: context.worktreeListCount })];
|
|
18222
18273
|
}
|
|
18274
|
+
if (state.activeView === 'conflicts' && context.conflictFileCount) {
|
|
18275
|
+
return [action({ type: 'moveConflictFile', delta: 1, count: context.conflictFileCount })];
|
|
18276
|
+
}
|
|
18223
18277
|
return [
|
|
18224
18278
|
action(state.focus === 'sidebar'
|
|
18225
18279
|
? { type: 'nextSidebarTab' }
|
|
@@ -18405,6 +18459,11 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18405
18459
|
fileIndex: state.selectedWorktreeFileIndex,
|
|
18406
18460
|
})];
|
|
18407
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
|
+
}
|
|
18408
18467
|
// Enter on a branch row checks the branch out. Non-destructive workflow
|
|
18409
18468
|
// action — no confirmation prompt. Fires from either the dedicated
|
|
18410
18469
|
// branches view or from the sidebar when the branches tab is focused
|
|
@@ -18546,6 +18605,32 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
18546
18605
|
if (inputValue === 'o' && state.activeView === 'diff' && state.diffSource === 'stash' && context.stashDiffSelectedPath) {
|
|
18547
18606
|
return [{ type: 'openFileInEditor', path: context.stashDiffSelectedPath }];
|
|
18548
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
|
+
}
|
|
18549
18634
|
// `c` on a stash diff cherry-picks the file under the cursor —
|
|
18550
18635
|
// materializes that single path from the stash into the working tree
|
|
18551
18636
|
// (`git checkout <stashRef> -- <path>`). Routed through the y-confirm
|
|
@@ -21842,6 +21927,21 @@ function skipOperation(git, operation) {
|
|
|
21842
21927
|
}
|
|
21843
21928
|
return runAction(() => git.raw(command.args), command.successMessage);
|
|
21844
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
|
+
}
|
|
21845
21945
|
|
|
21846
21946
|
function openProviderUrl(repository, target, openUrl = defaultOpenUrlRunner) {
|
|
21847
21947
|
const url = repository ? buildProviderUrl(repository, target) : undefined;
|
|
@@ -25224,6 +25324,51 @@ function LogInkApp(deps) {
|
|
|
25224
25324
|
}
|
|
25225
25325
|
return abortOperation(git, operation);
|
|
25226
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
|
+
},
|
|
25227
25372
|
'open-pr': async () => {
|
|
25228
25373
|
const repo = context.provider?.repository;
|
|
25229
25374
|
if (!repo || repo.provider !== 'github' || !repo.owner || !repo.name) {
|
|
@@ -25747,6 +25892,14 @@ function LogInkApp(deps) {
|
|
|
25747
25892
|
? selected?.hash
|
|
25748
25893
|
: undefined,
|
|
25749
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
|
+
})(),
|
|
25750
25903
|
// H / gH need the actual diff text (not just hunk offsets) to
|
|
25751
25904
|
// slice the cursored hunk into a `git apply` patch. Stash uses
|
|
25752
25905
|
// the full `git stash show -p` output; commit-diff uses the
|
|
@@ -26111,6 +26264,9 @@ function renderMainPanel(h, components, state, context, contextStatus, worktreeD
|
|
|
26111
26264
|
if (state.activeView === 'pull-request') {
|
|
26112
26265
|
return renderPullRequestSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
26113
26266
|
}
|
|
26267
|
+
if (state.activeView === 'conflicts') {
|
|
26268
|
+
return renderConflictsSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
26269
|
+
}
|
|
26114
26270
|
return renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits);
|
|
26115
26271
|
}
|
|
26116
26272
|
function renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits) {
|
|
@@ -26294,6 +26450,79 @@ function buildStatusSurfaceRows(groups) {
|
|
|
26294
26450
|
}
|
|
26295
26451
|
return rows;
|
|
26296
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
|
+
}
|
|
26297
26526
|
function renderStatusSurface(h, components, state, context, contextStatus, bodyRows, width, theme) {
|
|
26298
26527
|
const { Box, Text } = components;
|
|
26299
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",
|