@nseng-ai/ccc 0.1.1
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/package.json +48 -0
- package/src/api/handlers.ts +75 -0
- package/src/api/index.ts +11 -0
- package/src/cmux/branch-slug.ts +174 -0
- package/src/cmux/claude-plan-tab.ts +124 -0
- package/src/cmux/command-surfaces.ts +51 -0
- package/src/cmux/dispatch-from-trunk.ts +177 -0
- package/src/cmux/dispatch-prompt.ts +459 -0
- package/src/cmux/objective-sidebar.ts +373 -0
- package/src/cmux/prompt-file.ts +34 -0
- package/src/cmux/sidebar.ts +386 -0
- package/src/cmux/slot-checkout.ts +46 -0
- package/src/cmux/slot-dispatch-plan.ts +560 -0
- package/src/cmux/slot-open-branch.ts +241 -0
- package/src/cmux/slot.ts +126 -0
- package/src/cmux/workspace-summary.ts +222 -0
- package/src/cmux/worktree-description.ts +87 -0
- package/src/ns/autobranch/checkpoint.ts +35 -0
- package/src/ns/autobranch/flow.ts +18 -0
- package/src/ns/autoslot-presentation.ts +33 -0
- package/src/ns/autoslot.ts +2 -0
- package/src/ns/cli-command-io.ts +23 -0
- package/src/ns/cli.ts +186 -0
- package/src/ns/land.ts +12 -0
- package/src/ns/trunk-pull.ts +2 -0
- package/src/pi/claude-plan-tab.ts +32 -0
- package/src/pi/command-backed-skills.ts +7 -0
- package/src/pi/dispatch-from-trunk.ts +41 -0
- package/src/pi/dispatch-prompt.ts +41 -0
- package/src/pi/extension.ts +23 -0
- package/src/pi/index.ts +11 -0
- package/src/pi/sidebar.ts +58 -0
- package/src/pi/slot-dispatch-plan.ts +70 -0
- package/src/pi/slot-open-branch.ts +39 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BRANCH_CONTEXT_NAMESPACE,
|
|
3
|
+
buildBranchContextCreateOperation,
|
|
4
|
+
buildBranchContextOutputMessage,
|
|
5
|
+
createBranchContextFromFile,
|
|
6
|
+
createRealBranchContextContext,
|
|
7
|
+
derivePlanContentSlug,
|
|
8
|
+
formatBranchContextEvidence,
|
|
9
|
+
formatBranchContextCreateFailure,
|
|
10
|
+
formatBranchContextCreatePreview,
|
|
11
|
+
resolveBranchContextCreatePreviewContext,
|
|
12
|
+
type BranchContextContext,
|
|
13
|
+
type BranchContextContextFactory,
|
|
14
|
+
type BranchContextCreateOperation,
|
|
15
|
+
type BranchContextEvidence,
|
|
16
|
+
type BranchContextOutputDetails,
|
|
17
|
+
} from "@nseng-ai/branch-context/api";
|
|
18
|
+
import {
|
|
19
|
+
findLatestSessionSavedPlanFile,
|
|
20
|
+
resolvePlanStoreDirectory,
|
|
21
|
+
type PlanStoreDirectoryEvidence,
|
|
22
|
+
type ValidatedSessionSavedPlan,
|
|
23
|
+
} from "@nseng-ai/plans/api";
|
|
24
|
+
import { formatCommand, formatShellArg } from "@nseng-ai/foundation/command";
|
|
25
|
+
import { checkoutBranchCmuxSlot, openBranchInCmuxSlot } from "./slot.ts";
|
|
26
|
+
import { createCccSlotClient } from "./slot-checkout.ts";
|
|
27
|
+
import {
|
|
28
|
+
launchFocusedCmuxTab,
|
|
29
|
+
type FocusedCmuxTabLaunchResult,
|
|
30
|
+
} from "@nseng-ai/capability-kit/cmux/focused-terminal-tab";
|
|
31
|
+
import { buildPiLaunchCommand, getPiLaunchOptions } from "@nseng-ai/capability-kit/cmux/pi-launch";
|
|
32
|
+
import type { PiLaunchOptions } from "@nseng-ai/capability-kit/cmux/pi-launch";
|
|
33
|
+
import type { SlotCheckoutTarget, SlotClient } from "@nseng-ai/slots/api";
|
|
34
|
+
import { formatErrorMessage, optionalEntry } from "@nseng-ai/foundation/primitives";
|
|
35
|
+
import type {
|
|
36
|
+
CommandContext,
|
|
37
|
+
ExtensionAPI,
|
|
38
|
+
NotifyLevel,
|
|
39
|
+
} from "@nseng-ai/capability-kit/cmux/types";
|
|
40
|
+
|
|
41
|
+
const BRANCH_CREATION = "graphite";
|
|
42
|
+
|
|
43
|
+
export type DispatchDestination = "workspace" | "surface";
|
|
44
|
+
|
|
45
|
+
export interface DispatchPlanConfig {
|
|
46
|
+
commandName: string;
|
|
47
|
+
statusKey: string;
|
|
48
|
+
destination: DispatchDestination;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface CommandArgs {
|
|
52
|
+
isDryRun: boolean;
|
|
53
|
+
shouldShowHelp: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface HandleCommandOptions {
|
|
57
|
+
pi: ExtensionAPI;
|
|
58
|
+
rawArgs: string;
|
|
59
|
+
ctx: CommandContext;
|
|
60
|
+
options: CccSlotDispatchPlanOptions;
|
|
61
|
+
config: DispatchPlanConfig;
|
|
62
|
+
notifyProgress: (message: string) => void;
|
|
63
|
+
formatBranchContextCommand: (key: string) => string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface AttachSlotAndLaunchOptions {
|
|
67
|
+
pi: ExtensionAPI;
|
|
68
|
+
ctx: CommandContext;
|
|
69
|
+
checkout: PlanStoreDirectoryEvidence;
|
|
70
|
+
operation: BranchContextCreateOperation;
|
|
71
|
+
config: DispatchPlanConfig;
|
|
72
|
+
options: CccSlotDispatchPlanOptions;
|
|
73
|
+
notifyProgress: (message: string) => void;
|
|
74
|
+
formatBranchContextCommand: (key: string) => string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface FormatDryRunOptions {
|
|
78
|
+
plan: ValidatedSessionSavedPlan;
|
|
79
|
+
checkout: PlanStoreDirectoryEvidence;
|
|
80
|
+
operation: BranchContextCreateOperation;
|
|
81
|
+
branchContextPreview: string;
|
|
82
|
+
launchOptions: PiLaunchOptions;
|
|
83
|
+
config: DispatchPlanConfig;
|
|
84
|
+
formatBranchContextCommand: (key: string) => string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface FormatFinalSuccessOptions {
|
|
88
|
+
operation: Pick<BranchContextCreateOperation, "branch" | "key">;
|
|
89
|
+
target: SlotCheckoutTarget;
|
|
90
|
+
launchOptions: PiLaunchOptions;
|
|
91
|
+
formatBranchContextCommand: (key: string) => string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface FormatSurfaceSuccessOptions {
|
|
95
|
+
operation: Pick<BranchContextCreateOperation, "branch" | "key">;
|
|
96
|
+
target: SlotCheckoutTarget;
|
|
97
|
+
launch: Extract<FocusedCmuxTabLaunchResult, { type: "launched" }>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface CccSlotDispatchPlanOptions {
|
|
101
|
+
planStoreRoot?: string;
|
|
102
|
+
createBranchContextContext?: BranchContextContextFactory<[pi: ExtensionAPI, cwd: string]>;
|
|
103
|
+
slotClient?: SlotClient;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function handleCccSlotDispatchPlan({
|
|
107
|
+
pi,
|
|
108
|
+
rawArgs,
|
|
109
|
+
ctx,
|
|
110
|
+
options,
|
|
111
|
+
config,
|
|
112
|
+
notifyProgress,
|
|
113
|
+
formatBranchContextCommand,
|
|
114
|
+
}: HandleCommandOptions): Promise<void> {
|
|
115
|
+
const parsed = parseCommandArgs(rawArgs);
|
|
116
|
+
if ("error" in parsed) {
|
|
117
|
+
present(ctx, `${parsed.error}\n\n${formatUsage(config)}`, "error");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (parsed.shouldShowHelp) {
|
|
122
|
+
present(ctx, formatUsage(config), "info");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
notifyProgress("Finding latest saved plan…");
|
|
127
|
+
await ctx.waitForIdle();
|
|
128
|
+
|
|
129
|
+
setStatus(ctx, config, "finding latest saved plan…");
|
|
130
|
+
try {
|
|
131
|
+
const checkout = await resolveCurrentCheckout(pi, ctx.cwd, options);
|
|
132
|
+
if ("error" in checkout) {
|
|
133
|
+
present(ctx, checkout.error, "error");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const selected = await resolveLatestSavedPlanFromSession(ctx, checkout);
|
|
138
|
+
if ("error" in selected) {
|
|
139
|
+
present(ctx, selected.error, "error");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const selectedPlan = selected.plan;
|
|
144
|
+
setStatus(ctx, config, "deriving branch-context slug…");
|
|
145
|
+
const slugEvidence = await derivePlanContentSlug(pi, {
|
|
146
|
+
filePath: selectedPlan.filePath,
|
|
147
|
+
cwd: checkout.repoRoot,
|
|
148
|
+
});
|
|
149
|
+
const operation = buildBranchContextCreateOperation({
|
|
150
|
+
slug: slugEvidence.slug,
|
|
151
|
+
filePath: selectedPlan.filePath,
|
|
152
|
+
branchCreation: BRANCH_CREATION,
|
|
153
|
+
...optionalEntry("summary", selectedPlan.summary),
|
|
154
|
+
});
|
|
155
|
+
if (parsed.isDryRun) {
|
|
156
|
+
const launchOptions = getPiLaunchOptions(pi, ctx);
|
|
157
|
+
const previewContext = await resolveBranchContextCreatePreviewContext(pi, {
|
|
158
|
+
cwd: checkout.repoRoot,
|
|
159
|
+
context: dispatchBranchContextContext(pi, checkout.repoRoot, options),
|
|
160
|
+
});
|
|
161
|
+
const branchContextPreview = formatBranchContextCreatePreview(operation, {
|
|
162
|
+
...previewContext,
|
|
163
|
+
graphiteParentBranch: checkout.sourceBranch,
|
|
164
|
+
});
|
|
165
|
+
presentBranchContextMessage(
|
|
166
|
+
pi,
|
|
167
|
+
ctx,
|
|
168
|
+
formatDryRun({
|
|
169
|
+
plan: selectedPlan,
|
|
170
|
+
checkout,
|
|
171
|
+
operation,
|
|
172
|
+
branchContextPreview,
|
|
173
|
+
launchOptions,
|
|
174
|
+
config,
|
|
175
|
+
formatBranchContextCommand,
|
|
176
|
+
}),
|
|
177
|
+
{ status: "dry-run", targetBranch: operation.branch, key: operation.key },
|
|
178
|
+
"info",
|
|
179
|
+
);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await createAttachSlotAndLaunch({
|
|
184
|
+
pi,
|
|
185
|
+
ctx,
|
|
186
|
+
checkout,
|
|
187
|
+
operation,
|
|
188
|
+
config,
|
|
189
|
+
options,
|
|
190
|
+
notifyProgress,
|
|
191
|
+
formatBranchContextCommand,
|
|
192
|
+
});
|
|
193
|
+
} catch (error) {
|
|
194
|
+
present(ctx, formatUnexpectedError(error), "error");
|
|
195
|
+
} finally {
|
|
196
|
+
setStatus(ctx, config, undefined);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function dispatchBranchContextContext(
|
|
201
|
+
pi: ExtensionAPI,
|
|
202
|
+
cwd: string,
|
|
203
|
+
options: CccSlotDispatchPlanOptions,
|
|
204
|
+
): BranchContextContext {
|
|
205
|
+
return options.createBranchContextContext?.(pi, cwd) ?? createRealBranchContextContext({ cwd });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseCommandArgs(rawArgs: string): CommandArgs | { error: string } {
|
|
209
|
+
const tokens = rawArgs
|
|
210
|
+
.trim()
|
|
211
|
+
.split(/\s+/)
|
|
212
|
+
.filter((token) => token.length > 0);
|
|
213
|
+
const parsed: CommandArgs = { isDryRun: false, shouldShowHelp: false };
|
|
214
|
+
|
|
215
|
+
for (const token of tokens) {
|
|
216
|
+
if (token === "--dry-run") {
|
|
217
|
+
parsed.isDryRun = true;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (token === "--help" || token === "-h") {
|
|
221
|
+
parsed.shouldShowHelp = true;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (token.startsWith("-")) {
|
|
225
|
+
return { error: `Unknown flag: ${token}` };
|
|
226
|
+
}
|
|
227
|
+
return { error: `Positional arguments are not supported in v1: ${token}` };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return parsed;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function resolveLatestSavedPlanFromSession(
|
|
234
|
+
ctx: CommandContext,
|
|
235
|
+
directory: PlanStoreDirectoryEvidence,
|
|
236
|
+
): Promise<{ plan: ValidatedSessionSavedPlan } | { error: string }> {
|
|
237
|
+
const result = await findLatestSessionSavedPlanFile(
|
|
238
|
+
ctx.sessionManager?.getBranch?.() ?? [],
|
|
239
|
+
directory,
|
|
240
|
+
);
|
|
241
|
+
switch (result.type) {
|
|
242
|
+
case "found":
|
|
243
|
+
return { plan: result.plan };
|
|
244
|
+
case "unsafe":
|
|
245
|
+
return { error: result.message };
|
|
246
|
+
case "not-found":
|
|
247
|
+
return {
|
|
248
|
+
error: [
|
|
249
|
+
"No saved plan from /ns:plan:save was found in the current session branch.",
|
|
250
|
+
"Run /ns:plan:save first, then rerun the dispatch command.",
|
|
251
|
+
].join("\n"),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function resolveCurrentCheckout(
|
|
257
|
+
pi: ExtensionAPI,
|
|
258
|
+
cwd: string,
|
|
259
|
+
options: CccSlotDispatchPlanOptions,
|
|
260
|
+
): Promise<PlanStoreDirectoryEvidence | { error: string }> {
|
|
261
|
+
let directory: PlanStoreDirectoryEvidence;
|
|
262
|
+
try {
|
|
263
|
+
directory = await resolvePlanStoreDirectory(pi, {
|
|
264
|
+
cwd,
|
|
265
|
+
...optionalEntry("planStoreRoot", options.planStoreRoot),
|
|
266
|
+
});
|
|
267
|
+
} catch (error) {
|
|
268
|
+
return {
|
|
269
|
+
error: `Could not resolve current repository and source branch.\n${formatErrorMessage(error)}`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return directory;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function createAttachSlotAndLaunch(options: AttachSlotAndLaunchOptions): Promise<void> {
|
|
277
|
+
const { pi, ctx, checkout, operation, config } = options;
|
|
278
|
+
present(ctx, `Creating Graphite-tracked branch context ${operation.branch}…`, "info");
|
|
279
|
+
setStatus(ctx, config, "creating branch and attaching plan…");
|
|
280
|
+
let evidence: BranchContextEvidence;
|
|
281
|
+
try {
|
|
282
|
+
evidence = await createBranchContextFromFile(pi, operation.params, {
|
|
283
|
+
cwd: checkout.repoRoot,
|
|
284
|
+
context: dispatchBranchContextContext(pi, checkout.repoRoot, options.options),
|
|
285
|
+
});
|
|
286
|
+
} catch (error) {
|
|
287
|
+
present(ctx, formatCccBranchContextCreateFailure(operation, error), "error");
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
presentBranchContextMessage(
|
|
292
|
+
pi,
|
|
293
|
+
ctx,
|
|
294
|
+
formatBranchContextEvidence(evidence),
|
|
295
|
+
{ status: "success", evidence },
|
|
296
|
+
"info",
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const launchOptions = getPiLaunchOptions(pi, ctx);
|
|
300
|
+
if (config.destination === "workspace") {
|
|
301
|
+
await openBranchInCmuxSlot({
|
|
302
|
+
pi,
|
|
303
|
+
cwd: checkout.repoRoot,
|
|
304
|
+
branchName: operation.branch,
|
|
305
|
+
command: formatPiLaunchCommand(operation, launchOptions, options.formatBranchContextCommand),
|
|
306
|
+
description: `dispatch-plan from ${checkout.sourceBranch}`,
|
|
307
|
+
slotClient: options.options.slotClient ?? createCccSlotClient({ cwd: checkout.repoRoot }),
|
|
308
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
309
|
+
onStatus: (message) => setStatus(ctx, config, message),
|
|
310
|
+
successMessage: (target) =>
|
|
311
|
+
formatFinalSuccess({
|
|
312
|
+
operation,
|
|
313
|
+
target,
|
|
314
|
+
launchOptions,
|
|
315
|
+
formatBranchContextCommand: options.formatBranchContextCommand,
|
|
316
|
+
}),
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
await openBranchInCmuxSurface({
|
|
322
|
+
pi,
|
|
323
|
+
ctx,
|
|
324
|
+
cwd: checkout.repoRoot,
|
|
325
|
+
branchName: operation.branch,
|
|
326
|
+
command: formatPiLaunchCommand(operation, launchOptions, options.formatBranchContextCommand),
|
|
327
|
+
tabTitle: operation.branch,
|
|
328
|
+
operation,
|
|
329
|
+
config,
|
|
330
|
+
...optionalEntry("slotClient", options.options.slotClient),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
type PresentLevel = Exclude<NotifyLevel, "success">;
|
|
335
|
+
|
|
336
|
+
function presentBranchContextMessage(
|
|
337
|
+
pi: ExtensionAPI,
|
|
338
|
+
ctx: CommandContext,
|
|
339
|
+
content: string,
|
|
340
|
+
details: BranchContextOutputDetails,
|
|
341
|
+
level: PresentLevel,
|
|
342
|
+
): void {
|
|
343
|
+
if (pi.sendMessage) {
|
|
344
|
+
pi.sendMessage(buildBranchContextOutputMessage(content, details));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
present(ctx, content, level);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function present(ctx: CommandContext, message: string, level: PresentLevel): void {
|
|
352
|
+
ctx.ui.notify(message, level);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function setStatus(
|
|
356
|
+
ctx: CommandContext,
|
|
357
|
+
config: DispatchPlanConfig,
|
|
358
|
+
value: string | undefined,
|
|
359
|
+
): void {
|
|
360
|
+
ctx.ui.setStatus?.(config.statusKey, value);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function formatDryRun(options: FormatDryRunOptions): string {
|
|
364
|
+
const { plan, checkout, operation, branchContextPreview, launchOptions, config } = options;
|
|
365
|
+
const launchCommand = formatPiLaunchCommand(
|
|
366
|
+
operation,
|
|
367
|
+
launchOptions,
|
|
368
|
+
options.formatBranchContextCommand,
|
|
369
|
+
);
|
|
370
|
+
const description = `dispatch-plan from ${checkout.sourceBranch}`;
|
|
371
|
+
return [
|
|
372
|
+
`Dry run: no branch was created, no plan was attached, and no cmux ${config.destination} was opened.`,
|
|
373
|
+
"",
|
|
374
|
+
"Selected saved plan:",
|
|
375
|
+
`Path: ${plan.filePath}`,
|
|
376
|
+
`Saved-plan filename slug: ${plan.slug}`,
|
|
377
|
+
`Content-derived branch-context slug: ${operation.slug}`,
|
|
378
|
+
`Repo key: ${plan.repoKey}`,
|
|
379
|
+
`Repo root: ${plan.repoRoot}`,
|
|
380
|
+
`Repo identity source: ${plan.repoIdentitySource}`,
|
|
381
|
+
`Source branch: ${plan.sourceBranch}`,
|
|
382
|
+
`Branch path segment: ${plan.branchKey}`,
|
|
383
|
+
plan.summary ? `Summary: ${plan.summary}` : undefined,
|
|
384
|
+
"",
|
|
385
|
+
branchContextPreview,
|
|
386
|
+
formatCommand("ns", [
|
|
387
|
+
"slot",
|
|
388
|
+
"checkout",
|
|
389
|
+
operation.branch,
|
|
390
|
+
"--format",
|
|
391
|
+
"json",
|
|
392
|
+
"--no-clipboard",
|
|
393
|
+
]),
|
|
394
|
+
formatLaunchPreview({
|
|
395
|
+
destination: config.destination,
|
|
396
|
+
branch: operation.branch,
|
|
397
|
+
description,
|
|
398
|
+
launchCommand,
|
|
399
|
+
}),
|
|
400
|
+
]
|
|
401
|
+
.filter((line): line is string => line !== undefined)
|
|
402
|
+
.join("\n");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function formatCccBranchContextCreateFailure(
|
|
406
|
+
operation: BranchContextCreateOperation,
|
|
407
|
+
error: unknown,
|
|
408
|
+
): string {
|
|
409
|
+
const failure = formatBranchContextCreateFailure(operation, error);
|
|
410
|
+
return failure.replace("\n\n", "\nNo cmux workspace was opened.\n\n");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function formatFinalSuccess(options: FormatFinalSuccessOptions): string {
|
|
414
|
+
const { operation, target, launchOptions } = options;
|
|
415
|
+
return [
|
|
416
|
+
"Dispatched plan in cmux workspace.",
|
|
417
|
+
`Branch: ${operation.branch}`,
|
|
418
|
+
`Slot: ${target.slotName}`,
|
|
419
|
+
`Worktree: ${target.worktreePath}`,
|
|
420
|
+
`Attached plan: ${BRANCH_CONTEXT_NAMESPACE}/${operation.key}`,
|
|
421
|
+
`Command: ${formatPiLaunchCommand(operation, launchOptions, options.formatBranchContextCommand)}`,
|
|
422
|
+
].join("\n");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function formatPiLaunchCommand(
|
|
426
|
+
operation: Pick<BranchContextCreateOperation, "key">,
|
|
427
|
+
launchOptions: PiLaunchOptions,
|
|
428
|
+
formatBranchContextCommand: (key: string) => string,
|
|
429
|
+
): string {
|
|
430
|
+
return buildPiLaunchCommand(formatBranchContextCommand(operation.key), launchOptions);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function formatSurfaceSuccess(options: FormatSurfaceSuccessOptions): string {
|
|
434
|
+
const { operation, target, launch } = options;
|
|
435
|
+
return [
|
|
436
|
+
"Dispatched plan in cmux surface.",
|
|
437
|
+
`Branch: ${operation.branch}`,
|
|
438
|
+
`Slot: ${target.slotName}`,
|
|
439
|
+
`Worktree: ${target.worktreePath}`,
|
|
440
|
+
`Surface: ${launch.surfaceId}`,
|
|
441
|
+
`Workspace: ${launch.workspaceId}`,
|
|
442
|
+
`Attached plan: ${BRANCH_CONTEXT_NAMESPACE}/${operation.key}`,
|
|
443
|
+
`Command: ${launch.command}`,
|
|
444
|
+
].join("\n");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function formatLaunchPreview(options: {
|
|
448
|
+
destination: DispatchDestination;
|
|
449
|
+
branch: string;
|
|
450
|
+
description: string;
|
|
451
|
+
launchCommand: string;
|
|
452
|
+
}): string {
|
|
453
|
+
if (options.destination === "workspace") {
|
|
454
|
+
return [
|
|
455
|
+
"cmux new-workspace",
|
|
456
|
+
`--name ${formatShellArg(options.branch)}`,
|
|
457
|
+
`--description ${formatShellArg(options.description)}`,
|
|
458
|
+
"--cwd <slot-worktree-path>",
|
|
459
|
+
`--command ${formatShellArg(options.launchCommand)}`,
|
|
460
|
+
].join(" ");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const surfaceLaunchCommand = formatSurfaceLaunchCommand(
|
|
464
|
+
"<slot-worktree-path>",
|
|
465
|
+
options.launchCommand,
|
|
466
|
+
);
|
|
467
|
+
return [
|
|
468
|
+
"cmux new-surface --type terminal --workspace <caller-workspace> --pane <caller-pane> --focus true",
|
|
469
|
+
`cmux rename-tab --title ${formatShellArg(options.branch)}`,
|
|
470
|
+
`cmux send -- ${formatShellArg(`${surfaceLaunchCommand}\n`)}`,
|
|
471
|
+
].join("\n");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function formatSurfaceLaunchCommand(cwd: string, launchCommand: string): string {
|
|
475
|
+
return `cd ${formatShellArg(cwd)} && ${launchCommand}`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function formatUsage(config: DispatchPlanConfig): string {
|
|
479
|
+
return `Usage: /${config.commandName} [--dry-run]
|
|
480
|
+
|
|
481
|
+
Dispatch the latest saved plan into a new cmux ${config.destination} for implementation.
|
|
482
|
+
|
|
483
|
+
Options:
|
|
484
|
+
--dry-run Show the selected plan and commands without mutating.
|
|
485
|
+
--help, -h Show this help.
|
|
486
|
+
|
|
487
|
+
Run /ns:plan:save first, then rerun /${config.commandName}.`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function openBranchInCmuxSurface(options: {
|
|
491
|
+
pi: ExtensionAPI;
|
|
492
|
+
ctx: CommandContext;
|
|
493
|
+
cwd: string;
|
|
494
|
+
branchName: string;
|
|
495
|
+
command: string;
|
|
496
|
+
tabTitle: string;
|
|
497
|
+
operation: BranchContextCreateOperation;
|
|
498
|
+
config: DispatchPlanConfig;
|
|
499
|
+
slotClient?: SlotClient;
|
|
500
|
+
}): Promise<void> {
|
|
501
|
+
const { pi, ctx, cwd, branchName, command, tabTitle, operation, config } = options;
|
|
502
|
+
const target = await checkoutBranchCmuxSlot({
|
|
503
|
+
pi,
|
|
504
|
+
cwd,
|
|
505
|
+
branchName,
|
|
506
|
+
slotClient: options.slotClient ?? createCccSlotClient({ cwd }),
|
|
507
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
508
|
+
onStatus: (message) => setStatus(ctx, config, message),
|
|
509
|
+
});
|
|
510
|
+
if ("error" in target) return;
|
|
511
|
+
|
|
512
|
+
setStatus(ctx, config, "opening cmux surface…");
|
|
513
|
+
const surfaceLaunchCommand = formatSurfaceLaunchCommand(target.worktreePath, command);
|
|
514
|
+
const launched = await launchFocusedCmuxTab({
|
|
515
|
+
host: pi,
|
|
516
|
+
cwd: target.worktreePath,
|
|
517
|
+
tabTitle,
|
|
518
|
+
command: surfaceLaunchCommand,
|
|
519
|
+
signal: undefined,
|
|
520
|
+
onStage: (stage) => setStatus(ctx, config, formatSurfaceStageStatus(stage)),
|
|
521
|
+
});
|
|
522
|
+
if (launched.type === "failed") {
|
|
523
|
+
present(ctx, formatCmuxSurfaceFailure(branchName, target, launched), "error");
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
present(ctx, formatSurfaceSuccess({ operation, target, launch: launched }), "info");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function formatSurfaceStageStatus(
|
|
531
|
+
stage: "identify" | "create-surface" | "rename" | "send",
|
|
532
|
+
): string {
|
|
533
|
+
switch (stage) {
|
|
534
|
+
case "identify":
|
|
535
|
+
return "identifying cmux caller…";
|
|
536
|
+
case "create-surface":
|
|
537
|
+
return "creating cmux surface…";
|
|
538
|
+
case "rename":
|
|
539
|
+
return "renaming cmux tab…";
|
|
540
|
+
case "send":
|
|
541
|
+
return "sending launch command…";
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function formatCmuxSurfaceFailure(
|
|
546
|
+
branchName: string,
|
|
547
|
+
target: SlotCheckoutTarget,
|
|
548
|
+
launch: Extract<FocusedCmuxTabLaunchResult, { type: "failed" }>,
|
|
549
|
+
): string {
|
|
550
|
+
return [
|
|
551
|
+
"Checked out the branch slot, but failed to open the cmux surface.",
|
|
552
|
+
`Branch: ${branchName}`,
|
|
553
|
+
`Worktree: ${target.worktreePath}`,
|
|
554
|
+
launch.message,
|
|
555
|
+
].join("\n");
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function formatUnexpectedError(error: unknown): string {
|
|
559
|
+
return ["/dispatch-plan failed unexpectedly.", formatErrorMessage(error)].join("\n");
|
|
560
|
+
}
|