-
Notifications
You must be signed in to change notification settings - Fork 143
Expand file tree
/
Copy pathsplit-fork.ts
More file actions
130 lines (110 loc) · 4.15 KB
/
split-fork.ts
File metadata and controls
130 lines (110 loc) · 4.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
import { existsSync, promises as fs } from "node:fs";
import * as path from "node:path";
import { randomUUID } from "node:crypto";
const GHOSTTY_SPLIT_SCRIPT = `on run argv
set targetCwd to item 1 of argv
set startupInput to item 2 of argv
tell application "Ghostty"
set cfg to new surface configuration
set initial working directory of cfg to targetCwd
set initial input of cfg to startupInput
if (count of windows) > 0 then
try
set frontWindow to front window
set targetTerminal to focused terminal of selected tab of frontWindow
split targetTerminal direction right with configuration cfg
on error
new window with configuration cfg
end try
else
new window with configuration cfg
end if
activate
end tell
end run`;
function shellQuote(value: string): string {
if (value.length === 0) return "''";
return `'${value.replace(/'/g, `'"'"'`)}'`;
}
function getPiInvocationParts(): string[] {
const currentScript = process.argv[1];
if (currentScript && existsSync(currentScript)) {
return [process.execPath, currentScript];
}
const execName = path.basename(process.execPath).toLowerCase();
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
if (!isGenericRuntime) {
return [process.execPath];
}
return ["pi"];
}
function buildPiStartupInput(sessionFile: string | undefined, prompt: string): string {
const commandParts = [...getPiInvocationParts()];
if (sessionFile) {
commandParts.push("--session", sessionFile);
}
if (prompt.length > 0) {
commandParts.push("--", prompt);
}
return `${commandParts.map(shellQuote).join(" ")}\n`;
}
async function createForkedSession(ctx: ExtensionCommandContext): Promise<string | undefined> {
const sessionFile = ctx.sessionManager.getSessionFile();
if (!sessionFile) {
return undefined;
}
const sessionDir = path.dirname(sessionFile);
const branchEntries = ctx.sessionManager.getBranch();
const currentHeader = ctx.sessionManager.getHeader();
const timestamp = new Date().toISOString();
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
const newSessionId = randomUUID();
const newSessionFile = path.join(sessionDir, `${fileTimestamp}_${newSessionId}.jsonl`);
const newHeader = {
type: "session",
version: currentHeader?.version ?? 3,
id: newSessionId,
timestamp,
cwd: currentHeader?.cwd ?? ctx.cwd,
parentSession: sessionFile,
};
const lines = [JSON.stringify(newHeader), ...branchEntries.map((entry) => JSON.stringify(entry))].join("\n") + "\n";
await fs.mkdir(sessionDir, { recursive: true });
await fs.writeFile(newSessionFile, lines, "utf8");
return newSessionFile;
}
export default function (pi: ExtensionAPI): void {
pi.registerCommand("split-fork", {
description: "Fork this session into a new pi process in a right-hand Ghostty split. Usage: /split-fork [optional prompt]",
handler: async (args, ctx) => {
if (process.platform !== "darwin") {
ctx.ui.notify("/split-fork currently requires macOS (Ghostty AppleScript).", "warning");
return;
}
const wasBusy = !ctx.isIdle();
const prompt = args.trim();
const forkedSessionFile = await createForkedSession(ctx);
const startupInput = buildPiStartupInput(forkedSessionFile, prompt);
const result = await pi.exec("osascript", ["-e", GHOSTTY_SPLIT_SCRIPT, "--", ctx.cwd, startupInput]);
if (result.code !== 0) {
const reason = result.stderr?.trim() || result.stdout?.trim() || "unknown osascript error";
ctx.ui.notify(`Failed to launch Ghostty split: ${reason}`, "error");
if (forkedSessionFile) {
ctx.ui.notify(`Forked session was created: ${forkedSessionFile}`, "info");
}
return;
}
if (forkedSessionFile) {
const fileName = path.basename(forkedSessionFile);
const suffix = prompt ? " and sent prompt" : "";
ctx.ui.notify(`Forked to ${fileName} in a new Ghostty split${suffix}.`, "info");
if (wasBusy) {
ctx.ui.notify("Forked from current committed state (in-flight turn continues in original session).", "info");
}
} else {
ctx.ui.notify("Opened a new Ghostty split (no persisted session to fork).", "warning");
}
},
});
}