@senad-d/branchme 0.1.7 → 0.1.8
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/CHANGELOG.md +8 -5
- package/README.md +74 -13
- package/SECURITY.md +21 -3
- package/docs/PROJECT_DEFINITION_BRIEF.md +112 -77
- package/docs/SMOKE_TEST.md +21 -11
- package/docs/STRUCTURE.md +19 -14
- package/docs/TUI_CAPTURE.md +52 -22
- package/package.json +9 -5
- package/src/commands/branchme-command.ts +9 -1
- package/src/constants.ts +12 -0
- package/src/git.ts +1027 -2
- package/src/tools/branchme-tools.ts +190 -0
- package/src/types.ts +111 -0
- package/src/ui/branchme-panel.ts +14 -3
|
@@ -1,27 +1,35 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
1
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { Type } from "typebox";
|
|
3
4
|
import {
|
|
4
5
|
BRANCH_STATUS_TOOL_NAME,
|
|
5
6
|
CHANGE_BRANCH_TOOL_NAME,
|
|
6
7
|
CREATE_BRANCH_TOOL_NAME,
|
|
8
|
+
CREATE_WORKTREE_TOOL_NAME,
|
|
7
9
|
FETCH_BRANCH_TOOL_NAME,
|
|
10
|
+
GIT_WORKTREE_SUMMARY_LIMIT_CHARS,
|
|
11
|
+
LIST_WORKTREES_TOOL_NAME,
|
|
8
12
|
PULL_BRANCH_TOOL_NAME,
|
|
9
13
|
PULL_REQUEST_TOOL_NAME,
|
|
10
14
|
PUSH_BRANCH_TOOL_NAME,
|
|
11
15
|
REBASE_BRANCH_TOOL_NAME,
|
|
16
|
+
REMOVE_WORKTREE_TOOL_NAME,
|
|
12
17
|
} from "../constants.ts";
|
|
13
18
|
import {
|
|
14
19
|
changeExistingLocalBranch,
|
|
15
20
|
createLocalBranch,
|
|
21
|
+
createWorktree,
|
|
16
22
|
fetchCurrentBranch,
|
|
17
23
|
getGitRoot,
|
|
18
24
|
getLocalBranchCommit,
|
|
19
25
|
getPullRequestCommitSubjects,
|
|
20
26
|
inferPullRequestBaseBranch,
|
|
27
|
+
listWorktrees,
|
|
21
28
|
localBranchExists,
|
|
22
29
|
pullCurrentBranch,
|
|
23
30
|
pushCurrentBranch,
|
|
24
31
|
rebaseCurrentBranch,
|
|
32
|
+
removeWorktree,
|
|
25
33
|
requireCurrentBranch,
|
|
26
34
|
validateBranchName,
|
|
27
35
|
withRepositoryMutationQueue,
|
|
@@ -39,12 +47,16 @@ import {
|
|
|
39
47
|
} from "../github.ts";
|
|
40
48
|
import type {
|
|
41
49
|
ChangeBranchDetails,
|
|
50
|
+
CreateWorktreeDetails,
|
|
42
51
|
GitContextDetails,
|
|
52
|
+
ListWorktreesDetails,
|
|
43
53
|
PullRequestDetails,
|
|
44
54
|
PullRequestInput,
|
|
45
55
|
PullRequestInputField,
|
|
46
56
|
PullRequestToolDetails,
|
|
47
57
|
PullRequestToolInput,
|
|
58
|
+
RemoveWorktreeDetails,
|
|
59
|
+
WorktreeEntry,
|
|
48
60
|
} from "../types.ts";
|
|
49
61
|
|
|
50
62
|
const EmptyParametersSchema = Type.Object({}, { additionalProperties: false });
|
|
@@ -63,6 +75,24 @@ const ChangeBranchParametersSchema = Type.Object(
|
|
|
63
75
|
{ additionalProperties: false },
|
|
64
76
|
);
|
|
65
77
|
|
|
78
|
+
const CreateWorktreeParametersSchema = Type.Object(
|
|
79
|
+
{
|
|
80
|
+
worktreePath: Type.String({ minLength: 1, description: "Explicit absolute destination path for the linked worktree." }),
|
|
81
|
+
branchName: Type.String({ minLength: 1, description: "New or existing local branch name for the linked worktree." }),
|
|
82
|
+
branchMode: StringEnum(["new", "existing"] as const, {
|
|
83
|
+
description: "Whether create_worktree creates a new local branch from current HEAD or uses an existing local branch.",
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
{ additionalProperties: false },
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const RemoveWorktreeParametersSchema = Type.Object(
|
|
90
|
+
{
|
|
91
|
+
worktreePath: Type.String({ minLength: 1, description: "Explicit absolute path of the linked worktree to remove." }),
|
|
92
|
+
},
|
|
93
|
+
{ additionalProperties: false },
|
|
94
|
+
);
|
|
95
|
+
|
|
66
96
|
const PullRequestParametersSchema = Type.Object(
|
|
67
97
|
{
|
|
68
98
|
headBranch: Type.Optional(
|
|
@@ -100,6 +130,100 @@ export function formatPullRequest(details: PullRequestDetails, autofilledFields:
|
|
|
100
130
|
return `Created pull request #${details.number} (${details.state}) for ${repositoryLabel(details.repository)}: ${details.url}.${autofill}`;
|
|
101
131
|
}
|
|
102
132
|
|
|
133
|
+
const WORKTREE_FORMAT_PATH_LIMIT_CHARS = 512;
|
|
134
|
+
const WORKTREE_FORMAT_BRANCH_LIMIT_CHARS = 256;
|
|
135
|
+
|
|
136
|
+
function truncateWorktreeFormatValue(value: string, limit: number): string {
|
|
137
|
+
if (value.length <= limit) return value;
|
|
138
|
+
let end = limit - 1;
|
|
139
|
+
const lastCodePoint = value.codePointAt(end - 1);
|
|
140
|
+
if (lastCodePoint !== undefined && lastCodePoint > 0xffff) end -= 1;
|
|
141
|
+
return `${value.slice(0, end)}…`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function escapeWorktreeFormatControlCharacter(character: string): string {
|
|
145
|
+
const codePoint = character.codePointAt(0);
|
|
146
|
+
return codePoint === undefined ? "" : String.raw`\u${codePoint.toString(16).padStart(4, "0")}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function safeWorktreeFormatValue(value: string, limit: number): string {
|
|
150
|
+
const redacted = redactSecrets(value);
|
|
151
|
+
const escaped = redacted.replace(
|
|
152
|
+
/[\p{Cc}\p{Cf}\u2028\u2029]/gu,
|
|
153
|
+
escapeWorktreeFormatControlCharacter,
|
|
154
|
+
);
|
|
155
|
+
return truncateWorktreeFormatValue(escaped, limit);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function worktreeDisplayState(worktree: WorktreeEntry): string {
|
|
159
|
+
if (worktree.bare) return "bare";
|
|
160
|
+
if (worktree.detached) return "detached";
|
|
161
|
+
const branch = worktree.branch ?? "unknown";
|
|
162
|
+
return `branch ${safeWorktreeFormatValue(branch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS)}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function worktreeDisplayIndicators(worktree: WorktreeEntry): string[] {
|
|
166
|
+
const indicators: string[] = [];
|
|
167
|
+
if (worktree.main) indicators.push("main");
|
|
168
|
+
if (worktree.current) indicators.push("current");
|
|
169
|
+
if (worktree.locked) indicators.push("locked");
|
|
170
|
+
if (worktree.prunable) indicators.push("prunable");
|
|
171
|
+
return indicators;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function formatWorktreeEntry(worktree: WorktreeEntry): string {
|
|
175
|
+
const path = safeWorktreeFormatValue(worktree.path, WORKTREE_FORMAT_PATH_LIMIT_CHARS);
|
|
176
|
+
const head = worktree.head === null ? "none" : shortCommit(worktree.head);
|
|
177
|
+
const indicators = worktreeDisplayIndicators(worktree);
|
|
178
|
+
const indicatorText = indicators.length === 0 ? "" : ` | ${indicators.join(",")}`;
|
|
179
|
+
return `- ${path} | ${worktreeDisplayState(worktree)} | HEAD ${head}${indicatorText}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function worktreeOmissionLine(omitted: number): string {
|
|
183
|
+
return `${omitted} worktree entr${omitted === 1 ? "y" : "ies"} omitted.`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function formatListWorktrees(details: ListWorktreesDetails): string {
|
|
187
|
+
const repoRoot = safeWorktreeFormatValue(details.repoRoot, WORKTREE_FORMAT_PATH_LIMIT_CHARS);
|
|
188
|
+
const lines = [`Worktrees for ${repoRoot}:`];
|
|
189
|
+
let omitted = details.omitted;
|
|
190
|
+
|
|
191
|
+
for (const [index, worktree] of details.worktrees.entries()) {
|
|
192
|
+
const line = formatWorktreeEntry(worktree);
|
|
193
|
+
const remaining = details.worktrees.length - index - 1;
|
|
194
|
+
const candidateOmitted = details.omitted + remaining;
|
|
195
|
+
const candidate = [
|
|
196
|
+
...lines,
|
|
197
|
+
line,
|
|
198
|
+
...(candidateOmitted > 0 ? [worktreeOmissionLine(candidateOmitted)] : []),
|
|
199
|
+
].join("\n");
|
|
200
|
+
if (candidate.length > GIT_WORKTREE_SUMMARY_LIMIT_CHARS) {
|
|
201
|
+
omitted += details.worktrees.length - index;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
lines.push(line);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (omitted > 0) lines.push(worktreeOmissionLine(omitted));
|
|
208
|
+
return lines.join("\n");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function formatCreateWorktree(details: CreateWorktreeDetails): string {
|
|
212
|
+
const cwd = safeWorktreeFormatValue(details.handoff.cwd, WORKTREE_FORMAT_PATH_LIMIT_CHARS);
|
|
213
|
+
const branch = safeWorktreeFormatValue(details.handoff.branch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS);
|
|
214
|
+
const mode = details.request.branchMode === "new" ? "new local branch" : "existing local branch";
|
|
215
|
+
return `Created linked worktree ${cwd} for ${mode} ${branch}. Verified its canonical path, local branch, HEAD ${shortCommit(details.handoff.head)}, clean working tree, and ready handoff.`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function formatRemoveWorktree(details: RemoveWorktreeDetails): string {
|
|
219
|
+
const path = safeWorktreeFormatValue(
|
|
220
|
+
details.verified.before.worktree.path,
|
|
221
|
+
WORKTREE_FORMAT_PATH_LIMIT_CHARS,
|
|
222
|
+
);
|
|
223
|
+
const branch = safeWorktreeFormatValue(details.handoff.branch, WORKTREE_FORMAT_BRANCH_LIMIT_CHARS);
|
|
224
|
+
return `Removed linked worktree directory ${path}. Verified it is no longer registered and retained local branch ${branch} at HEAD ${shortCommit(details.handoff.head)}; the removed cwd is not ready for handoff.`;
|
|
225
|
+
}
|
|
226
|
+
|
|
103
227
|
const PULL_REQUEST_INPUT_FIELDS: PullRequestInputField[] = ["headBranch", "baseBranch", "title", "body", "draft"];
|
|
104
228
|
|
|
105
229
|
interface ResolvedPullRequestBranches {
|
|
@@ -464,4 +588,70 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
464
588
|
});
|
|
465
589
|
},
|
|
466
590
|
});
|
|
591
|
+
|
|
592
|
+
pi.registerTool({
|
|
593
|
+
name: LIST_WORKTREES_TOOL_NAME,
|
|
594
|
+
label: "List Worktrees",
|
|
595
|
+
description: "list_worktrees reads the current repository's bounded linked-worktree inventory without changing Git or filesystem state. list_worktrees accepts no parameters and reports paths, local branches or detached state, HEADs, and safety indicators.",
|
|
596
|
+
promptSnippet: "list_worktrees: explicitly inspect the bounded current-repository worktree inventory without mutation",
|
|
597
|
+
promptGuidelines: [
|
|
598
|
+
"Use list_worktrees when the user or task explicitly needs the current repository's worktree inventory; list_worktrees is read-only.",
|
|
599
|
+
"Use list_worktrees explicitly because automatic Git context remains focused on the active worktree and does not include worktree inventory.",
|
|
600
|
+
],
|
|
601
|
+
parameters: EmptyParametersSchema,
|
|
602
|
+
async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
|
|
603
|
+
const details = await listWorktrees(pi, ctx, signal);
|
|
604
|
+
return {
|
|
605
|
+
content: [{ type: "text", text: formatListWorktrees(details) }],
|
|
606
|
+
details,
|
|
607
|
+
};
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
pi.registerTool({
|
|
612
|
+
name: CREATE_WORKTREE_TOOL_NAME,
|
|
613
|
+
label: "Create Worktree",
|
|
614
|
+
description: "create_worktree creates and verifies one linked worktree at an explicit absolute worktreePath for either a new local branch from current HEAD or an existing unoccupied local branch. create_worktree returns exact lossless handoff identity fields and never infers paths, remotes, base refs, detached or orphan modes, or force behavior.",
|
|
615
|
+
promptSnippet: "create_worktree: create and verify a linked worktree at an explicit absolute path with a ready handoff cwd",
|
|
616
|
+
promptGuidelines: [
|
|
617
|
+
"Use create_worktree only when the user explicitly requests worktree creation and provides or approves the exact absolute worktreePath; create_worktree must never infer a filesystem path silently.",
|
|
618
|
+
"Use create_worktree with exactly worktreePath, branchName, and branchMode; create_worktree never accepts force, baseRef, remote, detach, orphan, move, prune, repair, lock, or unlock parameters.",
|
|
619
|
+
"Do not batch create_worktree with dependent worktree mutations; wait for create_worktree to complete and verify its ready exact handoff.cwd before passing that cwd to another agent or session.",
|
|
620
|
+
],
|
|
621
|
+
parameters: CreateWorktreeParametersSchema,
|
|
622
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
623
|
+
const details = await createWorktree(
|
|
624
|
+
pi,
|
|
625
|
+
ctx,
|
|
626
|
+
params.worktreePath,
|
|
627
|
+
params.branchName,
|
|
628
|
+
params.branchMode,
|
|
629
|
+
signal,
|
|
630
|
+
);
|
|
631
|
+
return {
|
|
632
|
+
content: [{ type: "text", text: formatCreateWorktree(details) }],
|
|
633
|
+
details,
|
|
634
|
+
};
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
pi.registerTool({
|
|
639
|
+
name: REMOVE_WORKTREE_TOOL_NAME,
|
|
640
|
+
label: "Remove Worktree",
|
|
641
|
+
description: "remove_worktree force-free removes one verified clean linked worktree at an explicit absolute worktreePath while retaining and returning its exact local branch identity. remove_worktree rejects main, current, dirty, detached, locked, prunable, missing, and foreign worktrees and never deletes branches.",
|
|
642
|
+
promptSnippet: "remove_worktree: force-free removal of an explicitly selected clean linked worktree while retaining its branch",
|
|
643
|
+
promptGuidelines: [
|
|
644
|
+
"Use remove_worktree only when the user explicitly requests worktree removal and provides or approves the exact absolute worktreePath; remove_worktree must never infer a filesystem path silently.",
|
|
645
|
+
"Use remove_worktree with exactly worktreePath; remove_worktree never accepts force, move, prune, repair, lock, unlock, branch deletion, remote, or refspec parameters.",
|
|
646
|
+
"Do not batch remove_worktree with dependent worktree mutations; wait for remove_worktree to complete and verify its non-ready handoff before continuing.",
|
|
647
|
+
],
|
|
648
|
+
parameters: RemoveWorktreeParametersSchema,
|
|
649
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
650
|
+
const details = await removeWorktree(pi, ctx, params.worktreePath, signal);
|
|
651
|
+
return {
|
|
652
|
+
content: [{ type: "text", text: formatRemoveWorktree(details) }],
|
|
653
|
+
details,
|
|
654
|
+
};
|
|
655
|
+
},
|
|
656
|
+
});
|
|
467
657
|
}
|
package/src/types.ts
CHANGED
|
@@ -20,6 +20,117 @@ export interface WorkingTreeDetails {
|
|
|
20
20
|
untracked: number;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export type CreateWorktreeMode = "new" | "existing";
|
|
24
|
+
|
|
25
|
+
export type ListWorktreesToolInput = Record<string, never>;
|
|
26
|
+
|
|
27
|
+
export interface CreateWorktreeToolInput {
|
|
28
|
+
worktreePath: string;
|
|
29
|
+
branchName: string;
|
|
30
|
+
branchMode: CreateWorktreeMode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RemoveWorktreeToolInput {
|
|
34
|
+
worktreePath: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WorktreeEntry {
|
|
38
|
+
path: string;
|
|
39
|
+
head: string | null;
|
|
40
|
+
branch: string | null;
|
|
41
|
+
detached: boolean;
|
|
42
|
+
bare: boolean;
|
|
43
|
+
locked: boolean;
|
|
44
|
+
lockReason: string | null;
|
|
45
|
+
prunable: boolean;
|
|
46
|
+
pruneReason: string | null;
|
|
47
|
+
main: boolean;
|
|
48
|
+
current: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface VerifiedLinkedWorktreeEntry extends Omit<WorktreeEntry, "path" | "branch"> {
|
|
52
|
+
/** Exact machine-readable identity validated to require no BranchMe display transformation. */
|
|
53
|
+
path: string;
|
|
54
|
+
head: string;
|
|
55
|
+
/** Exact machine-readable identity validated to require no BranchMe display transformation. */
|
|
56
|
+
branch: string;
|
|
57
|
+
detached: false;
|
|
58
|
+
bare: false;
|
|
59
|
+
locked: false;
|
|
60
|
+
lockReason: null;
|
|
61
|
+
prunable: false;
|
|
62
|
+
pruneReason: null;
|
|
63
|
+
main: false;
|
|
64
|
+
current: false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ListWorktreesDetails {
|
|
68
|
+
action: "list_worktrees";
|
|
69
|
+
repoRoot: string;
|
|
70
|
+
worktrees: WorktreeEntry[];
|
|
71
|
+
omitted: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ReadyWorktreeHandoffDetails {
|
|
75
|
+
cwd: string;
|
|
76
|
+
branch: string;
|
|
77
|
+
head: string;
|
|
78
|
+
ready: true;
|
|
79
|
+
summary: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface RetainedBranchHandoffDetails {
|
|
83
|
+
cwd: null;
|
|
84
|
+
branch: string;
|
|
85
|
+
head: string;
|
|
86
|
+
ready: false;
|
|
87
|
+
summary: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type WorktreeHandoffDetails = ReadyWorktreeHandoffDetails | RetainedBranchHandoffDetails;
|
|
91
|
+
|
|
92
|
+
export interface CreateWorktreeDetails {
|
|
93
|
+
action: "create_worktree";
|
|
94
|
+
repoRoot: string;
|
|
95
|
+
request: CreateWorktreeToolInput;
|
|
96
|
+
verified: {
|
|
97
|
+
before: {
|
|
98
|
+
sourcePath: string;
|
|
99
|
+
sourceBranch: string | null;
|
|
100
|
+
sourceDetached: boolean;
|
|
101
|
+
sourceHead: string;
|
|
102
|
+
canonicalWorktreePath: string;
|
|
103
|
+
branchExisted: boolean;
|
|
104
|
+
destinationRegistered: false;
|
|
105
|
+
};
|
|
106
|
+
after: {
|
|
107
|
+
worktreePresent: true;
|
|
108
|
+
worktree: VerifiedLinkedWorktreeEntry;
|
|
109
|
+
workingTree: WorkingTreeDetails;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
handoff: ReadyWorktreeHandoffDetails;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface RemoveWorktreeDetails {
|
|
116
|
+
action: "remove_worktree";
|
|
117
|
+
repoRoot: string;
|
|
118
|
+
request: RemoveWorktreeToolInput;
|
|
119
|
+
verified: {
|
|
120
|
+
before: {
|
|
121
|
+
worktree: VerifiedLinkedWorktreeEntry;
|
|
122
|
+
workingTree: WorkingTreeDetails;
|
|
123
|
+
};
|
|
124
|
+
after: {
|
|
125
|
+
worktreePresent: false;
|
|
126
|
+
branchRetained: true;
|
|
127
|
+
branch: string;
|
|
128
|
+
head: string;
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
handoff: RetainedBranchHandoffDetails;
|
|
132
|
+
}
|
|
133
|
+
|
|
23
134
|
export interface GitFileChange {
|
|
24
135
|
status: string;
|
|
25
136
|
path: string;
|
package/src/ui/branchme-panel.ts
CHANGED
|
@@ -24,14 +24,14 @@ type PanelCell =
|
|
|
24
24
|
preserveLeading?: boolean;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
export type BranchMePanelSection = "status" | "workflow";
|
|
27
|
+
export type BranchMePanelSection = "status" | "workflow" | "worktrees";
|
|
28
28
|
|
|
29
29
|
const NARROW_MIN_WIDTH = 24;
|
|
30
30
|
const WIDE_MIN_WIDTH = 72;
|
|
31
31
|
const MAX_PANEL_WIDTH = 96;
|
|
32
32
|
const MAX_PANEL_HEIGHT = 16;
|
|
33
33
|
const BODY_HEIGHT = 9;
|
|
34
|
-
const PANEL_SECTIONS: BranchMePanelSection[] = ["status", "workflow"];
|
|
34
|
+
const PANEL_SECTIONS: BranchMePanelSection[] = ["status", "workflow", "worktrees"];
|
|
35
35
|
const ANSI_RESET = "\u001b[0m";
|
|
36
36
|
|
|
37
37
|
function truncateVisible(value: string, width: number, ellipsis = "…", pad = false): string {
|
|
@@ -144,7 +144,7 @@ function statusDetailRow(label: string, value: string): PanelCell {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
function workflowDetailRow(tool: string, description: string): PanelCell {
|
|
147
|
-
return layoutCell(` ${tool.padEnd(
|
|
147
|
+
return layoutCell(` ${tool.padEnd(16, " ")}-> ${sanitize(description)}`);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
function sectionTitle(section: BranchMePanelSection): string {
|
|
@@ -153,6 +153,8 @@ function sectionTitle(section: BranchMePanelSection): string {
|
|
|
153
153
|
return "STATUS";
|
|
154
154
|
case "workflow":
|
|
155
155
|
return "WORKFLOW";
|
|
156
|
+
case "worktrees":
|
|
157
|
+
return "WORKTREES";
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
160
|
|
|
@@ -178,6 +180,8 @@ function sectionFooter(section: BranchMePanelSection, data: BranchMePanelData):
|
|
|
178
180
|
return "status • current repository only • tools perform actions";
|
|
179
181
|
case "workflow":
|
|
180
182
|
return "workflow • inspect → change → fetch/pull/rebase → create → push → PR";
|
|
183
|
+
case "worktrees":
|
|
184
|
+
return "worktrees • create → handoff cwd → next session • remove retains branch";
|
|
181
185
|
}
|
|
182
186
|
}
|
|
183
187
|
|
|
@@ -204,6 +208,13 @@ function sectionRows(section: BranchMePanelSection, data: BranchMePanelData): Pa
|
|
|
204
208
|
workflowDetailRow("push_branch", "current branch"),
|
|
205
209
|
workflowDetailRow("pull_request", "after push"),
|
|
206
210
|
];
|
|
211
|
+
case "worktrees":
|
|
212
|
+
return [
|
|
213
|
+
heading("WORKTREES"),
|
|
214
|
+
workflowDetailRow("list_worktrees", "inspect inventory"),
|
|
215
|
+
workflowDetailRow("create_worktree", "ready handoff.cwd"),
|
|
216
|
+
workflowDetailRow("remove_worktree", "clean linked; branch retained"),
|
|
217
|
+
];
|
|
207
218
|
}
|
|
208
219
|
}
|
|
209
220
|
|