pi-session-cleanup 1.0.0 → 1.1.0

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0 - 2026-05-04
4
+
5
+ - Added `/nix quit` for confirmed current-session deletion during graceful Pi shutdown
6
+ - Added `/nix agent [name]` for starting fresh sessions with persisted target-agent metadata
7
+ - Added `/nix` argument completions, explicit help output, and safer compatibility warnings for unsupported Pi builds
8
+ - Documented destructive `/nix` safeguards and added targeted release-readiness coverage for the new session flows
9
+
3
10
  ## 1.0.0 - 2026-03-05
4
11
 
5
12
  - Renamed extension from its previous package name to `pi-session-cleanup`
package/README.md CHANGED
@@ -14,6 +14,9 @@ Interactive session cleanup extension for the [Pi coding agent](https://github.c
14
14
  - **Scope Filtering** — View only orphaned sessions or all historical sessions
15
15
  - **Batch Selection Controls** — Multi-select with Space, select all with `a`, keyboard navigation
16
16
  - **Safe Delete Flow** — Excludes the currently active session and uses trash-first deletion with unlink fallback
17
+ - **Fresh Session Shortcut** — `/nix` starts a fresh session and removes the previous session after confirmation
18
+ - **Target Agent Handoff** — `/nix agent [name]` starts a fresh session with persisted active-agent metadata
19
+ - **Quit Cleanup Flow** — `/nix quit` schedules current-session deletion during Pi's graceful shutdown event
17
20
  - **Improved Modal UX** — Centered overlay with bordered layout, concise single-line legend, status summary, and automatic icon fallback
18
21
 
19
22
  ## Installation
@@ -51,6 +54,10 @@ pi install git:github.com/MasuRii/pi-session-cleanup
51
54
  | `/session-cleanup current` | — | Opens modal with sessions from the current directory |
52
55
  | `/session-cleanup all` | — | Opens modal showing all sessions |
53
56
  | `/session-cleanup help` | — | Displays usage help |
57
+ | `/nix` | — | Starts a fresh session after confirmation and deletes the previous session |
58
+ | `/nix quit` | — | Deletes the current session during graceful shutdown and quits Pi |
59
+ | `/nix agent` | `[name]` | Starts a fresh session with a selected or explicitly named target agent |
60
+ | `/nix help` | — | Displays `/nix` usage help |
54
61
 
55
62
  **Scopes:**
56
63
 
@@ -58,6 +65,16 @@ pi install git:github.com/MasuRii/pi-session-cleanup
58
65
  - **`current`** — Shows sessions from the current working directory
59
66
  - **`all`** — Shows all historical sessions across all directories
60
67
 
68
+ ### `/nix` Fresh Session Workflow
69
+
70
+ `/nix` is destructive by design and always asks for confirmation before deleting any session file.
71
+
72
+ - **`/nix`** starts a new session with the current agent and deletes the previous session only after `ctx.newSession()` succeeds.
73
+ - **`/nix agent [name]`** starts a new session with the selected target agent and writes an `active_agent` session entry so Pi can resume that agent context. Without `[name]`, the command opens an interactive agent picker; with `[name]`, it validates the name before continuing.
74
+ - **`/nix quit`** requires Pi builds that expose `ctx.shutdown()`. It schedules deletion of the current session and performs the delete from the `session_shutdown` event, so the session file is not removed until Pi has begun graceful shutdown.
75
+
76
+ Target agents are discovered from the nearest project agent folders (`.omp/agents`, `.pi/agents`, `.claude/agents`) plus user agent folders (`~/.omp/agents`, `$PI_CODING_AGENT_DIR/agents`, `~/.claude/agents`). If `pi-agent-router` is installed with custom `agentDiscovery` paths, those paths are reused.
77
+
61
78
  ### Modal Controls
62
79
 
63
80
  When the session picker modal is open:
@@ -79,8 +96,9 @@ The extension includes multiple safety mechanisms:
79
96
 
80
97
  1. **Active Session Protection** — The currently active session is never shown in the list and cannot be deleted
81
98
  2. **Trash-First Deletion** — Sessions are moved to trash first; only falls back to permanent deletion if trash is unavailable
82
- 3. **Confirmation Required** — The modal requires explicit `Enter` keypress to proceed with deletion
83
- 4. **Escapable** — `Esc` or `q` immediately cancels without any changes
99
+ 3. **Confirmation Required** — The modal requires explicit `Enter` keypress to proceed with deletion, and `/nix` commands require `ctx.ui.confirm()` approval
100
+ 4. **Graceful Quit Guard** — `/nix quit` refuses to delete anything when the active Pi build does not expose `ctx.shutdown()`
101
+ 5. **Escapable** — `Esc` or `q` immediately cancels without any changes
84
102
 
85
103
  ## Configuration
86
104
 
package/package.json CHANGED
@@ -1,66 +1,66 @@
1
- {
2
- "name": "pi-session-cleanup",
3
- "version": "1.0.0",
4
- "description": "Pi extension for interactive batch session cleanup and safe deletion.",
5
- "type": "module",
6
- "main": "./index.ts",
7
- "exports": {
8
- ".": "./index.ts"
9
- },
10
- "files": [
11
- "index.ts",
12
- "src",
13
- "config/config.example.json",
14
- "README.md",
15
- "CHANGELOG.md",
16
- "LICENSE"
17
- ],
18
- "scripts": {
19
- "build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
20
- "lint": "npm run build",
21
- "test:clean": "node -e \"require('node:fs').rmSync('.test-dist', { recursive: true, force: true })\"",
22
- "pretest": "npm run test:clean",
23
- "test": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.test.json && node --test .test-dist/test/*.test.js",
24
- "posttest": "npm run test:clean",
25
- "check": "npm run lint && npm run test",
26
- "package:dry-run": "npm pack --dry-run"
27
- },
28
- "keywords": [
29
- "pi-package",
30
- "pi",
31
- "pi-extension",
32
- "session",
33
- "cleanup",
34
- "delete",
35
- "pi-coding-agent",
36
- "pi-tui",
37
- "session-management",
38
- "tui",
39
- "safe-delete"
40
- ],
41
- "author": "MasuRii",
42
- "license": "MIT",
43
- "engines": {
44
- "node": ">=20"
45
- },
46
- "publishConfig": {
47
- "access": "public"
48
- },
49
- "pi": {
50
- "extensions": [
51
- "./index.ts"
52
- ]
53
- },
54
- "peerDependencies": {
55
- "@mariozechner/pi-coding-agent": "^0.70.5",
56
- "@mariozechner/pi-tui": "^0.70.5"
57
- },
58
- "repository": {
59
- "type": "git",
60
- "url": "git+https://github.com/MasuRii/pi-session-cleanup.git"
61
- },
62
- "bugs": {
63
- "url": "https://github.com/MasuRii/pi-session-cleanup/issues"
64
- },
65
- "homepage": "https://github.com/MasuRii/pi-session-cleanup#readme"
66
- }
1
+ {
2
+ "name": "pi-session-cleanup",
3
+ "version": "1.1.0",
4
+ "description": "Pi extension for interactive batch session cleanup and safe deletion.",
5
+ "type": "module",
6
+ "main": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts"
9
+ },
10
+ "files": [
11
+ "index.ts",
12
+ "src",
13
+ "config/config.example.json",
14
+ "README.md",
15
+ "CHANGELOG.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
20
+ "lint": "npm run build",
21
+ "test:clean": "node -e \"require('node:fs').rmSync('.test-dist', { recursive: true, force: true })\"",
22
+ "pretest": "npm run test:clean",
23
+ "test": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.test.json && node --test .test-dist/test/*.test.js",
24
+ "posttest": "npm run test:clean",
25
+ "check": "npm run lint && npm run test",
26
+ "package:dry-run": "npm pack --dry-run"
27
+ },
28
+ "keywords": [
29
+ "pi-package",
30
+ "pi",
31
+ "pi-extension",
32
+ "session",
33
+ "cleanup",
34
+ "delete",
35
+ "pi-coding-agent",
36
+ "pi-tui",
37
+ "session-management",
38
+ "tui",
39
+ "safe-delete"
40
+ ],
41
+ "author": "MasuRii",
42
+ "license": "MIT",
43
+ "engines": {
44
+ "node": ">=20"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "pi": {
50
+ "extensions": [
51
+ "./index.ts"
52
+ ]
53
+ },
54
+ "peerDependencies": {
55
+ "@mariozechner/pi-coding-agent": "^0.72.0",
56
+ "@mariozechner/pi-tui": "^0.72.0"
57
+ },
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/MasuRii/pi-session-cleanup.git"
61
+ },
62
+ "bugs": {
63
+ "url": "https://github.com/MasuRii/pi-session-cleanup/issues"
64
+ },
65
+ "homepage": "https://github.com/MasuRii/pi-session-cleanup#readme"
66
+ }
@@ -0,0 +1,361 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
7
+
8
+ const DEFAULT_PROJECT_SOURCE_DIRS = [".omp/agents", ".pi/agents", ".claude/agents"];
9
+ const DEFAULT_USER_SOURCE_DIRS = ["{home}/.omp/agents", "{agentDir}/agents", "{home}/.claude/agents"];
10
+ const ROUTER_CONFIG_FILE_NAME = "config.json";
11
+ const ROUTER_EXTENSION_NAME = "pi-agent-router";
12
+ const PI_AGENT_DIR_ENV_VAR = "PI_CODING_AGENT_DIR";
13
+
14
+ const SESSION_EXTENSION_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
15
+
16
+ type AgentMode = "primary" | "subagent" | "all";
17
+
18
+ interface AgentDiscoveryConfig {
19
+ projectSourceDirs: string[];
20
+ userSourceDirs: string[];
21
+ }
22
+
23
+ export interface SelectableAgent {
24
+ name: string;
25
+ description: string;
26
+ mode?: AgentMode;
27
+ }
28
+
29
+ interface AgentSelectionMenu {
30
+ labels: string[];
31
+ valueByLabel: Map<string, string>;
32
+ }
33
+
34
+ function normalizeStringArray(value: unknown): string[] | null {
35
+ if (!Array.isArray(value)) {
36
+ return null;
37
+ }
38
+
39
+ const normalized = value
40
+ .filter((entry): entry is string => typeof entry === "string")
41
+ .map((entry) => entry.trim())
42
+ .filter(Boolean);
43
+
44
+ return normalized.length > 0 ? normalized : null;
45
+ }
46
+
47
+ function toRecord(value: unknown): Record<string, unknown> | null {
48
+ return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
49
+ }
50
+
51
+ function expandHomeDirectory(configuredDir: string, homeDirectory: string): string {
52
+ if (configuredDir === "~") {
53
+ return homeDirectory;
54
+ }
55
+
56
+ if (configuredDir.startsWith("~/") || configuredDir.startsWith("~\\")) {
57
+ return join(homeDirectory, stripLeadingPathSeparators(configuredDir.slice(1)));
58
+ }
59
+
60
+ return configuredDir;
61
+ }
62
+
63
+ function resolvePiAgentDir(): string {
64
+ const configuredDir = process.env[PI_AGENT_DIR_ENV_VAR]?.trim();
65
+ if (configuredDir) {
66
+ return expandHomeDirectory(configuredDir, homedir());
67
+ }
68
+
69
+ return join(homedir(), ".pi", "agent");
70
+ }
71
+
72
+ function resolveRouterConfigCandidates(): string[] {
73
+ const agentDir = resolvePiAgentDir();
74
+ const extensionParentDir = dirname(SESSION_EXTENSION_ROOT);
75
+
76
+ return [
77
+ join(extensionParentDir, ROUTER_EXTENSION_NAME, ROUTER_CONFIG_FILE_NAME),
78
+ join(agentDir, "extensions", ROUTER_EXTENSION_NAME, ROUTER_CONFIG_FILE_NAME),
79
+ ];
80
+ }
81
+
82
+ function loadAgentDiscoveryConfig(): AgentDiscoveryConfig {
83
+ for (const configPath of resolveRouterConfigCandidates()) {
84
+ try {
85
+ const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as unknown;
86
+ const config = toRecord(parsed);
87
+ const agentDiscovery = toRecord(config?.agentDiscovery);
88
+ const projectSourceDirs = normalizeStringArray(agentDiscovery?.projectSourceDirs);
89
+ const userSourceDirs = normalizeStringArray(agentDiscovery?.userSourceDirs);
90
+
91
+ return {
92
+ projectSourceDirs: projectSourceDirs ?? [...DEFAULT_PROJECT_SOURCE_DIRS],
93
+ userSourceDirs: userSourceDirs ?? [...DEFAULT_USER_SOURCE_DIRS],
94
+ };
95
+ } catch {
96
+ continue;
97
+ }
98
+ }
99
+
100
+ return {
101
+ projectSourceDirs: [...DEFAULT_PROJECT_SOURCE_DIRS],
102
+ userSourceDirs: [...DEFAULT_USER_SOURCE_DIRS],
103
+ };
104
+ }
105
+
106
+ function stripLeadingPathSeparators(value: string): string {
107
+ return value.replace(/^[\\/]+/, "");
108
+ }
109
+
110
+ function resolveConfiguredUserPath(rawPath: string): string {
111
+ const trimmed = rawPath.trim();
112
+ if (!trimmed) {
113
+ return resolve(resolvePiAgentDir(), "agents");
114
+ }
115
+
116
+ if (trimmed === "~") {
117
+ return homedir();
118
+ }
119
+
120
+ if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
121
+ return join(homedir(), stripLeadingPathSeparators(trimmed.slice(1)));
122
+ }
123
+
124
+ if (trimmed === "{home}") {
125
+ return homedir();
126
+ }
127
+
128
+ if (trimmed.startsWith("{home}/") || trimmed.startsWith("{home}\\")) {
129
+ return join(homedir(), stripLeadingPathSeparators(trimmed.slice("{home}".length)));
130
+ }
131
+
132
+ if (trimmed === "{agentDir}") {
133
+ return resolvePiAgentDir();
134
+ }
135
+
136
+ if (trimmed.startsWith("{agentDir}/") || trimmed.startsWith("{agentDir}\\")) {
137
+ return join(resolvePiAgentDir(), stripLeadingPathSeparators(trimmed.slice("{agentDir}".length)));
138
+ }
139
+
140
+ return resolve(trimmed);
141
+ }
142
+
143
+ function isDirectory(path: string): boolean {
144
+ try {
145
+ readdirSync(path);
146
+ return true;
147
+ } catch {
148
+ return false;
149
+ }
150
+ }
151
+
152
+ function findNearestProjectAgentDirs(cwd: string, projectSourceDirs: readonly string[]): string[] {
153
+ let currentDir = resolve(cwd);
154
+
155
+ while (true) {
156
+ const candidates = projectSourceDirs
157
+ .map((sourceDir) => resolve(currentDir, sourceDir))
158
+ .filter((candidate) => isDirectory(candidate));
159
+
160
+ if (candidates.length > 0) {
161
+ return candidates;
162
+ }
163
+
164
+ const parentDir = dirname(currentDir);
165
+ if (parentDir === currentDir) {
166
+ return [];
167
+ }
168
+
169
+ currentDir = parentDir;
170
+ }
171
+ }
172
+
173
+ function parseAgentMode(value: unknown): AgentMode | undefined {
174
+ if (typeof value !== "string") {
175
+ return undefined;
176
+ }
177
+
178
+ const normalized = value.trim().toLowerCase();
179
+ if (normalized === "primary" || normalized === "subagent" || normalized === "all") {
180
+ return normalized;
181
+ }
182
+
183
+ return undefined;
184
+ }
185
+
186
+ function parseFrontmatter(content: string): Record<string, string> | null {
187
+ const normalized = content.replace(/\r\n/g, "\n");
188
+ if (!normalized.startsWith("---\n")) {
189
+ return null;
190
+ }
191
+
192
+ const end = normalized.indexOf("\n---", 4);
193
+ if (end === -1) {
194
+ return null;
195
+ }
196
+
197
+ const frontmatter: Record<string, string> = {};
198
+ const lines = normalized.slice(4, end).split("\n");
199
+
200
+ for (const line of lines) {
201
+ const separatorIndex = line.indexOf(":");
202
+ if (separatorIndex === -1) {
203
+ continue;
204
+ }
205
+
206
+ const key = line.slice(0, separatorIndex).trim();
207
+ const value = line.slice(separatorIndex + 1).trim().replace(/^['\"]|['\"]$/g, "");
208
+ if (key) {
209
+ frontmatter[key] = value;
210
+ }
211
+ }
212
+
213
+ return frontmatter;
214
+ }
215
+
216
+ function parseAgentFile(filePath: string): SelectableAgent | null {
217
+ try {
218
+ const content = readFileSync(filePath, "utf-8");
219
+ const frontmatter = parseFrontmatter(content);
220
+ if (!frontmatter?.name) {
221
+ return null;
222
+ }
223
+
224
+ return {
225
+ name: frontmatter.name,
226
+ description: frontmatter.description || `Agent ${frontmatter.name}`,
227
+ mode: parseAgentMode(frontmatter.mode),
228
+ };
229
+ } catch {
230
+ return null;
231
+ }
232
+ }
233
+
234
+ function loadAgentsFromDir(dirPath: string): SelectableAgent[] {
235
+ try {
236
+ return readdirSync(dirPath)
237
+ .filter((entry) => entry.endsWith(".md"))
238
+ .map((entry) => parseAgentFile(join(dirPath, entry)))
239
+ .filter((agent): agent is SelectableAgent => Boolean(agent));
240
+ } catch {
241
+ return [];
242
+ }
243
+ }
244
+
245
+ function truncateDescription(description: string, maxLength = 72): string {
246
+ const normalized = description.trim().replace(/\s+/g, " ");
247
+ if (normalized.length <= maxLength) {
248
+ return normalized;
249
+ }
250
+
251
+ return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
252
+ }
253
+
254
+ function formatModeBadge(agent: SelectableAgent): string {
255
+ return `[${agent.mode ?? "primary"}]`;
256
+ }
257
+
258
+ function formatCurrentMarker(currentAgentName: string | null, candidateAgentName: string): string {
259
+ return currentAgentName === candidateAgentName ? "●" : "○";
260
+ }
261
+
262
+ function buildAgentSelectionLabel(
263
+ agent: SelectableAgent,
264
+ currentAgentName: string | null,
265
+ ): string {
266
+ return [
267
+ formatCurrentMarker(currentAgentName, agent.name),
268
+ agent.name,
269
+ formatModeBadge(agent),
270
+ "—",
271
+ truncateDescription(agent.description),
272
+ ].join(" ");
273
+ }
274
+
275
+ export function buildAgentSelectionMenu(
276
+ agents: readonly SelectableAgent[],
277
+ currentAgentName: string | null,
278
+ ): AgentSelectionMenu {
279
+ const labels: string[] = [];
280
+ const valueByLabel = new Map<string, string>();
281
+
282
+ for (const agent of agents) {
283
+ const label = buildAgentSelectionLabel(agent, currentAgentName);
284
+ labels.push(label);
285
+ valueByLabel.set(label, agent.name);
286
+ }
287
+
288
+ return {
289
+ labels,
290
+ valueByLabel,
291
+ };
292
+ }
293
+
294
+ export function discoverSelectableAgents(cwd: string): SelectableAgent[] {
295
+ const config = loadAgentDiscoveryConfig();
296
+ const projectAgentDirs = findNearestProjectAgentDirs(cwd, config.projectSourceDirs);
297
+ const userAgentDirs = config.userSourceDirs
298
+ .map((sourceDir) => resolveConfiguredUserPath(sourceDir))
299
+ .filter((candidate) => isDirectory(candidate));
300
+
301
+ const byName = new Map<string, SelectableAgent>();
302
+ const precedenceOrder = [
303
+ ...userAgentDirs.slice().reverse(),
304
+ ...projectAgentDirs.slice().reverse(),
305
+ ];
306
+
307
+ for (const sourceDir of precedenceOrder) {
308
+ const agents = loadAgentsFromDir(sourceDir);
309
+ for (const agent of agents) {
310
+ byName.set(agent.name, agent);
311
+ }
312
+ }
313
+
314
+ return [...byName.values()].sort((left, right) => left.name.localeCompare(right.name));
315
+ }
316
+
317
+ export async function resolveTargetAgentForSessionNix(
318
+ ctx: ExtensionCommandContext,
319
+ input: string | undefined,
320
+ currentAgentName: string | null,
321
+ ): Promise<SelectableAgent | null | undefined> {
322
+ const agents = discoverSelectableAgents(ctx.cwd);
323
+ if (agents.length === 0) {
324
+ ctx.ui.notify(
325
+ "No agents were discovered. Check pi-agent-router agent directories before using /nix agent.",
326
+ "warning",
327
+ );
328
+ return undefined;
329
+ }
330
+
331
+ if (input) {
332
+ const matchedAgent = agents.find((agent) => agent.name === input);
333
+ if (!matchedAgent) {
334
+ const agentNames = agents.map((agent) => agent.name).join(", ");
335
+ ctx.ui.notify(`Unknown agent: ${input}\nAvailable agents: ${agentNames}`, "warning");
336
+ return undefined;
337
+ }
338
+
339
+ return matchedAgent;
340
+ }
341
+
342
+ if (!ctx.hasUI) {
343
+ ctx.ui.notify("/nix agent requires an explicit agent name in non-interactive mode.", "warning");
344
+ return undefined;
345
+ }
346
+
347
+ const { showAgentTargetPicker } = await import("./tui/agent-target-picker.js");
348
+ const selectedAgentName = await showAgentTargetPicker(ctx, agents, currentAgentName);
349
+
350
+ if (!selectedAgentName) {
351
+ return null;
352
+ }
353
+
354
+ const selectedAgent = agents.find((agent) => agent.name === selectedAgentName);
355
+ if (!selectedAgent) {
356
+ ctx.ui.notify("Unknown agent selection. Please try again.", "warning");
357
+ return undefined;
358
+ }
359
+
360
+ return selectedAgent;
361
+ }
package/src/index.ts CHANGED
@@ -5,9 +5,17 @@ import {
5
5
  getSessionCleanupArgumentCompletions,
6
6
  handleSessionCleanupCommand,
7
7
  } from "./session-cleanup-command.js";
8
- import { handleSessionNixCommand } from "./session-nix-command.js";
8
+ import {
9
+ getSessionNixArgumentCompletions,
10
+ handleSessionNixCommand,
11
+ } from "./session-nix-command.js";
12
+ import { flushScheduledSessionDeletionForQuit } from "./session-quit-shutdown.js";
9
13
 
10
14
  export default function sessionCleanupExtension(pi: ExtensionAPI): void {
15
+ pi.on("session_shutdown", async (_event, ctx) => {
16
+ await flushScheduledSessionDeletionForQuit(ctx);
17
+ });
18
+
11
19
  pi.registerCommand(SESSION_CLEANUP_COMMAND, {
12
20
  description:
13
21
  "Batch-select previous sessions and delete them with confirmation.",
@@ -19,7 +27,8 @@ export default function sessionCleanupExtension(pi: ExtensionAPI): void {
19
27
 
20
28
  pi.registerCommand(SESSION_NIX_COMMAND, {
21
29
  description:
22
- "Start a new session and automatically delete the previous session.",
30
+ "Start a fresh session, switch to a target agent, or delete the current session and quit Pi.",
31
+ getArgumentCompletions: getSessionNixArgumentCompletions,
23
32
  handler: async (args, ctx) => {
24
33
  await handleSessionNixCommand(args, ctx);
25
34
  },
@@ -66,6 +66,31 @@ export async function resolveResponsibleAgentName(sessionPath: string): Promise<
66
66
  }
67
67
  }
68
68
 
69
+ export function extractPersistedActiveAgentNameFromEntries(
70
+ entries: readonly unknown[],
71
+ ): string | null | undefined {
72
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
73
+ const entry = toRecord(entries[index]);
74
+ if (!entry || entry.type !== "custom" || entry.customType !== "active_agent") {
75
+ continue;
76
+ }
77
+
78
+ const data = toRecord(entry.data);
79
+ const normalizedAgentName = normalizeAgentName(data?.name);
80
+ if (normalizedAgentName) {
81
+ return normalizedAgentName;
82
+ }
83
+
84
+ if (data?.name === null) {
85
+ return null;
86
+ }
87
+
88
+ return null;
89
+ }
90
+
91
+ return undefined;
92
+ }
93
+
69
94
  export async function enrichSessionWithResponsibleAgent(
70
95
  session: SessionInfo,
71
96
  ): Promise<SessionCleanupSession> {
@@ -0,0 +1,23 @@
1
+ import type { SessionManager } from "@mariozechner/pi-coding-agent";
2
+
3
+ export interface ActiveAgentSessionEntryData {
4
+ name: string | null;
5
+ }
6
+
7
+ type SessionManagerWithCustomEntry = SessionManager & {
8
+ appendCustomEntry?: (customType: string, data?: unknown) => string;
9
+ };
10
+
11
+ export function appendActiveAgentSessionEntry(
12
+ sessionManager: SessionManager,
13
+ agentName: string | null,
14
+ ): void {
15
+ const writableSessionManager = sessionManager as SessionManagerWithCustomEntry;
16
+ if (typeof writableSessionManager.appendCustomEntry !== "function") {
17
+ throw new Error("The current Pi build does not expose sessionManager.appendCustomEntry().");
18
+ }
19
+
20
+ writableSessionManager.appendCustomEntry("active_agent", {
21
+ name: agentName,
22
+ } satisfies ActiveAgentSessionEntryData);
23
+ }