@xmoon76/dsh-pi-tui 0.4.5 → 0.4.6

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.en.md CHANGED
@@ -54,7 +54,7 @@ next compatibility range, and fallback paths.
54
54
 
55
55
  | TUI package line | Official DSH tags for the pairing | Notes |
56
56
  |---|---|---|
57
- | `0.4.5` (published `@latest`) | `dsh-v0.1.5-rc.1`, `dsh-v0.1.5-rc.2` | Current stable line; rc.1 minimum, rc.2 compatible |
57
+ | `0.4.6` (published `@latest`) | `dsh-v0.1.5-rc.1`, `dsh-v0.1.5-rc.2` | Current stable line; rc.1 minimum, rc.2 compatible |
58
58
  | Current `next` npm line (this checkout; version `0.4.5`) | `dsh-v0.1.5-rc.1`, `dsh-v0.1.5-rc.2` | Current next line |
59
59
 
60
60
  Do not mix the stable and `next` lines. The current `next` checkout declares
package/README.md CHANGED
@@ -52,7 +52,7 @@ pnpm compat:dsh:npm
52
52
 
53
53
  | TUI 包版本 | 对应的官方 DSH tags | 说明 |
54
54
  |---|---|---|
55
- | `0.4.5`(已发布 `@latest`) | `dsh-v0.1.5-rc.1`、`dsh-v0.1.5-rc.2` | 当前稳定版;最低 rc.1,兼容 rc.2 |
55
+ | `0.4.6`(已发布 `@latest`) | `dsh-v0.1.5-rc.1`、`dsh-v0.1.5-rc.2` | 当前稳定版;最低 rc.1,兼容 rc.2 |
56
56
  | 当前 `next` npm 线(本 checkout;版本 `0.4.5`) | `dsh-v0.1.5-rc.1`、`dsh-v0.1.5-rc.2` | 当前 next 线 |
57
57
 
58
58
  不要把稳定线与 `next` 线混装。当前 `next` checkout 的 peer floor 是
package/dist/builtins.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { W as PI_TUI_EXTENSIONS_SERVICE, z as dshVersion } from "./src-B65x1oP2.mjs";
1
+ import { W as PI_TUI_EXTENSIONS_SERVICE, z as dshVersion } from "./src-DNscMTdx.mjs";
2
2
  import { TUI_STARTUP_SERVICE } from "./startup.mjs";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
@@ -1,3 +1,3 @@
1
- import { G as API_VERSION, H as inject, K as describeKey, U as name, V as apply, W as PI_TUI_EXTENSIONS_SERVICE } from "./src-B65x1oP2.mjs";
1
+ import { G as API_VERSION, H as inject, K as describeKey, U as name, V as apply, W as PI_TUI_EXTENSIONS_SERVICE } from "./src-DNscMTdx.mjs";
2
2
  import "./startup.mjs";
3
3
  export { API_VERSION, PI_TUI_EXTENSIONS_SERVICE, apply, describeKey, inject, name };
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { A as resumeCommand, B as forkSeed, C as matchPendingSubagentCall, D as recordedPreset, E as recomposeBlank, F as subagentJobViewHint, I as subagentNoticeIsFailure, L as teardownViewerForSessionSwap, M as settleCompactionSurface, N as shouldConsumeAdvertisedMiss, O as resolveInitialCatalog, P as subagentJobTranscriptId, R as viewerActionCapability, S as isUserQueueInput, T as normalizeSkillInvocation, _ as interruptAgent, a as apply, b as isPlainExitPrompt, c as commandRejectsImages, d as contextRefreshKind, f as createViewerOpenToken, g as inject, h as foldQueueRows, i as SESSIONLESS_COMMANDS, j as runningProfile, k as resolveSubmitDelivery, l as compactingFromLog, m as foldCompactionEvent, n as HOST_COMMAND_CATALOG, o as busyAfterTurnBoundary, p as dangerCommand, r as LOCAL_COMMANDS, s as commandIsLocalForAttachments, t as Config, u as composeAgent, v as isBareCommandLine, w as name, x as isSubagentSettlementNotice, y as isLocalCommandLine } from "./src-B65x1oP2.mjs";
1
+ import { A as resumeCommand, B as forkSeed, C as matchPendingSubagentCall, D as recordedPreset, E as recomposeBlank, F as subagentJobViewHint, I as subagentNoticeIsFailure, L as teardownViewerForSessionSwap, M as settleCompactionSurface, N as shouldConsumeAdvertisedMiss, O as resolveInitialCatalog, P as subagentJobTranscriptId, R as viewerActionCapability, S as isUserQueueInput, T as normalizeSkillInvocation, _ as interruptAgent, a as apply, b as isPlainExitPrompt, c as commandRejectsImages, d as contextRefreshKind, f as createViewerOpenToken, g as inject, h as foldQueueRows, i as SESSIONLESS_COMMANDS, j as runningProfile, k as resolveSubmitDelivery, l as compactingFromLog, m as foldCompactionEvent, n as HOST_COMMAND_CATALOG, o as busyAfterTurnBoundary, p as dangerCommand, r as LOCAL_COMMANDS, s as commandIsLocalForAttachments, t as Config, u as composeAgent, v as isBareCommandLine, w as name, x as isSubagentSettlementNotice, y as isLocalCommandLine } from "./src-DNscMTdx.mjs";
2
2
  import "./startup.mjs";
3
3
  export { Config, HOST_COMMAND_CATALOG, LOCAL_COMMANDS, SESSIONLESS_COMMANDS, apply, busyAfterTurnBoundary, commandIsLocalForAttachments, commandRejectsImages, compactingFromLog, composeAgent, contextRefreshKind, createViewerOpenToken, dangerCommand, foldCompactionEvent, foldQueueRows, forkSeed, inject, interruptAgent, isBareCommandLine, isLocalCommandLine, isPlainExitPrompt, isSubagentSettlementNotice, isUserQueueInput, matchPendingSubagentCall, name, normalizeSkillInvocation, recomposeBlank, recordedPreset, resolveInitialCatalog, resolveSubmitDelivery, resumeCommand, runningProfile, settleCompactionSurface, shouldConsumeAdvertisedMiss, subagentJobTranscriptId, subagentJobViewHint, subagentNoticeIsFailure, teardownViewerForSessionSwap, viewerActionCapability };
@@ -813,6 +813,25 @@ function listAgentsCallPresentation(argsRaw) {
813
813
  if (scope === void 0) return { scope: "children" };
814
814
  return scope === "children" || scope === "descendants" ? { scope } : void 0;
815
815
  }
816
+ /** Parse the display-only file declarations from a `present` call. */
817
+ function presentCallPresentation(argsRaw) {
818
+ const parsed = parseArgs(argsRaw);
819
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
820
+ const files = parsed.files;
821
+ if (!Array.isArray(files)) return void 0;
822
+ const presented = [];
823
+ for (const file of files) {
824
+ if (typeof file !== "object" || file === null || Array.isArray(file)) return void 0;
825
+ const value = file;
826
+ if (typeof value.path !== "string" || value.path.trim() === "") return void 0;
827
+ if (value.description !== void 0 && typeof value.description !== "string") return void 0;
828
+ presented.push({
829
+ path: value.path,
830
+ ...value.description === void 0 ? {} : { description: value.description }
831
+ });
832
+ }
833
+ return { files: presented };
834
+ }
816
835
  /** Count one structured list_agents snapshot. */
817
836
  function summarizeAgentEntries(entries) {
818
837
  let running = 0;
@@ -998,6 +1017,16 @@ function actionResultSummary(kind, text) {
998
1017
  * the TUI, while replay remains deterministic and registry-independent.
999
1018
  */
1000
1019
  function compactToolPresentation(name, argsRaw, result = "", options = {}) {
1020
+ if (name === "present") {
1021
+ const call = presentCallPresentation(argsRaw);
1022
+ const resultText = options.isError === true || options.error !== void 0 ? actionErrorResult(result, options) : void 0;
1023
+ const count = call === void 0 ? void 0 : `${call.files.length} ${call.files.length === 1 ? "file" : "files"}`;
1024
+ return {
1025
+ title: "Present files",
1026
+ ...count === void 0 ? {} : { summary: count },
1027
+ ...resultText === void 0 ? {} : { result: resultText }
1028
+ };
1029
+ }
1001
1030
  const send = name === "send_message" ? sendMessageCallPresentation(argsRaw) : void 0;
1002
1031
  if (send !== void 0) {
1003
1032
  const resultText = options.isError === true || options.error !== void 0 ? actionErrorResult(result, options) : actionResultSummary("send_message", result);
@@ -1048,6 +1077,11 @@ function isCompactActionTool(name, argsRaw) {
1048
1077
  function compactToolExpandedLines(name, argsRaw, result = "", options = {}) {
1049
1078
  const presentation = compactToolPresentation(name, argsRaw, result, options);
1050
1079
  if (presentation === void 0) return void 0;
1080
+ if (name === "present") {
1081
+ const lines = presentCallPresentation(argsRaw)?.files.map((file) => file.description === void 0 || file.description === "" ? file.path : `${file.path} — ${file.description}`) ?? [];
1082
+ if (presentation.result !== void 0) lines.push(presentation.result);
1083
+ return lines;
1084
+ }
1051
1085
  if (name === "list_agents" && options.isError !== true && options.error === void 0) {
1052
1086
  const parsed = parseJsonValue(result);
1053
1087
  if (Array.isArray(parsed)) return JSON.stringify(parsed, null, 2).split("\n");
@@ -2668,6 +2702,7 @@ function transcriptSearchText(message, depth = 0) {
2668
2702
  }
2669
2703
  return `workflow ${message.name} ${message.status} ${[...phases].join(" ")} ${members.join(" ")}`;
2670
2704
  }
2705
+ if (message.kind === "assistant" && message.deliverables !== void 0 && message.deliverables.length > 0) return `${message.text} ${message.deliverables.map((file) => [file.path, file.description ?? ""].join(" ")).join(" ")}`;
2671
2706
  return message.text ?? "";
2672
2707
  }
2673
2708
  /** Normalize search text exactly like the legacy query path did (JS String
@@ -2975,6 +3010,7 @@ function assistantDisplayBlocksHaveInterruptionEvidence(blocks) {
2975
3010
  }
2976
3011
  /** Use the semantic or display-only projection for entry visibility. */
2977
3012
  function assistantEntryVisibleNow(entry) {
3013
+ if (entry.deliverables !== void 0 && entry.deliverables.length > 0) return true;
2978
3014
  return entry.displayBlocks === void 0 ? assistantBlocksVisibleNow(assistantEntryBlocks(entry)) : assistantDisplayBlocksVisibleNow(entry.displayBlocks);
2979
3015
  }
2980
3016
  /** Use the semantic or display-only projection for attempt evidence. */
@@ -3068,6 +3104,11 @@ var TranscriptFolder = class TranscriptFolder {
3068
3104
  claimedNextStepTurns = /* @__PURE__ */ new Map();
3069
3105
  /** The assistant message object per (turn, step); streaming text lands in place. */
3070
3106
  assistantEntries = /* @__PURE__ */ new Map();
3107
+ /** Durable delivery declarations retained with their event sequence until
3108
+ * the turn's closing assistant selects the valid prefix. */
3109
+ deliverableDeclarationsByTurn = /* @__PURE__ */ new Map();
3110
+ /** Durable assistant/message sequence per step, used as the closing boundary. */
3111
+ assistantSettlementSeqs = /* @__PURE__ */ new Map();
3071
3112
  /** In-flight live block state keyed by logical step. This is required for
3072
3113
  * authoritative block-end replacement: deltas may be partial, while a
3073
3114
  * completed block replaces the entire indexed state without duplication. */
@@ -4807,12 +4848,72 @@ var TranscriptFolder = class TranscriptFolder {
4807
4848
  if (compactionId !== void 0) this.compacting.delete(compactionId);
4808
4849
  }
4809
4850
  }
4851
+ /** Fold the structural durable payload emitted by the present tool. The
4852
+ * event is the only source of delivery facts; tool-result text is never
4853
+ * reverse-parsed. Invalid file entries are ignored at this event boundary,
4854
+ * while valid entries retain their event sequence and source order. */
4855
+ applyPresentedEvent(event) {
4856
+ const data = event.data;
4857
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return;
4858
+ const value = data;
4859
+ if (typeof value.turn !== "number" || !Number.isSafeInteger(value.turn) || value.turn < 1) return;
4860
+ if (typeof value.callId !== "string" || value.callId.length === 0 || !Array.isArray(value.files)) return;
4861
+ if (this.activityByTurn.get(value.turn)?.completed === true) return;
4862
+ const declarations = this.deliverableDeclarationsByTurn.get(value.turn) ?? [];
4863
+ const seq = Number(event.seq);
4864
+ if (!Number.isSafeInteger(seq) || seq < 0) return;
4865
+ for (const file of value.files) {
4866
+ if (typeof file !== "object" || file === null || Array.isArray(file)) continue;
4867
+ const entry = file;
4868
+ if (typeof entry.path !== "string" || entry.path.trim() === "") continue;
4869
+ if (entry.description !== void 0 && typeof entry.description !== "string") continue;
4870
+ declarations.push({
4871
+ path: entry.path,
4872
+ ...entry.description === void 0 ? {} : { description: entry.description },
4873
+ seq
4874
+ });
4875
+ }
4876
+ if (declarations.length > 0) this.deliverableDeclarationsByTurn.set(value.turn, declarations);
4877
+ }
4878
+ /** Attach declarations before the closing assistant's durable sequence,
4879
+ * preserving first-seen path order while letting the latest valid
4880
+ * declaration replace its description. */
4881
+ attachDeliverablesToClosingAssistant(turn) {
4882
+ const activity = this.activityByTurn.get(turn);
4883
+ const step = activity?.lastAssistantStep;
4884
+ if (step === void 0) return;
4885
+ const key = stepKey$1(turn, step);
4886
+ const assistant = this.assistantEntries.get(key);
4887
+ const closingSeq = this.assistantSettlementSeqs.get(key);
4888
+ if (assistant === void 0 || closingSeq === void 0) return;
4889
+ const declarations = this.deliverableDeclarationsByTurn.get(turn);
4890
+ if (declarations === void 0) return;
4891
+ const files = /* @__PURE__ */ new Map();
4892
+ for (const declaration of declarations) {
4893
+ if (declaration.seq >= closingSeq) continue;
4894
+ files.set(declaration.path, {
4895
+ path: declaration.path,
4896
+ ...declaration.description === void 0 ? {} : { description: declaration.description }
4897
+ });
4898
+ }
4899
+ if (files.size === 0) return;
4900
+ const wasVisible = this.isVisible(assistant);
4901
+ assistant.deliverables = [...files.values()];
4902
+ bumpAssistantPresentationRevision(assistant);
4903
+ this.markStreamingEntryDirty(`assistant:${key}`);
4904
+ this.syncAssistantVisibility(turn, step, assistant, wasVisible, false);
4905
+ if (activity !== void 0) activity.lastAssistantVisible = true;
4906
+ }
4810
4907
  applyEvent(event) {
4811
4908
  if (isReplacementSurfaceEvent(event)) {
4812
4909
  if (event.type === "user/message") this.claimedNextStepTurns.delete(event.data.id);
4813
4910
  return;
4814
4911
  }
4815
4912
  const kind = event.type;
4913
+ if (kind === "deliverables/presented") {
4914
+ this.applyPresentedEvent(event);
4915
+ return;
4916
+ }
4816
4917
  if (kind === "agent/inbox/spliced") {
4817
4918
  const data = event.data;
4818
4919
  const inserted = data.inserted.map((message) => ({
@@ -4945,6 +5046,7 @@ var TranscriptFolder = class TranscriptFolder {
4945
5046
  const activity = this.activityFor(event.data.turn);
4946
5047
  if (activity.completed) break;
4947
5048
  const key = stepKey$1(event.data.turn, event.data.step);
5049
+ this.assistantSettlementSeqs.set(key, Number(event.seq));
4948
5050
  const priorLast = activity.lastAssistantStep ?? -1;
4949
5051
  const entry = this.assistantEntries.get(key);
4950
5052
  this.liveAssistantBlocks.delete(key);
@@ -5173,6 +5275,8 @@ var TranscriptFolder = class TranscriptFolder {
5173
5275
  } }
5174
5276
  };
5175
5277
  this.resolveMessageAtTurnEnd(activity);
5278
+ this.attachDeliverablesToClosingAssistant(event.data.turn);
5279
+ this.deliverableDeclarationsByTurn.delete(event.data.turn);
5176
5280
  this.usage.onTurnEnd(event.data.turn);
5177
5281
  this.syncUsage(activity);
5178
5282
  activity.revision += 1;
@@ -34278,19 +34382,22 @@ function thoughtLeadBoundary(group) {
34278
34382
  if (firstInitialUserIndex >= 0) return firstInitialUserIndex + 1;
34279
34383
  return leadingInjectedContextPrefixEnd(group);
34280
34384
  }
34281
- /** Whether one Assistant entry has semantic/finalized content. Pending
34282
- * display-only open-opaque rows remain transcript evidence but cannot become a
34283
- * completed/max-token final; finalized generic blocks stay eligible. */
34385
+ /** Whether one Assistant entry has semantic/finalized content or an explicit
34386
+ * delivered-file tail. Pending display-only open-opaque rows remain transcript
34387
+ * evidence but cannot become a completed/max-token final; finalized generic
34388
+ * blocks stay eligible. */
34284
34389
  function assistantRenderable(assistant) {
34285
34390
  if (assistant.displayBlocks?.some((block) => block.kind === "open-opaque") === true) return false;
34391
+ if (assistant.deliverables !== void 0 && assistant.deliverables.length > 0) return true;
34286
34392
  if (assistant.content !== void 0) return assistantBlocksVisibleNow(assistant.content);
34287
34393
  return assistant.text.trim() !== "";
34288
34394
  }
34289
34395
  /** The turn's final assistant selection: only after the authoritative
34290
34396
  * turn/end, only for a reason the system presents output (completed /
34291
34397
  * max-tokens), and only when the Assistant owning the structural latest step
34292
- * has semantic/finalized content. An empty or pending latest step yields NO
34293
- * final — never an earlier assistant (review fix). The max-tokens final carries
34398
+ * has semantic/finalized content or explicit delivered files. An empty or
34399
+ * pending latest step without either yields NO final — never an earlier assistant
34400
+ * (review fix). The max-tokens final carries
34294
34401
  * the truncated marker (plan §13.8). */
34295
34402
  function finalAssistantSelection(activity, group) {
34296
34403
  if (activity === void 0 || !activity.completed) return void 0;
@@ -39692,6 +39799,25 @@ var TranscriptGutterComponent = class {
39692
39799
  }
39693
39800
  };
39694
39801
  /**
39802
+ * Layout-only rows that keep a running Focus turn's live height from
39803
+ * shrinking after a transient presentation reflow. It owns no semantic row;
39804
+ * its height is supplied by TuiApp's current render measurement and it is
39805
+ * hidden outside the fullscreen Focus presentation epoch.
39806
+ */
39807
+ var FocusLivePaddingComponent = class {
39808
+ rows;
39809
+ enabled;
39810
+ constructor(rows, enabled) {
39811
+ this.rows = rows;
39812
+ this.enabled = enabled;
39813
+ }
39814
+ invalidate() {}
39815
+ render(_width) {
39816
+ if (!this.enabled()) return [];
39817
+ return Array.from({ length: Math.max(0, Math.floor(this.rows())) }, () => "");
39818
+ }
39819
+ };
39820
+ /**
39695
39821
  * Bullet + continuation-indent wrapper that keeps its child LIVE, so a
39696
39822
  * terminal resize re-renders the child at the new width instead of
39697
39823
  * re-wrapping a frozen render (the 5a76526 regression: assistant/user
@@ -39844,6 +39970,41 @@ var UserBubbleComponent = class {
39844
39970
  return this.cached;
39845
39971
  }
39846
39972
  };
39973
+ /** Host-owned tail for explicit files delivered by the present tool. Paths
39974
+ * are always shown first; the folded view caps entries while an expanded
39975
+ * transcript view re-renders the complete declaration list. */
39976
+ var DeliveredFilesComponent = class DeliveredFilesComponent {
39977
+ static FOLDED_LIMIT = 4;
39978
+ files;
39979
+ workspaceRoot;
39980
+ expanded;
39981
+ cached = /* @__PURE__ */ new Map();
39982
+ constructor(files, workspaceRoot, expanded) {
39983
+ this.files = files;
39984
+ this.workspaceRoot = workspaceRoot;
39985
+ this.expanded = expanded;
39986
+ }
39987
+ invalidate() {
39988
+ this.cached.clear();
39989
+ }
39990
+ render(width) {
39991
+ const safeWidth = Math.max(1, Math.floor(width));
39992
+ const previous = this.cached.get(safeWidth);
39993
+ if (previous !== void 0) return previous;
39994
+ const shown = this.expanded ? this.files : this.files.slice(0, DeliveredFilesComponent.FOLDED_LIMIT);
39995
+ const rows = [truncateToWidth(color.textDim(`Delivered files · ${this.files.length}`), safeWidth, "…")];
39996
+ for (const file of shown) {
39997
+ const path = relativizeToCwd(file.path, this.workspaceRoot).replace(/\r\n|\r|\n/g, " ");
39998
+ rows.push(truncateToWidth(color.textDim(` ${path}`), safeWidth, "…"));
39999
+ if (file.description === void 0 || file.description === "") continue;
40000
+ const descriptionWidth = Math.max(1, safeWidth - 4);
40001
+ for (const line of wrapTextWithAnsi(file.description, descriptionWidth)) rows.push(truncateToWidth(color.textDim(` ${line}`), safeWidth, "…"));
40002
+ }
40003
+ if (!this.expanded && this.files.length > shown.length) rows.push(truncateToWidth(color.textDim(` … +${this.files.length - shown.length}`), safeWidth, "…"));
40004
+ this.cached.set(safeWidth, rows);
40005
+ return rows;
40006
+ }
40007
+ };
39847
40008
  /**
39848
40009
  * Longest prefix of `text` whose WRAPPED height fits `budget` rows at
39849
40010
  * `width`, with an ellipsis marking a cut — the approval dialog's height
@@ -40301,6 +40462,8 @@ var TuiApp = class TuiApp {
40301
40462
  }
40302
40463
  /** Set the Ctrl+O expansion master switch and repaint. */
40303
40464
  setToolOutputExpanded(expanded) {
40465
+ if (this.toolOutputExpanded === expanded) return;
40466
+ this.clearFocusLiveHeightState();
40304
40467
  this.toolOutputExpanded = expanded;
40305
40468
  this.rebuildMessages();
40306
40469
  }
@@ -40610,6 +40773,16 @@ var TuiApp = class TuiApp {
40610
40773
  * rows carry the activity (the whole collapsed Thought block — and the
40611
40774
  * expanded header — is the toggle hit area, plan §17.1). */
40612
40775
  messageRows = [];
40776
+ /** High-water presentation heights for running Focus turns. This is
40777
+ * presentation-only state: it is never folded into TranscriptFolder or the
40778
+ * session log; compatible floors remain active during historical browsing
40779
+ * and release only at explicit lifecycle or structural boundaries. */
40780
+ focusLiveHeightStates = /* @__PURE__ */ new Map();
40781
+ /** The currently measured inert padding after a Focus turn's boundary
40782
+ * spacer. Padding components read this map at paint time, retaining the
40783
+ * compatible floor while historical and releasing it with the same explicit
40784
+ * lifecycle or structural boundaries. */
40785
+ focusLivePaddingRows = /* @__PURE__ */ new Map();
40613
40786
  /** The terminal geometry of the LAST PAINTED frame (fullscreen only):
40614
40787
  * a zero-row probe rides the fullscreen layout root, and the fork
40615
40788
  * renders every layout child on EVERY frame, so these fields record
@@ -41033,6 +41206,7 @@ var TuiApp = class TuiApp {
41033
41206
  this.cancelSaveLocationPrompt();
41034
41207
  for (const dispose of this.schemeDisposers) dispose();
41035
41208
  this.schemeDisposers = [];
41209
+ this.clearFocusLiveHeightState();
41036
41210
  this.tui.stop();
41037
41211
  this.fullscreen?.stop();
41038
41212
  this.fullscreen = void 0;
@@ -41886,6 +42060,7 @@ var TuiApp = class TuiApp {
41886
42060
  */
41887
42061
  pushLocalMessage(message) {
41888
42062
  this.localMessages.push(message);
42063
+ this.clearFocusLiveHeightState();
41889
42064
  this.rebuildMessages();
41890
42065
  return message;
41891
42066
  }
@@ -41910,6 +42085,7 @@ var TuiApp = class TuiApp {
41910
42085
  const token = this.identityTokens.get(message);
41911
42086
  if (token !== void 0) this.identityTokens.set(next, token);
41912
42087
  this.localMessages[index] = next;
42088
+ this.clearFocusLiveHeightState();
41913
42089
  this.rebuildMessages();
41914
42090
  return next;
41915
42091
  }
@@ -41920,12 +42096,14 @@ var TuiApp = class TuiApp {
41920
42096
  const token = this.identityTokens.get(this.localMessages[index]);
41921
42097
  if (token !== void 0) this.identityTokens.set(message, token);
41922
42098
  this.localMessages[index] = message;
42099
+ this.clearFocusLiveHeightState();
41923
42100
  this.rebuildMessages();
41924
42101
  }
41925
42102
  /** Drop all local cards (session switch). */
41926
42103
  clearLocalMessages() {
41927
42104
  if (this.localMessages.length === 0) return;
41928
42105
  this.localMessages.length = 0;
42106
+ this.clearFocusLiveHeightState();
41929
42107
  this.rebuildMessages();
41930
42108
  }
41931
42109
  /**
@@ -41940,6 +42118,7 @@ var TuiApp = class TuiApp {
41940
42118
  if (running.length === this.localMessages.length) return;
41941
42119
  this.localMessages.length = 0;
41942
42120
  this.localMessages.push(...running);
42121
+ this.clearFocusLiveHeightState();
41943
42122
  this.rebuildMessages();
41944
42123
  }
41945
42124
  /**
@@ -41984,6 +42163,7 @@ var TuiApp = class TuiApp {
41984
42163
  this.clearExitConfirmation();
41985
42164
  this.keybindings.cancelLeader();
41986
42165
  if (enabled === (this.fullscreen !== void 0)) return;
42166
+ this.clearFocusLiveHeightState();
41987
42167
  const pending = this.activeApproval;
41988
42168
  const history = this.historyPanel;
41989
42169
  pending?.handle?.hide();
@@ -42236,18 +42416,22 @@ var TuiApp = class TuiApp {
42236
42416
  * presentation owner; omitted by legacy callers and never persisted.
42237
42417
  */
42238
42418
  setTranscript(messages, activities, window, streamingToolPreviews) {
42419
+ const previousWindow = this.transcriptWindow;
42420
+ const windowChanged = previousWindow?.mode !== window?.mode || previousWindow?.endTurn !== window?.endTurn;
42421
+ if (activities !== void 0 && activities !== this.turnActivities || windowChanged) this.clearFocusLiveHeightState();
42239
42422
  this.messages = messages;
42240
42423
  if (activities !== void 0) this.turnActivities = activities;
42241
42424
  this.streamingToolPreviews = [...streamingToolPreviews ?? []];
42242
42425
  this.transcriptWindow = window;
42243
42426
  this.refreshTranscriptWindowHint();
42244
- this.updateWorkflowDisclosure(messages);
42427
+ if (this.updateWorkflowDisclosure(messages)) this.clearFocusLiveHeightState();
42245
42428
  this.rebuildMessages();
42246
42429
  }
42247
42430
  /** Replace ONLY the turn activities (the messages stay). The runner
42248
42431
  * prefers the combined {@link setTranscript} snapshot — this setter is
42249
42432
  * for callers that repaint messages separately. */
42250
42433
  setTurnActivities(activities) {
42434
+ this.clearFocusLiveHeightState();
42251
42435
  this.turnActivities = activities;
42252
42436
  this.rebuildMessages();
42253
42437
  }
@@ -42261,6 +42445,7 @@ var TuiApp = class TuiApp {
42261
42445
  * immediately; the expansion set is kept but not consulted (plan §16.4). */
42262
42446
  setFocusMode(enabled) {
42263
42447
  if (this.focusModeEnabled === enabled) return;
42448
+ this.clearFocusLiveHeightState();
42264
42449
  this.focusModeEnabled = enabled;
42265
42450
  this.projectStatus({ interaction: { focusMode: enabled } });
42266
42451
  this.rebuildMessages();
@@ -42275,6 +42460,7 @@ var TuiApp = class TuiApp {
42275
42460
  * value) — same policy as theme/focus. */
42276
42461
  setIconStyle(style) {
42277
42462
  if (this.iconStyle === style) return;
42463
+ this.clearFocusLiveHeightState();
42278
42464
  this.iconStyle = style;
42279
42465
  this.working.setIconStyleFrames(workingFramesFor(style));
42280
42466
  this.rebuildMessages();
@@ -42328,7 +42514,10 @@ var TuiApp = class TuiApp {
42328
42514
  revealSearchMatch(message) {
42329
42515
  const turn = "turn" in message ? message.turn : void 0;
42330
42516
  if (turn !== void 0 && this.focusModeEnabled) this.setFocusTurnExpanded(turn, true);
42331
- if (isFocusSecondaryDisclosure(message)) this.expandedOverride.set(message, true);
42517
+ if (isFocusSecondaryDisclosure(message)) {
42518
+ if (this.expandedOverride.get(message) !== true) this.clearFocusLiveHeightState();
42519
+ this.expandedOverride.set(message, true);
42520
+ }
42332
42521
  this.rebuildMessages();
42333
42522
  }
42334
42523
  /** Clear every secondary expansion of one turn (the root Collapse All
@@ -42564,6 +42753,7 @@ var TuiApp = class TuiApp {
42564
42753
  * (pushing the live set itself would hand the stack the object this
42565
42754
  * method then empties). */
42566
42755
  enterFocusViewerScope() {
42756
+ this.clearFocusLiveHeightState();
42567
42757
  this.focusExpansionsStack.push(new Set(this.focusExpandedTurns));
42568
42758
  this.focusExpandedTurns.clear();
42569
42759
  this.rebuildMessages();
@@ -42572,6 +42762,7 @@ var TuiApp = class TuiApp {
42572
42762
  exitFocusViewerScope() {
42573
42763
  const restored = this.focusExpansionsStack.pop();
42574
42764
  if (restored === void 0) return;
42765
+ this.clearFocusLiveHeightState();
42575
42766
  this.focusExpandedTurns.clear();
42576
42767
  for (const turn of restored) this.focusExpandedTurns.add(turn);
42577
42768
  this.rebuildMessages();
@@ -42584,6 +42775,7 @@ var TuiApp = class TuiApp {
42584
42775
  * teardown's intent explicit and stays correct even if that ordering
42585
42776
  * ever changes. */
42586
42777
  discardFocusViewerScope() {
42778
+ this.clearFocusLiveHeightState();
42587
42779
  this.focusExpansionsStack.pop();
42588
42780
  }
42589
42781
  /**
@@ -42776,17 +42968,11 @@ var TuiApp = class TuiApp {
42776
42968
  transcriptRenderWidth() {
42777
42969
  return transcriptContentWidth(this.terminal.columns);
42778
42970
  }
42779
- /** Rebuild the message component tree from the current transcript state. */
42780
- rebuildMessages() {
42781
- const projectionExpanded = this.focusProjectionExpandedTurns();
42782
- this.pruneMessageComponents(projectionExpanded);
42783
- this.messagesView.clear();
42784
- this.messagesView.addChild(this.welcomeCard);
42971
+ /** Render every projected transcript block once for this rebuild. The same
42972
+ * metadata feeds the mounted component tree and the fullscreen row map. */
42973
+ renderTranscriptBlocks(projectionExpanded, width) {
42785
42974
  const boundary = this.expandBoundary();
42786
- const width = this.transcriptRenderWidth();
42787
- const rows = [];
42788
- const blocks = this.transcriptBlocks(projectionExpanded);
42789
- blocks.forEach((block, index) => {
42975
+ return this.transcriptBlocks(projectionExpanded).map((block) => {
42790
42976
  let component;
42791
42977
  let rendered;
42792
42978
  let truncatedMarker = false;
@@ -42816,32 +43002,145 @@ var TuiApp = class TuiApp {
42816
43002
  top: hit.top + rendered.length - workflowInfo.total
42817
43003
  }));
42818
43004
  }
42819
- if (rendered.length === 0 && !truncatedMarker) {
43005
+ return {
43006
+ block,
43007
+ component,
43008
+ rendered,
43009
+ truncatedMarker,
43010
+ attachments,
43011
+ ...collapseFocusOwnerOnClick === void 0 ? {} : { collapseFocusOwnerOnClick },
43012
+ ...subCallHits === void 0 ? {} : { subCallHits },
43013
+ ...workflowHits === void 0 ? {} : { workflowHits }
43014
+ };
43015
+ });
43016
+ }
43017
+ /** The semantic turn owner of a rendered block. Do not use the Focus
43018
+ * collapse owner here: that marker is an interaction target, not turn
43019
+ * membership, and final assistant rows deliberately have no marker. */
43020
+ focusLiveTurnOf(block) {
43021
+ if (block.kind === "activity") return block.activity.turn;
43022
+ if (block.kind === "streaming-tool-previews") return block.turn;
43023
+ return "turn" in block.message ? block.message.turn : void 0;
43024
+ }
43025
+ /** The normal physical height charged to one rendered block, preserving the
43026
+ * existing zero-row and original-index Spacer rules exactly. */
43027
+ normalTranscriptBlockHeight(entry, index, total) {
43028
+ if (entry.rendered.length === 0 && !entry.truncatedMarker) return 0;
43029
+ return entry.rendered.length + (entry.truncatedMarker ? 1 : 0) + (index < total - 1 ? 1 : 0);
43030
+ }
43031
+ /** Drop presentation floors and their measured padding. */
43032
+ clearFocusLiveHeightState() {
43033
+ this.focusLiveHeightStates.clear();
43034
+ this.focusLivePaddingRows.clear();
43035
+ }
43036
+ /** Whether the current render still owns a fullscreen Focus presentation
43037
+ * epoch. Leaving history keeps the existing floor active; explicit lifecycle
43038
+ * and structural boundaries release it. */
43039
+ focusLivePaddingEnabled() {
43040
+ const enabled = this.focusModeEnabled && this.fullscreen !== void 0 && this.fullscreenScroll !== void 0;
43041
+ if (!enabled) this.clearFocusLiveHeightState();
43042
+ return enabled;
43043
+ }
43044
+ /** Measure the current normal turn spans and update their running high-water
43045
+ * floors. Padding is kept in separate inert row entries after the existing
43046
+ * turn-boundary Spacer, so semantic block heights and click ownership stay
43047
+ * unchanged. */
43048
+ focusLivePaddingFor(renderedBlocks, projectionExpanded, width) {
43049
+ this.focusLivePaddingRows.clear();
43050
+ const scroll = this.fullscreenScroll;
43051
+ if (!this.focusLivePaddingEnabled() || scroll === void 0) return this.focusLivePaddingRows;
43052
+ const heights = renderedBlocks.map((entry, index) => this.normalTranscriptBlockHeight(entry, index, renderedBlocks.length));
43053
+ const groups = /* @__PURE__ */ new Map();
43054
+ for (let index = 0; index < renderedBlocks.length; index += 1) {
43055
+ const turn = this.focusLiveTurnOf(renderedBlocks[index].block);
43056
+ if (turn === void 0) continue;
43057
+ let group = groups.get(turn);
43058
+ if (group === void 0) {
43059
+ group = { indices: [] };
43060
+ groups.set(turn, group);
43061
+ }
43062
+ group.indices.push(index);
43063
+ const block = renderedBlocks[index].block;
43064
+ if (block.kind === "activity") group.activity = block.activity;
43065
+ }
43066
+ for (const turn of this.focusLiveHeightStates.keys()) if (!groups.has(turn)) this.focusLiveHeightStates.delete(turn);
43067
+ for (const [turn, group] of groups) {
43068
+ const activity = group.activity;
43069
+ if (activity === void 0 || activity.completed) {
43070
+ this.focusLiveHeightStates.delete(turn);
43071
+ continue;
43072
+ }
43073
+ const normalHeight = group.indices.reduce((sum, index) => sum + heights[index], 0);
43074
+ const expanded = projectionExpanded.has(turn);
43075
+ const previous = this.focusLiveHeightStates.get(turn);
43076
+ if (previous !== void 0 && (previous.activity !== activity || previous.expanded !== expanded || previous.width !== width)) this.focusLiveHeightStates.delete(turn);
43077
+ let state = this.focusLiveHeightStates.get(turn);
43078
+ if (state === void 0) {
43079
+ state = {
43080
+ activity,
43081
+ expanded,
43082
+ width,
43083
+ height: normalHeight
43084
+ };
43085
+ this.focusLiveHeightStates.set(turn, state);
43086
+ } else if (normalHeight > state.height) state.height = normalHeight;
43087
+ const padding = state.height - normalHeight;
43088
+ if (padding <= 0) continue;
43089
+ for (let index = group.indices.length - 1; index >= 0; index -= 1) {
43090
+ const blockIndex = group.indices[index];
43091
+ if (heights[blockIndex] > 0) {
43092
+ this.focusLivePaddingRows.set(blockIndex, padding);
43093
+ break;
43094
+ }
43095
+ }
43096
+ }
43097
+ return this.focusLivePaddingRows;
43098
+ }
43099
+ /** Build one row-map entry from the shared rendered metadata. */
43100
+ fullscreenRowEntry(entry, height, hasTrailingSpacer) {
43101
+ const block = entry.block;
43102
+ return {
43103
+ ...block.kind === "message" ? { message: block.message } : block.kind === "activity" ? { activity: block.activity } : {},
43104
+ ...entry.collapseFocusOwnerOnClick === void 0 ? {} : { collapseFocusOwnerOnClick: entry.collapseFocusOwnerOnClick },
43105
+ height,
43106
+ attachments: entry.attachments,
43107
+ ...entry.subCallHits === void 0 ? {} : { subCallHits: entry.subCallHits },
43108
+ ...entry.workflowHits === void 0 ? {} : { workflowHits: entry.workflowHits },
43109
+ hasTrailingSpacer
43110
+ };
43111
+ }
43112
+ /** Rebuild the message component tree from the current transcript state. */
43113
+ rebuildMessages() {
43114
+ const projectionExpanded = this.focusProjectionExpandedTurns();
43115
+ this.pruneMessageComponents(projectionExpanded);
43116
+ this.messagesView.clear();
43117
+ this.messagesView.addChild(this.welcomeCard);
43118
+ const width = this.transcriptRenderWidth();
43119
+ const renderedBlocks = this.renderTranscriptBlocks(projectionExpanded, width);
43120
+ const paddingRows = this.focusLivePaddingFor(renderedBlocks, projectionExpanded, width);
43121
+ const rows = [];
43122
+ renderedBlocks.forEach((entry, index) => {
43123
+ const height = this.normalTranscriptBlockHeight(entry, index, renderedBlocks.length);
43124
+ if (height === 0) rows.push(this.fullscreenRowEntry(entry, 0, false));
43125
+ else {
43126
+ this.messagesView.addChild(new TranscriptGutterComponent(entry.component));
43127
+ if (entry.truncatedMarker) {
43128
+ const marker = truncateToWidth(color.textMuted(" (output may be truncated)"), width, "…");
43129
+ this.messagesView.addChild(new TranscriptGutterComponent(new Text(marker, 0, 0)));
43130
+ }
43131
+ const hasTrailingSpacer = index < renderedBlocks.length - 1;
43132
+ rows.push(this.fullscreenRowEntry(entry, height, hasTrailingSpacer));
43133
+ if (hasTrailingSpacer) this.messagesView.addChild(new Spacer());
43134
+ }
43135
+ const padding = paddingRows.get(index) ?? 0;
43136
+ if (padding > 0) {
43137
+ this.messagesView.addChild(new FocusLivePaddingComponent(() => this.focusLivePaddingRows.get(index) ?? 0, () => this.focusLivePaddingEnabled()));
42820
43138
  rows.push({
42821
- ...block.kind === "message" ? { message: block.message } : block.kind === "activity" ? { activity: block.activity } : {},
42822
- ...collapseFocusOwnerOnClick !== void 0 ? { collapseFocusOwnerOnClick } : {},
42823
- height: 0,
43139
+ height: padding,
42824
43140
  attachments: [],
42825
43141
  hasTrailingSpacer: false
42826
43142
  });
42827
- return;
42828
43143
  }
42829
- this.messagesView.addChild(new TranscriptGutterComponent(component));
42830
- if (truncatedMarker) {
42831
- const marker = truncateToWidth(color.textMuted(" (output may be truncated)"), width, "…");
42832
- this.messagesView.addChild(new TranscriptGutterComponent(new Text(marker, 0, 0)));
42833
- }
42834
- const height = rendered.length + (truncatedMarker ? 1 : 0) + (index < blocks.length - 1 ? 1 : 0);
42835
- rows.push({
42836
- ...block.kind === "message" ? { message: block.message } : block.kind === "activity" ? { activity: block.activity } : {},
42837
- ...collapseFocusOwnerOnClick !== void 0 ? { collapseFocusOwnerOnClick } : {},
42838
- height,
42839
- attachments,
42840
- ...subCallHits === void 0 ? {} : { subCallHits },
42841
- ...workflowHits === void 0 ? {} : { workflowHits },
42842
- hasTrailingSpacer: index < blocks.length - 1
42843
- });
42844
- if (index < blocks.length - 1) this.messagesView.addChild(new Spacer());
42845
43144
  });
42846
43145
  if (this.transcriptWindowHint !== "") this.messagesView.addChild(new TranscriptGutterComponent(new Text(color.textDim(this.transcriptWindowHint), 0, 0)));
42847
43146
  if (this.notifyText !== "") {
@@ -42926,61 +43225,21 @@ var TuiApp = class TuiApp {
42926
43225
  * (the gutter contract), or the hit map drifts from the layout. */
42927
43226
  refreshMessageRows() {
42928
43227
  const width = this.transcriptRenderWidth();
42929
- const boundary = this.expandBoundary();
42930
43228
  const projectionExpanded = this.focusProjectionExpandedTurns();
42931
- const blocks = this.transcriptBlocks(projectionExpanded);
43229
+ const renderedBlocks = this.renderTranscriptBlocks(projectionExpanded, width);
43230
+ const paddingRows = this.focusLivePaddingFor(renderedBlocks, projectionExpanded, width);
42932
43231
  const rows = [];
42933
- blocks.forEach((block, index) => {
42934
- let component;
42935
- let rendered;
42936
- let truncatedMarker = false;
42937
- let attachments = [];
42938
- let subCallHits;
42939
- let workflowHits;
42940
- const collapseFocusOwnerOnClick = this.focusOwnerForRenderBlock(block);
42941
- if (block.kind === "activity") {
42942
- component = this.focusActivityComponentFor(block.activity, projectionExpanded.has(block.activity.turn), this.focusToolDisplayFor(block.activity), this.focusModeEnabled && !projectionExpanded.has(block.activity.turn) ? focusPreparingSummary(this.streamingToolPreviewsForTurn(block.activity.turn)) : void 0);
42943
- rendered = component.render(width);
42944
- } else if (block.kind === "streaming-tool-previews") {
42945
- component = this.streamingToolPreviewComponent(block.previews, width);
42946
- rendered = component.render(width);
42947
- } else {
42948
- component = this.componentForMessage(block.message, boundary, width);
42949
- rendered = component.render(width);
42950
- truncatedMarker = block.truncated === true;
42951
- attachments = this.attachmentRangesOf(component, width);
42952
- const subCallInfo = this.subCallHitsByMessage.get(block.message);
42953
- subCallHits = subCallInfo === void 0 ? void 0 : subCallInfo.hits.map((hit) => ({
42954
- ...hit,
42955
- top: hit.top + rendered.length - subCallInfo.total
42956
- }));
42957
- const workflowInfo = this.workflowHitsByMessage.get(block.message);
42958
- workflowHits = workflowInfo === void 0 ? void 0 : workflowInfo.hits.map((hit) => ({
42959
- ...hit,
42960
- top: hit.top + rendered.length - workflowInfo.total
42961
- }));
42962
- }
42963
- if (rendered.length === 0 && !truncatedMarker) {
42964
- rows.push({
42965
- ...block.kind === "message" ? { message: block.message } : block.kind === "activity" ? { activity: block.activity } : {},
42966
- ...collapseFocusOwnerOnClick !== void 0 ? { collapseFocusOwnerOnClick } : {},
42967
- height: 0,
42968
- attachments: [],
42969
- hasTrailingSpacer: false
42970
- });
42971
- return;
42972
- }
42973
- const height = rendered.length + (truncatedMarker ? 1 : 0) + (index < blocks.length - 1 ? 1 : 0);
42974
- rows.push({
42975
- ...block.kind === "message" ? { message: block.message } : block.kind === "activity" ? { activity: block.activity } : {},
42976
- ...collapseFocusOwnerOnClick !== void 0 ? { collapseFocusOwnerOnClick } : {},
42977
- height,
42978
- attachments,
42979
- ...subCallHits === void 0 ? {} : { subCallHits },
42980
- ...workflowHits === void 0 ? {} : { workflowHits },
42981
- hasTrailingSpacer: index < blocks.length - 1
43232
+ for (let index = 0; index < renderedBlocks.length; index += 1) {
43233
+ const entry = renderedBlocks[index];
43234
+ const height = this.normalTranscriptBlockHeight(entry, index, renderedBlocks.length);
43235
+ rows.push(this.fullscreenRowEntry(entry, height, height > 0 && index < renderedBlocks.length - 1));
43236
+ const padding = paddingRows.get(index) ?? 0;
43237
+ if (padding > 0) rows.push({
43238
+ height: padding,
43239
+ attachments: [],
43240
+ hasTrailingSpacer: false
42982
43241
  });
42983
- });
43242
+ }
42984
43243
  this.messageRows = rows;
42985
43244
  }
42986
43245
  /** The live collapse flag for ONE image-block occurrence (message object
@@ -43000,6 +43259,7 @@ var TuiApp = class TuiApp {
43000
43259
  }
43001
43260
  if (indices.has(imageIndex)) indices.delete(imageIndex);
43002
43261
  else indices.add(imageIndex);
43262
+ this.clearFocusLiveHeightState();
43003
43263
  this.rebuildMessages();
43004
43264
  }
43005
43265
  /**
@@ -43622,6 +43882,7 @@ var TuiApp = class TuiApp {
43622
43882
  } else this.expandedOverride.set(message, true);
43623
43883
  } else if (this.expandedOverride.get(message) === true) this.expandedOverride.delete(message);
43624
43884
  else this.expandedOverride.set(message, true);
43885
+ this.clearFocusLiveHeightState();
43625
43886
  this.rebuildMessages();
43626
43887
  }
43627
43888
  /** The effective Run disclosure of one Workflow card: the user's explicit
@@ -43654,6 +43915,7 @@ var TuiApp = class TuiApp {
43654
43915
  if (message === void 0) return;
43655
43916
  state.userOpen = !(state.userOpen ?? this.workflowRunOpen(message));
43656
43917
  this.workflowDisclosureRevision += 1;
43918
+ this.clearFocusLiveHeightState();
43657
43919
  this.rebuildMessages();
43658
43920
  }
43659
43921
  /** Toggle one Workflow Phase disclosure (click on the phase header). The
@@ -43668,6 +43930,7 @@ var TuiApp = class TuiApp {
43668
43930
  if (phase === void 0) return;
43669
43931
  state.userOpen = !(state.userOpen ?? this.workflowPhaseOpen(runId, phaseKey, phase));
43670
43932
  this.workflowDisclosureRevision += 1;
43933
+ this.clearFocusLiveHeightState();
43671
43934
  this.rebuildMessages();
43672
43935
  }
43673
43936
  /** Advance the Workflow disclosure state for one changed run (PR2 plan
@@ -43762,16 +44025,19 @@ var TuiApp = class TuiApp {
43762
44025
  * snapshot is compared against the last-seen one, and only a real
43763
44026
  * change (members array reference or run status) advances the state. */
43764
44027
  updateWorkflowDisclosure(messages) {
44028
+ let snapshotChanged = false;
43765
44029
  for (const message of messages) {
43766
44030
  if (message.kind !== "workflow") continue;
43767
44031
  const previous = this.workflowSeen.get(message.runId);
43768
44032
  if (previous !== void 0 && previous.members === message.members && previous.status === message.status) continue;
44033
+ snapshotChanged = true;
43769
44034
  this.workflowSeen.set(message.runId, {
43770
44035
  members: message.members,
43771
44036
  status: message.status
43772
44037
  });
43773
44038
  this.advanceWorkflowDisclosure(message);
43774
44039
  }
44040
+ return snapshotChanged;
43775
44041
  }
43776
44042
  /** Dispatch one Workflow card hit (PR2 plan §12.4/§12.5): run/phase
43777
44043
  * headers toggle their disclosure; a running inline member and the
@@ -43834,6 +44100,7 @@ var TuiApp = class TuiApp {
43834
44100
  /** Drop all per-message expansion overrides (session-scoped state: a
43835
44101
  * session switch must not leak the old session's click toggles). */
43836
44102
  clearSessionOverrides() {
44103
+ this.clearFocusLiveHeightState();
43837
44104
  this.expandedOverride.clear();
43838
44105
  this.fullscreenCellGesture = void 0;
43839
44106
  this.todoClickCoalesceUntil = 0;
@@ -44221,6 +44488,10 @@ var TuiApp = class TuiApp {
44221
44488
  * @param facts - directory, session id, model, version, and the optional agent preset to display.
44222
44489
  */
44223
44490
  setWelcomeCard(facts) {
44491
+ if (this.workspaceRoot !== facts.cwd) {
44492
+ this.workspaceRoot = facts.cwd;
44493
+ this.disposeMessageComponents();
44494
+ }
44224
44495
  this.welcomeCard.setFacts(facts);
44225
44496
  this.rebuildMessages();
44226
44497
  this.extensionHost?.updateSession({
@@ -45215,6 +45486,7 @@ var TuiApp = class TuiApp {
45215
45486
  * context keeps the existing rule. */
45216
45487
  effectiveMessageExpanded(message, boundary) {
45217
45488
  if (message.kind === "thinking") return this.effectiveThinkingExpanded(message);
45489
+ if (message.kind === "assistant" && message.deliverables !== void 0) return message.turn >= boundary || this.expandedOverride.get(message) === true;
45218
45490
  if ("turn" in message && this.isInsideExpandedFocus(message, boundary) && isFocusSecondaryDisclosure(message)) {
45219
45491
  if (this.fullscreen !== void 0) return this.expandedOverride.get(message) === true;
45220
45492
  return true;
@@ -45331,7 +45603,7 @@ var TuiApp = class TuiApp {
45331
45603
  this.workflowHitsByMessage.delete(message);
45332
45604
  }
45333
45605
  return {
45334
- component: hostBuilt ? this.renderMessage(message, state.expanded, state.expandHint, state.fullReveal, width) : rendered.component,
45606
+ component: hostBuilt ? this.renderMessage(message, state.expanded, state.expandHint, state.fullReveal, width) : this.withDeliveredFiles(rendered.component, message, state.expanded),
45335
45607
  boundary,
45336
45608
  builtWidth: hostBuilt && this.bakesFoldedWidth(message, state.expanded) ? width : void 0,
45337
45609
  themeRev: this.themeRevision,
@@ -45515,6 +45787,14 @@ var TuiApp = class TuiApp {
45515
45787
  * renderer, so a throwing renderer is invoked exactly once per build
45516
45788
  * and the host fallback is single-path).
45517
45789
  */
45790
+ withDeliveredFiles(body, message, expanded) {
45791
+ if (message.kind !== "assistant" || message.deliverables === void 0 || message.deliverables.length === 0) return body;
45792
+ const result = new Container();
45793
+ result.addChild(body);
45794
+ result.addChild(new Text("", 0, 0));
45795
+ result.addChild(new DeliveredFilesComponent(message.deliverables, this.workspaceRoot, expanded));
45796
+ return result;
45797
+ }
45518
45798
  renderMessage(message, expanded, expandHint, fullReveal, width) {
45519
45799
  if (message.kind === "user") {
45520
45800
  if (message.content !== void 0 && message.content.some((block) => block.type !== "text")) return this.renderUserBlocks(message.content, message);
@@ -45524,11 +45804,14 @@ var TuiApp = class TuiApp {
45524
45804
  const bullet = color.primary(iconPrefix("assistant-bullet", this.iconStyle));
45525
45805
  const makeAssistantText = (text) => new BulletedComponent(new Markdown(text, 0, 0, markdownTheme, void 0, HOST_MARKDOWN_OPTIONS), bullet);
45526
45806
  const body = message.displayBlocks !== void 0 ? this.renderAssistantDisplaySequence(message.displayBlocks, makeAssistantText, message) : message.content !== void 0 && message.content.some((block) => block.type !== "text") ? this.renderBlockSequence(message.content, makeAssistantText, message) : makeAssistantText(message.text);
45527
- if (!message.interrupted) return body;
45528
- const interrupted = new Container();
45529
- interrupted.addChild(body);
45530
- interrupted.addChild(new Text(color.textDimItalic(" (interrupted)"), 0, 0));
45531
- return interrupted;
45807
+ let bodyWithStatus = body;
45808
+ if (message.interrupted) {
45809
+ const interrupted = new Container();
45810
+ interrupted.addChild(body);
45811
+ interrupted.addChild(new Text(color.textDimItalic(" (interrupted)"), 0, 0));
45812
+ bodyWithStatus = interrupted;
45813
+ }
45814
+ return this.withDeliveredFiles(bodyWithStatus, message, expanded);
45532
45815
  }
45533
45816
  if (message.kind === "thinking") {
45534
45817
  if (!expanded) return new ThinkingCompactComponent(message, this.expandHint(expandHint), this.iconStyle);
@@ -45726,6 +46009,7 @@ var TuiApp = class TuiApp {
45726
46009
  if (this.subCallExpanded.has(subCallId)) this.subCallExpanded.delete(subCallId);
45727
46010
  else this.subCallExpanded.add(subCallId);
45728
46011
  this.subCallExpandedRevision += 1;
46012
+ this.clearFocusLiveHeightState();
45729
46013
  this.rebuildMessages();
45730
46014
  }
45731
46015
  /**
@@ -46271,6 +46555,7 @@ var TuiApp = class TuiApp {
46271
46555
  const revision = this.renderers.revisionOf();
46272
46556
  if (revision !== this.lastRendererRevision) {
46273
46557
  this.lastRendererRevision = revision;
46558
+ this.clearFocusLiveHeightState();
46274
46559
  this.rebuildMessages();
46275
46560
  }
46276
46561
  }
@@ -46514,6 +46799,7 @@ var TuiApp = class TuiApp {
46514
46799
  setThinkingExpanded(expanded) {
46515
46800
  const hadOverrides = this.clearThinkingExpansionOverrides();
46516
46801
  if (this.thinkingExpanded === expanded && !hadOverrides) return;
46802
+ this.clearFocusLiveHeightState();
46517
46803
  this.thinkingExpanded = expanded;
46518
46804
  this.rebuildMessages();
46519
46805
  }
@@ -46544,6 +46830,7 @@ var TuiApp = class TuiApp {
46544
46830
  * ALL Thinking compact (plan §9). */
46545
46831
  toggleThinkingExpanded() {
46546
46832
  this.clearThinkingExpansionOverrides();
46833
+ this.clearFocusLiveHeightState();
46547
46834
  this.thinkingExpanded = !this.thinkingExpanded;
46548
46835
  this.rebuildMessages();
46549
46836
  return this.thinkingExpanded;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoon76/dsh-pi-tui",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "The dsh-pi-tui bundle: a TUI surface for DeepSeek Harness profiles (dsh --profile pi-tui)",
5
5
  "license": "MIT",
6
6
  "author": "XMoon <xmoonox@gmail.com>",