git-coco 0.48.0 → 0.49.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 +1031 -8
- package/dist/index.js +1029 -8
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -78,7 +78,7 @@ var readline__namespace = /*#__PURE__*/_interopNamespaceDefault(readline);
|
|
|
78
78
|
/**
|
|
79
79
|
* Current build version from package.json
|
|
80
80
|
*/
|
|
81
|
-
const BUILD_VERSION = "0.
|
|
81
|
+
const BUILD_VERSION = "0.49.0";
|
|
82
82
|
|
|
83
83
|
const isInteractive = (config) => {
|
|
84
84
|
return config?.mode === 'interactive' || !!config?.interactive;
|
|
@@ -15530,6 +15530,54 @@ async function getDefaultBranch(repository, runner) {
|
|
|
15530
15530
|
return undefined;
|
|
15531
15531
|
}
|
|
15532
15532
|
}
|
|
15533
|
+
/**
|
|
15534
|
+
* Local-only fallback for the default branch — used when no GitHub
|
|
15535
|
+
* remote is configured, when `gh` isn't authenticated, or when
|
|
15536
|
+
* `gh repo view` fails (e.g. private repo we can't access, offline).
|
|
15537
|
+
*
|
|
15538
|
+
* Detection order, picking the first that resolves:
|
|
15539
|
+
* 1. `origin/HEAD` — the symbolic ref set by `git clone` pointing at
|
|
15540
|
+
* whatever the remote's default branch was at clone time. This is
|
|
15541
|
+
* the most authoritative local signal.
|
|
15542
|
+
* 2. Conventional branch names checked against local refs in order:
|
|
15543
|
+
* `main`, `master`, `develop`, `trunk`.
|
|
15544
|
+
*
|
|
15545
|
+
* Returns `undefined` when nothing matches — caller surfaces that as
|
|
15546
|
+
* "no default branch detected" without claiming any particular cause.
|
|
15547
|
+
*
|
|
15548
|
+
* Pure local-ref reads (no network) — safe to call on every overview
|
|
15549
|
+
* load regardless of provider state.
|
|
15550
|
+
*/
|
|
15551
|
+
async function detectLocalDefaultBranch(git) {
|
|
15552
|
+
// origin/HEAD — set by `git clone` to track the remote's HEAD. The
|
|
15553
|
+
// symbolic-ref output is the full ref (refs/remotes/origin/main); we
|
|
15554
|
+
// strip the prefix to get just the branch name. `--short` would do it
|
|
15555
|
+
// too but isn't supported on older git, and the prefix is fixed-length.
|
|
15556
|
+
try {
|
|
15557
|
+
const ref = (await git.raw(['symbolic-ref', 'refs/remotes/origin/HEAD'])).trim();
|
|
15558
|
+
const match = ref.match(/^refs\/remotes\/origin\/(.+)$/);
|
|
15559
|
+
if (match) {
|
|
15560
|
+
return match[1];
|
|
15561
|
+
}
|
|
15562
|
+
}
|
|
15563
|
+
catch {
|
|
15564
|
+
// symbolic-ref returns non-zero when origin/HEAD doesn't exist —
|
|
15565
|
+
// expected on fresh repos and `git init`-only working trees. Fall
|
|
15566
|
+
// through to the conventional-name check.
|
|
15567
|
+
}
|
|
15568
|
+
// Conventional names — most repos follow one of these. `rev-parse
|
|
15569
|
+
// --verify --quiet <ref>` returns 0 + hash on hit, non-zero on miss.
|
|
15570
|
+
for (const candidate of ['main', 'master', 'develop', 'trunk']) {
|
|
15571
|
+
try {
|
|
15572
|
+
await git.raw(['rev-parse', '--verify', '--quiet', `refs/heads/${candidate}`]);
|
|
15573
|
+
return candidate;
|
|
15574
|
+
}
|
|
15575
|
+
catch {
|
|
15576
|
+
// Not present — try the next one.
|
|
15577
|
+
}
|
|
15578
|
+
}
|
|
15579
|
+
return undefined;
|
|
15580
|
+
}
|
|
15533
15581
|
async function getCurrentPullRequest(runner) {
|
|
15534
15582
|
try {
|
|
15535
15583
|
return parsePullRequestJson(await runner([
|
|
@@ -15544,9 +15592,14 @@ async function getCurrentPullRequest(runner) {
|
|
|
15544
15592
|
}
|
|
15545
15593
|
}
|
|
15546
15594
|
async function getProviderOverview(git, runner = defaultGhRunner) {
|
|
15547
|
-
const [remotes, currentBranchOutput] = await Promise.all([
|
|
15595
|
+
const [remotes, currentBranchOutput, localDefaultBranch] = await Promise.all([
|
|
15548
15596
|
git.getRemotes(true),
|
|
15549
15597
|
git.raw(['branch', '--show-current']),
|
|
15598
|
+
// Read local default-branch signal up-front in parallel — used as
|
|
15599
|
+
// the fallback when gh is unavailable / unauthenticated / can't see
|
|
15600
|
+
// the repo. Coco aims to be platform-agnostic + work offline; the
|
|
15601
|
+
// GH-specific paths layer on top of this, they don't replace it.
|
|
15602
|
+
detectLocalDefaultBranch(git),
|
|
15550
15603
|
]);
|
|
15551
15604
|
const remote = remotes.find((entry) => entry.name === 'origin') || remotes[0];
|
|
15552
15605
|
const remoteUrl = remote?.refs.push || remote?.refs.fetch;
|
|
@@ -15560,7 +15613,10 @@ async function getProviderOverview(git, runner = defaultGhRunner) {
|
|
|
15560
15613
|
const currentBranch = currentBranchOutput.trim() || undefined;
|
|
15561
15614
|
if (repository.provider !== 'github') {
|
|
15562
15615
|
return {
|
|
15563
|
-
repository
|
|
15616
|
+
repository: {
|
|
15617
|
+
...repository,
|
|
15618
|
+
defaultBranch: localDefaultBranch,
|
|
15619
|
+
},
|
|
15564
15620
|
currentBranch,
|
|
15565
15621
|
authenticated: false,
|
|
15566
15622
|
message: repository.message || 'Unsupported remote provider.',
|
|
@@ -15571,20 +15627,27 @@ async function getProviderOverview(git, runner = defaultGhRunner) {
|
|
|
15571
15627
|
}
|
|
15572
15628
|
catch {
|
|
15573
15629
|
return {
|
|
15574
|
-
repository
|
|
15630
|
+
repository: {
|
|
15631
|
+
...repository,
|
|
15632
|
+
defaultBranch: localDefaultBranch,
|
|
15633
|
+
},
|
|
15575
15634
|
currentBranch,
|
|
15576
15635
|
authenticated: false,
|
|
15577
15636
|
message: 'GitHub CLI is missing or not authenticated.',
|
|
15578
15637
|
};
|
|
15579
15638
|
}
|
|
15580
|
-
const [
|
|
15639
|
+
const [providerDefaultBranch, currentPullRequest] = await Promise.all([
|
|
15581
15640
|
getDefaultBranch(repository, runner),
|
|
15582
15641
|
getCurrentPullRequest(runner),
|
|
15583
15642
|
]);
|
|
15584
15643
|
return {
|
|
15585
15644
|
repository: {
|
|
15586
15645
|
...repository,
|
|
15587
|
-
|
|
15646
|
+
// gh's answer wins when it has one — it knows the remote's
|
|
15647
|
+
// current state, including custom default-branch settings the
|
|
15648
|
+
// local refs can't reflect. Fall back to local detection when gh
|
|
15649
|
+
// returns undefined (offline, private repo, transient failure).
|
|
15650
|
+
defaultBranch: providerDefaultBranch || localDefaultBranch,
|
|
15588
15651
|
},
|
|
15589
15652
|
currentBranch,
|
|
15590
15653
|
currentPullRequest,
|
|
@@ -16550,6 +16613,149 @@ async function runCommitDraftWorkflow(input = {}) {
|
|
|
16550
16613
|
}
|
|
16551
16614
|
}
|
|
16552
16615
|
|
|
16616
|
+
function createChangelogArgv(input) {
|
|
16617
|
+
return {
|
|
16618
|
+
$0: 'coco',
|
|
16619
|
+
_: ['changelog'],
|
|
16620
|
+
interactive: false,
|
|
16621
|
+
verbose: true,
|
|
16622
|
+
version: false,
|
|
16623
|
+
help: false,
|
|
16624
|
+
mode: 'stdout',
|
|
16625
|
+
range: '',
|
|
16626
|
+
branch: '',
|
|
16627
|
+
tag: '',
|
|
16628
|
+
sinceLastTag: false,
|
|
16629
|
+
withDiff: false,
|
|
16630
|
+
onlyDiff: false,
|
|
16631
|
+
author: false,
|
|
16632
|
+
...input,
|
|
16633
|
+
};
|
|
16634
|
+
}
|
|
16635
|
+
async function captureStdout(action) {
|
|
16636
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
16637
|
+
let output = '';
|
|
16638
|
+
process.stdout.write = ((chunk, ...args) => {
|
|
16639
|
+
output += typeof chunk === 'string' ? chunk : chunk.toString();
|
|
16640
|
+
const callback = args.find((arg) => typeof arg === 'function');
|
|
16641
|
+
callback?.();
|
|
16642
|
+
return true;
|
|
16643
|
+
});
|
|
16644
|
+
try {
|
|
16645
|
+
await action();
|
|
16646
|
+
return output;
|
|
16647
|
+
}
|
|
16648
|
+
finally {
|
|
16649
|
+
process.stdout.write = originalWrite;
|
|
16650
|
+
}
|
|
16651
|
+
}
|
|
16652
|
+
/**
|
|
16653
|
+
* Generate a pull-request body for the current branch by running
|
|
16654
|
+
* `coco changelog --branch <base>` and parsing the title / content
|
|
16655
|
+
* out of the captured stdout.
|
|
16656
|
+
*
|
|
16657
|
+
* The changelog handler emits `${title}\n\n${content}[\n\nPart of <ticket>]`
|
|
16658
|
+
* (see `commands/changelog/handler.ts` line 306). We split on the first
|
|
16659
|
+
* blank-line boundary so the caller gets a clean title + body pair to
|
|
16660
|
+
* pre-fill the PR creation prompt with. Ticket footer (when present)
|
|
16661
|
+
* stays in the body so the resulting PR keeps the reference.
|
|
16662
|
+
*
|
|
16663
|
+
* Captures the raw stdout (rather than going through `runChangelogAction`,
|
|
16664
|
+
* which strips blank lines via its `compactOutputLines` filter) so the
|
|
16665
|
+
* title-vs-body separator survives intact.
|
|
16666
|
+
*
|
|
16667
|
+
* Returns the standard LogAiActionResult plus extracted `title` / `body`
|
|
16668
|
+
* fields. Falls back to undefined `title` / `body` when the changelog
|
|
16669
|
+
* fails or produces no parseable output; the caller is expected to
|
|
16670
|
+
* surface that as a prompt with empty fields rather than aborting.
|
|
16671
|
+
*/
|
|
16672
|
+
async function runPullRequestBodyWorkflow(input = {}) {
|
|
16673
|
+
const baseBranch = input.baseBranch || 'main';
|
|
16674
|
+
const argv = createChangelogArgv({ branch: baseBranch });
|
|
16675
|
+
let raw = '';
|
|
16676
|
+
try {
|
|
16677
|
+
raw = await captureStdout(() => handler$7(argv, new Logger({
|
|
16678
|
+
verbose: true,
|
|
16679
|
+
silent: false,
|
|
16680
|
+
})));
|
|
16681
|
+
}
|
|
16682
|
+
catch (error) {
|
|
16683
|
+
return {
|
|
16684
|
+
ok: false,
|
|
16685
|
+
message: error.message,
|
|
16686
|
+
};
|
|
16687
|
+
}
|
|
16688
|
+
const text = raw.trim();
|
|
16689
|
+
if (!text) {
|
|
16690
|
+
return {
|
|
16691
|
+
ok: false,
|
|
16692
|
+
message: 'No changelog output produced — branch may have no commits ahead of base.',
|
|
16693
|
+
};
|
|
16694
|
+
}
|
|
16695
|
+
// First blank-line boundary separates title from body. Falls back to
|
|
16696
|
+
// "everything is the title" when no blank line is found — typical of
|
|
16697
|
+
// very small changesets where the changelog content collapsed to one
|
|
16698
|
+
// line.
|
|
16699
|
+
const blankIdx = text.indexOf('\n\n');
|
|
16700
|
+
const title = blankIdx > 0 ? text.slice(0, blankIdx).trim() : text.split('\n')[0].trim();
|
|
16701
|
+
const body = blankIdx > 0 ? text.slice(blankIdx + 2).trim() : '';
|
|
16702
|
+
// Keep the standard LogAiActionResult shape (message + telemetry
|
|
16703
|
+
// details + editable text) so palette callers get a consistent
|
|
16704
|
+
// surface. The captured telemetry lines are dropped here — the PR
|
|
16705
|
+
// body should be the actionable content, not the LLM trace.
|
|
16706
|
+
return {
|
|
16707
|
+
ok: true,
|
|
16708
|
+
message: title || 'Pull request body drafted.',
|
|
16709
|
+
details: [],
|
|
16710
|
+
editable: text,
|
|
16711
|
+
title,
|
|
16712
|
+
body,
|
|
16713
|
+
};
|
|
16714
|
+
}
|
|
16715
|
+
/**
|
|
16716
|
+
* Run `coco changelog` and return the raw captured stdout, intact —
|
|
16717
|
+
* blank lines preserved, no telemetry stripping. Use this when you
|
|
16718
|
+
* want to show or copy the changelog as the user would see it from
|
|
16719
|
+
* the CLI (the chromed-up `runChangelogAction` collapses blank lines
|
|
16720
|
+
* via `compactOutputLines` which is wrong for any UI that wants the
|
|
16721
|
+
* full prose output).
|
|
16722
|
+
*
|
|
16723
|
+
* The argv defaults match `createChangelogArgv` — pass overrides via
|
|
16724
|
+
* `input`. Common shapes:
|
|
16725
|
+
*
|
|
16726
|
+
* - { branch: 'main' } — commits on current branch vs main
|
|
16727
|
+
* - { sinceLastTag: true } — since last tag
|
|
16728
|
+
* - { tag: 'v1.0.0' } — since a specific tag
|
|
16729
|
+
* - { range: 'abc..def' } — between two refs
|
|
16730
|
+
*
|
|
16731
|
+
* Returns:
|
|
16732
|
+
* - { ok: true, message, text } on success (message = first non-blank
|
|
16733
|
+
* line, useful for status surface; text = full raw output)
|
|
16734
|
+
* - { ok: false, message } on changelog handler error or empty output
|
|
16735
|
+
*/
|
|
16736
|
+
async function runChangelogTextWorkflow(input = {}) {
|
|
16737
|
+
const argv = createChangelogArgv(input);
|
|
16738
|
+
let raw = '';
|
|
16739
|
+
try {
|
|
16740
|
+
raw = await captureStdout(() => handler$7(argv, new Logger({
|
|
16741
|
+
verbose: true,
|
|
16742
|
+
silent: false,
|
|
16743
|
+
})));
|
|
16744
|
+
}
|
|
16745
|
+
catch (error) {
|
|
16746
|
+
return { ok: false, message: error.message };
|
|
16747
|
+
}
|
|
16748
|
+
const text = raw.trim();
|
|
16749
|
+
if (!text) {
|
|
16750
|
+
return {
|
|
16751
|
+
ok: false,
|
|
16752
|
+
message: 'No changelog output produced — branch may have no commits ahead of base.',
|
|
16753
|
+
};
|
|
16754
|
+
}
|
|
16755
|
+
const firstLine = text.split('\n').find((line) => line.trim()) || 'Changelog generated.';
|
|
16756
|
+
return { ok: true, message: firstLine, text };
|
|
16757
|
+
}
|
|
16758
|
+
|
|
16553
16759
|
const LOG_INK_CONTEXT_KEYS = [
|
|
16554
16760
|
'bisect',
|
|
16555
16761
|
'branches',
|
|
@@ -17427,7 +17633,14 @@ const LOG_INK_KEY_BINDINGS = [
|
|
|
17427
17633
|
id: 'editCommit',
|
|
17428
17634
|
keys: ['e'],
|
|
17429
17635
|
label: 'edit commit',
|
|
17430
|
-
description: 'Edit the manual commit summary or body.',
|
|
17636
|
+
description: 'Edit the manual commit summary or body inline.',
|
|
17637
|
+
contexts: ['commits'],
|
|
17638
|
+
},
|
|
17639
|
+
{
|
|
17640
|
+
id: 'editCommitExternal',
|
|
17641
|
+
keys: ['E'],
|
|
17642
|
+
label: 'edit in $EDITOR',
|
|
17643
|
+
description: 'Open the current commit draft in $EDITOR (or $VISUAL) for full editing, write-back on save.',
|
|
17431
17644
|
contexts: ['commits'],
|
|
17432
17645
|
},
|
|
17433
17646
|
{
|
|
@@ -17670,7 +17883,7 @@ function getLogInkFooterHints(options) {
|
|
|
17670
17883
|
}
|
|
17671
17884
|
if (options.activeView === 'compose') {
|
|
17672
17885
|
return {
|
|
17673
|
-
contextual: ['e edit', 'c commit', 'I AI draft', 'gs hunks', 'esc back'],
|
|
17886
|
+
contextual: ['e edit', 'E $EDITOR', 'c commit', 'I AI draft', 'gs hunks', 'esc back'],
|
|
17674
17887
|
global: NORMAL_GLOBAL_HINTS,
|
|
17675
17888
|
};
|
|
17676
17889
|
}
|
|
@@ -17738,6 +17951,12 @@ function getLogInkFooterHints(options) {
|
|
|
17738
17951
|
global: NORMAL_GLOBAL_HINTS,
|
|
17739
17952
|
};
|
|
17740
17953
|
}
|
|
17954
|
+
if (options.activeView === 'changelog') {
|
|
17955
|
+
return {
|
|
17956
|
+
contextual: ['j/k scroll', 'pg up/dn', 'y yank', 'E $EDITOR', 'c PR', 'r regen', '< back'],
|
|
17957
|
+
global: NORMAL_GLOBAL_HINTS,
|
|
17958
|
+
};
|
|
17959
|
+
}
|
|
17741
17960
|
if (options.compareBaseSet) {
|
|
17742
17961
|
// History view with a compare base set — Enter is overridden to
|
|
17743
17962
|
// open the compare diff; show the override + the bail-out key.
|
|
@@ -17970,6 +18189,10 @@ function formatSortIndicator(mode, options = {}) {
|
|
|
17970
18189
|
return `${options.ascii ? 'v' : '▼'} ${mode}`;
|
|
17971
18190
|
}
|
|
17972
18191
|
|
|
18192
|
+
const DEFAULT_CHANGELOG_VIEW_STATE = {
|
|
18193
|
+
status: 'idle',
|
|
18194
|
+
scrollOffset: 0,
|
|
18195
|
+
};
|
|
17973
18196
|
const DEFAULT_LOG_INK_STATUS_FILTER_MASK = {
|
|
17974
18197
|
staged: true,
|
|
17975
18198
|
unstaged: true,
|
|
@@ -18310,6 +18533,8 @@ function createLogInkState(rows, options = {}) {
|
|
|
18310
18533
|
inspectorTab: 'inspector',
|
|
18311
18534
|
inspectorActionIndex: 0,
|
|
18312
18535
|
bootLoading: options.bootLoading ?? false,
|
|
18536
|
+
changelogView: { ...DEFAULT_CHANGELOG_VIEW_STATE },
|
|
18537
|
+
changelogCache: {},
|
|
18313
18538
|
};
|
|
18314
18539
|
}
|
|
18315
18540
|
function getSelectedInkCommit(state) {
|
|
@@ -18933,6 +19158,105 @@ function applyLogInkAction(state, action) {
|
|
|
18933
19158
|
pendingKey: undefined,
|
|
18934
19159
|
};
|
|
18935
19160
|
}
|
|
19161
|
+
case 'setChangelogLoading':
|
|
19162
|
+
return {
|
|
19163
|
+
...state,
|
|
19164
|
+
changelogView: {
|
|
19165
|
+
status: 'loading',
|
|
19166
|
+
branch: action.branch,
|
|
19167
|
+
baseLabel: action.baseLabel,
|
|
19168
|
+
scrollOffset: 0,
|
|
19169
|
+
},
|
|
19170
|
+
pendingKey: undefined,
|
|
19171
|
+
};
|
|
19172
|
+
case 'setChangelogReady': {
|
|
19173
|
+
// Cache the result so re-entry (or `c` to PR) reuses it instead of
|
|
19174
|
+
// re-running the LLM. Keyed by branch so a checkout naturally
|
|
19175
|
+
// produces a fresh generation.
|
|
19176
|
+
const cached = {
|
|
19177
|
+
text: action.text,
|
|
19178
|
+
baseLabel: action.baseLabel,
|
|
19179
|
+
generatedAt: Date.now(),
|
|
19180
|
+
};
|
|
19181
|
+
return {
|
|
19182
|
+
...state,
|
|
19183
|
+
changelogView: {
|
|
19184
|
+
status: 'ready',
|
|
19185
|
+
text: action.text,
|
|
19186
|
+
branch: action.branch,
|
|
19187
|
+
baseLabel: action.baseLabel,
|
|
19188
|
+
scrollOffset: 0,
|
|
19189
|
+
},
|
|
19190
|
+
changelogCache: {
|
|
19191
|
+
...state.changelogCache,
|
|
19192
|
+
[action.branch]: cached,
|
|
19193
|
+
},
|
|
19194
|
+
pendingKey: undefined,
|
|
19195
|
+
};
|
|
19196
|
+
}
|
|
19197
|
+
case 'setChangelogError':
|
|
19198
|
+
return {
|
|
19199
|
+
...state,
|
|
19200
|
+
changelogView: {
|
|
19201
|
+
status: 'error',
|
|
19202
|
+
branch: action.branch,
|
|
19203
|
+
baseLabel: action.baseLabel,
|
|
19204
|
+
error: action.error,
|
|
19205
|
+
scrollOffset: 0,
|
|
19206
|
+
},
|
|
19207
|
+
pendingKey: undefined,
|
|
19208
|
+
};
|
|
19209
|
+
case 'setChangelogText': {
|
|
19210
|
+
// Used by the $EDITOR round-trip: user edits the cached text, we
|
|
19211
|
+
// update the view AND the cache entry so subsequent re-entry
|
|
19212
|
+
// reflects the edits. Branch key is taken from the current view
|
|
19213
|
+
// (which is what the user just edited against).
|
|
19214
|
+
if (state.changelogView.status !== 'ready' || !state.changelogView.branch) {
|
|
19215
|
+
return state;
|
|
19216
|
+
}
|
|
19217
|
+
const branch = state.changelogView.branch;
|
|
19218
|
+
const existing = state.changelogCache[branch];
|
|
19219
|
+
return {
|
|
19220
|
+
...state,
|
|
19221
|
+
changelogView: {
|
|
19222
|
+
...state.changelogView,
|
|
19223
|
+
text: action.text,
|
|
19224
|
+
},
|
|
19225
|
+
changelogCache: {
|
|
19226
|
+
...state.changelogCache,
|
|
19227
|
+
[branch]: {
|
|
19228
|
+
text: action.text,
|
|
19229
|
+
baseLabel: existing?.baseLabel || state.changelogView.baseLabel || '',
|
|
19230
|
+
// Updated-at timestamp reflects the edit. Not the original
|
|
19231
|
+
// generation time — `r` (regenerate) is the explicit knob
|
|
19232
|
+
// for "I want fresh LLM output, not my edits".
|
|
19233
|
+
generatedAt: Date.now(),
|
|
19234
|
+
},
|
|
19235
|
+
},
|
|
19236
|
+
pendingKey: undefined,
|
|
19237
|
+
};
|
|
19238
|
+
}
|
|
19239
|
+
case 'pageChangelog':
|
|
19240
|
+
return {
|
|
19241
|
+
...state,
|
|
19242
|
+
changelogView: {
|
|
19243
|
+
...state.changelogView,
|
|
19244
|
+
scrollOffset: clampIndex(state.changelogView.scrollOffset + action.delta, action.lineCount),
|
|
19245
|
+
},
|
|
19246
|
+
pendingKey: undefined,
|
|
19247
|
+
};
|
|
19248
|
+
case 'clearChangelogCache': {
|
|
19249
|
+
// Targeted clear for a single branch, or wholesale wipe when
|
|
19250
|
+
// `branch` is omitted. Wholesale used on session reset / config
|
|
19251
|
+
// change; targeted reserved for future "this generation looks
|
|
19252
|
+
// wrong, drop it" UX.
|
|
19253
|
+
if (!action.branch) {
|
|
19254
|
+
return { ...state, changelogCache: {}, pendingKey: undefined };
|
|
19255
|
+
}
|
|
19256
|
+
const next = { ...state.changelogCache };
|
|
19257
|
+
delete next[action.branch];
|
|
19258
|
+
return { ...state, changelogCache: next, pendingKey: undefined };
|
|
19259
|
+
}
|
|
18936
19260
|
default:
|
|
18937
19261
|
return state;
|
|
18938
19262
|
}
|
|
@@ -19474,6 +19798,17 @@ function submitInputPrompt(state) {
|
|
|
19474
19798
|
action({ type: 'closeInputPrompt' }),
|
|
19475
19799
|
];
|
|
19476
19800
|
}
|
|
19801
|
+
if (state.inputPrompt.kind === 'create-pr') {
|
|
19802
|
+
// Multi-line content: line 1 is the PR title, lines 2+ are the body
|
|
19803
|
+
// (leading blank line tolerated). The generic empty-value guard
|
|
19804
|
+
// above (line ~627) covers truly-empty submissions; the workflow
|
|
19805
|
+
// handler in app.ts has the belt-and-suspenders title check for
|
|
19806
|
+
// the "newline-then-body" edge.
|
|
19807
|
+
return [
|
|
19808
|
+
{ type: 'runWorkflowAction', id: 'create-pr', payload: value },
|
|
19809
|
+
action({ type: 'closeInputPrompt' }),
|
|
19810
|
+
];
|
|
19811
|
+
}
|
|
19477
19812
|
const id = state.inputPrompt.kind;
|
|
19478
19813
|
return [
|
|
19479
19814
|
{ type: 'runWorkflowAction', id, payload: value },
|
|
@@ -19870,6 +20205,46 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
19870
20205
|
return [action({ type: 'setPendingConfirmation', value: 'bisect-reset' })];
|
|
19871
20206
|
}
|
|
19872
20207
|
}
|
|
20208
|
+
// Changelog view local keymap. Scoped to `activeView === 'changelog'`
|
|
20209
|
+
// so the letters stay free everywhere else. Bindings:
|
|
20210
|
+
//
|
|
20211
|
+
// j / k → scroll line down / up (1 line)
|
|
20212
|
+
// pgdn / pgup → scroll page down / up (10 lines)
|
|
20213
|
+
// y → yank text to clipboard
|
|
20214
|
+
// E → open in $EDITOR (companion to compose's `E` from #913)
|
|
20215
|
+
// c → create-PR seeded with this changelog
|
|
20216
|
+
// r → regenerate (skip cache, re-run LLM)
|
|
20217
|
+
//
|
|
20218
|
+
// Back-out is `<` / Esc handled by the global pop-view path lower
|
|
20219
|
+
// down. The view only renders when `state.changelogView.status`
|
|
20220
|
+
// is 'ready' — scroll keystrokes early-return when changelogLineCount
|
|
20221
|
+
// is missing so they no-op gracefully during loading / error states.
|
|
20222
|
+
if (state.activeView === 'changelog') {
|
|
20223
|
+
if (inputValue === 'j' && context.changelogLineCount) {
|
|
20224
|
+
return [action({ type: 'pageChangelog', delta: 1, lineCount: context.changelogLineCount })];
|
|
20225
|
+
}
|
|
20226
|
+
if (inputValue === 'k' && context.changelogLineCount) {
|
|
20227
|
+
return [action({ type: 'pageChangelog', delta: -1, lineCount: context.changelogLineCount })];
|
|
20228
|
+
}
|
|
20229
|
+
if (key.pageDown && context.changelogLineCount) {
|
|
20230
|
+
return [action({ type: 'pageChangelog', delta: 10, lineCount: context.changelogLineCount })];
|
|
20231
|
+
}
|
|
20232
|
+
if (key.pageUp && context.changelogLineCount) {
|
|
20233
|
+
return [action({ type: 'pageChangelog', delta: -10, lineCount: context.changelogLineCount })];
|
|
20234
|
+
}
|
|
20235
|
+
if (inputValue === 'y') {
|
|
20236
|
+
return [{ type: 'yankChangelog' }];
|
|
20237
|
+
}
|
|
20238
|
+
if (inputValue === 'E') {
|
|
20239
|
+
return [{ type: 'openChangelogInEditor' }];
|
|
20240
|
+
}
|
|
20241
|
+
if (inputValue === 'c') {
|
|
20242
|
+
return [{ type: 'startCreatePullRequest' }];
|
|
20243
|
+
}
|
|
20244
|
+
if (inputValue === 'r') {
|
|
20245
|
+
return [{ type: 'regenerateChangelog' }];
|
|
20246
|
+
}
|
|
20247
|
+
}
|
|
19873
20248
|
if (inputValue === 'g') {
|
|
19874
20249
|
if (state.pendingKey === 'g') {
|
|
19875
20250
|
return [
|
|
@@ -20654,6 +21029,34 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
20654
21029
|
if (inputValue === 'C' && state.activeView === 'conflicts') {
|
|
20655
21030
|
return [action({ type: 'setStatus', value: 'Resolve all conflicts before continuing' })];
|
|
20656
21031
|
}
|
|
21032
|
+
// Global `C` — create a pull request from the current branch. The
|
|
21033
|
+
// runtime callback handles pre-flight (current branch resolution,
|
|
21034
|
+
// provider check) and seeds the input prompt with a changelog-derived
|
|
21035
|
+
// title + body before handing control back to the user for editing.
|
|
21036
|
+
// Conflicts view handles `C` above (continue-operation). Compose view
|
|
21037
|
+
// gets an explicit guard — claiming the keystroke with a status
|
|
21038
|
+
// message — so users mid-draft don't fat-finger out of their commit
|
|
21039
|
+
// into a PR-creation flow. Without this guard the keystroke would
|
|
21040
|
+
// fall through to the generic workflow-by-key dispatch at the end of
|
|
21041
|
+
// this function, which would fire `create-pr` to its handler.
|
|
21042
|
+
if (inputValue === 'C' && state.activeView === 'compose') {
|
|
21043
|
+
return [action({
|
|
21044
|
+
type: 'setStatus',
|
|
21045
|
+
value: 'Finish or cancel the commit draft before creating a PR.',
|
|
21046
|
+
})];
|
|
21047
|
+
}
|
|
21048
|
+
if (inputValue === 'C' && state.activeView !== 'conflicts') {
|
|
21049
|
+
return [{ type: 'startCreatePullRequest' }];
|
|
21050
|
+
}
|
|
21051
|
+
// Global `L` — generate the changelog for the current branch and
|
|
21052
|
+
// push the dedicated `changelog` view. Scoped to history and branches
|
|
21053
|
+
// — those are the natural "where am I, what landed here recently"
|
|
21054
|
+
// entry points. Avoids polluting every view's global namespace; the
|
|
21055
|
+
// changelog is reachable from anywhere via `g L` (added in keymap).
|
|
21056
|
+
if (inputValue === 'L' &&
|
|
21057
|
+
(state.activeView === 'history' || state.activeView === 'branches')) {
|
|
21058
|
+
return [{ type: 'startChangelogView' }];
|
|
21059
|
+
}
|
|
20657
21060
|
// `c` on a stash diff cherry-picks the file under the cursor —
|
|
20658
21061
|
// materializes that single path from the stash into the working tree
|
|
20659
21062
|
// (`git checkout <stashRef> -- <path>`). Routed through the y-confirm
|
|
@@ -20837,6 +21240,24 @@ function getLogInkInputEvents(state, inputValue, key = {}, context = {}) {
|
|
|
20837
21240
|
events.push(action({ type: 'commitCompose', action: { type: 'setEditing', value: true } }));
|
|
20838
21241
|
return events;
|
|
20839
21242
|
}
|
|
21243
|
+
// Capital `E` — open the commit draft in $EDITOR (or $VISUAL). Companion
|
|
21244
|
+
// to lowercase `e` which activates inline editing inside the panel:
|
|
21245
|
+
// `e` for quick tweaks in-place, `E` for "I want the full power of my
|
|
21246
|
+
// editor — syntax highlighting, multi-line nav, paste buffers, etc."
|
|
21247
|
+
// The runtime callback handles the temp-file write, editor session,
|
|
21248
|
+
// and read-back; the input handler emits a single event the
|
|
21249
|
+
// dispatcher routes there. As with lowercase `e`, fires from status
|
|
21250
|
+
// and diff views too (auto-pushes into compose first), since those
|
|
21251
|
+
// are the natural entry points to commit-message work.
|
|
21252
|
+
if (inputValue === 'E' &&
|
|
21253
|
+
(state.activeView === 'status' || state.activeView === 'diff' || state.activeView === 'compose')) {
|
|
21254
|
+
const events = [];
|
|
21255
|
+
if (state.activeView !== 'compose') {
|
|
21256
|
+
events.push(action({ type: 'pushView', value: 'compose' }));
|
|
21257
|
+
}
|
|
21258
|
+
events.push({ type: 'openComposeInEditor' });
|
|
21259
|
+
return events;
|
|
21260
|
+
}
|
|
20840
21261
|
if (inputValue === 'c' &&
|
|
20841
21262
|
(state.activeView === 'status' || state.activeView === 'diff' || state.activeView === 'compose')) {
|
|
20842
21263
|
const events = [];
|
|
@@ -21901,6 +22322,12 @@ function stageConflictResolved(git, path) {
|
|
|
21901
22322
|
return runAction$1(() => git.raw(['add', '--', path]), `Staged ${path} (marked resolved)`);
|
|
21902
22323
|
}
|
|
21903
22324
|
|
|
22325
|
+
function parseCreatedPullRequestUrl(output) {
|
|
22326
|
+
return output
|
|
22327
|
+
.split('\n')
|
|
22328
|
+
.map((line) => line.trim())
|
|
22329
|
+
.find((line) => line.startsWith('https://'));
|
|
22330
|
+
}
|
|
21904
22331
|
async function runGhAction(runner, args, successMessage) {
|
|
21905
22332
|
try {
|
|
21906
22333
|
return successMessage(await runner(args));
|
|
@@ -21912,6 +22339,34 @@ async function runGhAction(runner, args, successMessage) {
|
|
|
21912
22339
|
};
|
|
21913
22340
|
}
|
|
21914
22341
|
}
|
|
22342
|
+
function buildCreatePullRequestArgs(input) {
|
|
22343
|
+
const args = [
|
|
22344
|
+
'pr',
|
|
22345
|
+
'create',
|
|
22346
|
+
'--base',
|
|
22347
|
+
input.base,
|
|
22348
|
+
'--head',
|
|
22349
|
+
input.head,
|
|
22350
|
+
'--title',
|
|
22351
|
+
input.title,
|
|
22352
|
+
'--body',
|
|
22353
|
+
input.body,
|
|
22354
|
+
];
|
|
22355
|
+
if (input.draft) {
|
|
22356
|
+
args.push('--draft');
|
|
22357
|
+
}
|
|
22358
|
+
return args;
|
|
22359
|
+
}
|
|
22360
|
+
function createPullRequest(input, runner = defaultGhRunner) {
|
|
22361
|
+
return runGhAction(runner, buildCreatePullRequestArgs(input), (output) => {
|
|
22362
|
+
const url = parseCreatedPullRequestUrl(output);
|
|
22363
|
+
return {
|
|
22364
|
+
ok: true,
|
|
22365
|
+
message: url ? `Created pull request: ${url}` : 'Created pull request',
|
|
22366
|
+
url,
|
|
22367
|
+
};
|
|
22368
|
+
});
|
|
22369
|
+
}
|
|
21915
22370
|
function isPullRequestMergeStrategy(value) {
|
|
21916
22371
|
return value === 'merge' || value === 'squash' || value === 'rebase';
|
|
21917
22372
|
}
|
|
@@ -23517,6 +23972,121 @@ function renderBranchesSurface(h, components, state, context, contextStatus, bod
|
|
|
23517
23972
|
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle('Branches', focused)), h(Text, { dimColor: true }, headerRight)), ...renderPromotedFilterAffordance(h, Text, state, theme), ...lines);
|
|
23518
23973
|
}
|
|
23519
23974
|
|
|
23975
|
+
/**
|
|
23976
|
+
* Changelog surface — full-screen view that renders LLM-generated
|
|
23977
|
+
* release notes for the current branch. Reached via `L` from history
|
|
23978
|
+
* or branches; rendered as a real surface (not an input prompt) so the
|
|
23979
|
+
* content gets proper scroll, editing, yank, and create-PR follow-ups.
|
|
23980
|
+
*
|
|
23981
|
+
* Replaces the input-prompt-in-sidebar implementation from #906 (PR
|
|
23982
|
+
* feedback: cramped, no useful navigation, hotkeys invisible).
|
|
23983
|
+
*
|
|
23984
|
+
* Display states:
|
|
23985
|
+
* - loading : spinner + "generating changelog vs main…"
|
|
23986
|
+
* - ready : full text with scroll, header showing branch + base +
|
|
23987
|
+
* cache age, footer hints driven by the keymap
|
|
23988
|
+
* - error : error message + "press r to retry"
|
|
23989
|
+
*
|
|
23990
|
+
* View-local bindings (also reflected in footer hints + help):
|
|
23991
|
+
* - j/k scroll line
|
|
23992
|
+
* - pgup/pgdn scroll page
|
|
23993
|
+
* - y yank to clipboard
|
|
23994
|
+
* - E open in $EDITOR (write-back updates view + cache)
|
|
23995
|
+
* - c create-PR seeded with this content
|
|
23996
|
+
* - r regenerate (force-refresh, skip cache)
|
|
23997
|
+
* - </Esc pop back to prior view
|
|
23998
|
+
*
|
|
23999
|
+
* Caching: state.changelogCache is keyed by branch name. Re-entering
|
|
24000
|
+
* the view for the same branch hits the cache (no LLM call); switching
|
|
24001
|
+
* branches naturally produces a fresh generation. `r` is the explicit
|
|
24002
|
+
* "I want fresh output right now" knob.
|
|
24003
|
+
*/
|
|
24004
|
+
/**
|
|
24005
|
+
* Pluralization-free relative-time string for cache age. Coarse on
|
|
24006
|
+
* purpose — exact seconds don't help, but "5 minutes ago" vs "2 hours
|
|
24007
|
+
* ago" tells the user whether the cached content might be stale.
|
|
24008
|
+
*/
|
|
24009
|
+
function formatCacheAge(generatedAt, now) {
|
|
24010
|
+
const diffMs = Math.max(0, now - generatedAt);
|
|
24011
|
+
const sec = Math.floor(diffMs / 1000);
|
|
24012
|
+
if (sec < 5)
|
|
24013
|
+
return 'just now';
|
|
24014
|
+
if (sec < 60)
|
|
24015
|
+
return `${sec}s ago`;
|
|
24016
|
+
const min = Math.floor(sec / 60);
|
|
24017
|
+
if (min < 60)
|
|
24018
|
+
return `${min}m ago`;
|
|
24019
|
+
const hr = Math.floor(min / 60);
|
|
24020
|
+
if (hr < 24)
|
|
24021
|
+
return `${hr}h ago`;
|
|
24022
|
+
const day = Math.floor(hr / 24);
|
|
24023
|
+
return `${day}d ago`;
|
|
24024
|
+
}
|
|
24025
|
+
function renderChangelogSurface(h, components, state, _context, _contextStatus, bodyRows, width, theme) {
|
|
24026
|
+
const { Box, Text } = components;
|
|
24027
|
+
const focused = state.focus === 'commits';
|
|
24028
|
+
const view = state.changelogView;
|
|
24029
|
+
// Reserve rows for the header (1) + cache hint line (1) + 1 for
|
|
24030
|
+
// borders. Body fills the rest. Min of 4 so even ultra-short terminals
|
|
24031
|
+
// don't collapse to negative space.
|
|
24032
|
+
const listRows = Math.max(4, bodyRows - 3);
|
|
24033
|
+
const maxLineWidth = Math.max(20, width - 4);
|
|
24034
|
+
const headerLeft = view.branch
|
|
24035
|
+
? `Changelog: ${view.branch}${view.baseLabel ? ` (${view.baseLabel})` : ''}`
|
|
24036
|
+
: 'Changelog';
|
|
24037
|
+
let headerRight = '';
|
|
24038
|
+
let lines;
|
|
24039
|
+
if (view.status === 'loading') {
|
|
24040
|
+
headerRight = 'generating…';
|
|
24041
|
+
lines = [
|
|
24042
|
+
h(Text, { key: 'changelog-loading', dimColor: true }, `Generating changelog ${view.baseLabel ? `(${view.baseLabel})` : ''}…`),
|
|
24043
|
+
h(Text, { key: 'changelog-loading-hint', dimColor: true }, ''),
|
|
24044
|
+
h(Text, { key: 'changelog-loading-hint-2', dimColor: true }, 'Esc cancels and returns to the previous view.'),
|
|
24045
|
+
];
|
|
24046
|
+
}
|
|
24047
|
+
else if (view.status === 'error') {
|
|
24048
|
+
headerRight = 'error';
|
|
24049
|
+
lines = [
|
|
24050
|
+
h(Text, { key: 'changelog-error', color: 'red' }, `Changelog generation failed.`),
|
|
24051
|
+
h(Text, { key: 'changelog-error-msg', dimColor: true }, view.error || 'No additional detail.'),
|
|
24052
|
+
h(Text, { key: 'changelog-error-hint', dimColor: true }, ''),
|
|
24053
|
+
h(Text, { key: 'changelog-error-retry', dimColor: true }, 'Press `r` to retry, `<` / Esc to go back.'),
|
|
24054
|
+
];
|
|
24055
|
+
}
|
|
24056
|
+
else if (view.status === 'ready' && view.text) {
|
|
24057
|
+
const allLines = view.text.split('\n');
|
|
24058
|
+
const totalLines = allLines.length;
|
|
24059
|
+
const scrollOffset = Math.min(view.scrollOffset, Math.max(0, totalLines - 1));
|
|
24060
|
+
const visible = allLines.slice(scrollOffset, scrollOffset + listRows);
|
|
24061
|
+
const cached = view.branch ? state.changelogCache[view.branch] : undefined;
|
|
24062
|
+
const ageHint = cached ? formatCacheAge(cached.generatedAt, Date.now()) : 'just now';
|
|
24063
|
+
headerRight = `${scrollOffset + 1}–${Math.min(totalLines, scrollOffset + listRows)} / ${totalLines} · ${ageHint}`;
|
|
24064
|
+
lines = visible.length === 0
|
|
24065
|
+
? [h(Text, { key: 'changelog-empty', dimColor: true }, '(empty changelog)')]
|
|
24066
|
+
: visible.map((line, offset) => h(Text, {
|
|
24067
|
+
key: `changelog-line-${scrollOffset + offset}`,
|
|
24068
|
+
dimColor: false,
|
|
24069
|
+
}, truncateCells(line || ' ', maxLineWidth)));
|
|
24070
|
+
}
|
|
24071
|
+
else {
|
|
24072
|
+
// 'idle' — view was pushed but loading hasn't started yet. Should
|
|
24073
|
+
// be a single-frame transient; we render the same loading copy so
|
|
24074
|
+
// there's no jarring "empty" frame.
|
|
24075
|
+
headerRight = '';
|
|
24076
|
+
lines = [
|
|
24077
|
+
h(Text, { key: 'changelog-idle', dimColor: true }, 'Preparing changelog…'),
|
|
24078
|
+
];
|
|
24079
|
+
}
|
|
24080
|
+
return h(Box, {
|
|
24081
|
+
borderColor: focusBorderColor(theme, focused),
|
|
24082
|
+
borderStyle: theme.borderStyle,
|
|
24083
|
+
flexDirection: 'column',
|
|
24084
|
+
flexShrink: 0,
|
|
24085
|
+
paddingX: 1,
|
|
24086
|
+
width,
|
|
24087
|
+
}, h(Box, { justifyContent: 'space-between' }, h(Text, { bold: true }, panelTitle(headerLeft, focused)), h(Text, { dimColor: true }, headerRight)), ...lines);
|
|
24088
|
+
}
|
|
24089
|
+
|
|
23520
24090
|
/**
|
|
23521
24091
|
* Compose surface — the in-TUI commit-message composer. Combines a
|
|
23522
24092
|
* summary line, a body field, and a state-line footer; an inline
|
|
@@ -25520,6 +26090,9 @@ function renderMainPanel(h, components, state, context, contextStatus, worktreeD
|
|
|
25520
26090
|
if (state.activeView === 'conflicts') {
|
|
25521
26091
|
return renderConflictsSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
25522
26092
|
}
|
|
26093
|
+
if (state.activeView === 'changelog') {
|
|
26094
|
+
return renderChangelogSurface(h, components, state, context, contextStatus, bodyRows, width, theme);
|
|
26095
|
+
}
|
|
25523
26096
|
return renderHistoryPanel(h, components, state, context, bodyRows, width, theme, hasMoreCommits, loadingMoreCommits);
|
|
25524
26097
|
}
|
|
25525
26098
|
|
|
@@ -27374,6 +27947,292 @@ function LogInkApp(deps) {
|
|
|
27374
27947
|
});
|
|
27375
27948
|
dispatch({ type: 'setStatus', value: result.message });
|
|
27376
27949
|
}, [dispatch]);
|
|
27950
|
+
// `C` keystroke handler — start the create-pull-request flow. Resolves
|
|
27951
|
+
// the head + base branches from the live context, runs
|
|
27952
|
+
// `coco changelog --branch <base>` (via `runPullRequestBodyWorkflow`)
|
|
27953
|
+
// to seed a title + body, then opens a multi-line input prompt
|
|
27954
|
+
// pre-filled with that content for the user to edit before submission.
|
|
27955
|
+
//
|
|
27956
|
+
// On submit, the workflow handler `'create-pr'` parses the prompt
|
|
27957
|
+
// value (line 1 = title, lines 2+ = body) and runs
|
|
27958
|
+
// `createPullRequest({ base, head, title, body })`. If anything in the
|
|
27959
|
+
// pre-flight goes sideways (no current branch, no provider, gh CLI
|
|
27960
|
+
// missing) we surface the failure on the status line and skip the
|
|
27961
|
+
// prompt entirely — better than opening a prompt the user can't
|
|
27962
|
+
// actually submit successfully.
|
|
27963
|
+
const startCreatePullRequest = React.useCallback(async () => {
|
|
27964
|
+
const head = context.branches?.currentBranch || context.provider?.currentBranch;
|
|
27965
|
+
if (!head) {
|
|
27966
|
+
dispatch({ type: 'setStatus', value: 'No current branch to create a PR from.' });
|
|
27967
|
+
return;
|
|
27968
|
+
}
|
|
27969
|
+
const defaultBranch = context.provider?.repository.defaultBranch;
|
|
27970
|
+
if (!defaultBranch) {
|
|
27971
|
+
dispatch({
|
|
27972
|
+
type: 'setStatus',
|
|
27973
|
+
value: 'No default branch detected. Set origin/HEAD or ensure main/master exists locally.',
|
|
27974
|
+
});
|
|
27975
|
+
return;
|
|
27976
|
+
}
|
|
27977
|
+
if (head === defaultBranch) {
|
|
27978
|
+
dispatch({ type: 'setStatus', value: `Current branch is ${defaultBranch}; check out a feature branch first.` });
|
|
27979
|
+
return;
|
|
27980
|
+
}
|
|
27981
|
+
if (context.pullRequest?.currentPullRequest || context.provider?.currentPullRequest) {
|
|
27982
|
+
const existing = context.pullRequest?.currentPullRequest || context.provider?.currentPullRequest;
|
|
27983
|
+
dispatch({
|
|
27984
|
+
type: 'setStatus',
|
|
27985
|
+
value: existing
|
|
27986
|
+
? `PR #${existing.number} already open for ${head}. Use the PR view to manage it.`
|
|
27987
|
+
: `A pull request is already open for ${head}.`,
|
|
27988
|
+
});
|
|
27989
|
+
return;
|
|
27990
|
+
}
|
|
27991
|
+
dispatch({ type: 'setStatus', value: `generating PR body from changelog (vs ${defaultBranch})…` });
|
|
27992
|
+
const body = await runPullRequestBodyWorkflow({ baseBranch: defaultBranch });
|
|
27993
|
+
// Fallback shape when the changelog generation fails — open the
|
|
27994
|
+
// prompt with empty title + body rather than aborting, so the user
|
|
27995
|
+
// can still author the PR manually. The status line surfaces why
|
|
27996
|
+
// we couldn't pre-fill.
|
|
27997
|
+
const initialTitle = body.title || head.replace(/^(feat|fix|chore|docs|refactor|test)\//, '').replace(/[-_]/g, ' ');
|
|
27998
|
+
const initialBody = body.body || '';
|
|
27999
|
+
const initial = initialBody ? `${initialTitle}\n\n${initialBody}` : initialTitle;
|
|
28000
|
+
if (!body.ok) {
|
|
28001
|
+
dispatch({ type: 'setStatus', value: `PR body generation failed: ${body.message}. Edit manually.` });
|
|
28002
|
+
}
|
|
28003
|
+
else {
|
|
28004
|
+
dispatch({ type: 'setStatus', value: 'PR body drafted — review and Ctrl+D to submit.' });
|
|
28005
|
+
}
|
|
28006
|
+
dispatch({
|
|
28007
|
+
type: 'openInputPrompt',
|
|
28008
|
+
kind: 'create-pr',
|
|
28009
|
+
label: `Create PR: ${head} → ${defaultBranch} (line 1 title · rest body · Enter newline · Ctrl+D submit)`,
|
|
28010
|
+
initial,
|
|
28011
|
+
multiline: true,
|
|
28012
|
+
});
|
|
28013
|
+
}, [
|
|
28014
|
+
context.branches?.currentBranch,
|
|
28015
|
+
context.provider?.currentBranch,
|
|
28016
|
+
context.provider?.currentPullRequest,
|
|
28017
|
+
context.provider?.repository.defaultBranch,
|
|
28018
|
+
context.pullRequest?.currentPullRequest,
|
|
28019
|
+
dispatch,
|
|
28020
|
+
]);
|
|
28021
|
+
// Copy an arbitrary string to the system clipboard. Distinct from
|
|
28022
|
+
// `yankFromActiveView` which derives the value from the current view
|
|
28023
|
+
// — this one takes the value as an explicit event payload, used by
|
|
28024
|
+
// the changelog view's `y` keystroke (and a candidate for future
|
|
28025
|
+
// "copy this" surfaces). Surfaces a status confirming what landed
|
|
28026
|
+
// in clipboard.
|
|
28027
|
+
const yankText = React.useCallback(async (value, label) => {
|
|
28028
|
+
const clipboard = clipboardRunner || defaultClipboardRunner;
|
|
28029
|
+
if (!value) {
|
|
28030
|
+
dispatch({ type: 'setStatus', value: `Nothing to copy — ${label} is empty.` });
|
|
28031
|
+
return;
|
|
28032
|
+
}
|
|
28033
|
+
try {
|
|
28034
|
+
await clipboard(value);
|
|
28035
|
+
dispatch({ type: 'setStatus', value: `Copied ${label} to clipboard.` });
|
|
28036
|
+
}
|
|
28037
|
+
catch (error) {
|
|
28038
|
+
dispatch({
|
|
28039
|
+
type: 'setStatus',
|
|
28040
|
+
value: `Copy failed (${label}): ${error.message}`,
|
|
28041
|
+
});
|
|
28042
|
+
}
|
|
28043
|
+
}, [clipboardRunner, dispatch]);
|
|
28044
|
+
// `L` keystroke handler — generate (or recall from cache) a changelog
|
|
28045
|
+
// for the current branch and push the dedicated `changelog` surface
|
|
28046
|
+
// to display it. The view renders the full text in the main panel
|
|
28047
|
+
// (not cramped into an input prompt), with its own keymap for scroll,
|
|
28048
|
+
// yank, $EDITOR, create-PR, and regenerate.
|
|
28049
|
+
//
|
|
28050
|
+
// Caching: `state.changelogCache` is keyed by branch name. On `L`,
|
|
28051
|
+
// we check the cache first and reuse if hit (no LLM call); the user
|
|
28052
|
+
// presses `r` from inside the view to force a regenerate. Switching
|
|
28053
|
+
// branches naturally produces a fresh generation since the cache key
|
|
28054
|
+
// changes.
|
|
28055
|
+
//
|
|
28056
|
+
// Surface lifecycle: we push the `changelog` view BEFORE awaiting the
|
|
28057
|
+
// workflow, so the user sees a loading state instead of a blank
|
|
28058
|
+
// history view while the LLM runs. On error, we keep the view pushed
|
|
28059
|
+
// and render the error there (with `r` to retry) instead of bailing
|
|
28060
|
+
// back to history with a status-line message that may scroll past.
|
|
28061
|
+
const startChangelogView = React.useCallback(async (options = {}) => {
|
|
28062
|
+
const head = context.branches?.currentBranch || context.provider?.currentBranch;
|
|
28063
|
+
if (!head) {
|
|
28064
|
+
dispatch({ type: 'setStatus', value: 'No current branch — check out a branch first.' });
|
|
28065
|
+
return;
|
|
28066
|
+
}
|
|
28067
|
+
const defaultBranch = context.provider?.repository.defaultBranch;
|
|
28068
|
+
// The changelog command will fall back to its own defaults when no
|
|
28069
|
+
// branch arg is passed, but being explicit about the base is more
|
|
28070
|
+
// honest about what the user is seeing. With the local default-
|
|
28071
|
+
// branch fallback in providerData (#912), `defaultBranch` is
|
|
28072
|
+
// populated even for non-GitHub / offline scenarios — we only fall
|
|
28073
|
+
// through to `--since-last-tag` when truly nothing resolves.
|
|
28074
|
+
const argv = defaultBranch && head !== defaultBranch
|
|
28075
|
+
? { branch: defaultBranch }
|
|
28076
|
+
: { sinceLastTag: true };
|
|
28077
|
+
const baseLabel = defaultBranch && head !== defaultBranch
|
|
28078
|
+
? `vs ${defaultBranch}`
|
|
28079
|
+
: 'since last tag';
|
|
28080
|
+
// Cache hit — skip the LLM, push view with ready content. The
|
|
28081
|
+
// generated-at timestamp on the cache entry drives the "(cached, N
|
|
28082
|
+
// ago)" hint in the header, so the user knows whether to press `r`.
|
|
28083
|
+
const cached = !options.force ? state.changelogCache[head] : undefined;
|
|
28084
|
+
if (cached) {
|
|
28085
|
+
dispatch({ type: 'pushView', value: 'changelog' });
|
|
28086
|
+
dispatch({
|
|
28087
|
+
type: 'setChangelogReady',
|
|
28088
|
+
branch: head,
|
|
28089
|
+
baseLabel: cached.baseLabel,
|
|
28090
|
+
text: cached.text,
|
|
28091
|
+
});
|
|
28092
|
+
dispatch({
|
|
28093
|
+
type: 'setStatus',
|
|
28094
|
+
value: `Changelog loaded from cache (${cached.baseLabel}). r to regenerate.`,
|
|
28095
|
+
});
|
|
28096
|
+
return;
|
|
28097
|
+
}
|
|
28098
|
+
// No cache (or force=true via `r`) — push view with loading state,
|
|
28099
|
+
// then run the workflow.
|
|
28100
|
+
dispatch({ type: 'pushView', value: 'changelog' });
|
|
28101
|
+
dispatch({ type: 'setChangelogLoading', branch: head, baseLabel });
|
|
28102
|
+
dispatch({ type: 'setStatus', value: `generating changelog (${baseLabel})…` });
|
|
28103
|
+
const result = await runChangelogTextWorkflow(argv);
|
|
28104
|
+
if (!result.ok || !result.text) {
|
|
28105
|
+
dispatch({
|
|
28106
|
+
type: 'setChangelogError',
|
|
28107
|
+
branch: head,
|
|
28108
|
+
baseLabel,
|
|
28109
|
+
error: result.message,
|
|
28110
|
+
});
|
|
28111
|
+
dispatch({ type: 'setStatus', value: `Changelog failed: ${result.message}` });
|
|
28112
|
+
return;
|
|
28113
|
+
}
|
|
28114
|
+
dispatch({
|
|
28115
|
+
type: 'setChangelogReady',
|
|
28116
|
+
branch: head,
|
|
28117
|
+
baseLabel,
|
|
28118
|
+
text: result.text,
|
|
28119
|
+
});
|
|
28120
|
+
dispatch({
|
|
28121
|
+
type: 'setStatus',
|
|
28122
|
+
value: 'Changelog ready — y yank · E $EDITOR · c PR · r regen · < back.',
|
|
28123
|
+
});
|
|
28124
|
+
}, [
|
|
28125
|
+
context.branches?.currentBranch,
|
|
28126
|
+
context.provider?.currentBranch,
|
|
28127
|
+
context.provider?.repository.defaultBranch,
|
|
28128
|
+
dispatch,
|
|
28129
|
+
state.changelogCache,
|
|
28130
|
+
]);
|
|
28131
|
+
// `r` keystroke inside the changelog view — re-run generation
|
|
28132
|
+
// ignoring any cached result. Thin wrapper since the underlying
|
|
28133
|
+
// logic in `startChangelogView` already supports the force path.
|
|
28134
|
+
const regenerateChangelog = React.useCallback(() => {
|
|
28135
|
+
void startChangelogView({ force: true });
|
|
28136
|
+
}, [startChangelogView]);
|
|
28137
|
+
// `y` keystroke inside the changelog view — yank the current text
|
|
28138
|
+
// to the system clipboard. Pulled from view state rather than from
|
|
28139
|
+
// wherever the cursor is (no per-row selection on this surface).
|
|
28140
|
+
const yankChangelog = React.useCallback(() => {
|
|
28141
|
+
const text = state.changelogView.text;
|
|
28142
|
+
if (!text) {
|
|
28143
|
+
dispatch({ type: 'setStatus', value: 'No changelog text to copy.' });
|
|
28144
|
+
return;
|
|
28145
|
+
}
|
|
28146
|
+
void yankText(text, 'changelog');
|
|
28147
|
+
}, [dispatch, state.changelogView.text, yankText]);
|
|
28148
|
+
// `E` keystroke inside the changelog view — open the current text in
|
|
28149
|
+
// $EDITOR / $VISUAL, read it back, update view + cache. Mirrors the
|
|
28150
|
+
// compose `E` flow (#913) but on the changelog-view state slice.
|
|
28151
|
+
// After save, `setChangelogText` updates both view and cache so the
|
|
28152
|
+
// edits persist across view re-entry.
|
|
28153
|
+
const openChangelogInEditor = React.useCallback(() => {
|
|
28154
|
+
const current = state.changelogView.text;
|
|
28155
|
+
if (current === undefined) {
|
|
28156
|
+
dispatch({ type: 'setStatus', value: 'Changelog not loaded yet — wait for generation.' });
|
|
28157
|
+
return;
|
|
28158
|
+
}
|
|
28159
|
+
let dir;
|
|
28160
|
+
try {
|
|
28161
|
+
dir = fs$1.mkdtempSync(path__namespace$1.join(os$1.tmpdir(), 'coco-changelog-'));
|
|
28162
|
+
}
|
|
28163
|
+
catch (error) {
|
|
28164
|
+
dispatch({
|
|
28165
|
+
type: 'setStatus',
|
|
28166
|
+
value: `Failed to create temp file for editor: ${error.message}`,
|
|
28167
|
+
});
|
|
28168
|
+
return;
|
|
28169
|
+
}
|
|
28170
|
+
const file = path__namespace$1.join(dir, 'CHANGELOG.md');
|
|
28171
|
+
try {
|
|
28172
|
+
fs$1.writeFileSync(file, current, 'utf8');
|
|
28173
|
+
}
|
|
28174
|
+
catch (error) {
|
|
28175
|
+
dispatch({
|
|
28176
|
+
type: 'setStatus',
|
|
28177
|
+
value: `Failed to seed temp file: ${error.message}`,
|
|
28178
|
+
});
|
|
28179
|
+
try {
|
|
28180
|
+
fs$1.rmSync(dir, { recursive: true, force: true });
|
|
28181
|
+
}
|
|
28182
|
+
catch { /* ignore */ }
|
|
28183
|
+
return;
|
|
28184
|
+
}
|
|
28185
|
+
const editorEnv = process.env.VISUAL || process.env.EDITOR || 'vi';
|
|
28186
|
+
const editorArgs = editorEnv.trim().split(/\s+/).filter(Boolean);
|
|
28187
|
+
const editor = editorArgs[0] || 'vi';
|
|
28188
|
+
const editorPrefixArgs = editorArgs.slice(1);
|
|
28189
|
+
const out = process.stdout;
|
|
28190
|
+
const stdin = process.stdin;
|
|
28191
|
+
const ENTER_ALT = '\x1b[?1049h';
|
|
28192
|
+
const EXIT_ALT = '\x1b[?1049l';
|
|
28193
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
28194
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
28195
|
+
let editorOk = false;
|
|
28196
|
+
try {
|
|
28197
|
+
stdin.setRawMode?.(false);
|
|
28198
|
+
out.write(`${SHOW_CURSOR}${EXIT_ALT}`);
|
|
28199
|
+
const result = node_child_process.spawnSync(editor, [...editorPrefixArgs, file], { stdio: 'inherit' });
|
|
28200
|
+
if (result.error) {
|
|
28201
|
+
dispatch({ type: 'setStatus', value: `Failed to launch ${editor}: ${result.error.message}` });
|
|
28202
|
+
}
|
|
28203
|
+
else if (result.signal) {
|
|
28204
|
+
dispatch({ type: 'setStatus', value: `${editor} interrupted by ${result.signal}` });
|
|
28205
|
+
}
|
|
28206
|
+
else if (typeof result.status === 'number' && result.status !== 0) {
|
|
28207
|
+
dispatch({ type: 'setStatus', value: `${editor} exited with status ${result.status}` });
|
|
28208
|
+
}
|
|
28209
|
+
else {
|
|
28210
|
+
editorOk = true;
|
|
28211
|
+
}
|
|
28212
|
+
}
|
|
28213
|
+
finally {
|
|
28214
|
+
out.write(`${ENTER_ALT}${HIDE_CURSOR}`);
|
|
28215
|
+
stdin.setRawMode?.(true);
|
|
28216
|
+
resumeRef?.current?.();
|
|
28217
|
+
}
|
|
28218
|
+
if (editorOk) {
|
|
28219
|
+
try {
|
|
28220
|
+
const content = fs$1.readFileSync(file, 'utf8');
|
|
28221
|
+
dispatch({ type: 'setChangelogText', text: content });
|
|
28222
|
+
dispatch({ type: 'setStatus', value: 'Changelog updated from editor.' });
|
|
28223
|
+
}
|
|
28224
|
+
catch (error) {
|
|
28225
|
+
dispatch({
|
|
28226
|
+
type: 'setStatus',
|
|
28227
|
+
value: `Failed to read back edited changelog: ${error.message}`,
|
|
28228
|
+
});
|
|
28229
|
+
}
|
|
28230
|
+
}
|
|
28231
|
+
try {
|
|
28232
|
+
fs$1.rmSync(dir, { recursive: true, force: true });
|
|
28233
|
+
}
|
|
28234
|
+
catch { /* ignore */ }
|
|
28235
|
+
}, [dispatch, resumeRef, state.changelogView.text]);
|
|
27377
28236
|
// Open a file in $EDITOR (or $VISUAL) by suspending Ink's hold on the
|
|
27378
28237
|
// terminal, spawning the editor synchronously inheriting stdio, then
|
|
27379
28238
|
// restoring the alt screen + raw mode and forcing a re-render. The
|
|
@@ -27432,6 +28291,116 @@ function LogInkApp(deps) {
|
|
|
27432
28291
|
// refresh so the file row reflects the new staged/unstaged state.
|
|
27433
28292
|
void refreshWorktreeContext({ silent: true });
|
|
27434
28293
|
}, [dispatch, refreshWorktreeContext, resumeRef]);
|
|
28294
|
+
// `E` keystroke handler — open the current commit draft in $EDITOR
|
|
28295
|
+
// (or $VISUAL), then read the file back and update the compose state
|
|
28296
|
+
// with the saved content. Mirrors the suspend → spawn → resume
|
|
28297
|
+
// terminal dance of `openInEditor` but operates on an in-memory
|
|
28298
|
+
// draft (round-tripped through a temp file) rather than a worktree
|
|
28299
|
+
// file. Useful when the inline compose editor isn't enough — long
|
|
28300
|
+
// bodies, markdown highlighting, paste from elsewhere, etc.
|
|
28301
|
+
//
|
|
28302
|
+
// Empty drafts are still written to the temp file so the user gets
|
|
28303
|
+
// a blank canvas; the read-back uses `setDraft` which splits content
|
|
28304
|
+
// into summary + body via `splitCommitDraft`, so the new content
|
|
28305
|
+
// re-populates both fields correctly regardless of which one was
|
|
28306
|
+
// active before.
|
|
28307
|
+
const openComposeInEditor = React.useCallback(() => {
|
|
28308
|
+
// Build the current draft text the same way `createManualCommit`
|
|
28309
|
+
// would — single string, blank line between summary and body.
|
|
28310
|
+
// Round-tripping through this format keeps the parse symmetric:
|
|
28311
|
+
// the editor sees what a real commit message would look like, and
|
|
28312
|
+
// `splitCommitDraft` on the way back reverses it cleanly.
|
|
28313
|
+
const composeState = state.commitCompose;
|
|
28314
|
+
const draft = formatCommitComposeMessage(composeState.summary, composeState.body);
|
|
28315
|
+
// Temp dir + file. mkdtemp is cleaned up at the end regardless of
|
|
28316
|
+
// editor success/failure (`finally` block below). `.md` extension
|
|
28317
|
+
// helps editors pick up markdown highlighting — most commit-
|
|
28318
|
+
// message workflows treat the body as markdown-ish.
|
|
28319
|
+
let dir;
|
|
28320
|
+
try {
|
|
28321
|
+
dir = fs$1.mkdtempSync(path__namespace$1.join(os$1.tmpdir(), 'coco-compose-'));
|
|
28322
|
+
}
|
|
28323
|
+
catch (error) {
|
|
28324
|
+
dispatch({
|
|
28325
|
+
type: 'setStatus',
|
|
28326
|
+
value: `Failed to create temp file for editor: ${error.message}`,
|
|
28327
|
+
});
|
|
28328
|
+
return;
|
|
28329
|
+
}
|
|
28330
|
+
const file = path__namespace$1.join(dir, 'COMMIT_EDITMSG.md');
|
|
28331
|
+
try {
|
|
28332
|
+
fs$1.writeFileSync(file, draft, 'utf8');
|
|
28333
|
+
}
|
|
28334
|
+
catch (error) {
|
|
28335
|
+
dispatch({
|
|
28336
|
+
type: 'setStatus',
|
|
28337
|
+
value: `Failed to seed temp file: ${error.message}`,
|
|
28338
|
+
});
|
|
28339
|
+
try {
|
|
28340
|
+
fs$1.rmSync(dir, { recursive: true, force: true });
|
|
28341
|
+
}
|
|
28342
|
+
catch { /* ignore */ }
|
|
28343
|
+
return;
|
|
28344
|
+
}
|
|
28345
|
+
const editorEnv = process.env.VISUAL || process.env.EDITOR || 'vi';
|
|
28346
|
+
const editorArgs = editorEnv.trim().split(/\s+/).filter(Boolean);
|
|
28347
|
+
const editor = editorArgs[0] || 'vi';
|
|
28348
|
+
const editorPrefixArgs = editorArgs.slice(1);
|
|
28349
|
+
const out = process.stdout;
|
|
28350
|
+
const stdin = process.stdin;
|
|
28351
|
+
const ENTER_ALT = '\x1b[?1049h';
|
|
28352
|
+
const EXIT_ALT = '\x1b[?1049l';
|
|
28353
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
28354
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
28355
|
+
let editorOk = false;
|
|
28356
|
+
try {
|
|
28357
|
+
stdin.setRawMode?.(false);
|
|
28358
|
+
out.write(`${SHOW_CURSOR}${EXIT_ALT}`);
|
|
28359
|
+
const result = node_child_process.spawnSync(editor, [...editorPrefixArgs, file], { stdio: 'inherit' });
|
|
28360
|
+
if (result.error) {
|
|
28361
|
+
dispatch({ type: 'setStatus', value: `Failed to launch ${editor}: ${result.error.message}` });
|
|
28362
|
+
}
|
|
28363
|
+
else if (result.signal) {
|
|
28364
|
+
dispatch({ type: 'setStatus', value: `${editor} interrupted by ${result.signal}` });
|
|
28365
|
+
}
|
|
28366
|
+
else if (typeof result.status === 'number' && result.status !== 0) {
|
|
28367
|
+
dispatch({ type: 'setStatus', value: `${editor} exited with status ${result.status}` });
|
|
28368
|
+
}
|
|
28369
|
+
else {
|
|
28370
|
+
editorOk = true;
|
|
28371
|
+
}
|
|
28372
|
+
}
|
|
28373
|
+
finally {
|
|
28374
|
+
out.write(`${ENTER_ALT}${HIDE_CURSOR}`);
|
|
28375
|
+
stdin.setRawMode?.(true);
|
|
28376
|
+
resumeRef?.current?.();
|
|
28377
|
+
}
|
|
28378
|
+
// Read the (possibly edited) file back and update compose state.
|
|
28379
|
+
// We only do this when the editor exited cleanly — a crash / kill
|
|
28380
|
+
// shouldn't blow away the user's draft. The setDraft action
|
|
28381
|
+
// re-splits into summary + body via splitCommitDraft.
|
|
28382
|
+
if (editorOk) {
|
|
28383
|
+
try {
|
|
28384
|
+
const content = fs$1.readFileSync(file, 'utf8');
|
|
28385
|
+
dispatch({ type: 'commitCompose', action: { type: 'setDraft', value: content } });
|
|
28386
|
+
dispatch({ type: 'setStatus', value: 'Commit draft updated from editor.' });
|
|
28387
|
+
}
|
|
28388
|
+
catch (error) {
|
|
28389
|
+
dispatch({
|
|
28390
|
+
type: 'setStatus',
|
|
28391
|
+
value: `Failed to read back edited draft: ${error.message}`,
|
|
28392
|
+
});
|
|
28393
|
+
}
|
|
28394
|
+
}
|
|
28395
|
+
// Always clean up the temp dir — even on failure paths above. We
|
|
28396
|
+
// don't want abandoned coco-compose-* directories accumulating in
|
|
28397
|
+
// /tmp across sessions. Best-effort; ignore errors (e.g. file
|
|
28398
|
+
// already removed by the user from inside their editor).
|
|
28399
|
+
try {
|
|
28400
|
+
fs$1.rmSync(dir, { recursive: true, force: true });
|
|
28401
|
+
}
|
|
28402
|
+
catch { /* ignore */ }
|
|
28403
|
+
}, [dispatch, resumeRef, state.commitCompose]);
|
|
27435
28404
|
// Resolve the destructive-action target from the live filtered+sorted
|
|
27436
28405
|
// list the user is looking at, run the action against it, surface the
|
|
27437
28406
|
// result on the status line, and silently refresh so the deleted item
|
|
@@ -27859,6 +28828,32 @@ function LogInkApp(deps) {
|
|
|
27859
28828
|
// — input prompts validate before they reach here, but the
|
|
27860
28829
|
// strategy guard stays as a defensive belt-and-suspenders since
|
|
27861
28830
|
// a future palette path could call us with a raw value.
|
|
28831
|
+
'create-pr': async () => {
|
|
28832
|
+
// The input-prompt submit handler validates non-empty title
|
|
28833
|
+
// already; this is the defensive belt-and-suspenders for
|
|
28834
|
+
// future palette callers passing in a raw payload.
|
|
28835
|
+
const text = (payload || '').trim();
|
|
28836
|
+
if (!text) {
|
|
28837
|
+
return { ok: false, message: 'Pull request title is required (first line of the prompt).' };
|
|
28838
|
+
}
|
|
28839
|
+
const lines = text.split('\n');
|
|
28840
|
+
const title = lines[0].trim();
|
|
28841
|
+
if (!title) {
|
|
28842
|
+
return { ok: false, message: 'Pull request title cannot be blank.' };
|
|
28843
|
+
}
|
|
28844
|
+
// Body: lines 2+, with the leading blank line tolerated. Empty
|
|
28845
|
+
// body is allowed — GitHub renders an empty PR body fine.
|
|
28846
|
+
const body = lines.slice(1).join('\n').replace(/^\n+/, '').trimEnd();
|
|
28847
|
+
const head = context.branches?.currentBranch || context.provider?.currentBranch;
|
|
28848
|
+
const base = context.provider?.repository.defaultBranch;
|
|
28849
|
+
if (!head) {
|
|
28850
|
+
return { ok: false, message: 'No current branch detected.' };
|
|
28851
|
+
}
|
|
28852
|
+
if (!base) {
|
|
28853
|
+
return { ok: false, message: 'No default branch detected. Configure the GitHub remote.' };
|
|
28854
|
+
}
|
|
28855
|
+
return createPullRequest({ base, head, title, body });
|
|
28856
|
+
},
|
|
27862
28857
|
'merge-pr': async () => {
|
|
27863
28858
|
const strategy = (payload || 'merge').toLowerCase();
|
|
27864
28859
|
if (!isPullRequestMergeStrategy(strategy)) {
|
|
@@ -28335,6 +29330,11 @@ function LogInkApp(deps) {
|
|
|
28335
29330
|
: state.diffSource === 'commit'
|
|
28336
29331
|
? filePreview?.hunks
|
|
28337
29332
|
: undefined,
|
|
29333
|
+
// Line count of the changelog text, used by the changelog view's
|
|
29334
|
+
// j/k/PgUp/PgDn scroll bindings to clamp `pageChangelog` deltas.
|
|
29335
|
+
// Computed from view state rather than threaded through context
|
|
29336
|
+
// because the surface owns its own content — no external loader.
|
|
29337
|
+
changelogLineCount: state.changelogView.text?.split('\n').length,
|
|
28338
29338
|
}).forEach((event) => {
|
|
28339
29339
|
if (event.type === 'exit') {
|
|
28340
29340
|
exit();
|
|
@@ -28360,6 +29360,27 @@ function LogInkApp(deps) {
|
|
|
28360
29360
|
else if (event.type === 'runAiCommitDraft') {
|
|
28361
29361
|
void runAiCommitDraft();
|
|
28362
29362
|
}
|
|
29363
|
+
else if (event.type === 'startCreatePullRequest') {
|
|
29364
|
+
void startCreatePullRequest();
|
|
29365
|
+
}
|
|
29366
|
+
else if (event.type === 'startChangelogView') {
|
|
29367
|
+
void startChangelogView();
|
|
29368
|
+
}
|
|
29369
|
+
else if (event.type === 'regenerateChangelog') {
|
|
29370
|
+
regenerateChangelog();
|
|
29371
|
+
}
|
|
29372
|
+
else if (event.type === 'yankChangelog') {
|
|
29373
|
+
yankChangelog();
|
|
29374
|
+
}
|
|
29375
|
+
else if (event.type === 'openChangelogInEditor') {
|
|
29376
|
+
openChangelogInEditor();
|
|
29377
|
+
}
|
|
29378
|
+
else if (event.type === 'openComposeInEditor') {
|
|
29379
|
+
openComposeInEditor();
|
|
29380
|
+
}
|
|
29381
|
+
else if (event.type === 'yankText') {
|
|
29382
|
+
void yankText(event.value, event.label);
|
|
29383
|
+
}
|
|
28363
29384
|
else if (event.type === 'runWorkflowAction') {
|
|
28364
29385
|
void runWorkflowAction(event.id, event.payload);
|
|
28365
29386
|
}
|