refactor: wire worktree-session-state.js and auto-runtime-state.js
Instead of deleting these planned-extraction modules, implement them
properly:
worktree-session-state.js:
- Upgraded to canonical module with JSDoc, node:path imports
- Fixed getActiveWorktreeName() to use normalize/join/basename (was
using fragile string.replaceAll + split('/') approach)
- Fixed ensureWorktreeOriginalCwdFromPath() to use sep instead of regex
- worktree-command.js now imports/re-exports all state functions from
this module and removes its local 'let originalCwd = null'
- registerWorktreeCommand() recovery logic replaced with
ensureWorktreeOriginalCwdFromPath() call
auto-runtime-state.js:
- Fixed to use getAutoSession() singleton instead of 'new AutoSession()'
(was creating an isolated instance disconnected from auto.js state)
- auto.js now re-exports isAutoActive, isAutoPaused, markToolStart,
markToolEnd from this module, removing duplicate implementations
- All state reads in auto-runtime-state.js delegate to the same
singleton that auto.js manages
Test: updated worktree-fixes.test.mjs guard to match clearWorktreeOriginalCwd()
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
5be5d6d438
commit
181a19ac65
5 changed files with 156 additions and 80 deletions
53
src/resources/extensions/sf/auto-runtime-state.js
Normal file
53
src/resources/extensions/sf/auto-runtime-state.js
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
/**
|
||||||
|
* auto-runtime-state.js — Thin facade over the AutoSession singleton for
|
||||||
|
* consumers that only need active/paused state and tool tracking, without
|
||||||
|
* importing the full auto.js module.
|
||||||
|
*
|
||||||
|
* Purpose: break the forced dependency on auto.js (>1700 lines) for modules
|
||||||
|
* that only need isAutoActive(), isAutoPaused(), markToolStart/End, or
|
||||||
|
* recordToolInvocationError. All state reads delegate to the same singleton
|
||||||
|
* that auto.js manages, so there is no separate instance.
|
||||||
|
*
|
||||||
|
* Consumer: forensics.js, rethink.js, ui/index.js, commands-handlers.js,
|
||||||
|
* bootstrap/ask-gate.js, steerable-autonomous-extension.js.
|
||||||
|
*/
|
||||||
|
import { getAutoSession } from "./auto/session.js";
|
||||||
|
import {
|
||||||
|
isDeterministicPolicyError,
|
||||||
|
isQueuedUserMessageSkip,
|
||||||
|
isToolInvocationError,
|
||||||
|
markToolEnd as markTrackedToolEnd,
|
||||||
|
markToolStart as markTrackedToolStart,
|
||||||
|
} from "./auto-tool-tracking.js";
|
||||||
|
export function getAutoRuntimeSnapshot() {
|
||||||
|
const s = getAutoSession();
|
||||||
|
return {
|
||||||
|
active: s.active,
|
||||||
|
paused: s.paused,
|
||||||
|
currentUnit: s.currentUnit ? { ...s.currentUnit } : null,
|
||||||
|
basePath: s.basePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export function isAutoActive() {
|
||||||
|
return getAutoSession().active;
|
||||||
|
}
|
||||||
|
export function isAutoPaused() {
|
||||||
|
return getAutoSession().paused;
|
||||||
|
}
|
||||||
|
export function markToolStart(toolCallId, toolName) {
|
||||||
|
markTrackedToolStart(toolCallId, getAutoSession().active, toolName);
|
||||||
|
}
|
||||||
|
export function markToolEnd(toolCallId) {
|
||||||
|
markTrackedToolEnd(toolCallId);
|
||||||
|
}
|
||||||
|
export function recordToolInvocationError(toolName, errorMsg) {
|
||||||
|
const s = getAutoSession();
|
||||||
|
if (!s.active) return;
|
||||||
|
if (
|
||||||
|
isToolInvocationError(errorMsg) ||
|
||||||
|
isQueuedUserMessageSkip(errorMsg) ||
|
||||||
|
isDeterministicPolicyError(errorMsg)
|
||||||
|
) {
|
||||||
|
s.lastToolInvocationError = `${toolName}: ${errorMsg}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -79,12 +79,16 @@ import {
|
||||||
import { startUnitSupervision } from "./auto-timers.js";
|
import { startUnitSupervision } from "./auto-timers.js";
|
||||||
import {
|
import {
|
||||||
getOldestInFlightToolAgeMs as _getOldestInFlightToolAgeMs,
|
getOldestInFlightToolAgeMs as _getOldestInFlightToolAgeMs,
|
||||||
markToolEnd as _markToolEnd,
|
|
||||||
markToolStart as _markToolStart,
|
|
||||||
clearInFlightTools,
|
clearInFlightTools,
|
||||||
isQueuedUserMessageSkip,
|
isQueuedUserMessageSkip,
|
||||||
isToolInvocationError,
|
isToolInvocationError,
|
||||||
} from "./auto-tool-tracking.js";
|
} from "./auto-tool-tracking.js";
|
||||||
|
export {
|
||||||
|
isAutoActive,
|
||||||
|
isAutoPaused,
|
||||||
|
markToolEnd,
|
||||||
|
markToolStart,
|
||||||
|
} from "./auto-runtime-state.js";
|
||||||
import {
|
import {
|
||||||
autoWorktreeBranch,
|
autoWorktreeBranch,
|
||||||
checkResourcesStale,
|
checkResourcesStale,
|
||||||
|
|
@ -439,12 +443,7 @@ export function getAutoDashboardData() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// ─── Public API ───────────────────────────────────────────────────────────────
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
||||||
export function isAutoActive() {
|
// isAutoActive, isAutoPaused, markToolStart, markToolEnd re-exported from auto-runtime-state.js above
|
||||||
return s.active;
|
|
||||||
}
|
|
||||||
export function isAutoPaused() {
|
|
||||||
return s.paused;
|
|
||||||
}
|
|
||||||
export function getAutoCommandContext() {
|
export function getAutoCommandContext() {
|
||||||
return s.cmdCtx;
|
return s.cmdCtx;
|
||||||
}
|
}
|
||||||
|
|
@ -543,13 +542,7 @@ export function markResearchTerminalTransition() {
|
||||||
export function hasResearchTerminalTransition() {
|
export function hasResearchTerminalTransition() {
|
||||||
return getAutoSession().researchTerminalTransition;
|
return getAutoSession().researchTerminalTransition;
|
||||||
}
|
}
|
||||||
// Tool tracking — delegates to auto-tool-tracking.ts
|
// Tool tracking — delegates to auto-runtime-state.js (re-exported above)
|
||||||
export function markToolStart(toolCallId, toolName) {
|
|
||||||
_markToolStart(toolCallId, s.active, toolName);
|
|
||||||
}
|
|
||||||
export function markToolEnd(toolCallId) {
|
|
||||||
_markToolEnd(toolCallId);
|
|
||||||
}
|
|
||||||
const TASK_COMPLETE_TOOL_NAMES = new Set(["complete_task"]);
|
const TASK_COMPLETE_TOOL_NAMES = new Set(["complete_task"]);
|
||||||
function normalizeTaskCompleteFailure(errorMsg) {
|
function normalizeTaskCompleteFailure(errorMsg) {
|
||||||
return errorMsg
|
return errorMsg
|
||||||
|
|
|
||||||
|
|
@ -253,12 +253,12 @@ describe("WorktreeResolver.enterMilestone", () => {
|
||||||
// ─── originalCwd clear-on-success (worktree-command.js) ────────────────────
|
// ─── originalCwd clear-on-success (worktree-command.js) ────────────────────
|
||||||
|
|
||||||
describe("originalCwd lifecycle", () => {
|
describe("originalCwd lifecycle", () => {
|
||||||
test("merge succeeds: originalCwd set to null", () => {
|
test("merge succeeds: originalCwd cleared via clearWorktreeOriginalCwd()", () => {
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const sourcePath = join(__dirname, "..", "worktree-command.js");
|
const sourcePath = join(__dirname, "..", "worktree-command.js");
|
||||||
const source = readFileSync(sourcePath, "utf-8");
|
const source = readFileSync(sourcePath, "utf-8");
|
||||||
const mergeSuccessPattern =
|
const mergeSuccessPattern =
|
||||||
/mergeWorktreeToMain\([^)]+\);\s*\n\s*\/\/ Merge succeeded[^\n]*\n\s*originalCwd = null;/;
|
/mergeWorktreeToMain\([^)]+\);\s*\n\s*\/\/ Merge succeeded[^\n]*\n\s*clearWorktreeOriginalCwd\(\);/;
|
||||||
expect(source).toMatch(mergeSuccessPattern);
|
expect(source).toMatch(mergeSuccessPattern);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import {
|
||||||
rmSync,
|
rmSync,
|
||||||
unlinkSync,
|
unlinkSync,
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { basename, join, normalize, sep } from "node:path";
|
import { join, normalize } from "node:path";
|
||||||
import { showConfirm } from "../shared/tui.js";
|
import { showConfirm } from "../shared/tui.js";
|
||||||
import { runWorktreePostCreateHook } from "./auto-worktree.js";
|
import { runWorktreePostCreateHook } from "./auto-worktree.js";
|
||||||
import { inferCommitType } from "./git-service.js";
|
import { inferCommitType } from "./git-service.js";
|
||||||
|
|
@ -41,34 +41,20 @@ import {
|
||||||
worktreeBranchName,
|
worktreeBranchName,
|
||||||
worktreePath,
|
worktreePath,
|
||||||
} from "./worktree-manager.js";
|
} from "./worktree-manager.js";
|
||||||
|
export {
|
||||||
/**
|
clearWorktreeOriginalCwd,
|
||||||
* Tracks the original project root so we can switch back.
|
ensureWorktreeOriginalCwdFromPath,
|
||||||
* Set when we first chdir into a worktree, cleared on return.
|
getActiveWorktreeName,
|
||||||
*/
|
getWorktreeOriginalCwd,
|
||||||
let originalCwd = null;
|
setWorktreeOriginalCwd,
|
||||||
/**
|
} from "./worktree-session-state.js";
|
||||||
* Get the original project root if currently in a worktree, or null.
|
import {
|
||||||
* Used to restore context after `/worktree merge` or `/worktree return`.
|
clearWorktreeOriginalCwd,
|
||||||
*/
|
ensureWorktreeOriginalCwdFromPath,
|
||||||
export function getWorktreeOriginalCwd() {
|
getActiveWorktreeName,
|
||||||
return originalCwd;
|
getWorktreeOriginalCwd,
|
||||||
}
|
setWorktreeOriginalCwd,
|
||||||
/**
|
} from "./worktree-session-state.js";
|
||||||
* Get the name of the active worktree, or null if not in one.
|
|
||||||
* Extracts from .sf/worktrees/ path segment.
|
|
||||||
*/
|
|
||||||
export function getActiveWorktreeName() {
|
|
||||||
if (!originalCwd) return null;
|
|
||||||
const cwd = normalize(process.cwd());
|
|
||||||
const wtDir = normalize(join(originalCwd, ".sf", "worktrees"));
|
|
||||||
if (!cwd.startsWith(wtDir)) return null;
|
|
||||||
// Use basename on the first path segment after wtDir to handle both separators
|
|
||||||
// and avoid empty strings from trailing backslashes (split("/")[0] is fragile).
|
|
||||||
const rel = cwd.slice(wtDir.length).replace(/^[\\/]+/, "");
|
|
||||||
const name = basename(rel.split(/[\\/]/)[0] ?? rel);
|
|
||||||
return name || null;
|
|
||||||
}
|
|
||||||
// ─── Shared completions and handler (used by both /worktree and /wt) ────────
|
// ─── Shared completions and handler (used by both /worktree and /wt) ────────
|
||||||
function worktreeCompletions(prefix) {
|
function worktreeCompletions(prefix) {
|
||||||
const parts = prefix.trim().split(/\s+/);
|
const parts = prefix.trim().split(/\s+/);
|
||||||
|
|
@ -150,7 +136,7 @@ async function worktreeHandler(args, ctx, pi, alias) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// create and switch both do the same thing: switch if exists, create if not
|
// create and switch both do the same thing: switch if exists, create if not
|
||||||
const mainBase = originalCwd ?? basePath;
|
const mainBase = getWorktreeOriginalCwd() ?? basePath;
|
||||||
const existing = listWorktrees(mainBase);
|
const existing = listWorktrees(mainBase);
|
||||||
if (existing.some((wt) => wt.name === name)) {
|
if (existing.some((wt) => wt.name === name)) {
|
||||||
await handleSwitch(basePath, name, ctx);
|
await handleSwitch(basePath, name, ctx);
|
||||||
|
|
@ -165,7 +151,7 @@ async function worktreeHandler(args, ctx, pi, alias) {
|
||||||
.trim()
|
.trim()
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
const mainBase = originalCwd ?? basePath;
|
const mainBase = getWorktreeOriginalCwd() ?? basePath;
|
||||||
const activeWt = getActiveWorktreeName();
|
const activeWt = getActiveWorktreeName();
|
||||||
if (mergeArgs.length === 0) {
|
if (mergeArgs.length === 0) {
|
||||||
// Bare "/worktree merge" — only valid when inside a worktree
|
// Bare "/worktree merge" — only valid when inside a worktree
|
||||||
|
|
@ -197,7 +183,7 @@ async function worktreeHandler(args, ctx, pi, alias) {
|
||||||
}
|
}
|
||||||
if (trimmed === "remove" || trimmed.startsWith("remove ")) {
|
if (trimmed === "remove" || trimmed.startsWith("remove ")) {
|
||||||
const name = trimmed.replace(/^remove\s*/, "").trim();
|
const name = trimmed.replace(/^remove\s*/, "").trim();
|
||||||
const mainBase = originalCwd ?? basePath;
|
const mainBase = getWorktreeOriginalCwd() ?? basePath;
|
||||||
if (name === "all") {
|
if (name === "all") {
|
||||||
await handleRemoveAll(mainBase, ctx);
|
await handleRemoveAll(mainBase, ctx);
|
||||||
return;
|
return;
|
||||||
|
|
@ -217,7 +203,7 @@ async function worktreeHandler(args, ctx, pi, alias) {
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const mainBase = originalCwd ?? basePath;
|
const mainBase = getWorktreeOriginalCwd() ?? basePath;
|
||||||
const nameOnly = trimmed.split(/\s+/)[0];
|
const nameOnly = trimmed.split(/\s+/)[0];
|
||||||
if (trimmed !== nameOnly) {
|
if (trimmed !== nameOnly) {
|
||||||
ctx.ui.notify(
|
ctx.ui.notify(
|
||||||
|
|
@ -242,23 +228,15 @@ export async function handleWorktreeCommand(args, ctx, pi, alias) {
|
||||||
}
|
}
|
||||||
/** Register /worktree and /wt commands with completion support. */
|
/** Register /worktree and /wt commands with completion support. */
|
||||||
export function registerWorktreeCommand(pi) {
|
export function registerWorktreeCommand(pi) {
|
||||||
// Restore worktree state after /reload.
|
// Restore worktree state after /reload — detects if process.cwd() is still
|
||||||
// The module-level originalCwd resets to null when extensions are re-loaded,
|
// inside a worktree and recovers originalCwd from the path.
|
||||||
// but process.cwd() is still inside the worktree. Detect this and recover.
|
ensureWorktreeOriginalCwdFromPath();
|
||||||
if (!originalCwd) {
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const marker = `${sep}.sf${sep}worktrees${sep}`;
|
|
||||||
const markerIdx = cwd.indexOf(marker);
|
|
||||||
if (markerIdx !== -1) {
|
|
||||||
originalCwd = cwd.slice(0, markerIdx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Orphaned-worktree recovery: a crash or hang between the pre-merge chdir and
|
// Orphaned-worktree recovery: a crash or hang between the pre-merge chdir and
|
||||||
// merge completion may leave a worktree registered in git but not tracked by
|
// merge completion may leave a worktree registered in git but not tracked by
|
||||||
// originalCwd (because the old code cleared it prematurely). Detect such
|
// originalCwd (because the old code cleared it prematurely). Detect such
|
||||||
// worktrees on reload and warn — so the user knows to run /worktree list and
|
// worktrees on reload and warn — so the user knows to run /worktree list and
|
||||||
// merge or remove them manually.
|
// merge or remove them manually.
|
||||||
if (!originalCwd) {
|
if (!getWorktreeOriginalCwd()) {
|
||||||
try {
|
try {
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const worktrees = listWorktrees(cwd);
|
const worktrees = listWorktrees(cwd);
|
||||||
|
|
@ -343,7 +321,7 @@ async function handleCreate(basePath, name, ctx) {
|
||||||
name,
|
name,
|
||||||
);
|
);
|
||||||
// Create from the main tree, not from inside another worktree
|
// Create from the main tree, not from inside another worktree
|
||||||
const mainBase = originalCwd ?? basePath;
|
const mainBase = getWorktreeOriginalCwd() ?? basePath;
|
||||||
const info = createWorktree(mainBase, name);
|
const info = createWorktree(mainBase, name);
|
||||||
// Run user-configured post-create hook (#597) — e.g. copy .env, symlink assets
|
// Run user-configured post-create hook (#597) — e.g. copy .env, symlink assets
|
||||||
const hookError = runWorktreePostCreateHook(mainBase, info.path);
|
const hookError = runWorktreePostCreateHook(mainBase, info.path);
|
||||||
|
|
@ -351,7 +329,7 @@ async function handleCreate(basePath, name, ctx) {
|
||||||
ctx.ui.notify(hookError, "warning");
|
ctx.ui.notify(hookError, "warning");
|
||||||
}
|
}
|
||||||
// Track original cwd before switching
|
// Track original cwd before switching
|
||||||
if (!originalCwd) originalCwd = basePath;
|
if (!getWorktreeOriginalCwd()) setWorktreeOriginalCwd(basePath);
|
||||||
const prevCwd = process.cwd();
|
const prevCwd = process.cwd();
|
||||||
process.chdir(info.path);
|
process.chdir(info.path);
|
||||||
nudgeGitBranchCache(prevCwd);
|
nudgeGitBranchCache(prevCwd);
|
||||||
|
|
@ -405,7 +383,7 @@ async function handleCreate(basePath, name, ctx) {
|
||||||
}
|
}
|
||||||
async function handleSwitch(basePath, name, ctx) {
|
async function handleSwitch(basePath, name, ctx) {
|
||||||
try {
|
try {
|
||||||
const mainBase = originalCwd ?? basePath;
|
const mainBase = getWorktreeOriginalCwd() ?? basePath;
|
||||||
const wtPath = worktreePath(mainBase, name);
|
const wtPath = worktreePath(mainBase, name);
|
||||||
if (!existsSync(wtPath)) {
|
if (!existsSync(wtPath)) {
|
||||||
ctx.ui.notify(
|
ctx.ui.notify(
|
||||||
|
|
@ -421,7 +399,7 @@ async function handleSwitch(basePath, name, ctx) {
|
||||||
name,
|
name,
|
||||||
);
|
);
|
||||||
// Track original cwd before switching
|
// Track original cwd before switching
|
||||||
if (!originalCwd) originalCwd = basePath;
|
if (!getWorktreeOriginalCwd()) setWorktreeOriginalCwd(basePath);
|
||||||
const prevCwd = process.cwd();
|
const prevCwd = process.cwd();
|
||||||
process.chdir(wtPath);
|
process.chdir(wtPath);
|
||||||
nudgeGitBranchCache(prevCwd);
|
nudgeGitBranchCache(prevCwd);
|
||||||
|
|
@ -448,7 +426,7 @@ async function handleSwitch(basePath, name, ctx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function handleReturn(ctx) {
|
async function handleReturn(ctx) {
|
||||||
if (!originalCwd) {
|
if (!getWorktreeOriginalCwd()) {
|
||||||
ctx.ui.notify("Already in the main project tree.", "info");
|
ctx.ui.notify("Already in the main project tree.", "info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -458,8 +436,8 @@ async function handleReturn(ctx) {
|
||||||
"worktree-return",
|
"worktree-return",
|
||||||
"worktree",
|
"worktree",
|
||||||
);
|
);
|
||||||
const returnTo = originalCwd;
|
const returnTo = getWorktreeOriginalCwd();
|
||||||
originalCwd = null;
|
clearWorktreeOriginalCwd();
|
||||||
const prevCwd = process.cwd();
|
const prevCwd = process.cwd();
|
||||||
process.chdir(returnTo);
|
process.chdir(returnTo);
|
||||||
nudgeGitBranchCache(prevCwd);
|
nudgeGitBranchCache(prevCwd);
|
||||||
|
|
@ -514,7 +492,7 @@ const CLR = {
|
||||||
};
|
};
|
||||||
async function handleList(basePath, ctx) {
|
async function handleList(basePath, ctx) {
|
||||||
try {
|
try {
|
||||||
const mainBase = originalCwd ?? basePath;
|
const mainBase = getWorktreeOriginalCwd() ?? basePath;
|
||||||
const worktrees = listWorktrees(mainBase);
|
const worktrees = listWorktrees(mainBase);
|
||||||
if (worktrees.length === 0) {
|
if (worktrees.length === 0) {
|
||||||
ctx.ui.notify(
|
ctx.ui.notify(
|
||||||
|
|
@ -566,8 +544,8 @@ async function handleList(basePath, ctx) {
|
||||||
}
|
}
|
||||||
lines.push("");
|
lines.push("");
|
||||||
}
|
}
|
||||||
if (originalCwd) {
|
if (getWorktreeOriginalCwd()) {
|
||||||
lines.push(` ${CLR.label("main tree")} ${CLR.path(originalCwd)}`);
|
lines.push(` ${CLR.label("main tree")} ${CLR.path(getWorktreeOriginalCwd())}`);
|
||||||
}
|
}
|
||||||
ctx.ui.notify(lines.join("\n"), "info");
|
ctx.ui.notify(lines.join("\n"), "info");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -668,7 +646,7 @@ async function handleMerge(basePath, name, ctx, pi, targetBranch) {
|
||||||
// worktree on restart. originalCwd is cleared only in the success path below.
|
// worktree on restart. originalCwd is cleared only in the success path below.
|
||||||
// The registerWorktreeCommand recovery logic reads process.cwd() on reload and
|
// The registerWorktreeCommand recovery logic reads process.cwd() on reload and
|
||||||
// can restore originalCwd for orphaned worktree sessions.
|
// can restore originalCwd for orphaned worktree sessions.
|
||||||
if (originalCwd) {
|
if (getWorktreeOriginalCwd()) {
|
||||||
const prevCwd = process.cwd();
|
const prevCwd = process.cwd();
|
||||||
process.chdir(basePath);
|
process.chdir(basePath);
|
||||||
nudgeGitBranchCache(prevCwd);
|
nudgeGitBranchCache(prevCwd);
|
||||||
|
|
@ -691,7 +669,7 @@ async function handleMerge(basePath, name, ctx, pi, targetBranch) {
|
||||||
try {
|
try {
|
||||||
mergeWorktreeToMain(basePath, name, commitMessage);
|
mergeWorktreeToMain(basePath, name, commitMessage);
|
||||||
// Merge succeeded — safe to clear the worktree tracking state now
|
// Merge succeeded — safe to clear the worktree tracking state now
|
||||||
originalCwd = null;
|
clearWorktreeOriginalCwd();
|
||||||
ctx.ui.notify(
|
ctx.ui.notify(
|
||||||
[
|
[
|
||||||
`${CLR.ok("✓")} Merged ${CLR.name(name)} → ${CLR.branch(mainBranch)} ${CLR.muted("(deterministic squash)")}`,
|
`${CLR.ok("✓")} Merged ${CLR.name(name)} → ${CLR.branch(mainBranch)} ${CLR.muted("(deterministic squash)")}`,
|
||||||
|
|
@ -763,7 +741,7 @@ async function handleMerge(basePath, name, ctx, pi, targetBranch) {
|
||||||
}
|
}
|
||||||
async function handleRemove(basePath, name, ctx) {
|
async function handleRemove(basePath, name, ctx) {
|
||||||
try {
|
try {
|
||||||
const mainBase = originalCwd ?? basePath;
|
const mainBase = getWorktreeOriginalCwd() ?? basePath;
|
||||||
// Validate the worktree exists before attempting removal
|
// Validate the worktree exists before attempting removal
|
||||||
const worktrees = listWorktrees(mainBase);
|
const worktrees = listWorktrees(mainBase);
|
||||||
const wt = worktrees.find((w) => w.name === name);
|
const wt = worktrees.find((w) => w.name === name);
|
||||||
|
|
@ -787,9 +765,9 @@ async function handleRemove(basePath, name, ctx) {
|
||||||
const prevCwd = process.cwd();
|
const prevCwd = process.cwd();
|
||||||
removeWorktree(mainBase, name, { deleteBranch: true });
|
removeWorktree(mainBase, name, { deleteBranch: true });
|
||||||
// If we were in that worktree, removeWorktree chdir'd us out — clear tracking
|
// If we were in that worktree, removeWorktree chdir'd us out — clear tracking
|
||||||
if (originalCwd && process.cwd() !== prevCwd) {
|
if (getWorktreeOriginalCwd() && process.cwd() !== prevCwd) {
|
||||||
nudgeGitBranchCache(prevCwd);
|
nudgeGitBranchCache(prevCwd);
|
||||||
originalCwd = null;
|
clearWorktreeOriginalCwd();
|
||||||
}
|
}
|
||||||
ctx.ui.notify(
|
ctx.ui.notify(
|
||||||
`${CLR.ok("✓")} Worktree ${CLR.name(name)} removed ${CLR.muted("(branch deleted)")}.`,
|
`${CLR.ok("✓")} Worktree ${CLR.name(name)} removed ${CLR.muted("(branch deleted)")}.`,
|
||||||
|
|
@ -802,7 +780,7 @@ async function handleRemove(basePath, name, ctx) {
|
||||||
}
|
}
|
||||||
async function handleRemoveAll(basePath, ctx) {
|
async function handleRemoveAll(basePath, ctx) {
|
||||||
try {
|
try {
|
||||||
const mainBase = originalCwd ?? basePath;
|
const mainBase = getWorktreeOriginalCwd() ?? basePath;
|
||||||
const worktrees = listWorktrees(mainBase);
|
const worktrees = listWorktrees(mainBase);
|
||||||
if (worktrees.length === 0) {
|
if (worktrees.length === 0) {
|
||||||
ctx.ui.notify("No worktrees to remove.", "info");
|
ctx.ui.notify("No worktrees to remove.", "info");
|
||||||
|
|
@ -831,9 +809,9 @@ async function handleRemoveAll(basePath, ctx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If we were in a worktree that got removed, clear tracking
|
// If we were in a worktree that got removed, clear tracking
|
||||||
if (originalCwd && process.cwd() !== prevCwd) {
|
if (getWorktreeOriginalCwd() && process.cwd() !== prevCwd) {
|
||||||
nudgeGitBranchCache(prevCwd);
|
nudgeGitBranchCache(prevCwd);
|
||||||
originalCwd = null;
|
clearWorktreeOriginalCwd();
|
||||||
}
|
}
|
||||||
const lines = [];
|
const lines = [];
|
||||||
if (removed.length > 0)
|
if (removed.length > 0)
|
||||||
|
|
|
||||||
52
src/resources/extensions/sf/worktree-session-state.js
Normal file
52
src/resources/extensions/sf/worktree-session-state.js
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/**
|
||||||
|
* worktree-session-state.js — Shared mutable state for the active worktree session.
|
||||||
|
*
|
||||||
|
* Purpose: isolate the originalCwd tracking variable so it can be read by other
|
||||||
|
* worktree modules (auto-worktree, worktree-root, etc.) without importing the full
|
||||||
|
* worktree-command.js command handler.
|
||||||
|
*
|
||||||
|
* Consumer: worktree-command.js (primary owner), any module that needs
|
||||||
|
* getActiveWorktreeName() or getWorktreeOriginalCwd() without the command handler.
|
||||||
|
*/
|
||||||
|
import { basename, join, normalize, sep } from "node:path";
|
||||||
|
let originalCwd = null;
|
||||||
|
/** Return the original project root if currently in a worktree, or null. */
|
||||||
|
export function getWorktreeOriginalCwd() {
|
||||||
|
return originalCwd;
|
||||||
|
}
|
||||||
|
/** Set the original project root when entering a worktree. */
|
||||||
|
export function setWorktreeOriginalCwd(cwd) {
|
||||||
|
originalCwd = cwd;
|
||||||
|
}
|
||||||
|
/** Clear the stored root when returning from a worktree. */
|
||||||
|
export function clearWorktreeOriginalCwd() {
|
||||||
|
originalCwd = null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Detect and set originalCwd from the current process path if not already set.
|
||||||
|
* Used during /reload recovery when process.cwd() is still inside a worktree but
|
||||||
|
* the module-level variable was reset.
|
||||||
|
*/
|
||||||
|
export function ensureWorktreeOriginalCwdFromPath(cwd = process.cwd()) {
|
||||||
|
if (originalCwd) return originalCwd;
|
||||||
|
const marker = `${sep}.sf${sep}worktrees${sep}`;
|
||||||
|
const markerIdx = cwd.indexOf(marker);
|
||||||
|
if (markerIdx !== -1) {
|
||||||
|
originalCwd = cwd.slice(0, markerIdx);
|
||||||
|
}
|
||||||
|
return originalCwd;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return the name of the active worktree, or null if not in one.
|
||||||
|
* Extracts the first path segment after .sf/worktrees/ using normalize to handle
|
||||||
|
* both separators correctly.
|
||||||
|
*/
|
||||||
|
export function getActiveWorktreeName() {
|
||||||
|
if (!originalCwd) return null;
|
||||||
|
const cwd = normalize(process.cwd());
|
||||||
|
const wtDir = normalize(join(originalCwd, ".sf", "worktrees"));
|
||||||
|
if (!cwd.startsWith(wtDir)) return null;
|
||||||
|
const rel = cwd.slice(wtDir.length).replace(/^[\\/]+/, "");
|
||||||
|
const name = basename(rel.split(/[\\/]/)[0] ?? rel);
|
||||||
|
return name || null;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue