pi-fabric 0.83.0 → 0.84.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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/dist/chunks/{chunk-PGN56E56.js → chunk-HOKY6UCZ.js} +8 -3
  3. package/dist/chunks/chunk-HOKY6UCZ.js.map +7 -0
  4. package/dist/index.js +137 -28
  5. package/dist/index.js.map +3 -3
  6. package/dist/ui/controller.d.ts.map +1 -1
  7. package/dist/ui/conversation-chrome.d.ts +1 -0
  8. package/dist/ui/conversation-chrome.d.ts.map +1 -1
  9. package/dist/ui/conversation-chrome.js +1 -1
  10. package/dist/ui/conversation-commands.d.ts +12 -0
  11. package/dist/ui/conversation-commands.d.ts.map +1 -0
  12. package/dist/ui/conversation-history.d.ts +6 -0
  13. package/dist/ui/conversation-history.d.ts.map +1 -0
  14. package/dist/ui/conversation-native-reader-checkpoint.d.ts +8 -0
  15. package/dist/ui/conversation-native-reader-checkpoint.d.ts.map +1 -0
  16. package/dist/ui/conversation-native-reader-replay.d.ts +10 -0
  17. package/dist/ui/conversation-native-reader-replay.d.ts.map +1 -0
  18. package/dist/ui/conversation-native-reader.d.ts +5 -1
  19. package/dist/ui/conversation-native-reader.d.ts.map +1 -1
  20. package/dist/ui/conversation-native-reader.js +399 -67
  21. package/dist/ui/conversation-native-reader.js.map +4 -4
  22. package/dist/ui/conversation-queue-store.d.ts +2 -0
  23. package/dist/ui/conversation-queue-store.d.ts.map +1 -1
  24. package/dist/ui/conversation-render.d.ts +22 -10
  25. package/dist/ui/conversation-render.d.ts.map +1 -1
  26. package/dist/ui/conversation-selection.d.ts +18 -0
  27. package/dist/ui/conversation-selection.d.ts.map +1 -0
  28. package/dist/ui/conversation-shortcut.d.ts +1 -1
  29. package/dist/ui/conversation.d.ts +47 -1
  30. package/dist/ui/conversation.d.ts.map +1 -1
  31. package/dist/ui/conversation.js +982 -411
  32. package/dist/ui/conversation.js.map +4 -4
  33. package/dist/ui/snapshot.d.ts +9 -1
  34. package/dist/ui/snapshot.d.ts.map +1 -1
  35. package/docs/interface.md +31 -3
  36. package/package.json +3 -2
  37. package/skills/fabric-exec/references/agents.md +1 -1
  38. package/dist/chunks/chunk-PGN56E56.js.map +0 -7
package/README.md CHANGED
@@ -114,7 +114,7 @@ The foundation is the `fabric-exec` reference skill: the model loads it before i
114
114
 
115
115
  ## Agent conversations
116
116
 
117
- Press **Ctrl+Shift+A** or run **`/fabric chat <agent-id-or-name>`** to open a live, full-screen child conversation with a multiline editor. Send steering or follow-ups directly, switch between nested agents, and return to Main without stopping its work. Drafts and scroll positions stay with each conversation. Completed one-shot agents are read-only; persistent actors accept further messages. See [focused conversations](docs/interface.md#focused-conversations) for controls and current limitations.
117
+ Press **ctrl+shift+a** or run **`/fabric chat <agent-id-or-name>`** to open a live, full-screen child conversation with a multiline editor. Send steering or follow-ups directly, switch between nested agents, and return to Main without stopping its work. Drafts and scroll positions stay with each conversation. Completed one-shot agents are read-only; persistent actors accept further messages. Drag to select transcript text, use `/copy` or `/copy selection`, and type `/help` for the small set of preview-local commands with slash completion. See [focused conversations](docs/interface.md#focused-conversations) for controls and current limitations.
118
118
 
119
119
  ## The dashboard
120
120
 
@@ -16,7 +16,8 @@ function readConversationAppearance(cwd, agentDir, projectTrusted) {
16
16
  codeBlockIndent: settings.getCodeBlockIndent(),
17
17
  hideThinkingBlock: settings.getHideThinkingBlock(),
18
18
  showImages: settings.getShowImages(),
19
- imageWidthCells: settings.getImageWidthCells()
19
+ imageWidthCells: settings.getImageWidthCells(),
20
+ copyOnSelect: settings.getFullscreenCopyOnSelect()
20
21
  };
21
22
  }
22
23
  var footerPath = (cwd) => {
@@ -29,6 +30,10 @@ function conversationFooter(target, theme, width) {
29
30
  const location = target.cwd ? footerPath(target.cwd) : "cwd unavailable";
30
31
  const branch = target.branch ? ` (${target.branch})` : "";
31
32
  const pwd = safeText(`${location}${branch} \u2022 ${target.name}`);
33
+ const access = target.readOnlyReason ? theme.fg("warning", "read-only") : "";
34
+ const separator = theme.fg("dim", " \u2022 ");
35
+ const locationWidth = width - (access ? visibleWidth(separator) + visibleWidth(access) : 0);
36
+ const pathLine = locationWidth > 0 ? theme.fg("dim", truncateToWidth(pwd, locationWidth, "\u2026")) + (access ? separator + access : "") : truncateToWidth(access, width, "\u2026");
32
37
  const stats = [safeText(target.status)];
33
38
  if (target.usage) {
34
39
  const usage = target.usage;
@@ -46,7 +51,7 @@ function conversationFooter(target, theme, width) {
46
51
  const right = truncateToWidth(`${model}${thinking}`, Math.max(0, width - visibleWidth(left) - 2), "");
47
52
  const gap = " ".repeat(Math.max(0, width - visibleWidth(left) - visibleWidth(right)));
48
53
  return [
49
- truncateToWidth(theme.fg("dim", pwd), width, "\u2026"),
54
+ pathLine,
50
55
  theme.fg("dim", `${left}${gap}${right}`)
51
56
  ];
52
57
  }
@@ -55,4 +60,4 @@ export {
55
60
  readConversationAppearance,
56
61
  conversationFooter
57
62
  };
58
- //# sourceMappingURL=chunk-PGN56E56.js.map
63
+ //# sourceMappingURL=chunk-HOKY6UCZ.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ui/conversation-chrome.ts"],
4
+ "sourcesContent": ["import { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { SettingsManager, type Theme } from \"@earendil-works/pi-coding-agent\";\nimport { truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport { formatTokens, safeText } from \"./format.js\";\nimport type { FabricConversationTarget } from \"./conversation.js\";\n\nexport interface FabricConversationAppearance {\n editorPaddingX: number;\n outputPad: 0 | 1;\n codeBlockIndent?: string;\n hideThinkingBlock?: boolean;\n showImages?: boolean;\n imageWidthCells?: number;\n copyOnSelect?: boolean;\n}\n\n/** Read the host's public Pi settings without changing its editor or footer. */\nexport function readConversationAppearance(cwd: string, agentDir: string, projectTrusted: boolean): FabricConversationAppearance {\n const settings = SettingsManager.create(cwd, agentDir, { projectTrusted });\n return {\n editorPaddingX: settings.getEditorPaddingX(), outputPad: settings.getOutputPad(),\n codeBlockIndent: settings.getCodeBlockIndent(), hideThinkingBlock: settings.getHideThinkingBlock(),\n showImages: settings.getShowImages(), imageWidthCells: settings.getImageWidthCells(),\n copyOnSelect: settings.getFullscreenCopyOnSelect(),\n };\n}\n\nconst footerPath = (cwd: string): string => {\n const home = homedir();\n if (cwd === home) return \"~\";\n return cwd.startsWith(`${home}${path.sep}`) ? `~${cwd.slice(home.length)}` : cwd;\n};\n\n/** Native-shaped footer backed by this participant, never by Main's session stats. */\nexport function conversationFooter(target: FabricConversationTarget | undefined, theme: Theme, width: number): string[] {\n if (!target || width <= 0) return [];\n const location = target.cwd ? footerPath(target.cwd) : \"cwd unavailable\";\n const branch = target.branch ? ` (${target.branch})` : \"\";\n const pwd = safeText(`${location}${branch} \u2022 ${target.name}`);\n const access = target.readOnlyReason ? theme.fg(\"warning\", \"read-only\") : \"\";\n const separator = theme.fg(\"dim\", \" \u2022 \");\n const locationWidth = width - (access ? visibleWidth(separator) + visibleWidth(access) : 0);\n const pathLine = locationWidth > 0\n ? theme.fg(\"dim\", truncateToWidth(pwd, locationWidth, \"\u2026\")) + (access ? separator + access : \"\")\n : truncateToWidth(access, width, \"\u2026\");\n const stats = [safeText(target.status)];\n if (target.usage) {\n const usage = target.usage;\n // Agent usage is run-scoped; actor values describe the current/last activation.\n stats.push(\"run\", `\u2191${formatTokens(usage.input)}`, `\u2193${formatTokens(usage.output)}`);\n if (usage.cacheRead > 0) stats.push(`R${formatTokens(usage.cacheRead)}`);\n if (usage.cacheWrite > 0) stats.push(`W${formatTokens(usage.cacheWrite)}`);\n if (Number.isFinite(usage.cost)) stats.push(`$${usage.cost.toFixed(3)}`);\n } else {\n stats.push(\"usage unavailable\");\n }\n // Cumulative run tokens are not current context occupancy. Do not invent a percentage.\n stats.push(target.contextWindow ? `?/${formatTokens(target.contextWindow)} ctx` : \"ctx ?\");\n const model = safeText(target.model ?? `${target.runner ?? \"\"} model unavailable`).trim();\n const thinking = target.thinking ? ` \u2022 ${safeText(target.thinking === \"off\" ? \"thinking off\" : target.thinking)}` : \"\";\n const left = truncateToWidth(stats.join(\" \"), width, \"\u2026\");\n const right = truncateToWidth(`${model}${thinking}`, Math.max(0, width - visibleWidth(left) - 2), \"\");\n const gap = \" \".repeat(Math.max(0, width - visibleWidth(left) - visibleWidth(right)));\n return [\n pathLine,\n theme.fg(\"dim\", `${left}${gap}${right}`),\n ];\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/dist/index.js CHANGED
@@ -721,7 +721,7 @@ var autoArmFabricPrewalk = async (state, context, pi) => {
721
721
 
722
722
  // src/ui/conversation-shortcut.ts
723
723
  var FABRIC_CONVERSATION_SHORTCUT = "ctrl+shift+a";
724
- var FABRIC_CONVERSATION_HINT = "Ctrl+Shift+A chat";
724
+ var FABRIC_CONVERSATION_HINT = "ctrl+shift+a chat";
725
725
 
726
726
  // src/commands/fabric.ts
727
727
  import fs from "node:fs";
@@ -3930,6 +3930,7 @@ var piHostCompatibilityWarning = (version = detectPiHostVersion()) => {
3930
3930
  import path8 from "node:path";
3931
3931
 
3932
3932
  // src/ui/snapshot.ts
3933
+ import { isDeepStrictEqual } from "node:util";
3933
3934
  var MAX_UI_AGENTS = 240;
3934
3935
  var boundedUiAgents = (local, remote) => {
3935
3936
  const selected = /* @__PURE__ */ new Map();
@@ -3969,9 +3970,46 @@ var stateEntry = (entry) => {
3969
3970
  ...detail ? { detail: detail.slice(0, 500) } : {}
3970
3971
  };
3971
3972
  };
3972
- var createDashboardSnapshot = (state, events, context, activityRuns) => {
3973
+ var FabricDashboardSnapshotCache = class {
3974
+ inputs;
3975
+ snapshot;
3976
+ get(inputs) {
3977
+ return this.snapshot && isDeepStrictEqual(this.inputs, inputs) ? { ...this.snapshot, now: Date.now() } : void 0;
3978
+ }
3979
+ set(inputs, snapshot) {
3980
+ this.inputs = structuredClone(inputs);
3981
+ this.snapshot = snapshot;
3982
+ }
3983
+ clear() {
3984
+ this.inputs = void 0;
3985
+ this.snapshot = void 0;
3986
+ }
3987
+ };
3988
+ var createDashboardSnapshot = (state, events, context, activityRuns, cache) => {
3973
3989
  const runs = activityRuns ?? state.activity.runs();
3974
3990
  const agentRecords = typeof state.agents.listForUi === "function" ? state.agents.listForUi() : state.agents.list();
3991
+ const participants = typeof state.participantInfos === "function" ? state.participantInfos({ scope: "project" }) : [];
3992
+ const actorRecords = state.actors.list();
3993
+ const main = state.mainAgentInfo(context);
3994
+ const peers = typeof state.peerInfos === "function" ? state.peerInfos() : [];
3995
+ const globalActors = state.globalActors.list();
3996
+ const componentGraph = typeof state.componentGraph === "function" ? state.componentGraph() : { components: [], edges: [], cycles: [] };
3997
+ const meshEntries = state.config.mesh.enabled ? state.mesh.list("", 200) : [];
3998
+ const inputs = {
3999
+ runs,
4000
+ agentRecords,
4001
+ actorRecords,
4002
+ participants,
4003
+ main,
4004
+ peers,
4005
+ globalActors,
4006
+ componentGraph,
4007
+ meshEntries,
4008
+ events,
4009
+ widgetDismissedAt: state.widgetDismissedAt
4010
+ };
4011
+ const previous = cache?.get(inputs);
4012
+ if (previous) return previous;
3975
4013
  const agentLinks = [];
3976
4014
  for (const run of runs) {
3977
4015
  for (const call of run.calls) {
@@ -4035,15 +4073,18 @@ var createDashboardSnapshot = (state, events, context, activityRuns) => {
4035
4073
  }
4036
4074
  };
4037
4075
  for (const record of agentRecords) appendAgent(record, 0);
4038
- const participants = typeof state.participantInfos === "function" ? state.participantInfos({ scope: "project" }) : [];
4076
+ const workerByActor = /* @__PURE__ */ new Map();
4077
+ for (const agent of allAgents) {
4078
+ if (!agent.actorId) continue;
4079
+ const previous2 = workerByActor.get(agent.actorId);
4080
+ const active = Number(activeStatuses.has(agent.status)) - Number(previous2 !== void 0 && activeStatuses.has(previous2.status));
4081
+ const recency = (numberFrom(agent.updatedAt) ?? numberFrom(agent.startedAt) ?? 0) - (numberFrom(previous2?.updatedAt) ?? numberFrom(previous2?.startedAt) ?? 0);
4082
+ if (!previous2 || (active || recency) > 0) workerByActor.set(agent.actorId, agent);
4083
+ }
4039
4084
  const participantById = new Map(participants.map((participant) => [participant.id, participant]));
4040
- const actors = state.actors.list().map((actor) => {
4085
+ const actors = actorRecords.map((actor) => {
4041
4086
  const participant = participantById.get(actor.id);
4042
- const worker = allAgents.filter((agent) => agent.actorId === actor.id).sort((left, right) => {
4043
- const active = Number(activeStatuses.has(right.status)) - Number(activeStatuses.has(left.status));
4044
- const recency = (numberFrom(right.updatedAt) ?? numberFrom(right.startedAt) ?? 0) - (numberFrom(left.updatedAt) ?? numberFrom(left.startedAt) ?? 0);
4045
- return active || recency;
4046
- })[0];
4087
+ const worker = workerByActor.get(actor.id);
4047
4088
  return {
4048
4089
  ...actor,
4049
4090
  instructions: state.actors.instructions(actor.id),
@@ -4102,7 +4143,6 @@ var createDashboardSnapshot = (state, events, context, activityRuns) => {
4102
4143
  const rightActive = activeRunIds.has(right.run.id) ? 1 : 0;
4103
4144
  return rightActive - leftActive || left.index - right.index;
4104
4145
  }).map(({ run }) => run);
4105
- const meshEntries = state.config.mesh.enabled ? state.mesh.list("", 200) : [];
4106
4146
  const stateEntries = meshEntries.filter(
4107
4147
  (entry) => !entry.key.startsWith("actors/") && !entry.key.startsWith("sessions/") && !entry.key.startsWith("topology/")
4108
4148
  ).map(stateEntry).sort((left, right) => {
@@ -4110,16 +4150,16 @@ var createDashboardSnapshot = (state, events, context, activityRuns) => {
4110
4150
  const rightActive = activeStatuses.has(right.status) ? 1 : 0;
4111
4151
  return rightActive - leftActive || right.updatedAt - left.updatedAt;
4112
4152
  }).slice(0, 120);
4113
- return {
4153
+ const snapshot = {
4114
4154
  now: Date.now(),
4115
4155
  runs: orderedRuns,
4116
- main: state.mainAgentInfo(context),
4117
- peers: typeof state.peerInfos === "function" ? state.peerInfos() : [],
4156
+ main,
4157
+ peers,
4118
4158
  participants,
4119
4159
  widgetDismissedAt: state.widgetDismissedAt,
4120
- globalActors: state.globalActors.list(),
4160
+ globalActors,
4121
4161
  agents: visibleAgents,
4122
- componentGraph: typeof state.componentGraph === "function" ? state.componentGraph() : { components: [], edges: [], cycles: [] },
4162
+ componentGraph,
4123
4163
  actors: actors.sort((left, right) => {
4124
4164
  const leftActive = activeStatuses.has(left.status) ? 1 : 0;
4125
4165
  const rightActive = activeStatuses.has(right.status) ? 1 : 0;
@@ -4128,6 +4168,8 @@ var createDashboardSnapshot = (state, events, context, activityRuns) => {
4128
4168
  state: stateEntries,
4129
4169
  events: events.map((event) => structuredClone(event))
4130
4170
  };
4171
+ cache?.set(inputs, snapshot);
4172
+ return snapshot;
4131
4173
  };
4132
4174
 
4133
4175
  // src/ui/widget.ts
@@ -4361,6 +4403,9 @@ var FabricUiController = class {
4361
4403
  #activityRuns = [];
4362
4404
  #transcripts = new AgentTranscriptReader();
4363
4405
  #conversationReaders = /* @__PURE__ */ new Map();
4406
+ #activeConversationReader;
4407
+ #snapshotCache = new FabricDashboardSnapshotCache();
4408
+ #refreshGeneration = 0;
4364
4409
  start(context) {
4365
4410
  this.stop();
4366
4411
  this.#context = context;
@@ -4415,6 +4460,8 @@ var FabricUiController = class {
4415
4460
  this.#transcripts.clear();
4416
4461
  for (const reader of this.#conversationReaders.values()) reader.clear();
4417
4462
  this.#conversationReaders.clear();
4463
+ this.#activeConversationReader = void 0;
4464
+ this.#snapshotCache.clear();
4418
4465
  }
4419
4466
  /** True while Fabric owns keyboard input, including asynchronous view setup. */
4420
4467
  get ownsInput() {
@@ -4450,13 +4497,64 @@ var FabricUiController = class {
4450
4497
  }
4451
4498
  if (initialTarget?.kind === "main") return;
4452
4499
  this.#conversationState ??= new FabricConversationState();
4500
+ let sourceAgents;
4501
+ let sourceActors;
4502
+ let agentsById = /* @__PURE__ */ new Map();
4503
+ let actorsById = /* @__PURE__ */ new Map();
4504
+ let targetDomains = [];
4505
+ let baseTargets = [];
4506
+ let enrichedTargets = [];
4507
+ let targetsById = /* @__PURE__ */ new Map();
4508
+ let modelGeneration = -1;
4509
+ let modelWindows = /* @__PURE__ */ new Map();
4510
+ const targets = () => {
4511
+ const snapshot = this.#snapshot;
4512
+ const domains = [snapshot.main, snapshot.agents, snapshot.actors, snapshot.peers, snapshot.participants];
4513
+ const changed = domains.some((domain, index) => domain !== targetDomains[index]);
4514
+ if (changed) {
4515
+ targetDomains = domains;
4516
+ baseTargets = conversationTargets(snapshot);
4517
+ }
4518
+ if (changed || modelGeneration !== this.#refreshGeneration) {
4519
+ modelGeneration = this.#refreshGeneration;
4520
+ const windows = /* @__PURE__ */ new Map();
4521
+ for (const target of baseTargets) {
4522
+ if (!target.model || windows.has(target.model)) continue;
4523
+ const split = target.model.indexOf("/");
4524
+ if (split < 1) continue;
4525
+ windows.set(target.model, context.modelRegistry?.find?.(
4526
+ target.model.slice(0, split),
4527
+ target.model.slice(split + 1)
4528
+ )?.contextWindow);
4529
+ }
4530
+ if (changed || windows.size !== modelWindows.size || [...windows].some(([model, window]) => modelWindows.get(model) !== window)) {
4531
+ enrichedTargets = baseTargets.map((target) => {
4532
+ const contextWindow = target.model ? windows.get(target.model) : void 0;
4533
+ return contextWindow !== void 0 ? { ...target, contextWindow } : target;
4534
+ });
4535
+ targetsById = new Map(enrichedTargets.map((target) => [target.id, target]));
4536
+ }
4537
+ modelWindows = windows;
4538
+ }
4539
+ return enrichedTargets;
4540
+ };
4453
4541
  const source = (id) => {
4454
- const actor = this.#snapshot.actors.find((candidate) => candidate.id === id);
4542
+ if (sourceActors !== this.#snapshot.actors) {
4543
+ sourceActors = this.#snapshot.actors;
4544
+ actorsById = new Map(sourceActors.map((actor2) => [actor2.id, actor2]));
4545
+ }
4546
+ if (sourceAgents !== this.#snapshot.agents) {
4547
+ sourceAgents = this.#snapshot.agents;
4548
+ agentsById = new Map(sourceAgents.map((agent2) => [agent2.id, agent2]));
4549
+ }
4550
+ const actor = actorsById.get(id);
4455
4551
  if (actor) return { ...this.#actorTranscriptSource(actor), id, ...actor.sessionFile ? { sessionFile: actor.sessionFile } : {} };
4456
- const agent = this.#snapshot.agents.find((candidate) => candidate.id === id);
4552
+ const agent = agentsById.get(id);
4457
4553
  return agent ? this.#agentTranscriptSource(agent) : { id, status: "unavailable" };
4458
4554
  };
4459
4555
  const readerFor = (id) => {
4556
+ if (this.#activeConversationReader !== id) this.#suspendConversationReader();
4557
+ this.#activeConversationReader = id;
4460
4558
  let reader = this.#conversationReaders.get(id);
4461
4559
  if (!reader) {
4462
4560
  reader = new NativeConversationReader();
@@ -4469,7 +4567,8 @@ var FabricUiController = class {
4469
4567
  throw new Error("This Fabric conversation is no longer attached");
4470
4568
  }
4471
4569
  this.#refresh();
4472
- const target = conversationTargets(this.#snapshot).find((candidate) => candidate.id === id);
4570
+ targets();
4571
+ const target = targetsById.get(id);
4473
4572
  const allowed = action === "stop" ? target?.canStop : action === "steer" ? target?.canSteer : target?.canFollowUp;
4474
4573
  if (!target || target.kind === "main" || !allowed) {
4475
4574
  throw new Error(target?.readOnlyReason ?? `Participant ${id} cannot receive ${action}`);
@@ -4485,12 +4584,10 @@ var FabricUiController = class {
4485
4584
  this.#conversationTui = tui;
4486
4585
  this.#closeConversation = () => done(void 0);
4487
4586
  const view = new FabricConversationView(tui, theme, {
4488
- targets: () => conversationTargets(this.#snapshot).map((target) => {
4489
- const split = target.model?.indexOf("/") ?? -1;
4490
- if (split < 1 || !target.model) return target;
4491
- const model = context.modelRegistry?.find?.(target.model.slice(0, split), target.model.slice(split + 1));
4492
- return model ? { ...target, contextWindow: model.contextWindow } : target;
4493
- }),
4587
+ targets,
4588
+ onTargetChange: (id) => {
4589
+ if (this.#activeConversationReader !== id) this.#suspendConversationReader();
4590
+ },
4494
4591
  appearance: readConversationAppearance(context.cwd, resolveAgentDir(), context.isProjectTrusted?.() ?? false),
4495
4592
  ...initialTarget ? { initialTargetId: initialTarget.id } : {},
4496
4593
  state: this.#conversationState,
@@ -4543,6 +4640,7 @@ var FabricUiController = class {
4543
4640
  } finally {
4544
4641
  if (!alreadyOpen && epoch === this.#epoch) {
4545
4642
  this.#conversationView?.dispose();
4643
+ this.#suspendConversationReader();
4546
4644
  this.#conversationView = void 0;
4547
4645
  this.#conversationTui = void 0;
4548
4646
  this.#closeConversation = void 0;
@@ -4751,7 +4849,7 @@ var FabricUiController = class {
4751
4849
  if (!this.ownsInput && !active) return;
4752
4850
  this.#timer = setTimeout(() => {
4753
4851
  this.#timer = void 0;
4754
- this.#refresh();
4852
+ this.#refresh(false);
4755
4853
  this.#schedulePoll();
4756
4854
  }, this.state.config.ui.refreshMs);
4757
4855
  this.#timer.unref();
@@ -4791,7 +4889,13 @@ var FabricUiController = class {
4791
4889
  #transcriptSource(target) {
4792
4890
  return "recentMessages" in target ? this.#actorTranscriptSource(target) : this.#agentTranscriptSource(target);
4793
4891
  }
4794
- #refresh() {
4892
+ #suspendConversationReader() {
4893
+ if (!this.#activeConversationReader) return;
4894
+ const reader = this.#conversationReaders.get(this.#activeConversationReader);
4895
+ reader?.suspend?.();
4896
+ this.#activeConversationReader = void 0;
4897
+ }
4898
+ #refresh(force = true) {
4795
4899
  this.#lastRefreshAt = performance.now();
4796
4900
  const context = this.#context;
4797
4901
  if (!context || !this.state.initialized) return;
@@ -4804,14 +4908,19 @@ var FabricUiController = class {
4804
4908
  this.#activityRevision = revision;
4805
4909
  this.#activityRunsDetailed = detailed;
4806
4910
  }
4911
+ if (force || this.#dashboardOpen) this.#snapshotCache.clear();
4912
+ this.#refreshGeneration++;
4807
4913
  this.#snapshot = createDashboardSnapshot(
4808
4914
  this.state,
4809
4915
  this.#events,
4810
4916
  context,
4811
- this.#activityRuns
4917
+ this.#activityRuns,
4918
+ this.#dashboardOpen ? void 0 : this.#snapshotCache
4812
4919
  );
4813
4920
  this.#renderWidget(context);
4814
- if (this.#conversationTui) this.#conversationTui.requestRender();
4921
+ if (this.#conversationTui && this.#conversationView?.refresh?.() !== false) {
4922
+ this.#conversationTui.requestRender();
4923
+ }
4815
4924
  if (this.#dashboardTui) this.#dashboardTui.requestRender();
4816
4925
  else if (this.#widgetTui && this.#widget?.hasChanged()) this.#widgetTui.requestRender();
4817
4926
  } catch (error) {