@esso0428/pi-subagents 0.15.2 → 0.15.4

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
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.15.4] - 2026-09-11
11
+
12
+ ### Fixed
13
+ - **Unavailable history records no longer load the subagent panels.** Reload-restored metadata is shown in AgentWidget, FleetView, and `/agents` only when its project-local transcript still exists; stale records remain available through the normal session result/history data without creating a dead-end Enter target.
14
+
15
+ ## [0.15.3] - 2026-09-11
16
+
17
+ ### Added
18
+ - **Reload-safe transcript history.** Agents now keep a project-local copy under `.pi-subagents/agent-transcripts/`; terminal records persist a relative locator and `/agents`/FleetView can open a read-only history viewer after reload. The shared `.pi-subagents/.gitignore` is created with `*` to keep private transcripts out of git.
19
+
10
20
  ## [0.15.2] - 2026-09-10
11
21
 
12
22
  ### Added
package/README.md CHANGED
@@ -140,7 +140,7 @@ Individual agent results render Claude Code-style in the conversation:
140
140
 
141
141
  Completed results can be expanded (ctrl+o in pi) to show the agent output inline with Markdown formatting. Expanded output is capped at 50 rendered lines (background notification results at 30 lines) and ends with a marker when more output is available; use `get_subagent_result` for the complete result.
142
142
 
143
- By default, foreground and background agents each stream their full conversation to a per-subagent transcript a JSON-lines file at `<os-tmpdir>/pi-subagents-<uid>/<cwd>/<session>/tasks/<agent-id>.output` (owner-only `0700`, cleared on reboot). Set `output_transcript: false` on a custom agent to write no transcript path or file for it, or set `outputTranscript: false` in `subagents.json` to make transcripts opt-in for the whole project (frontmatter overrides the project default). This governs **only** the transcript: it is independent of `persist_session` (the pi session on disk), and it does not affect `isolation: worktree` (which commits the agent's work to a git branch) or `memory:` (durable files) — set those accordingly if the goal is to keep a run off disk entirely. Background agent completion notifications render as styled boxes:
143
+ By default, foreground and background agents stream their full conversation to two per-subagent transcripts: the existing temporary JSON-lines file at `<os-tmpdir>/pi-subagents-<uid>/<cwd>/<session>/tasks/<agent-id>.output` (owner-only `0700`, cleared on reboot), plus a durable copy at `<cwd>/.pi-subagents/agent-transcripts/<agent-id>_<agent-type>_transcript.jsonl`. The project-local `.pi-subagents/.gitignore` contains `*` so these private transcripts are not accidentally uploaded to git. After `/reload`, terminal records use the durable locator to open a read-only history viewer. Set `output_transcript: false` on a custom agent, or `outputTranscript: false` in `subagents.json`, to suppress both transcript files. This governs **only** the transcript: it is independent of `persist_session` (the pi session on disk), and it does not affect `isolation: worktree` or `memory:`. Background agent completion notifications render as styled boxes:
144
144
 
145
145
  ```
146
146
  ✓ Find auth files completed
@@ -222,7 +222,7 @@ All fields are optional — sensible defaults for everything.
222
222
  | `thinking` | inherit | off, minimal, low, medium, high, xhigh, max — actual availability depends on your pi version and model; pi clamps unsupported levels down |
223
223
  | `max_turns` | unlimited | Max agentic turns before graceful shutdown. `0` or omit for unlimited |
224
224
  | `persist_session` | `false` | Persist this subagent as a normal pi session instead of keeping the session in memory only. The subagent's `.output` transcript is still written either way unless `output_transcript: false` |
225
- | `output_transcript` | `true` (or `subagents.json` `outputTranscript`) | Write this subagent's `.output` transcript; when set, overrides the `subagents.json` `outputTranscript` default. Set `false` to write no transcript file or path. Governs only the transcript — independent of `persist_session`, `isolation: worktree`, and `memory:` |
225
+ | `output_transcript` | `true` (or `subagents.json` `outputTranscript`) | Write the temporary `.output` transcript and durable `.pi-subagents/agent-transcripts/` copy; when set, overrides the `subagents.json` `outputTranscript` default. Set `false` to write neither. Governs only the transcript — independent of `persist_session`, `isolation: worktree`, and `memory:` |
226
226
  | `session_dir` | pi default | Optional session directory when `persist_session: true`; omitted uses pi's normal session location, and relative paths resolve from the agent cwd |
227
227
  | `prompt_mode` | `replace` | `replace`: body is the full system prompt (no AGENTS.md / CLAUDE.md inheritance). `append`: body appended to parent's prompt (agent acts as a "parent twin" — inherits parent's AGENTS.md / CLAUDE.md) |
228
228
  | `inherit_context` | `false` | Fork parent conversation into agent |
@@ -508,7 +508,7 @@ Runtime tuning values set via `/agents` → Settings (max concurrency, default m
508
508
 
509
509
  **Disable defaults** (`disableDefaultAgents`, default `false`): when on, the three built-in agents (general-purpose, Explore, Plan) are not registered — only your project/global custom agents are advertised and spawnable. User-defined agents are unaffected, including ones that override a default by name. The Agent tool's type list updates on the next pi session (the tool schema is registered at startup).
510
510
 
511
- **Output transcript** (`outputTranscript`, default `true`): the project/global default for writing each subagent's `.output` transcript. Toggle via `/agents → Settings → Output transcript`, or set `false` in `subagents.json` to make transcripts opt-in project-wide — useful when run transcripts shouldn't sit on disk for backup or DLP tooling to pick up. A custom agent's `output_transcript` frontmatter overrides this per agent. Applied live at spawn time. Governs only the transcript, not `persist_session`, worktree commits, or memory files.
511
+ **Output transcript** (`outputTranscript`, default `true`): the project/global default for writing each subagent's temporary `.output` transcript and durable project-local history copy under `.pi-subagents/agent-transcripts/`. Toggle via `/agents → Settings → Output transcript`, or set `false` in `subagents.json` to make both transcripts opt-in project-wide. A custom agent's `output_transcript` frontmatter overrides this per agent. Applied live at spawn time. Governs only the transcript, not `persist_session`, worktree commits, or memory files.
512
512
 
513
513
  **Tool description** (`toolDescriptionMode`, default `"full"`): which Agent tool description the LLM sees. `"full"` is the rich Claude Code-style prompt (~1,400 tokens with the default agents); `"compact"` is ~75% smaller — one-line agent type list, terse usage notes — for small/local models where tool-spec tokens are expensive. Per-option details stay in the parameter descriptions in every mode (the parameter schema is never customizable). Applies on the next pi session.
514
514
 
@@ -0,0 +1,24 @@
1
+ /** Durable, project-local transcript storage for subagents. */
2
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
3
+ /**
4
+ * Ensure project-local subagent artifacts are ignored by git.
5
+ *
6
+ * The rule is appended instead of rewriting the file. This preserves any
7
+ * existing user-owned rules and makes concurrent callers harmless (duplicate
8
+ * `*` rules are semantically equivalent).
9
+ */
10
+ export declare function ensureSubagentsGitignore(cwd: string): string;
11
+ /** Return the durable transcript path for an agent. */
12
+ export declare function createAgentHistoryPath(cwd: string, agentId: string, agentType?: string): string;
13
+ /** Return the project-relative path stored in the parent session record. */
14
+ export declare function agentHistoryLocator(cwd: string, historyPath: string): string;
15
+ /** Resolve only paths in this package's project-local transcript namespace. */
16
+ export declare function resolveAgentHistoryPath(cwd: string, locator: string): string | undefined;
17
+ /** Return true only when a valid project-local transcript file exists. */
18
+ export declare function hasAgentHistory(cwd: string, locator: string | undefined): boolean;
19
+ /**
20
+ * Read persisted transcript entries into the message shape used by the live
21
+ * conversation viewer. Malformed lines and unknown records are skipped so one
22
+ * damaged entry cannot hide the rest of a history.
23
+ */
24
+ export declare function readAgentHistory(cwd: string, locator: string): AgentSession["messages"] | undefined;
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ /** Durable, project-local transcript storage for subagents. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ensureSubagentsGitignore = ensureSubagentsGitignore;
5
+ exports.createAgentHistoryPath = createAgentHistoryPath;
6
+ exports.agentHistoryLocator = agentHistoryLocator;
7
+ exports.resolveAgentHistoryPath = resolveAgentHistoryPath;
8
+ exports.hasAgentHistory = hasAgentHistory;
9
+ exports.readAgentHistory = readAgentHistory;
10
+ const node_fs_1 = require("node:fs");
11
+ const node_path_1 = require("node:path");
12
+ const SUBAGENTS_DIR = ".pi-subagents";
13
+ const TRANSCRIPTS_DIR = "agent-transcripts";
14
+ const MAX_HISTORY_BYTES = 20 * 1024 * 1024;
15
+ /**
16
+ * Ensure project-local subagent artifacts are ignored by git.
17
+ *
18
+ * The rule is appended instead of rewriting the file. This preserves any
19
+ * existing user-owned rules and makes concurrent callers harmless (duplicate
20
+ * `*` rules are semantically equivalent).
21
+ */
22
+ function ensureSubagentsGitignore(cwd) {
23
+ const directory = (0, node_path_1.join)(cwd, SUBAGENTS_DIR);
24
+ (0, node_fs_1.mkdirSync)(directory, { recursive: true });
25
+ const path = (0, node_path_1.join)(directory, ".gitignore");
26
+ let content = "";
27
+ try {
28
+ content = (0, node_fs_1.readFileSync)(path, "utf8");
29
+ }
30
+ catch {
31
+ // The append below also creates a missing file.
32
+ }
33
+ if (!content.split(/\r?\n/).some((line) => line.trim() === "*")) {
34
+ const separator = content.length > 0 && !/[\r\n]$/.test(content) ? "\n" : "";
35
+ (0, node_fs_1.appendFileSync)(path, `${separator}*\n`, "utf8");
36
+ }
37
+ return path;
38
+ }
39
+ /** Return the durable transcript path for an agent. */
40
+ function createAgentHistoryPath(cwd, agentId, agentType = "agent") {
41
+ ensureSubagentsGitignore(cwd);
42
+ const directory = (0, node_path_1.join)(cwd, SUBAGENTS_DIR, TRANSCRIPTS_DIR);
43
+ (0, node_fs_1.mkdirSync)(directory, { recursive: true });
44
+ const safeId = agentId.replace(/[^A-Za-z0-9._-]+/g, "-") || "agent";
45
+ const safeType = agentType.replace(/[^A-Za-z0-9._-]+/g, "-") || "agent";
46
+ return (0, node_path_1.join)(directory, `${safeId}_${safeType}_transcript.jsonl`);
47
+ }
48
+ /** Return the project-relative path stored in the parent session record. */
49
+ function agentHistoryLocator(cwd, historyPath) {
50
+ return (0, node_path_1.relative)(cwd, historyPath).split(node_path_1.sep).join("/");
51
+ }
52
+ /** Resolve only paths in this package's project-local transcript namespace. */
53
+ function resolveAgentHistoryPath(cwd, locator) {
54
+ if (!locator || (0, node_path_1.isAbsolute)(locator))
55
+ return undefined;
56
+ const root = (0, node_path_1.resolve)(cwd, SUBAGENTS_DIR, TRANSCRIPTS_DIR);
57
+ const candidate = (0, node_path_1.resolve)(cwd, locator);
58
+ if (candidate !== root && !candidate.startsWith(`${root}${node_path_1.sep}`))
59
+ return undefined;
60
+ return candidate;
61
+ }
62
+ /** Return true only when a valid project-local transcript file exists. */
63
+ function hasAgentHistory(cwd, locator) {
64
+ if (!locator)
65
+ return false;
66
+ const path = resolveAgentHistoryPath(cwd, locator);
67
+ return path !== undefined && (0, node_fs_1.existsSync)(path);
68
+ }
69
+ /**
70
+ * Read persisted transcript entries into the message shape used by the live
71
+ * conversation viewer. Malformed lines and unknown records are skipped so one
72
+ * damaged entry cannot hide the rest of a history.
73
+ */
74
+ function readAgentHistory(cwd, locator) {
75
+ const path = resolveAgentHistoryPath(cwd, locator);
76
+ if (!path)
77
+ return undefined;
78
+ let raw;
79
+ try {
80
+ raw = (0, node_fs_1.readFileSync)(path, "utf8");
81
+ }
82
+ catch {
83
+ return undefined;
84
+ }
85
+ if (raw.length > MAX_HISTORY_BYTES)
86
+ raw = raw.slice(0, MAX_HISTORY_BYTES);
87
+ const messages = [];
88
+ for (const line of raw.split(/\r?\n/)) {
89
+ if (!line.trim())
90
+ continue;
91
+ try {
92
+ const entry = JSON.parse(line);
93
+ const message = entry.message;
94
+ if (!message || typeof message !== "object")
95
+ continue;
96
+ const role = message.role;
97
+ if (typeof role !== "string")
98
+ continue;
99
+ messages.push(message);
100
+ }
101
+ catch {
102
+ // Ignore malformed/truncated JSONL records.
103
+ }
104
+ }
105
+ return messages.length > 0 ? messages : undefined;
106
+ }
@@ -57,6 +57,12 @@ function isRestorableRecord(value) {
57
57
  if (!value || typeof value !== "object")
58
58
  return false;
59
59
  const record = value;
60
+ const transcriptPath = record.transcriptPath;
61
+ const validTranscriptPath = transcriptPath === undefined ||
62
+ (typeof transcriptPath === "string" &&
63
+ transcriptPath.startsWith(".pi-subagents/agent-transcripts/") &&
64
+ !transcriptPath.includes("..") &&
65
+ !transcriptPath.includes("\\"));
60
66
  return (typeof record.id === "string" &&
61
67
  record.id.length > 0 &&
62
68
  typeof record.type === "string" &&
@@ -67,7 +73,8 @@ function isRestorableRecord(value) {
67
73
  typeof record.completedAt === "number" &&
68
74
  Number.isFinite(record.completedAt) &&
69
75
  (record.result === undefined || typeof record.result === "string") &&
70
- (record.error === undefined || typeof record.error === "string"));
76
+ (record.error === undefined || typeof record.error === "string") &&
77
+ validTranscriptPath);
71
78
  }
72
79
  class AgentManager {
73
80
  agents = new Map();
@@ -478,6 +485,7 @@ class AgentManager {
478
485
  status: value.status,
479
486
  result: value.result,
480
487
  error: value.error,
488
+ transcriptPath: value.transcriptPath,
481
489
  toolUses: 0,
482
490
  startedAt: value.startedAt,
483
491
  completedAt: value.completedAt,
package/dist/index.js CHANGED
@@ -10,39 +10,6 @@
10
10
  * Commands:
11
11
  * /agents — Interactive agent management menu
12
12
  */
13
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
- if (k2 === undefined) k2 = k;
15
- var desc = Object.getOwnPropertyDescriptor(m, k);
16
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
- desc = { enumerable: true, get: function() { return m[k]; } };
18
- }
19
- Object.defineProperty(o, k2, desc);
20
- }) : (function(o, m, k, k2) {
21
- if (k2 === undefined) k2 = k;
22
- o[k2] = m[k];
23
- }));
24
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
- Object.defineProperty(o, "default", { enumerable: true, value: v });
26
- }) : function(o, v) {
27
- o["default"] = v;
28
- });
29
- var __importStar = (this && this.__importStar) || (function () {
30
- var ownKeys = function(o) {
31
- ownKeys = Object.getOwnPropertyNames || function (o) {
32
- var ar = [];
33
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
- return ar;
35
- };
36
- return ownKeys(o);
37
- };
38
- return function (mod) {
39
- if (mod && mod.__esModule) return mod;
40
- var result = {};
41
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
- __setModuleDefault(result, mod);
43
- return result;
44
- };
45
- })();
46
13
  Object.defineProperty(exports, "__esModule", { value: true });
47
14
  exports.renderRunningAgentStatus = renderRunningAgentStatus;
48
15
  exports.default = default_1;
@@ -60,6 +27,7 @@ const enabled_models_js_1 = require("./enabled-models.js");
60
27
  const group_join_js_1 = require("./group-join.js");
61
28
  const invocation_config_js_1 = require("./invocation-config.js");
62
29
  const model_resolver_js_1 = require("./model-resolver.js");
30
+ const agent_history_js_1 = require("./agent-history.js");
63
31
  const output_file_js_1 = require("./output-file.js");
64
32
  const schedule_js_1 = require("./schedule.js");
65
33
  const schedule_store_js_1 = require("./schedule-store.js");
@@ -443,11 +411,14 @@ function default_1(pi) {
443
411
  else {
444
412
  pi.events.emit("subagents:completed", eventData);
445
413
  }
446
- // Persist final record for cross-extension history reconstruction
414
+ // Persist final record for cross-extension history reconstruction. Keep
415
+ // the transcript body in a project-local file rather than inflating the
416
+ // parent session JSONL; only its project-relative locator is persisted.
447
417
  pi.appendEntry("subagents:record", {
448
418
  id: record.id, type: record.type, description: record.description,
449
419
  status: record.status, result: record.result, error: record.error,
450
420
  startedAt: record.startedAt, completedAt: record.completedAt,
421
+ transcriptPath: record.transcriptPath,
451
422
  });
452
423
  // Skip notification if result was already consumed via get_subagent_result
453
424
  if (record.resultConsumed) {
@@ -544,12 +515,23 @@ function default_1(pi) {
544
515
  // bound session_start, so a filtered-out activation never advertises (#142).
545
516
  pi.on("session_start", async (_event, ctx) => {
546
517
  currentCtx = ctx;
518
+ try {
519
+ (0, agent_history_js_1.ensureSubagentsGitignore)(ctx.cwd);
520
+ }
521
+ catch (err) {
522
+ console.warn("[pi-subagents] Failed to protect project-local history:", err);
523
+ }
547
524
  manager.clearCompleted(true);
548
525
  const historicalRecords = ctx.sessionManager
549
526
  .getBranch()
550
527
  .filter((entry) => entry?.type === "custom" && entry.customType === "subagents:record")
551
528
  .map((entry) => entry.data);
552
529
  manager.restoreCompleted(historicalRecords);
530
+ // Bind the UI here as well as on tool execution so rehydrated terminal
531
+ // records are immediately visible and openable from FleetView after reload.
532
+ widget.setUICtx(ctx.ui);
533
+ fleet.setUICtx(ctx.ui);
534
+ fleet.update();
553
535
  // Guard mirrors the `!scheduler.isActive()` pattern below: session_start
554
536
  // fires once per activation, but a double-bind must not leak listeners.
555
537
  if (!rpcHandle) {
@@ -599,10 +581,10 @@ function default_1(pi) {
599
581
  // everything else; "off" = hide the widget entirely. Read live at render time.
600
582
  let widgetMode = "background";
601
583
  function getWidgetMode() { return widgetMode; }
602
- const widget = new agent_widget_js_1.AgentWidget(manager, agentActivity, getWidgetMode);
584
+ const widget = new agent_widget_js_1.AgentWidget(manager, agentActivity, getWidgetMode, () => currentCtx?.cwd);
603
585
  function setWidgetMode(m) { widgetMode = m; widget.update(); }
604
586
  // Claude Code-style FleetView: navigable list of main + subagents below the editor.
605
- const fleet = new fleet_list_js_1.FleetList(manager, agentActivity);
587
+ const fleet = new fleet_list_js_1.FleetList(manager, agentActivity, () => currentCtx?.cwd);
606
588
  let fleetViewEnabled = true;
607
589
  function isFleetViewEnabled() { return fleetViewEnabled; }
608
590
  function setFleetViewEnabled(b) { fleetViewEnabled = b; fleet.setEnabled(b); }
@@ -1072,6 +1054,19 @@ Terse command-style prompts produce shallow, generic work.
1072
1054
  return;
1073
1055
  rec.outputFile = (0, output_file_js_1.createOutputFilePath)(ctx.cwd, agentId, ctx.sessionManager.getSessionId());
1074
1056
  (0, output_file_js_1.writeInitialEntry)(rec.outputFile, agentId, params.prompt, ctx.cwd);
1057
+ // Keep the existing /tmp output for compatibility, while also writing a
1058
+ // durable project-local copy for reload-safe, read-only history viewing.
1059
+ try {
1060
+ rec.historyFile = (0, agent_history_js_1.createAgentHistoryPath)(ctx.cwd, agentId, subagentType);
1061
+ rec.transcriptPath = (0, agent_history_js_1.agentHistoryLocator)(ctx.cwd, rec.historyFile);
1062
+ (0, output_file_js_1.writeInitialEntry)(rec.historyFile, agentId, params.prompt, ctx.cwd);
1063
+ }
1064
+ catch (err) {
1065
+ // History is best effort; never disable the existing output transcript.
1066
+ rec.historyFile = undefined;
1067
+ rec.transcriptPath = undefined;
1068
+ console.warn("[pi-subagents] Failed to create durable transcript:", err);
1069
+ }
1075
1070
  };
1076
1071
  const parentModelId = ctx.model?.id;
1077
1072
  const effectiveModelId = model?.id;
@@ -1172,7 +1167,7 @@ Terse command-style prompts produce shallow, generic work.
1172
1167
  origBgOnSession(session);
1173
1168
  const rec = manager.getRecord(id);
1174
1169
  if (rec?.outputFile) {
1175
- rec.outputCleanup = (0, output_file_js_1.streamToOutputFile)(session, rec.outputFile, id, ctx.cwd);
1170
+ rec.outputCleanup = (0, output_file_js_1.streamToOutputFile)(session, rec.outputFile, id, ctx.cwd, rec.historyFile);
1176
1171
  }
1177
1172
  };
1178
1173
  try {
@@ -1278,7 +1273,7 @@ Terse command-style prompts produce shallow, generic work.
1278
1273
  if (fgId) {
1279
1274
  const rec = manager.getRecord(fgId);
1280
1275
  if (rec?.outputFile) {
1281
- rec.outputCleanup = (0, output_file_js_1.streamToOutputFile)(session, rec.outputFile, fgId, ctx.cwd);
1276
+ rec.outputCleanup = (0, output_file_js_1.streamToOutputFile)(session, rec.outputFile, fgId, ctx.cwd, rec.historyFile);
1282
1277
  }
1283
1278
  }
1284
1279
  };
@@ -1622,7 +1617,7 @@ Terse command-style prompts produce shallow, generic work.
1622
1617
  }
1623
1618
  }
1624
1619
  async function showRunningAgents(ctx) {
1625
- const agents = manager.listAgents();
1620
+ const agents = manager.listAgents().filter((record) => record.session !== undefined || (0, agent_history_js_1.hasAgentHistory)(ctx.cwd, record.transcriptPath));
1626
1621
  if (agents.length === 0) {
1627
1622
  ctx.ui.notify("No agents.", "info");
1628
1623
  return;
@@ -1649,15 +1644,25 @@ Terse command-style prompts produce shallow, generic work.
1649
1644
  ctx.ui.notify(`Agent is ${record.status === "queued" ? "queued" : "expired"} — no session available.`, "info");
1650
1645
  return;
1651
1646
  }
1652
- const { ConversationViewer, VIEWPORT_HEIGHT_PCT } = await Promise.resolve().then(() => __importStar(require("./ui/conversation-viewer.js")));
1653
- const session = record.session;
1647
+ const { ConversationViewer, VIEWPORT_HEIGHT_PCT, createStaticConversationSource } = await import("./ui/conversation-viewer.js");
1648
+ const session = record.session ?? (record.transcriptPath
1649
+ ? (() => {
1650
+ const messages = (0, agent_history_js_1.readAgentHistory)(ctx.cwd, record.transcriptPath);
1651
+ return messages ? createStaticConversationSource(messages) : undefined;
1652
+ })()
1653
+ : undefined);
1654
+ if (!session) {
1655
+ ctx.ui.notify(`Agent is ${record.status === "queued" ? "queued" : "expired"} — no history available.`, "info");
1656
+ return;
1657
+ }
1654
1658
  const activity = agentActivity.get(record.id);
1659
+ const isLive = record.session !== undefined;
1655
1660
  await ctx.ui.custom((tui, theme, keybindings, done) => {
1656
- return new ConversationViewer(tui, session, record, activity, theme, done, () => {
1661
+ return new ConversationViewer(tui, session, record, activity, theme, done, isLive ? () => {
1657
1662
  if (manager.abort(record.id)) {
1658
1663
  ctx.ui.notify(`Stopped "${record.description}".`, "info");
1659
1664
  }
1660
- }, keybindings, (message) => manager.steer(record.id, message));
1665
+ } : undefined, keybindings, isLive ? (message) => manager.steer(record.id, message) : undefined);
1661
1666
  }, {
1662
1667
  overlay: true,
1663
1668
  overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
@@ -1698,7 +1703,7 @@ Terse command-style prompts produce shallow, generic work.
1698
1703
  const content = (0, node_fs_1.readFileSync)(file.path, "utf-8");
1699
1704
  const edited = await ctx.ui.editor(`Edit ${name}`, content);
1700
1705
  if (edited !== undefined && edited !== content) {
1701
- const { writeFileSync } = await Promise.resolve().then(() => __importStar(require("node:fs")));
1706
+ const { writeFileSync } = await import("node:fs");
1702
1707
  writeFileSync(file.path, edited, "utf-8");
1703
1708
  reloadCustomAgents();
1704
1709
  ctx.ui.notify(`Updated ${file.path}`, "info");
@@ -1786,7 +1791,7 @@ Terse command-style prompts produce shallow, generic work.
1786
1791
  if (cfg.isolation)
1787
1792
  fmFields.push(`isolation: ${cfg.isolation}`);
1788
1793
  const content = `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
1789
- const { writeFileSync } = await Promise.resolve().then(() => __importStar(require("node:fs")));
1794
+ const { writeFileSync } = await import("node:fs");
1790
1795
  writeFileSync(targetPath, content, "utf-8");
1791
1796
  reloadCustomAgents();
1792
1797
  ctx.ui.notify(`Ejected ${name} to ${targetPath}`, "info");
@@ -1802,7 +1807,7 @@ Terse command-style prompts produce shallow, generic work.
1802
1807
  return;
1803
1808
  }
1804
1809
  const updated = content.replace(/^---\n/, "---\nenabled: false\n");
1805
- const { writeFileSync } = await Promise.resolve().then(() => __importStar(require("node:fs")));
1810
+ const { writeFileSync } = await import("node:fs");
1806
1811
  writeFileSync(file.path, updated, "utf-8");
1807
1812
  reloadCustomAgents();
1808
1813
  ctx.ui.notify(`Disabled ${name} (${file.path})`, "info");
@@ -1818,7 +1823,7 @@ Terse command-style prompts produce shallow, generic work.
1818
1823
  const targetDir = location.startsWith("Project") ? projectAgentsDir() : personalAgentsDir();
1819
1824
  (0, node_fs_1.mkdirSync)(targetDir, { recursive: true });
1820
1825
  const targetPath = (0, node_path_1.join)(targetDir, `${name}.md`);
1821
- const { writeFileSync } = await Promise.resolve().then(() => __importStar(require("node:fs")));
1826
+ const { writeFileSync } = await import("node:fs");
1822
1827
  writeFileSync(targetPath, "---\nenabled: false\n---\n", "utf-8");
1823
1828
  reloadCustomAgents();
1824
1829
  ctx.ui.notify(`Disabled ${name} (${targetPath})`, "info");
@@ -1830,7 +1835,7 @@ Terse command-style prompts produce shallow, generic work.
1830
1835
  return;
1831
1836
  const content = (0, node_fs_1.readFileSync)(file.path, "utf-8");
1832
1837
  const updated = content.replace(/^(---\n)enabled: false\n/, "$1");
1833
- const { writeFileSync } = await Promise.resolve().then(() => __importStar(require("node:fs")));
1838
+ const { writeFileSync } = await import("node:fs");
1834
1839
  // If the file was just a stub ("---\n---\n"), delete it to restore the built-in default
1835
1840
  if (updated.trim() === "---\n---" || updated.trim() === "---\n---\n") {
1836
1841
  (0, node_fs_1.unlinkSync)(file.path);
@@ -2013,7 +2018,7 @@ ${systemPrompt}
2013
2018
  if (!overwrite)
2014
2019
  return;
2015
2020
  }
2016
- const { writeFileSync } = await Promise.resolve().then(() => __importStar(require("node:fs")));
2021
+ const { writeFileSync } = await import("node:fs");
2017
2022
  writeFileSync(targetPath, content, "utf-8");
2018
2023
  reloadCustomAgents();
2019
2024
  ctx.ui.notify(`Created ${targetPath}`, "info");
@@ -21,4 +21,4 @@ export declare function writeInitialEntry(path: string, agentId: string, prompt:
21
21
  * Subscribe to session events and flush new messages to the output file on each turn_end.
22
22
  * Returns a cleanup function that does a final flush and unsubscribes.
23
23
  */
24
- export declare function streamToOutputFile(session: AgentSession, path: string, agentId: string, cwd: string): () => void;
24
+ export declare function streamToOutputFile(session: AgentSession, path: string, agentId: string, cwd: string, historyPath?: string): () => void;
@@ -60,7 +60,8 @@ function writeInitialEntry(path, agentId, prompt, cwd) {
60
60
  * Subscribe to session events and flush new messages to the output file on each turn_end.
61
61
  * Returns a cleanup function that does a final flush and unsubscribes.
62
62
  */
63
- function streamToOutputFile(session, path, agentId, cwd) {
63
+ function streamToOutputFile(session, path, agentId, cwd, historyPath) {
64
+ const outputPaths = historyPath && historyPath !== path ? [path, historyPath] : [path];
64
65
  let writtenCount = 1; // initial user prompt already written
65
66
  const flush = () => {
66
67
  const messages = session.messages;
@@ -74,10 +75,12 @@ function streamToOutputFile(session, path, agentId, cwd) {
74
75
  timestamp: new Date().toISOString(),
75
76
  cwd,
76
77
  };
77
- try {
78
- (0, node_fs_1.appendFileSync)(path, JSON.stringify(entry) + "\n", "utf-8");
78
+ for (const outputPath of outputPaths) {
79
+ try {
80
+ (0, node_fs_1.appendFileSync)(outputPath, JSON.stringify(entry) + "\n", "utf-8");
81
+ }
82
+ catch { /* ignore write errors */ }
79
83
  }
80
- catch { /* ignore write errors */ }
81
84
  writtenCount++;
82
85
  }
83
86
  };
package/dist/types.d.ts CHANGED
@@ -103,6 +103,10 @@ export interface AgentRecord {
103
103
  toolCallId?: string;
104
104
  /** Path to the streaming output transcript file. */
105
105
  outputFile?: string;
106
+ /** Absolute path to the durable project-local transcript while live. */
107
+ historyFile?: string;
108
+ /** Project-relative durable transcript path persisted in the parent session. */
109
+ transcriptPath?: string;
106
110
  /** Cleanup function for the output file stream subscription. */
107
111
  outputCleanup?: () => void;
108
112
  /**
@@ -102,6 +102,8 @@ export declare class AgentWidget {
102
102
  * extension supplies one defaulting to `"background"`.
103
103
  */
104
104
  private mode;
105
+ /** Current project cwd used to validate reload-restored transcripts. */
106
+ private getCwd;
105
107
  private uiCtx;
106
108
  private widgetFrame;
107
109
  private widgetInterval;
@@ -121,7 +123,9 @@ export declare class AgentWidget {
121
123
  * `WidgetMode`. Defaults to `"all"` when a caller supplies no policy; the
122
124
  * extension supplies one defaulting to `"background"`.
123
125
  */
124
- mode?: () => WidgetMode);
126
+ mode?: () => WidgetMode,
127
+ /** Current project cwd used to validate reload-restored transcripts. */
128
+ getCwd?: () => string | undefined);
125
129
  /**
126
130
  * Agents eligible for the widget, per the current `WidgetMode`:
127
131
  * - `off`: none (the widget's existing empty-state path hides it entirely).
@@ -18,6 +18,7 @@ exports.getPromptModeLabel = getPromptModeLabel;
18
18
  exports.buildInvocationTags = buildInvocationTags;
19
19
  exports.describeActivity = describeActivity;
20
20
  const pi_tui_1 = require("@earendil-works/pi-tui");
21
+ const agent_history_js_1 = require("../agent-history.js");
21
22
  const agent_types_js_1 = require("../agent-types.js");
22
23
  const usage_js_1 = require("../usage.js");
23
24
  // ---- Constants ----
@@ -155,6 +156,7 @@ class AgentWidget {
155
156
  manager;
156
157
  agentActivity;
157
158
  mode;
159
+ getCwd;
158
160
  uiCtx;
159
161
  widgetFrame = 0;
160
162
  widgetInterval;
@@ -174,10 +176,13 @@ class AgentWidget {
174
176
  * `WidgetMode`. Defaults to `"all"` when a caller supplies no policy; the
175
177
  * extension supplies one defaulting to `"background"`.
176
178
  */
177
- mode = () => "all") {
179
+ mode = () => "all",
180
+ /** Current project cwd used to validate reload-restored transcripts. */
181
+ getCwd = () => undefined) {
178
182
  this.manager = manager;
179
183
  this.agentActivity = agentActivity;
180
184
  this.mode = mode;
185
+ this.getCwd = getCwd;
181
186
  }
182
187
  /**
183
188
  * Agents eligible for the widget, per the current `WidgetMode`:
@@ -191,7 +196,12 @@ class AgentWidget {
191
196
  * - `all`: every agent.
192
197
  */
193
198
  widgetAgents() {
194
- const all = this.manager.listAgents();
199
+ const all = this.manager.listAgents().filter((record) => {
200
+ if (record.status === "running" || record.status === "queued")
201
+ return true;
202
+ const cwd = this.getCwd();
203
+ return cwd !== undefined && (0, agent_history_js_1.hasAgentHistory)(cwd, record.transcriptPath);
204
+ });
195
205
  switch (this.mode()) {
196
206
  case "off": return [];
197
207
  case "background": return all.filter(a => a.isBackground !== false);
@@ -12,6 +12,9 @@ import { type AgentActivity } from "./agent-widget.js";
12
12
  import { type ViewerKeybindings } from "./viewer-keys.js";
13
13
  /** Height ceiling shared by the overlay's `maxHeight` and the viewer's internal viewport cap. */
14
14
  export declare const VIEWPORT_HEIGHT_PCT = 70;
15
+ /** The live fields needed by the viewer; historical viewers use a static source. */
16
+ export type ConversationSource = Pick<AgentSession, "messages" | "subscribe">;
17
+ export declare function createStaticConversationSource(messages: AgentSession["messages"]): ConversationSource;
15
18
  export declare class ConversationViewer implements Component {
16
19
  private tui;
17
20
  private session;
@@ -33,7 +36,7 @@ export declare class ConversationViewer implements Component {
33
36
  private keys;
34
37
  /** Steering composer — present while the user is typing a message to the agent. */
35
38
  private composer;
36
- constructor(tui: TUI, session: AgentSession, record: AgentRecord, activity: AgentActivity | undefined, theme: Theme, done: (result: undefined) => void,
39
+ constructor(tui: TUI, session: ConversationSource, record: AgentRecord, activity: AgentActivity | undefined, theme: Theme, done: (result: undefined) => void,
37
40
  /** Abort the agent shown here. Omitted → no stop affordance (e.g. read-only history). */
38
41
  onStop?: (() => void) | undefined,
39
42
  /** User keybindings from `ctx.ui.custom()`. Omitted → hardcoded defaults. */
@@ -7,6 +7,7 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.ConversationViewer = exports.VIEWPORT_HEIGHT_PCT = void 0;
10
+ exports.createStaticConversationSource = createStaticConversationSource;
10
11
  const pi_tui_1 = require("@earendil-works/pi-tui");
11
12
  const context_js_1 = require("../context.js");
12
13
  const usage_js_1 = require("../usage.js");
@@ -17,6 +18,9 @@ const CHROME_LINES_BASE = 6;
17
18
  const MIN_VIEWPORT = 3;
18
19
  /** Height ceiling shared by the overlay's `maxHeight` and the viewer's internal viewport cap. */
19
20
  exports.VIEWPORT_HEIGHT_PCT = 70;
21
+ function createStaticConversationSource(messages) {
22
+ return { messages, subscribe: () => () => { } };
23
+ }
20
24
  class ConversationViewer {
21
25
  tui;
22
26
  session;
@@ -44,6 +44,7 @@ export declare function formatFleetTokens(count: number): string;
44
44
  export declare class FleetList {
45
45
  private manager;
46
46
  private agentActivity;
47
+ private getCwd;
47
48
  private ui;
48
49
  private tui;
49
50
  private inputUnsub;
@@ -57,7 +58,7 @@ export declare class FleetList {
57
58
  /** Set while a conversation overlay is open; calling it closes the overlay. */
58
59
  private viewerClose;
59
60
  private viewingAgentId;
60
- constructor(manager: AgentManager, agentActivity: Map<string, AgentActivity>);
61
+ constructor(manager: AgentManager, agentActivity: Map<string, AgentActivity>, getCwd?: () => string | undefined);
61
62
  setEnabled(enabled: boolean): void;
62
63
  /** Capture the UI context and (re)register the global input handler. */
63
64
  setUICtx(ui: FleetUICtx): void;
@@ -73,9 +74,11 @@ export declare class FleetList {
73
74
  update(): void;
74
75
  /**
75
76
  * Agents shown in the list, ordered earliest-launched first so the ones you
76
- * started sooner sit at the top. Every row is openable (has a session), so Enter
77
- * never dead-ends. Included: running/queued, plus the agent currently being
78
- * viewed, plus recently-finished ones (they linger briefly before dropping out).
77
+ * started sooner sit at the top. Live rows are openable through their session;
78
+ * terminal rows are included only when their durable transcript exists. This
79
+ * prevents Enter from dead-ending on stale rehydrated metadata.
80
+ * Included: running/queued, plus the agent currently being viewed, plus
81
+ * recently-finished ones (they linger briefly before dropping out).
79
82
  * Pending agents with no session yet are hidden until they start.
80
83
  * (`listAgents()` is newest-first, so we re-sort.)
81
84
  */