@ohzw/worktree-command-tui 0.1.0 → 0.1.2
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/README.md +5 -0
- package/dist/app.d.ts +3 -2
- package/dist/app.js +458 -42
- package/dist/components/ActionPanel.d.ts +6 -2
- package/dist/components/ActionPanel.js +141 -82
- package/dist/components/ContextBar.d.ts +4 -1
- package/dist/components/ContextBar.js +37 -4
- package/dist/components/FloatingLogWindow.d.ts +7 -0
- package/dist/components/FloatingLogWindow.js +5 -0
- package/dist/components/HelpWindow.d.ts +7 -0
- package/dist/components/HelpWindow.js +29 -0
- package/dist/components/LogPanel.d.ts +22 -0
- package/dist/components/LogPanel.js +260 -0
- package/dist/components/WorktreeList.d.ts +3 -1
- package/dist/components/WorktreeList.js +25 -30
- package/dist/core/command-runner.d.ts +11 -0
- package/dist/core/command-runner.js +44 -0
- package/dist/core/config-lifecycle.d.ts +25 -0
- package/dist/core/config-lifecycle.js +143 -0
- package/dist/core/config.d.ts +2 -3
- package/dist/core/config.js +0 -48
- package/dist/core/git-metadata.d.ts +25 -0
- package/dist/core/git-metadata.js +84 -0
- package/dist/core/git-worktrees.d.ts +2 -1
- package/dist/core/git-worktrees.js +30 -11
- package/dist/core/github-metadata.d.ts +14 -0
- package/dist/core/github-metadata.js +137 -0
- package/dist/core/init.d.ts +3 -2
- package/dist/core/init.js +9 -57
- package/dist/core/log-reader.d.ts +7 -0
- package/dist/core/log-reader.js +43 -0
- package/dist/core/runtime-state.d.ts +42 -0
- package/dist/core/runtime-state.js +125 -0
- package/dist/core/runtime.d.ts +20 -33
- package/dist/core/runtime.js +116 -173
- package/dist/core/tui-interaction.d.ts +31 -0
- package/dist/core/tui-interaction.js +59 -0
- package/dist/core/worktree-projection.d.ts +76 -0
- package/dist/core/worktree-projection.js +124 -0
- package/dist/main.js +24 -2
- package/dist/render-options.d.ts +1 -0
- package/dist/render-options.js +1 -0
- package/dist/repro.d.ts +1 -0
- package/dist/repro.js +13 -0
- package/dist/terminal/viewport.d.ts +15 -0
- package/dist/terminal/viewport.js +49 -0
- package/dist/ui-theme.d.ts +3 -0
- package/dist/ui-theme.js +38 -0
- package/package.json +2 -1
package/dist/core/runtime.d.ts
CHANGED
|
@@ -1,29 +1,8 @@
|
|
|
1
1
|
import { type WorktreeRow } from './git-worktrees.js';
|
|
2
|
+
import { type UpstreamInfo, type WorkingTreeInfo } from './git-metadata.js';
|
|
3
|
+
import { type PullRequestInfo } from './github-metadata.js';
|
|
4
|
+
import { type LogEntry } from './log-reader.js';
|
|
2
5
|
export type RowTag = 'main' | 'active' | 'invalid' | 'external' | 'legacy';
|
|
3
|
-
export interface UpstreamInfo {
|
|
4
|
-
branch: string;
|
|
5
|
-
ahead: number;
|
|
6
|
-
behind: number;
|
|
7
|
-
}
|
|
8
|
-
export interface WorkingTreeInfo {
|
|
9
|
-
staged: number;
|
|
10
|
-
unstaged: number;
|
|
11
|
-
untracked: number;
|
|
12
|
-
conflicts: number;
|
|
13
|
-
}
|
|
14
|
-
export type PullRequestInfo = {
|
|
15
|
-
kind: 'found';
|
|
16
|
-
number: number;
|
|
17
|
-
title: string;
|
|
18
|
-
url: string;
|
|
19
|
-
state: 'OPEN' | 'CLOSED' | 'MERGED';
|
|
20
|
-
isDraft: boolean;
|
|
21
|
-
baseBranch: string;
|
|
22
|
-
} | {
|
|
23
|
-
kind: 'none';
|
|
24
|
-
} | {
|
|
25
|
-
kind: 'unavailable';
|
|
26
|
-
};
|
|
27
6
|
export interface AppRow {
|
|
28
7
|
path: string;
|
|
29
8
|
shortPath: string;
|
|
@@ -34,12 +13,14 @@ export interface AppRow {
|
|
|
34
13
|
upstreamUnavailable?: boolean;
|
|
35
14
|
workingTree?: WorkingTreeInfo;
|
|
36
15
|
pullRequest?: PullRequestInfo;
|
|
16
|
+
branchCreatedAtMs?: number;
|
|
37
17
|
invalidReason?: string;
|
|
38
18
|
}
|
|
39
19
|
export interface AppStatus {
|
|
40
|
-
kind: 'idle' | 'starting' | 'running' | 'stopping' | 'error';
|
|
20
|
+
kind: 'idle' | 'setting-up' | 'starting' | 'running' | 'stopping' | 'error';
|
|
41
21
|
message: string;
|
|
42
22
|
}
|
|
23
|
+
export type AppLogEntry = LogEntry;
|
|
43
24
|
export interface AppModel {
|
|
44
25
|
repoName: string;
|
|
45
26
|
namespace: string;
|
|
@@ -47,19 +28,25 @@ export interface AppModel {
|
|
|
47
28
|
activePath: string | null;
|
|
48
29
|
activeBranch: string | null;
|
|
49
30
|
status: AppStatus;
|
|
31
|
+
setupAvailable: boolean;
|
|
32
|
+
editorAvailable: boolean;
|
|
33
|
+
logs: AppLogEntry[];
|
|
34
|
+
}
|
|
35
|
+
export interface AppLogRefresh {
|
|
36
|
+
logs: AppLogEntry[];
|
|
37
|
+
activePath: string | null;
|
|
38
|
+
activeBranch: string | null;
|
|
50
39
|
}
|
|
51
40
|
export interface AppActions {
|
|
41
|
+
setup: (worktreePath: string) => Promise<AppModel>;
|
|
52
42
|
start: (worktreePath: string) => Promise<AppModel>;
|
|
53
43
|
stop: () => Promise<AppModel>;
|
|
54
44
|
refresh: () => Promise<AppModel>;
|
|
45
|
+
refreshLogs: () => Promise<AppLogRefresh>;
|
|
46
|
+
openEditor: (worktreePath: string) => Promise<AppModel>;
|
|
47
|
+
openPullRequest: (worktreePath: string) => Promise<AppModel>;
|
|
48
|
+
deleteWorktree: (worktreePath: string) => Promise<AppModel>;
|
|
55
49
|
}
|
|
56
|
-
|
|
57
|
-
upstream?: UpstreamInfo;
|
|
58
|
-
upstreamUnavailable: boolean;
|
|
59
|
-
workingTree?: WorkingTreeInfo;
|
|
60
|
-
}
|
|
61
|
-
export declare function parseGitStatusSummary(output: string): GitStatusSummary;
|
|
62
|
-
export declare function toAppRow(mainWorktreePath: string, worktree: WorktreeRow, activePath: string | null, invalidReason: string | null, metadata: Pick<AppRow, 'upstream' | 'upstreamUnavailable' | 'workingTree' | 'pullRequest'>): AppRow;
|
|
50
|
+
export declare function toAppRow(mainWorktreePath: string, worktree: WorktreeRow, activePath: string | null, invalidReason: string | null, metadata: Pick<AppRow, 'upstream' | 'upstreamUnavailable' | 'workingTree' | 'pullRequest' | 'branchCreatedAtMs'>): AppRow;
|
|
63
51
|
export declare function buildInitialModel(cwd: string): Promise<AppModel>;
|
|
64
52
|
export declare function buildActions(cwd: string): Promise<AppActions>;
|
|
65
|
-
export {};
|
package/dist/core/runtime.js
CHANGED
|
@@ -1,133 +1,36 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
1
2
|
import path from 'node:path';
|
|
2
|
-
import { execFile } from 'node:child_process';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
|
-
import { loadToolConfig } from './config.js';
|
|
4
|
+
import { loadToolConfig } from './config-lifecycle.js';
|
|
5
5
|
import { readWorktrees, sortWorktrees, toShortPath } from './git-worktrees.js';
|
|
6
|
+
import { readGitStatusSummary, readBranchCreatedAtMs, resolveRepoContext } from './git-metadata.js';
|
|
7
|
+
import { readPullRequestInfo } from './github-metadata.js';
|
|
8
|
+
import { readLogs } from './log-reader.js';
|
|
6
9
|
import { getInvalidReason } from './validation.js';
|
|
7
10
|
import { getSessionPaths, readSessionRecord, writeSessionRecord, clearSessionRecord } from './session-store.js';
|
|
8
|
-
import { startDetachedCommand } from './command-runner.js';
|
|
11
|
+
import { runCommandToLog, startDetachedCommand } from './command-runner.js';
|
|
9
12
|
import { stopSessionWithFallback } from './process-control.js';
|
|
10
13
|
import { isProcessGroupAlive, killProcessGroup, killPortOwner, killOrphans } from './posix-process.js';
|
|
14
|
+
import { createRuntimeStateActions } from './runtime-state.js';
|
|
11
15
|
const execFileAsync = promisify(execFile);
|
|
12
16
|
const SHORT_SHA_LENGTH = 8;
|
|
13
|
-
const GH_TIMEOUT_MS = 2500;
|
|
14
17
|
function shortenSha(headSha) {
|
|
15
18
|
return headSha.slice(0, SHORT_SHA_LENGTH);
|
|
16
19
|
}
|
|
17
|
-
function createEmptyWorkingTree() {
|
|
18
|
-
return { staged: 0, unstaged: 0, untracked: 0, conflicts: 0 };
|
|
19
|
-
}
|
|
20
|
-
export function parseGitStatusSummary(output) {
|
|
21
|
-
const workingTree = createEmptyWorkingTree();
|
|
22
|
-
let upstreamBranch;
|
|
23
|
-
let ahead = 0;
|
|
24
|
-
let behind = 0;
|
|
25
|
-
for (const line of output.split('\n')) {
|
|
26
|
-
if (line.startsWith('# branch.upstream ')) {
|
|
27
|
-
upstreamBranch = line.slice('# branch.upstream '.length).trim();
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (line.startsWith('# branch.ab ')) {
|
|
31
|
-
const match = /# branch\.ab \+(\d+) -(\d+)/.exec(line);
|
|
32
|
-
ahead = Number(match?.[1] ?? 0);
|
|
33
|
-
behind = Number(match?.[2] ?? 0);
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (line.startsWith('1 ') || line.startsWith('2 ')) {
|
|
37
|
-
const [, xy = '..'] = line.split(' ', 3);
|
|
38
|
-
if (xy[0] !== '.') {
|
|
39
|
-
workingTree.staged += 1;
|
|
40
|
-
}
|
|
41
|
-
if (xy[1] !== '.') {
|
|
42
|
-
workingTree.unstaged += 1;
|
|
43
|
-
}
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (line.startsWith('u ')) {
|
|
47
|
-
workingTree.conflicts += 1;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (line.startsWith('? ')) {
|
|
51
|
-
workingTree.untracked += 1;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
upstream: upstreamBranch ? { branch: upstreamBranch, ahead, behind } : undefined,
|
|
56
|
-
upstreamUnavailable: false,
|
|
57
|
-
workingTree,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
async function readGitStatusSummary(cwd) {
|
|
61
|
-
try {
|
|
62
|
-
const { stdout } = await execFileAsync('git', ['status', '--branch', '--porcelain=v2'], { cwd });
|
|
63
|
-
return parseGitStatusSummary(stdout);
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return { upstreamUnavailable: true };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
async function readPullRequestList(cwd, branch, state) {
|
|
70
|
-
const { stdout } = await execFileAsync('gh', [
|
|
71
|
-
'pr',
|
|
72
|
-
'list',
|
|
73
|
-
'--head',
|
|
74
|
-
branch,
|
|
75
|
-
'--state',
|
|
76
|
-
state,
|
|
77
|
-
'--limit',
|
|
78
|
-
'1',
|
|
79
|
-
'--json',
|
|
80
|
-
'number,title,url,state,isDraft,baseRefName',
|
|
81
|
-
], { cwd, timeout: GH_TIMEOUT_MS });
|
|
82
|
-
return JSON.parse(stdout);
|
|
83
|
-
}
|
|
84
|
-
async function readPullRequestInfo(cwd, branch) {
|
|
85
|
-
if (branch.startsWith('(')) {
|
|
86
|
-
return { kind: 'none' };
|
|
87
|
-
}
|
|
88
|
-
try {
|
|
89
|
-
const openPullRequests = await readPullRequestList(cwd, branch, 'open');
|
|
90
|
-
const parsed = openPullRequests.length > 0 ? openPullRequests : await readPullRequestList(cwd, branch, 'all');
|
|
91
|
-
const pr = parsed[0];
|
|
92
|
-
if (!pr) {
|
|
93
|
-
return { kind: 'none' };
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
kind: 'found',
|
|
97
|
-
number: pr.number,
|
|
98
|
-
title: pr.title,
|
|
99
|
-
url: pr.url,
|
|
100
|
-
state: pr.state,
|
|
101
|
-
isDraft: pr.isDraft,
|
|
102
|
-
baseBranch: pr.baseRefName,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return { kind: 'unavailable' };
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
20
|
async function readRowMetadata(worktreePath, branch) {
|
|
110
|
-
const [statusSummary, pullRequest] = await Promise.all([
|
|
21
|
+
const [statusSummary, pullRequest, branchCreatedAtMs] = await Promise.all([
|
|
111
22
|
readGitStatusSummary(worktreePath),
|
|
112
23
|
readPullRequestInfo(worktreePath, branch),
|
|
24
|
+
readBranchCreatedAtMs(worktreePath, branch),
|
|
113
25
|
]);
|
|
114
26
|
return {
|
|
115
27
|
upstream: statusSummary.upstream,
|
|
116
28
|
upstreamUnavailable: statusSummary.upstreamUnavailable,
|
|
117
29
|
workingTree: statusSummary.workingTree,
|
|
118
30
|
pullRequest,
|
|
31
|
+
branchCreatedAtMs: branchCreatedAtMs ?? undefined,
|
|
119
32
|
};
|
|
120
33
|
}
|
|
121
|
-
async function resolveRepoContext(cwd) {
|
|
122
|
-
const [{ stdout: workspaceRootRaw }, { stdout: gitCommonDirRaw }] = await Promise.all([
|
|
123
|
-
execFileAsync('git', ['rev-parse', '--show-toplevel'], { cwd }),
|
|
124
|
-
execFileAsync('git', ['rev-parse', '--git-common-dir'], { cwd }),
|
|
125
|
-
]);
|
|
126
|
-
const workspaceRoot = workspaceRootRaw.trim();
|
|
127
|
-
const gitCommonDir = path.resolve(workspaceRoot, gitCommonDirRaw.trim());
|
|
128
|
-
const mainWorktreePath = path.dirname(gitCommonDir);
|
|
129
|
-
return { workspaceRoot, mainWorktreePath, gitCommonDir };
|
|
130
|
-
}
|
|
131
34
|
export function toAppRow(mainWorktreePath, worktree, activePath, invalidReason, metadata) {
|
|
132
35
|
const tags = [];
|
|
133
36
|
if (worktree.isMain) {
|
|
@@ -152,6 +55,7 @@ export function toAppRow(mainWorktreePath, worktree, activePath, invalidReason,
|
|
|
152
55
|
upstreamUnavailable: metadata.upstreamUnavailable,
|
|
153
56
|
workingTree: metadata.workingTree,
|
|
154
57
|
pullRequest: metadata.pullRequest,
|
|
58
|
+
branchCreatedAtMs: metadata.branchCreatedAtMs,
|
|
155
59
|
invalidReason: invalidReason ?? undefined,
|
|
156
60
|
};
|
|
157
61
|
}
|
|
@@ -177,6 +81,42 @@ async function stopRecordedSession(pgid, port, orphanMatchers) {
|
|
|
177
81
|
throw new Error(`Failed to stop existing session pgid=${pgid}`);
|
|
178
82
|
}
|
|
179
83
|
}
|
|
84
|
+
async function launchDetachedCommand(command, cwd) {
|
|
85
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
86
|
+
let settled = false;
|
|
87
|
+
const child = spawn(command[0], command.slice(1), { cwd, detached: true, stdio: 'ignore' });
|
|
88
|
+
const finalize = (callback) => {
|
|
89
|
+
if (settled) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
settled = true;
|
|
93
|
+
callback();
|
|
94
|
+
};
|
|
95
|
+
child.once('error', error => {
|
|
96
|
+
finalize(() => reject(error));
|
|
97
|
+
});
|
|
98
|
+
child.once('spawn', () => {
|
|
99
|
+
finalize(() => {
|
|
100
|
+
child.unref();
|
|
101
|
+
resolve();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
await promise;
|
|
105
|
+
}
|
|
106
|
+
function getBrowserOpenCommand(url) {
|
|
107
|
+
switch (process.platform) {
|
|
108
|
+
case 'darwin':
|
|
109
|
+
return ['open', url];
|
|
110
|
+
case 'win32':
|
|
111
|
+
return ['cmd', '/c', 'start', '', url];
|
|
112
|
+
default:
|
|
113
|
+
return ['xdg-open', url];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function readSelectedWorktree(workspaceRoot, mainWorktreePath, worktreePath) {
|
|
117
|
+
const rows = await readWorktrees(workspaceRoot, mainWorktreePath);
|
|
118
|
+
return rows.find(row => row.path === worktreePath) ?? null;
|
|
119
|
+
}
|
|
180
120
|
export async function buildInitialModel(cwd) {
|
|
181
121
|
const { workspaceRoot, mainWorktreePath, gitCommonDir } = await resolveRepoContext(cwd);
|
|
182
122
|
const config = await loadToolConfig({ repoRoot: workspaceRoot });
|
|
@@ -189,75 +129,78 @@ export async function buildInitialModel(cwd) {
|
|
|
189
129
|
activePath: active?.worktreePath ?? null,
|
|
190
130
|
activeBranch: active?.branch ?? null,
|
|
191
131
|
status: active ? { kind: 'running', message: `Active: ${active.branch}` } : { kind: 'idle', message: 'ready' },
|
|
132
|
+
setupAvailable: config.setupCommand !== undefined,
|
|
133
|
+
editorAvailable: config.editorCommand !== undefined,
|
|
134
|
+
logs: await readLogs(paths.logsDir, active?.logPath ?? null),
|
|
192
135
|
};
|
|
193
136
|
}
|
|
194
137
|
export async function buildActions(cwd) {
|
|
195
|
-
const { workspaceRoot, gitCommonDir } = await resolveRepoContext(cwd);
|
|
138
|
+
const { workspaceRoot, gitCommonDir, mainWorktreePath } = await resolveRepoContext(cwd);
|
|
196
139
|
const config = await loadToolConfig({ repoRoot: workspaceRoot });
|
|
197
140
|
const paths = getSessionPaths(gitCommonDir, config.namespace);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
141
|
+
return createRuntimeStateActions({
|
|
142
|
+
config,
|
|
143
|
+
paths,
|
|
144
|
+
adapter: {
|
|
145
|
+
refresh: async () => buildInitialModel(cwd),
|
|
146
|
+
readActive: async () => readSessionRecord(paths, { isSessionAlive: isProcessGroupAlive }),
|
|
147
|
+
readLogs: async (logPath) => readLogs(paths.logsDir, logPath),
|
|
148
|
+
readWorktreeBranch: async (worktreePath) => {
|
|
149
|
+
const selected = await readSelectedWorktree(workspaceRoot, mainWorktreePath, worktreePath);
|
|
150
|
+
if (!selected) {
|
|
151
|
+
throw new Error(`Worktree disappeared: ${worktreePath}`);
|
|
152
|
+
}
|
|
153
|
+
return selected.branch;
|
|
154
|
+
},
|
|
155
|
+
getInvalidReason: async (worktreePath) => getInvalidReason(worktreePath, config.requiredFiles),
|
|
156
|
+
runSetup: runCommandToLog,
|
|
157
|
+
startCommand: startDetachedCommand,
|
|
158
|
+
stopSession: async (active) => stopRecordedSession(active.pgid, active.port, config.orphanMatchers),
|
|
159
|
+
clearSession: async () => clearSessionRecord(paths),
|
|
160
|
+
writeSession: async (record) => writeSessionRecord(paths, record),
|
|
161
|
+
openEditor: async (worktreePath) => {
|
|
162
|
+
const selected = await readSelectedWorktree(workspaceRoot, mainWorktreePath, worktreePath);
|
|
163
|
+
if (!selected) {
|
|
164
|
+
return { kind: 'idle', message: 'worktree no longer exists' };
|
|
165
|
+
}
|
|
166
|
+
if (config.editorCommand === undefined) {
|
|
167
|
+
return { kind: 'idle', message: 'editor command is not configured' };
|
|
168
|
+
}
|
|
169
|
+
await launchDetachedCommand([...config.editorCommand, worktreePath], worktreePath);
|
|
170
|
+
return { kind: 'idle', message: `opened editor for ${selected.branch}` };
|
|
171
|
+
},
|
|
172
|
+
openPullRequest: async (worktreePath) => {
|
|
173
|
+
const selected = await readSelectedWorktree(workspaceRoot, mainWorktreePath, worktreePath);
|
|
174
|
+
if (!selected) {
|
|
175
|
+
return { kind: 'idle', message: 'worktree no longer exists' };
|
|
176
|
+
}
|
|
177
|
+
const pullRequest = await readPullRequestInfo(worktreePath, selected.branch);
|
|
178
|
+
if (pullRequest.kind === 'none') {
|
|
179
|
+
return { kind: 'idle', message: `no pull request found for ${selected.branch}` };
|
|
180
|
+
}
|
|
181
|
+
if (pullRequest.kind === 'unavailable') {
|
|
182
|
+
return { kind: 'error', message: `pull request metadata is unavailable for ${selected.branch}` };
|
|
183
|
+
}
|
|
184
|
+
await launchDetachedCommand(getBrowserOpenCommand(pullRequest.url), worktreePath);
|
|
185
|
+
return { kind: 'idle', message: `opened pull request #${pullRequest.number} for ${selected.branch}` };
|
|
186
|
+
},
|
|
187
|
+
deleteWorktree: async (worktreePath) => {
|
|
188
|
+
const selected = await readSelectedWorktree(workspaceRoot, mainWorktreePath, worktreePath);
|
|
189
|
+
if (!selected) {
|
|
190
|
+
return { kind: 'idle', message: 'worktree no longer exists' };
|
|
191
|
+
}
|
|
192
|
+
if (selected.isMain) {
|
|
193
|
+
return { kind: 'idle', message: 'cannot delete the main worktree' };
|
|
194
|
+
}
|
|
195
|
+
const active = await readSessionRecord(paths, { isSessionAlive: isProcessGroupAlive });
|
|
196
|
+
if (active?.worktreePath === worktreePath) {
|
|
197
|
+
await stopRecordedSession(active.pgid, active.port, config.orphanMatchers);
|
|
198
|
+
await clearSessionRecord(paths);
|
|
199
|
+
}
|
|
200
|
+
await execFileAsync('git', ['worktree', 'remove', worktreePath], { cwd: workspaceRoot });
|
|
201
|
+
return { kind: 'idle', message: `deleted ${selected.branch}` };
|
|
202
|
+
},
|
|
203
|
+
nowIso: () => new Date().toISOString(),
|
|
204
|
+
},
|
|
205
|
+
});
|
|
263
206
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AppRow, AppStatus } from './runtime.js';
|
|
2
|
+
export type EnterInteractionDecision = {
|
|
3
|
+
kind: 'ignore';
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'set-status';
|
|
6
|
+
status: AppStatus;
|
|
7
|
+
suppressesBackgroundRefreshes: true;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'start';
|
|
10
|
+
path: string;
|
|
11
|
+
status: AppStatus;
|
|
12
|
+
};
|
|
13
|
+
export type SetupInteractionDecision = {
|
|
14
|
+
kind: 'ignore';
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'setup';
|
|
17
|
+
path: string;
|
|
18
|
+
status: AppStatus;
|
|
19
|
+
};
|
|
20
|
+
export interface AsyncInteractionState {
|
|
21
|
+
generation: number;
|
|
22
|
+
currentGeneration: number;
|
|
23
|
+
userActionInFlight: boolean;
|
|
24
|
+
blocksInput: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function getNextSelectedPath(rows: readonly AppRow[], currentPath: string | null): string | null;
|
|
27
|
+
export declare function getSelectedIndex(rows: readonly AppRow[], selectedPath: string | null): number;
|
|
28
|
+
export declare function clampSelectionIndex(nextIndex: number, rowCount: number): number | null;
|
|
29
|
+
export declare function decideEnterInteraction(selected: AppRow | undefined, activePath: string | null): EnterInteractionDecision;
|
|
30
|
+
export declare function decideSetupInteraction(selected: AppRow | undefined, setupAvailable: boolean): SetupInteractionDecision;
|
|
31
|
+
export declare function shouldApplyAsyncResult(state: AsyncInteractionState): boolean;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function getNextSelectedPath(rows, currentPath) {
|
|
2
|
+
if (rows.length === 0) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
if (currentPath && rows.some(row => row.path === currentPath)) {
|
|
6
|
+
return currentPath;
|
|
7
|
+
}
|
|
8
|
+
return rows[0].path;
|
|
9
|
+
}
|
|
10
|
+
export function getSelectedIndex(rows, selectedPath) {
|
|
11
|
+
if (selectedPath === null) {
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
const foundIndex = rows.findIndex(row => row.path === selectedPath);
|
|
15
|
+
return foundIndex >= 0 ? foundIndex : 0;
|
|
16
|
+
}
|
|
17
|
+
export function clampSelectionIndex(nextIndex, rowCount) {
|
|
18
|
+
if (rowCount <= 0) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return Math.min(Math.max(nextIndex, 0), rowCount - 1);
|
|
22
|
+
}
|
|
23
|
+
export function decideEnterInteraction(selected, activePath) {
|
|
24
|
+
if (selected === undefined) {
|
|
25
|
+
return { kind: 'ignore' };
|
|
26
|
+
}
|
|
27
|
+
if (selected.invalidReason) {
|
|
28
|
+
return {
|
|
29
|
+
kind: 'set-status',
|
|
30
|
+
status: { kind: 'error', message: selected.invalidReason },
|
|
31
|
+
suppressesBackgroundRefreshes: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (selected.path === activePath) {
|
|
35
|
+
return {
|
|
36
|
+
kind: 'set-status',
|
|
37
|
+
status: { kind: 'idle', message: 'already active' },
|
|
38
|
+
suppressesBackgroundRefreshes: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
kind: 'start',
|
|
43
|
+
path: selected.path,
|
|
44
|
+
status: { kind: 'starting', message: `Starting ${selected.branch}...` },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function decideSetupInteraction(selected, setupAvailable) {
|
|
48
|
+
if (selected === undefined || !setupAvailable) {
|
|
49
|
+
return { kind: 'ignore' };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
kind: 'setup',
|
|
53
|
+
path: selected.path,
|
|
54
|
+
status: { kind: 'setting-up', message: `Running setup for ${selected.branch}...` },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function shouldApplyAsyncResult(state) {
|
|
58
|
+
return state.blocksInput || (state.generation === state.currentGeneration && !state.userActionInFlight);
|
|
59
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { AppRow, RowTag } from './runtime.js';
|
|
2
|
+
export type ProjectionSeverity = 'success' | 'error' | 'info';
|
|
3
|
+
export type ProjectionTag = RowTag | string;
|
|
4
|
+
export type WorktreeListRowState = 'active' | 'invalid' | 'external' | 'normal';
|
|
5
|
+
export interface WorktreeListRowProjection {
|
|
6
|
+
state: WorktreeListRowState;
|
|
7
|
+
isSelected: boolean;
|
|
8
|
+
isMain: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface TagProjection {
|
|
11
|
+
tag: ProjectionTag;
|
|
12
|
+
}
|
|
13
|
+
export type ActionProjection = {
|
|
14
|
+
kind: 'blocked';
|
|
15
|
+
severity: 'error';
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'active';
|
|
18
|
+
severity: 'success';
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'startable';
|
|
21
|
+
severity: 'error' | 'info';
|
|
22
|
+
};
|
|
23
|
+
export type NoteProjection = {
|
|
24
|
+
kind: 'invalid';
|
|
25
|
+
severity: 'error';
|
|
26
|
+
invalidReason: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'external';
|
|
29
|
+
severity: 'info';
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'ready';
|
|
32
|
+
severity: 'error' | 'info';
|
|
33
|
+
};
|
|
34
|
+
export type UpstreamProjection = {
|
|
35
|
+
kind: 'unavailable';
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'none';
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'found';
|
|
40
|
+
branch: string;
|
|
41
|
+
ahead: number;
|
|
42
|
+
behind: number;
|
|
43
|
+
};
|
|
44
|
+
export type WorkingTreePartKind = 'staged' | 'unstaged' | 'untracked' | 'conflicts';
|
|
45
|
+
export type WorkingTreeProjection = {
|
|
46
|
+
kind: 'unavailable';
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'clean';
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'dirty';
|
|
51
|
+
parts: Array<{
|
|
52
|
+
kind: WorkingTreePartKind;
|
|
53
|
+
count: number;
|
|
54
|
+
}>;
|
|
55
|
+
};
|
|
56
|
+
export type PullRequestProjection = {
|
|
57
|
+
kind: 'none';
|
|
58
|
+
} | {
|
|
59
|
+
kind: 'unavailable';
|
|
60
|
+
} | {
|
|
61
|
+
kind: 'found';
|
|
62
|
+
number: number;
|
|
63
|
+
title: string;
|
|
64
|
+
state: 'OPEN' | 'CLOSED' | 'MERGED';
|
|
65
|
+
isDraft: boolean;
|
|
66
|
+
baseBranch: string;
|
|
67
|
+
isHistorical: boolean;
|
|
68
|
+
};
|
|
69
|
+
export declare function sanitizeInlineText(value: string): string;
|
|
70
|
+
export declare function projectWorktreeListRow(row: AppRow, isSelected: boolean): WorktreeListRowProjection;
|
|
71
|
+
export declare function getOrderedNonActiveTags(tags: readonly string[]): TagProjection[];
|
|
72
|
+
export declare function projectAction(row: AppRow, activePath: string | null): ActionProjection;
|
|
73
|
+
export declare function projectNote(row: AppRow): NoteProjection;
|
|
74
|
+
export declare function projectUpstream(row: AppRow): UpstreamProjection;
|
|
75
|
+
export declare function projectWorkingTree(row: AppRow): WorkingTreeProjection;
|
|
76
|
+
export declare function projectPullRequest(row: AppRow): PullRequestProjection;
|