pum-agent 0.2.20-beta.1 → 0.2.21-beta.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/README.md CHANGED
@@ -38,7 +38,14 @@ empty prompt to see every control.
38
38
 
39
39
  </details>
40
40
 
41
- Both are real OpenTUI renders, not mockups — `bun run scripts/capture-screenshots.tsx`
41
+ <details>
42
+ <summary><strong>Controls panel</strong></summary>
43
+
44
+ ![PUM's controls panel, showing prompt, agent, session, command, and application shortcuts](docs/images/pum-controls.svg)
45
+
46
+ </details>
47
+
48
+ These are real OpenTUI renders, not mockups. `bun run scripts/capture-screenshots.tsx`
42
49
  drives the actual TUI and converts the captured cells to SVG.
43
50
 
44
51
  ## What it does
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pum-agent",
3
- "version": "0.2.20-beta.1",
3
+ "version": "0.2.21-beta.1",
4
4
  "description": "A compact terminal coding agent powered by pi and OpenTUI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/app.tsx CHANGED
@@ -626,6 +626,7 @@ export function App({
626
626
  sandboxWarningSource,
627
627
  forcedSandboxMode,
628
628
  forcedCheckPaths = [],
629
+ initialRelocation,
629
630
  initialCwd,
630
631
  userBashOperations,
631
632
  }: {
@@ -666,6 +667,8 @@ export function App({
666
667
  /** Process-local sandbox floor that does not overwrite persisted user settings. */
667
668
  forcedSandboxMode?: NonNullable<PumSettings["sandboxMode"]>;
668
669
  forcedCheckPaths?: readonly string[];
670
+ /** Relocation created before the TUI mounted, such as a `pum worktree` launch. */
671
+ initialRelocation?: RelocationRecord;
669
672
  /** Directory the session starts in. Defaults to the process working directory. */
670
673
  initialCwd?: string;
671
674
  /** User commands bypass Check mode but use this native sandbox execution path. */
@@ -1990,7 +1993,7 @@ export function App({
1990
1993
  }, [activeAgent?.id, activeAgent?.status, activeAgent?.runStartedAt, visibleBusy]);
1991
1994
 
1992
1995
  const [relocation, setRelocation] = useState(
1993
- () => loadRelocation(initialSession.sessionFile),
1996
+ () => loadRelocation(initialSession.sessionFile) ?? initialRelocation ?? null,
1994
1997
  );
1995
1998
  const relocationRef = useRef(relocation);
1996
1999
  relocationRef.current = relocation;
@@ -2135,7 +2138,6 @@ export function App({
2135
2138
  if (!record || record.location !== "worktree") return;
2136
2139
  if (restoredRelocationRef.current === record.id) return;
2137
2140
  restoredRelocationRef.current = record.id;
2138
- if (pathIdentity(cwdRef.current) === pathIdentity(record.worktreePath)) return;
2139
2141
  void (async () => {
2140
2142
  const trusted = relocationPathsTrusted(record, {
2141
2143
  worktreeExists: existsSync(record.worktreePath),
@@ -2143,16 +2145,42 @@ export function App({
2143
2145
  sourceRoot: record.sourceRoot,
2144
2146
  });
2145
2147
  if (!trusted) {
2148
+ const alreadyInWorktree = pathIdentity(cwdRef.current) === pathIdentity(record.worktreePath);
2149
+ if (alreadyInWorktree && onRelocate) {
2150
+ const moved = await onRelocate(record.sourceRoot).catch(() => null);
2151
+ if (moved) {
2152
+ applyRelocation({
2153
+ ...record,
2154
+ generation: record.generation + 1,
2155
+ location: "source",
2156
+ updatedAt: Date.now(),
2157
+ });
2158
+ appendMainLine({
2159
+ kind: "text",
2160
+ role: "error",
2161
+ text: `worktree ${record.name} no longer matches ${record.branch}; returned to ${record.sourceRoot}`,
2162
+ });
2163
+ return;
2164
+ }
2165
+ }
2146
2166
  relocationRef.current = null;
2147
2167
  setRelocation(null);
2148
2168
  saveRelocation(session.sessionFile, null);
2149
2169
  appendMainLine({
2150
2170
  kind: "text",
2151
2171
  role: "error",
2152
- text: `worktree ${record.name} no longer matches ${record.branch}; staying in ${record.sourceRoot}`,
2172
+ text: alreadyInWorktree
2173
+ ? `worktree ${record.name} no longer matches ${record.branch}; its relocation record was removed`
2174
+ : `worktree ${record.name} no longer matches ${record.branch}; staying in ${record.sourceRoot}`,
2153
2175
  });
2154
2176
  return;
2155
2177
  }
2178
+ // A CLI worktree launch resumes in the recorded checkout already. It
2179
+ // still needs trust validation and the source root in the live roots.
2180
+ if (pathIdentity(cwdRef.current) === pathIdentity(record.worktreePath)) {
2181
+ applyRelocation(record);
2182
+ return;
2183
+ }
2156
2184
  if (!onRelocate) return;
2157
2185
  const moved = await onRelocate(record.worktreePath).catch(() => null);
2158
2186
  if (moved) applyRelocation(record);
package/src/index.tsx CHANGED
@@ -70,7 +70,7 @@ if (result.kind === "help") {
70
70
  process.stdout.write(`${worktreeStartMessage(started)}\n`);
71
71
  process.chdir(started.worktree.path);
72
72
  const { start } = await import("./main");
73
- await start(result.options, { worktreeSourceRoot: started.sourceRoot });
73
+ await start(result.options, { worktreeStart: started });
74
74
  } catch (error) {
75
75
  process.stderr.write(formatCliError(errorMessage(error)));
76
76
  process.exitCode = 1;
package/src/main.tsx CHANGED
@@ -69,14 +69,16 @@ import {
69
69
  ManagedShellLifecycleController,
70
70
  lifecycleEventFromSnapshot,
71
71
  } from "./shells/lifecycle";
72
+ import { initializeWorktreeLaunchRelocation, type RelocationRecord } from "./relocation";
73
+ import type { WorktreeStart } from "./worktree-start";
72
74
 
73
75
  /**
74
76
  * Process-local launch context. These are facts about how this process was
75
77
  * started, never user settings, so nothing here reaches pum.json.
76
78
  */
77
79
  export type LaunchContext = {
78
- /** Source repository of a `pum worktree` launch, authorized as a writable root. */
79
- worktreeSourceRoot?: string;
80
+ /** Generated checkout and source repository from a `pum worktree` launch. */
81
+ worktreeStart?: WorktreeStart;
80
82
  };
81
83
 
82
84
  export async function start(
@@ -98,7 +100,7 @@ export async function start(
98
100
  // learn about it.
99
101
  const forcedCheckPaths = [
100
102
  ...(outerSandbox ? outerSandboxAdditionalRoots(outerSandbox, process.cwd()) : []),
101
- ...(context.worktreeSourceRoot ? [context.worktreeSourceRoot] : []),
103
+ ...(context.worktreeStart ? [context.worktreeStart.sourceRoot] : []),
102
104
  ];
103
105
  const sandboxController = new SandboxController({
104
106
  mode: outerSandbox ? "off" : settings.sandboxMode ?? "auto",
@@ -314,6 +316,15 @@ export async function start(
314
316
  statsManager.bindMainSession(sessionRuntime.session);
315
317
  if (sessionRuntime.modelFallbackMessage) console.error(sessionRuntime.modelFallbackMessage);
316
318
 
319
+ // `pum worktree` creates the checkout before the session exists. Persist it
320
+ // now so return and resume use the ordinary relocation path.
321
+ const initialRelocation: RelocationRecord | undefined = context.worktreeStart
322
+ ? initializeWorktreeLaunchRelocation(
323
+ sessionRuntime.session.sessionManager.getSessionFile(),
324
+ context.worktreeStart,
325
+ )
326
+ : undefined;
327
+
317
328
  const renderer = await createCliRenderer({ exitOnCtrlC: false });
318
329
  const terminalTitle = new TerminalTitleController((title) => renderer.setTerminalTitle(title));
319
330
  const selectionClipboard = installSelectionClipboard(renderer);
@@ -431,6 +442,7 @@ export async function start(
431
442
  }}
432
443
  forcedSandboxMode={outerSandbox ? "off" : undefined}
433
444
  forcedCheckPaths={forcedCheckPaths}
445
+ initialRelocation={initialRelocation}
434
446
  sandboxWarningSource={sandboxController}
435
447
  loginRequired={loginRequired}
436
448
  triggerManager={triggerManager}
package/src/relocation.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { companionFileFor, readCompanion, writeCompanion } from "./session-companion";
2
3
  import { isPathInsideOrSame, pathIdentity } from "./platform";
4
+ import type { WorktreeStart } from "./worktree-start";
3
5
 
4
6
  /**
5
7
  * Where a relocated session is currently running, and what move it still owes.
@@ -76,6 +78,34 @@ export function saveRelocation(
76
78
  writeCompanion(sessionFile, RELOCATION_SUFFIX, record);
77
79
  }
78
80
 
81
+ /**
82
+ * Persist the generated worktree that a `pum worktree` CLI launch entered.
83
+ *
84
+ * The CLI creates the checkout before the session exists. This bridges that
85
+ * launch fact into the same companion record used by in-session start/return.
86
+ */
87
+ export function initializeWorktreeLaunchRelocation(
88
+ sessionFile: string | undefined,
89
+ start: WorktreeStart,
90
+ now = Date.now(),
91
+ ): RelocationRecord {
92
+ const record: RelocationRecord = {
93
+ id: `reloc-${randomUUID().slice(0, 8)}`,
94
+ generation: 1,
95
+ sourceRoot: start.sourceRoot,
96
+ worktreePath: start.worktree.path,
97
+ name: start.worktree.name,
98
+ branch: start.worktree.branch,
99
+ baseBranch: start.worktree.baseBranch,
100
+ baseCommit: start.worktree.baseCommit,
101
+ location: "worktree",
102
+ createdAt: now,
103
+ updatedAt: now,
104
+ };
105
+ saveRelocation(sessionFile, record);
106
+ return record;
107
+ }
108
+
79
109
  export type RelocationGuardInput = {
80
110
  relocation: RelocationRecord | null;
81
111
  /** True while the main agent is working. A move mid-turn would change its roots. */