pi-ui-extend 0.1.33 → 0.1.35

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 (63) hide show
  1. package/README.md +20 -0
  2. package/dist/app/app.js +4 -2
  3. package/dist/app/constants.d.ts +2 -1
  4. package/dist/app/constants.js +6 -1
  5. package/dist/app/icons.js +1 -1
  6. package/dist/app/input/input-controller.d.ts +4 -1
  7. package/dist/app/input/input-controller.js +95 -16
  8. package/dist/app/input/input-paste-handler.js +3 -1
  9. package/dist/app/input/terminal-edit-shortcuts.d.ts +20 -0
  10. package/dist/app/input/terminal-edit-shortcuts.js +50 -16
  11. package/dist/app/model/model-usage-status.d.ts +2 -1
  12. package/dist/app/model/model-usage-status.js +33 -25
  13. package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
  14. package/dist/app/rendering/conversation-entry-renderer.js +1 -1
  15. package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
  16. package/dist/app/rendering/conversation-tool-renderer.js +21 -0
  17. package/dist/app/rendering/conversation-viewport.d.ts +3 -0
  18. package/dist/app/rendering/conversation-viewport.js +41 -5
  19. package/dist/app/rendering/editor-layout-renderer.js +3 -2
  20. package/dist/app/rendering/editor-panels.js +27 -10
  21. package/dist/app/rendering/status-line-renderer.d.ts +1 -0
  22. package/dist/app/rendering/status-line-renderer.js +15 -1
  23. package/dist/app/runtime.d.ts +1 -0
  24. package/dist/app/runtime.js +33 -14
  25. package/dist/app/session/session-event-controller.d.ts +7 -0
  26. package/dist/app/session/session-event-controller.js +110 -7
  27. package/dist/app/session/tabs-controller.js +3 -1
  28. package/dist/app/subagents/subagents-widget-controller.d.ts +10 -2
  29. package/dist/app/subagents/subagents-widget-controller.js +141 -70
  30. package/dist/app/terminal/terminal-controller.d.ts +10 -0
  31. package/dist/app/terminal/terminal-controller.js +91 -2
  32. package/dist/app/todo/todo-model.js +2 -0
  33. package/dist/app/todo/todo-widget-controller.d.ts +2 -0
  34. package/dist/app/todo/todo-widget-controller.js +17 -7
  35. package/dist/app/types.d.ts +4 -0
  36. package/dist/bundled-extensions/question/tui.js +8 -1
  37. package/dist/bundled-extensions/session-title/index.js +65 -14
  38. package/dist/input-editor-files.js +23 -4
  39. package/dist/markdown-format.d.ts +4 -1
  40. package/dist/markdown-format.js +76 -9
  41. package/external/pi-tools-suite/README.md +71 -1
  42. package/external/pi-tools-suite/package.json +3 -3
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -6
  44. package/external/pi-tools-suite/src/async-subagents/index.ts +133 -37
  45. package/external/pi-tools-suite/src/context-usage.ts +6 -1
  46. package/external/pi-tools-suite/src/dcp/commands.ts +3 -2
  47. package/external/pi-tools-suite/src/dcp/compress-tool.ts +9 -4
  48. package/external/pi-tools-suite/src/dcp/config.ts +142 -6
  49. package/external/pi-tools-suite/src/dcp/index.ts +20 -8
  50. package/external/pi-tools-suite/src/dcp/prompts.ts +17 -9
  51. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +59 -15
  52. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +6 -8
  53. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
  54. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +51 -1
  55. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +16 -11
  56. package/external/pi-tools-suite/src/model-tools/index.ts +24 -12
  57. package/external/pi-tools-suite/src/prompt-commands/index.ts +11 -2
  58. package/external/pi-tools-suite/src/telegram-mirror/index.ts +66 -27
  59. package/external/pi-tools-suite/src/todo/index.ts +87 -16
  60. package/external/pi-tools-suite/src/todo/state/store.ts +41 -10
  61. package/external/pi-tools-suite/src/todo/todo.ts +49 -6
  62. package/external/pi-tools-suite/src/tool-descriptions.ts +37 -56
  63. package/package.json +7 -5
@@ -49,11 +49,14 @@ export declare class ConversationViewport {
49
49
  private blockCacheForWidth;
50
50
  private refreshDynamicLayoutEntries;
51
51
  private ensureEntryMeasured;
52
+ private hasDynamicConversationBlock;
53
+ private isDynamicConversationBlock;
52
54
  private refreshLayoutEntry;
53
55
  private measuredLineCountForEntry;
54
56
  private estimatedLineCountForEntry;
55
57
  private lineCountWithGap;
56
58
  private estimatedBlockLineCountForEntry;
59
+ private estimatedToolEntryLineCount;
57
60
  private nextVisibleEntry;
58
61
  private nextEstimatedVisibleEntry;
59
62
  private gapAfterEntry;
@@ -1,4 +1,5 @@
1
1
  import { resolveToolRule } from "../../config.js";
2
+ import { renderToolDisplay } from "../../tool-renderers/index.js";
2
3
  import { stringDisplayWidth } from "../../terminal-width.js";
3
4
  import { renderConversationEntry as renderConversationEntryLines } from "./conversation-entry-renderer.js";
4
5
  import { horizontalPaddingLayout } from "./render-text.js";
@@ -70,7 +71,7 @@ export class ConversationViewport {
70
71
  + (this.host.superCompactTools ? 1_000_000_000 : 0)
71
72
  + (this.host.allThinkingExpanded ? 2_000_000_000 : 0);
72
73
  const cached = blockCache.get(entry.id);
73
- const dynamic = this.host.isDynamicConversationBlock(entry);
74
+ const dynamic = this.isDynamicConversationBlock(entry);
74
75
  if (!dynamic && cached?.version === version)
75
76
  return cached;
76
77
  const availableThinkingLevels = this.host.availableThinkingLevels?.();
@@ -82,6 +83,7 @@ export class ConversationViewport {
82
83
  ...(availableThinkingLevels ? { availableThinkingLevels } : {}),
83
84
  superCompactTools: Boolean(this.host.superCompactTools),
84
85
  allThinkingExpanded: Boolean(this.host.allThinkingExpanded),
86
+ currentTimeMs: Date.now(),
85
87
  renderInlineUserMessageMenu: (userEntry, context) => this.host.renderInlineUserMessageMenu(userEntry, context),
86
88
  });
87
89
  const block = {
@@ -156,7 +158,7 @@ export class ConversationViewport {
156
158
  }
157
159
  }
158
160
  this.refreshDirtyLayoutEntries(layout, width);
159
- if (this.host.hasDynamicConversationBlock?.()) {
161
+ if (this.hasDynamicConversationBlock(layout.entries)) {
160
162
  this.refreshDynamicLayoutEntries(layout, width);
161
163
  }
162
164
  return layout;
@@ -278,7 +280,7 @@ export class ConversationViewport {
278
280
  }
279
281
  refreshDynamicLayoutEntries(layout, width) {
280
282
  for (let index = 0; index < layout.entries.length; index += 1) {
281
- if (this.host.isDynamicConversationBlock(layout.entries[index]))
283
+ if (this.isDynamicConversationBlock(layout.entries[index]))
282
284
  this.refreshLayoutEntry(layout, width, index, true);
283
285
  }
284
286
  }
@@ -286,10 +288,17 @@ export class ConversationViewport {
286
288
  const entry = layout.entries[index];
287
289
  if (!entry)
288
290
  return false;
289
- if (layout.measuredLineCounts[index] === true && !this.host.isDynamicConversationBlock(entry))
291
+ if (layout.measuredLineCounts[index] === true && !this.isDynamicConversationBlock(entry))
290
292
  return false;
291
293
  return this.refreshLayoutEntry(layout, width, index, true);
292
294
  }
295
+ hasDynamicConversationBlock(entries) {
296
+ return this.host.hasDynamicConversationBlock?.() === true || entries.some((entry) => this.isDynamicConversationBlock(entry));
297
+ }
298
+ isDynamicConversationBlock(entry) {
299
+ return (entry.kind === "thinking" && entry.status === "running" && entry.startedAt !== undefined)
300
+ || this.host.isDynamicConversationBlock(entry);
301
+ }
293
302
  refreshLayoutEntry(layout, width, index, measure) {
294
303
  const entry = layout.entries[index];
295
304
  if (!entry)
@@ -348,11 +357,38 @@ export class ConversationViewport {
348
357
  case "shell":
349
358
  return estimateToolLikeLineCount("shell", entry.expanded, `${entry.output}\n${entry.status}`, width, this.host.pixConfig, this.host.superCompactTools === true, true);
350
359
  case "tool":
351
- return estimateToolLikeLineCount(entry.toolName, entry.expanded, entry.output, width, this.host.pixConfig, this.host.superCompactTools === true, false);
360
+ return this.estimatedToolEntryLineCount(entry, width);
352
361
  default:
353
362
  return 1;
354
363
  }
355
364
  }
365
+ estimatedToolEntryLineCount(entry, width) {
366
+ const display = renderToolDisplay({
367
+ toolName: entry.toolName,
368
+ argsText: entry.argsText,
369
+ output: entry.output,
370
+ details: entry.details,
371
+ isError: entry.isError,
372
+ status: entry.status,
373
+ cwd: this.host.cwd,
374
+ colors: this.host.colors,
375
+ });
376
+ const toolName = display.toolName ?? entry.toolName;
377
+ const rule = resolveToolRule(toolName, this.host.pixConfig.toolRenderer);
378
+ if (rule.hidden)
379
+ return 0;
380
+ const bodyWidth = Math.max(1, width - 2);
381
+ if (entry.expanded)
382
+ return 1 + estimateWrappedLineCount(display.expandedText, bodyWidth);
383
+ if (rule.compactHidden || (rule.defaultExpanded === true && this.host.superCompactTools !== true))
384
+ return 1;
385
+ const body = display.collapsedBody.trimEnd();
386
+ if (!body || rule.previewLines === 0)
387
+ return 1;
388
+ const bodyLineCount = estimateWrappedLineCount(body, bodyWidth);
389
+ const previewLineCount = Math.min(rule.previewLines, bodyLineCount);
390
+ return this.host.superCompactTools === true ? 1 : 1 + previewLineCount;
391
+ }
356
392
  nextVisibleEntry(entries, index, width) {
357
393
  for (let nextIndex = index + 1; nextIndex < entries.length; nextIndex += 1) {
358
394
  const nextEntry = entries[nextIndex];
@@ -1,4 +1,4 @@
1
- import { ABOVE_EDITOR_WIDGET_KEY_GROUPS, BUILT_IN_SUBAGENTS_WIDGET_KEYS, INPUT_MAX_ROWS, LEGACY_TODO_WIDGET_KEYS, } from "../constants.js";
1
+ import { ABOVE_EDITOR_WIDGET_KEY_GROUPS, BUILT_IN_SUBAGENTS_WIDGET_KEYS, LEGACY_TODO_WIDGET_KEYS, } from "../constants.js";
2
2
  import { renderSubagentsPanel, renderTodoPanel } from "./editor-panels.js";
3
3
  import { ellipsizeDisplay, horizontalPaddingLayout, padHorizontalText, sanitizeText, wrapText } from "./render-text.js";
4
4
  import { APP_ICONS } from "../icons.js";
@@ -9,7 +9,8 @@ export class EditorLayoutRenderer {
9
9
  }
10
10
  computeLayout(width, rows) {
11
11
  const maxAvailableInputRows = Math.max(1, rows - 4);
12
- const renderedInput = this.renderInput(width, Math.min(INPUT_MAX_ROWS, maxAvailableInputRows), maxAvailableInputRows);
12
+ const maxComposerRows = Math.max(1, Math.min(maxAvailableInputRows, Math.floor(rows * 0.7)));
13
+ const renderedInput = this.renderInput(width, maxComposerRows, maxComposerRows);
13
14
  const maxEntityRows = Math.max(0, rows - renderedInput.lines.length - 4);
14
15
  const editorEntityWidth = inputFrameContentWidth(width);
15
16
  const aboveEditorEntities = this.renderAboveEditorEntities(editorEntityWidth);
@@ -71,30 +71,45 @@ export function renderTodoPanel(details, expanded, width, colors) {
71
71
  export function renderSubagentsPanel(state, expanded, width, colors) {
72
72
  if (!state)
73
73
  return [];
74
- const activeAgents = activeSubagentStates(state.agents);
74
+ const runs = state.runs?.length
75
+ ? state.runs
76
+ : [{ runDir: state.runDir, agents: state.agents, ...(state.tasks === undefined ? {} : { tasks: state.tasks }) }];
77
+ const activeRuns = runs
78
+ .map((run) => ({ ...run, activeAgents: activeSubagentStates(run.agents) }))
79
+ .filter((run) => run.activeAgents.length > 0);
80
+ const activeAgents = activeRuns.flatMap((run) => run.activeAgents);
75
81
  if (activeAgents.length === 0)
76
82
  return [];
77
83
  const target = { kind: "subagents-panel" };
78
- const previewById = taskPreviewMap(state.tasks);
79
- const runName = subagentRunName(state.runDir);
80
84
  const titleSuffix = state.live ? "" : " (snapshot)";
81
85
  const stats = formatSubagentsPanelStats(activeAgents);
82
86
  const headerText = `subagents ${expanded ? "▾" : "▸"}${stats ? ` ${stats}` : ""}${titleSuffix}`;
83
87
  const contentWidth = Math.max(1, width);
84
88
  if (!expanded) {
85
- const collapsedText = `${headerText} — ${runName}`;
89
+ const runSummary = activeRuns.length === 1 ? subagentRunName(activeRuns[0]?.runDir ?? state.runDir) : `${activeRuns.length} runs`;
90
+ const collapsedText = `${headerText} — ${runSummary}`;
86
91
  return [{ text: padOrTrimPlain(ellipsizeDisplay(collapsedText, contentWidth), width), colorOverride: colors.accent, target }];
87
92
  }
88
93
  const lines = [];
89
- const visibleAgents = activeAgents.slice(0, SUBAGENTS_WIDGET_MAX_ROWS);
94
+ const flattenedRuns = activeRuns.flatMap((run) => {
95
+ const previewById = taskPreviewMap(run.tasks);
96
+ return run.activeAgents.map((agent, index) => ({
97
+ runDir: run.runDir,
98
+ agent,
99
+ preview: previewById.get(agent.id),
100
+ showRunLabel: activeRuns.length > 1 && index === 0,
101
+ }));
102
+ });
103
+ const visibleAgents = flattenedRuns.slice(0, SUBAGENTS_WIDGET_MAX_ROWS);
90
104
  const rowWidth = contentWidth;
91
105
  const now = Date.now();
92
- for (const agent of visibleAgents) {
93
- const preview = previewById.get(agent.id);
106
+ for (const visibleRun of visibleAgents) {
107
+ const { agent, preview } = visibleRun;
94
108
  const model = subagentModelThinkingLabel(preview);
95
109
  const task = preview?.task?.trim() || preview?.scope?.trim() || "task unavailable";
96
110
  const icon = subagentStatusIcon(agent.status);
97
- const prefix = `${icon} ${agent.id} ${model} `;
111
+ const runLabel = visibleRun.showRunLabel ? `${subagentRunName(visibleRun.runDir)} ` : "";
112
+ const prefix = `${runLabel}${icon} ${agent.id} ${model} `;
98
113
  const suffix = ` ${formatElapsedSince(agent.startedAt, now)}`;
99
114
  const taskWidth = Math.max(8, rowWidth - stringDisplayWidth(prefix) - stringDisplayWidth(suffix));
100
115
  const taskText = ellipsizeDisplay(task, taskWidth);
@@ -102,22 +117,24 @@ export function renderSubagentsPanel(state, expanded, width, colors) {
102
117
  lines.push({
103
118
  text: padOrTrimPlain(text, width),
104
119
  colorOverride: colors.muted,
105
- segments: subagentPanelLineSegments({ text, icon, agentId: agent.id, model, taskText, prefix, status: agent.status }, colors),
120
+ segments: subagentPanelLineSegments({ text, icon, agentId: agent.id, model, taskText, prefix, runLabel, status: agent.status }, colors),
106
121
  target,
107
122
  });
108
123
  }
109
- const hidden = activeAgents.length - visibleAgents.length;
124
+ const hidden = flattenedRuns.length - visibleAgents.length;
110
125
  if (hidden > 0)
111
126
  lines.push({ text: padOrTrimPlain(`+${hidden} more`, width), variant: "muted", target });
112
127
  return lines;
113
128
  }
114
129
  function subagentPanelLineSegments(input, colors) {
115
130
  const iconStart = input.text.indexOf(input.icon);
131
+ const runLabelStart = input.runLabel ? input.text.indexOf(input.runLabel) : -1;
116
132
  const nameStart = input.text.indexOf(input.agentId, iconStart + input.icon.length);
117
133
  const modelStart = input.text.indexOf(input.model, nameStart + input.agentId.length);
118
134
  const taskStart = input.prefix.length;
119
135
  const suffixStart = taskStart + input.taskText.length;
120
136
  return [
137
+ ...(runLabelStart >= 0 ? [{ start: runLabelStart, end: runLabelStart + input.runLabel.length, foreground: colors.warning }] : []),
121
138
  { start: iconStart, end: iconStart + input.icon.length, foreground: subagentStatusColor(input.status, colors), bold: true },
122
139
  { start: nameStart, end: nameStart + input.agentId.length, foreground: colors.accent, bold: true },
123
140
  { start: modelStart, end: modelStart + input.model.length, foreground: colors.info },
@@ -71,6 +71,7 @@ export declare class StatusLineRenderer {
71
71
  private pushVoiceWidgetSegment;
72
72
  private pushWorkspaceSegments;
73
73
  private pushModelUsageSegments;
74
+ private modelUsageHasWarning;
74
75
  private modelUsageResetLength;
75
76
  private pushSegment;
76
77
  private pushContextBarSegments;
@@ -491,11 +491,25 @@ export class StatusLineRenderer {
491
491
  fill: color,
492
492
  track: this.host.theme.colors.statusDotBase,
493
493
  }, MODEL_USAGE_PROGRESS_BAR_WIDTH));
494
- const resetStart = barStart + MODEL_USAGE_PROGRESS_BAR_WIDTH + 1;
494
+ const defaultResetStart = barStart + MODEL_USAGE_PROGRESS_BAR_WIDTH + 1;
495
+ const warningStart = this.modelUsageHasWarning(modelUsageLabel, defaultResetStart - labelStart)
496
+ ? defaultResetStart
497
+ : undefined;
498
+ const resetStart = warningStart === undefined
499
+ ? defaultResetStart
500
+ : warningStart + APP_ICONS.alert.length + 1;
501
+ if (warningStart !== undefined) {
502
+ this.pushSegment(segments, warningStart, APP_ICONS.alert.length, this.host.theme.colors.warning);
503
+ }
495
504
  const resetLength = this.modelUsageResetLength(modelUsageLabel, resetStart - labelStart);
496
505
  this.pushSegment(segments, resetStart, resetLength, this.host.theme.colors.muted);
497
506
  }
498
507
  }
508
+ modelUsageHasWarning(modelUsageLabel, localStart) {
509
+ if (localStart < 0 || localStart >= modelUsageLabel.length)
510
+ return false;
511
+ return modelUsageLabel.startsWith(APP_ICONS.alert, localStart);
512
+ }
499
513
  modelUsageResetLength(modelUsageLabel, localStart) {
500
514
  if (localStart < 0 || localStart >= modelUsageLabel.length)
501
515
  return 0;
@@ -38,6 +38,7 @@ export declare function prioritizeBundledQuestionExtension(base: LoadExtensionsR
38
38
  export type CreatePixRuntimeOptions = {
39
39
  eventBus?: EventBus;
40
40
  config?: PixConfig;
41
+ reuseServicesFrom?: AgentSessionRuntime;
41
42
  };
42
43
  type RuntimeSessionManagerModelState = Pick<SessionManager, "getEntries" | "getBranch">;
43
44
  export declare function resolvePixRuntimeModelRef(options: Pick<AppOptions, "modelRef">, sessionManager: RuntimeSessionManagerModelState, config?: PixConfig): string | undefined;
@@ -220,26 +220,20 @@ export function resolveSessionModelRefFromTail(entries) {
220
220
  }
221
221
  export async function createPixRuntime(options, runtimeOptions = {}) {
222
222
  const agentDir = getAgentDir();
223
+ const reusableServices = reusableRuntimeServices(runtimeOptions.reuseServicesFrom, options.cwd, agentDir);
223
224
  const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
224
225
  const config = runtimeOptions.config ?? loadPixConfig(cwd);
225
226
  const effectiveModelRef = resolvePixRuntimeModelRef(options, sessionManager, config);
226
227
  const parsedModel = effectiveModelRef ? parseModelRef(effectiveModelRef) : undefined;
227
228
  const initialThinkingLevel = resolvePixRuntimeInitialThinkingLevel(options, sessionManager, config);
228
- await ensureBundledSkillsInstalledOnce();
229
- await ensurePiToolsSuiteExtensionInstalledOnce({ agentDir });
230
- const bundledExtensionPaths = await getBundledExtensionPathsAsync();
231
- const services = await createAgentSessionServices({
232
- cwd,
233
- agentDir,
234
- resourceLoaderOptions: {
235
- ...(config.ignoreContextFiles ? { noContextFiles: true } : {}),
229
+ const services = reusableServices && sameRuntimeServiceTarget(reusableServices, cwd, agentDir)
230
+ ? reusableServices
231
+ : await createPixRuntimeServices({
232
+ cwd,
233
+ agentDir,
234
+ config,
236
235
  ...(runtimeOptions.eventBus === undefined ? {} : { eventBus: runtimeOptions.eventBus }),
237
- ...(bundledExtensionPaths.length === 0 ? {} : {
238
- additionalExtensionPaths: bundledExtensionPaths,
239
- extensionsOverride: prioritizeBundledQuestionExtension,
240
- }),
241
- },
242
- });
236
+ });
243
237
  services.modelRegistry.refresh();
244
238
  const model = parsedModel ? services.modelRegistry.find(parsedModel.provider, parsedModel.modelId) : undefined;
245
239
  if (parsedModel && !model) {
@@ -285,3 +279,28 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
285
279
  : SessionManager.create(options.cwd),
286
280
  });
287
281
  }
282
+ async function createPixRuntimeServices(options) {
283
+ await ensureBundledSkillsInstalledOnce();
284
+ await ensurePiToolsSuiteExtensionInstalledOnce({ agentDir: options.agentDir });
285
+ const bundledExtensionPaths = await getBundledExtensionPathsAsync();
286
+ return await createAgentSessionServices({
287
+ cwd: options.cwd,
288
+ agentDir: options.agentDir,
289
+ resourceLoaderOptions: {
290
+ ...(options.config.ignoreContextFiles ? { noContextFiles: true } : {}),
291
+ ...(options.eventBus === undefined ? {} : { eventBus: options.eventBus }),
292
+ ...(bundledExtensionPaths.length === 0 ? {} : {
293
+ additionalExtensionPaths: bundledExtensionPaths,
294
+ extensionsOverride: prioritizeBundledQuestionExtension,
295
+ }),
296
+ },
297
+ });
298
+ }
299
+ function reusableRuntimeServices(runtime, cwd, agentDir) {
300
+ const services = runtime?.services;
301
+ return services && sameRuntimeServiceTarget(services, cwd, agentDir) ? services : undefined;
302
+ }
303
+ function sameRuntimeServiceTarget(services, cwd, agentDir) {
304
+ return normalizePathForCompare(services.cwd) === normalizePathForCompare(cwd)
305
+ && normalizePathForCompare(services.agentDir) === normalizePathForCompare(agentDir);
306
+ }
@@ -19,6 +19,7 @@ export type AppSessionEventControllerState = {
19
19
  currentAssistantTextBlockContentIndex: number | undefined;
20
20
  assistantTextBlocksByContentIndex: Map<number, string>;
21
21
  currentThinkingEntryId: string | undefined;
22
+ currentThinkingEntryStartedAt: number | undefined;
22
23
  assistantMessageClosed: boolean;
23
24
  assistantTextBuffer: string;
24
25
  entryRenderVersions: Map<string, number>;
@@ -74,6 +75,8 @@ export declare class AppSessionEventController {
74
75
  private historyEntries;
75
76
  private historyWindowStart;
76
77
  private currentThinkingEntryId;
78
+ private currentThinkingEntryStartedAt;
79
+ private thinkingElapsedRenderTimer;
77
80
  private assistantMessageClosed;
78
81
  private assistantTextBuffer;
79
82
  constructor(host: AppSessionEventControllerHost);
@@ -133,7 +136,11 @@ export declare class AppSessionEventController {
133
136
  private appendThinkingText;
134
137
  private finishCurrentThinkingEntry;
135
138
  private reconcileThinkingText;
139
+ private syncThinkingElapsedRenderTimer;
140
+ private startThinkingElapsedRenderTimer;
141
+ private stopThinkingElapsedRenderTimer;
136
142
  private currentThinkingLevel;
143
+ private reconcileAssistantTextFromFinalMessage;
137
144
  private renderAssistantToolCallsFromMessage;
138
145
  private upsertPendingToolCall;
139
146
  private upsertToolEntry;
@@ -26,6 +26,8 @@ export class AppSessionEventController {
26
26
  historyEntries = [];
27
27
  historyWindowStart = 0;
28
28
  currentThinkingEntryId;
29
+ currentThinkingEntryStartedAt;
30
+ thinkingElapsedRenderTimer;
29
31
  assistantMessageClosed = false;
30
32
  assistantTextBuffer = "";
31
33
  constructor(host) {
@@ -44,6 +46,7 @@ export class AppSessionEventController {
44
46
  currentAssistantTextBlockContentIndex: this.currentAssistantTextBlockContentIndex,
45
47
  assistantTextBlocksByContentIndex: new Map(this.assistantTextBlocksByContentIndex),
46
48
  currentThinkingEntryId: this.currentThinkingEntryId,
49
+ currentThinkingEntryStartedAt: this.currentThinkingEntryStartedAt,
47
50
  assistantMessageClosed: this.assistantMessageClosed,
48
51
  assistantTextBuffer: this.assistantTextBuffer,
49
52
  entryRenderVersions: new Map(this.entryRenderVersions),
@@ -71,6 +74,8 @@ export class AppSessionEventController {
71
74
  for (const [key, value] of state.assistantTextBlocksByContentIndex)
72
75
  this.assistantTextBlocksByContentIndex.set(key, value);
73
76
  this.currentThinkingEntryId = state.currentThinkingEntryId;
77
+ this.currentThinkingEntryStartedAt = state.currentThinkingEntryStartedAt;
78
+ this.syncThinkingElapsedRenderTimer();
74
79
  this.assistantMessageClosed = state.assistantMessageClosed;
75
80
  this.assistantTextBuffer = state.assistantTextBuffer;
76
81
  this.entryRenderVersions.clear();
@@ -94,6 +99,8 @@ export class AppSessionEventController {
94
99
  this.assistantTextBlocksByContentIndex.clear();
95
100
  this.finalizedToolCallContentIndexes.clear();
96
101
  this.currentThinkingEntryId = undefined;
102
+ this.currentThinkingEntryStartedAt = undefined;
103
+ this.stopThinkingElapsedRenderTimer();
97
104
  this.assistantMessageClosed = false;
98
105
  this.assistantTextBuffer = "";
99
106
  this.olderHistoryLoader = undefined;
@@ -195,7 +202,7 @@ export class AppSessionEventController {
195
202
  this.host.updateQueuedMessageStatus();
196
203
  break;
197
204
  case "message_update":
198
- this.handleMessageUpdate(event.assistantMessageEvent);
205
+ this.handleMessageUpdate(event);
199
206
  break;
200
207
  case "tool_execution_start":
201
208
  this.finishCurrentThinkingEntry();
@@ -506,7 +513,8 @@ export class AppSessionEventController {
506
513
  return;
507
514
  this.host.recordWorkspaceMutationForUserEntry(prepared.userEntryId, mutation);
508
515
  }
509
- handleMessageUpdate(assistantEvent) {
516
+ handleMessageUpdate(event) {
517
+ const assistantEvent = event.assistantMessageEvent;
510
518
  if (this.assistantMessageClosed && assistantEvent.type !== "done")
511
519
  return;
512
520
  this.assistantMessageClosed = false;
@@ -522,7 +530,13 @@ export class AppSessionEventController {
522
530
  case "text_delta":
523
531
  this.finishCurrentThinkingEntry();
524
532
  this.host.setSessionActivity("running");
525
- this.appendAssistantText(assistantEvent.delta, assistantEvent.contentIndex);
533
+ {
534
+ const snapshotText = assistantTextSnapshotForContentIndex(event.message, assistantEvent.partial, assistantEvent.contentIndex);
535
+ if (snapshotText === undefined)
536
+ this.appendAssistantText(assistantEvent.delta, assistantEvent.contentIndex);
537
+ else
538
+ this.reconcileAssistantTextBlock(snapshotText, assistantEvent.contentIndex, { keepOpen: true });
539
+ }
526
540
  break;
527
541
  case "text_end":
528
542
  this.finishCurrentThinkingEntry();
@@ -556,6 +570,7 @@ export class AppSessionEventController {
556
570
  this.handleToolCallStreamUpdate(assistantEvent.contentIndex, assistantEvent.partial, assistantEvent.toolCall);
557
571
  break;
558
572
  case "done":
573
+ this.reconcileAssistantTextFromFinalMessage(assistantEvent.message);
559
574
  this.renderAssistantToolCallsFromMessage(assistantEvent.message);
560
575
  this.finishCurrentThinkingEntry();
561
576
  this.flushAssistantTextBuffer(true);
@@ -616,7 +631,7 @@ export class AppSessionEventController {
616
631
  entry.text += visibleText;
617
632
  this.touchEntry(entry);
618
633
  }
619
- reconcileAssistantTextBlock(content, contentIndex) {
634
+ reconcileAssistantTextBlock(content, contentIndex, options = {}) {
620
635
  this.flushAssistantTextBuffer(true);
621
636
  const hasVisibleTextBeforeBlock = this.hasVisibleTextBeforeCurrentAssistantBlock();
622
637
  // C.11: normalise CRLF in the final block content (see appendAssistantText).
@@ -667,9 +682,16 @@ export class AppSessionEventController {
667
682
  this.currentAssistantEntryId = entry.id;
668
683
  if (contentIndex !== undefined)
669
684
  this.assistantTextBlocksByContentIndex.set(contentIndex, visibleText);
670
- this.currentAssistantTextBlockEntryId = undefined;
671
- this.currentAssistantTextBlockStartLength = undefined;
672
- this.currentAssistantTextBlockContentIndex = undefined;
685
+ if (options.keepOpen) {
686
+ this.currentAssistantTextBlockEntryId = entry.id;
687
+ this.currentAssistantTextBlockStartLength = startLength;
688
+ this.currentAssistantTextBlockContentIndex = contentIndex;
689
+ }
690
+ else {
691
+ this.currentAssistantTextBlockEntryId = undefined;
692
+ this.currentAssistantTextBlockStartLength = undefined;
693
+ this.currentAssistantTextBlockContentIndex = undefined;
694
+ }
673
695
  this.assistantTextBuffer = "";
674
696
  }
675
697
  ensureAssistantTextBlockStarted(entry) {
@@ -740,12 +762,15 @@ export class AppSessionEventController {
740
762
  : undefined;
741
763
  if (!entry || entry.kind !== "thinking") {
742
764
  const level = this.currentThinkingLevel();
765
+ const startedAt = this.currentThinkingEntryStartedAt ?? Date.now();
766
+ this.currentThinkingEntryStartedAt = startedAt;
743
767
  entry = {
744
768
  id: createId("thinking"),
745
769
  kind: "thinking",
746
770
  text: "",
747
771
  expanded: this.host.toolDefaultExpanded(THINKING_TOOL_NAME),
748
772
  ...(level === undefined ? {} : { level }),
773
+ startedAt,
749
774
  status: "running",
750
775
  };
751
776
  this.addEntry(entry);
@@ -756,17 +781,24 @@ export class AppSessionEventController {
756
781
  delete entry.level;
757
782
  else
758
783
  entry.level = level;
784
+ entry.startedAt ??= this.currentThinkingEntryStartedAt ?? Date.now();
785
+ this.currentThinkingEntryStartedAt = entry.startedAt;
786
+ delete entry.finishedAt;
759
787
  entry.status = "running";
760
788
  entry.text += delta;
789
+ this.startThinkingElapsedRenderTimer();
761
790
  this.touchEntry(entry);
762
791
  }
763
792
  finishCurrentThinkingEntry() {
764
793
  const entry = this.currentThinkingEntryId ? this.findEntry(this.currentThinkingEntryId) : undefined;
765
794
  if (entry?.kind === "thinking" && entry.status !== "done") {
766
795
  entry.status = "done";
796
+ entry.finishedAt ??= Date.now();
767
797
  this.touchEntry(entry);
768
798
  }
769
799
  this.currentThinkingEntryId = undefined;
800
+ this.currentThinkingEntryStartedAt = undefined;
801
+ this.stopThinkingElapsedRenderTimer();
770
802
  }
771
803
  reconcileThinkingText(content) {
772
804
  let entry = this.currentThinkingEntryId
@@ -774,12 +806,15 @@ export class AppSessionEventController {
774
806
  : undefined;
775
807
  if (!entry || entry.kind !== "thinking") {
776
808
  const level = this.currentThinkingLevel();
809
+ const startedAt = this.currentThinkingEntryStartedAt ?? Date.now();
810
+ this.currentThinkingEntryStartedAt = startedAt;
777
811
  entry = {
778
812
  id: createId("thinking"),
779
813
  kind: "thinking",
780
814
  text: "",
781
815
  expanded: this.host.toolDefaultExpanded(THINKING_TOOL_NAME),
782
816
  ...(level === undefined ? {} : { level }),
817
+ startedAt,
783
818
  status: "running",
784
819
  };
785
820
  this.addEntry(entry);
@@ -792,13 +827,63 @@ export class AppSessionEventController {
792
827
  delete entry.level;
793
828
  else
794
829
  entry.level = level;
830
+ entry.startedAt ??= this.currentThinkingEntryStartedAt ?? Date.now();
831
+ this.currentThinkingEntryStartedAt = entry.startedAt;
832
+ delete entry.finishedAt;
795
833
  entry.status = "running";
834
+ this.startThinkingElapsedRenderTimer();
796
835
  this.touchEntry(entry);
797
836
  }
798
837
  }
838
+ syncThinkingElapsedRenderTimer() {
839
+ const entry = this.currentThinkingEntryId ? this.findEntry(this.currentThinkingEntryId) : undefined;
840
+ if (entry?.kind === "thinking" && entry.status === "running" && entry.startedAt !== undefined) {
841
+ this.startThinkingElapsedRenderTimer();
842
+ return;
843
+ }
844
+ this.stopThinkingElapsedRenderTimer();
845
+ }
846
+ startThinkingElapsedRenderTimer() {
847
+ if (this.thinkingElapsedRenderTimer)
848
+ return;
849
+ this.thinkingElapsedRenderTimer = setInterval(() => {
850
+ if (!this.host.isRunning()) {
851
+ this.stopThinkingElapsedRenderTimer();
852
+ return;
853
+ }
854
+ this.host.scheduleRender();
855
+ }, 1000);
856
+ this.thinkingElapsedRenderTimer.unref?.();
857
+ }
858
+ stopThinkingElapsedRenderTimer() {
859
+ if (!this.thinkingElapsedRenderTimer)
860
+ return;
861
+ clearInterval(this.thinkingElapsedRenderTimer);
862
+ this.thinkingElapsedRenderTimer = undefined;
863
+ }
799
864
  currentThinkingLevel() {
800
865
  return this.host.runtime()?.session.thinkingLevel;
801
866
  }
867
+ reconcileAssistantTextFromFinalMessage(message) {
868
+ const openContentIndex = this.currentAssistantTextBlockContentIndex;
869
+ if (openContentIndex !== undefined) {
870
+ const content = assistantTextContentAt(message, openContentIndex);
871
+ if (content !== undefined)
872
+ this.reconcileAssistantTextBlock(content, openContentIndex);
873
+ return;
874
+ }
875
+ const textBlocks = assistantTextContents(message);
876
+ if (textBlocks.length !== 1)
877
+ return;
878
+ const entry = this.currentAssistantEntryId ? this.findEntry(this.currentAssistantEntryId) : undefined;
879
+ if (entry?.kind !== "assistant")
880
+ return;
881
+ const visibleText = assistantStreamVisibleTextForCompleteBlock(textBlocks[0] ?? "", false);
882
+ if (!visibleText || entry.text === visibleText)
883
+ return;
884
+ entry.text = visibleText;
885
+ this.touchEntry(entry);
886
+ }
802
887
  renderAssistantToolCallsFromMessage(message) {
803
888
  if (!isRecord(message) || !Array.isArray(message.content))
804
889
  return;
@@ -879,6 +964,8 @@ export class AppSessionEventController {
879
964
  this.currentAssistantTextBlockStartLength = undefined;
880
965
  this.currentAssistantTextBlockContentIndex = undefined;
881
966
  this.currentThinkingEntryId = undefined;
967
+ this.currentThinkingEntryStartedAt = undefined;
968
+ this.stopThinkingElapsedRenderTimer();
882
969
  this.assistantTextBuffer = "";
883
970
  this.assistantTextBlocksByContentIndex.clear();
884
971
  this.finalizedToolCallContentIndexes.clear();
@@ -891,6 +978,22 @@ function partialToolCallAt(partial, contentIndex) {
891
978
  const block = partial.content[contentIndex];
892
979
  return isRecord(block) && block.type === "toolCall" ? block : undefined;
893
980
  }
981
+ function assistantTextSnapshotForContentIndex(message, partial, contentIndex) {
982
+ if (contentIndex === undefined)
983
+ return undefined;
984
+ return assistantTextContentAt(message, contentIndex) ?? assistantTextContentAt(partial, contentIndex);
985
+ }
986
+ function assistantTextContentAt(value, contentIndex) {
987
+ if (!isRecord(value) || !Array.isArray(value.content))
988
+ return undefined;
989
+ const block = value.content[contentIndex];
990
+ return isRecord(block) && block.type === "text" && typeof block.text === "string" ? block.text : undefined;
991
+ }
992
+ function assistantTextContents(value) {
993
+ if (!isRecord(value) || !Array.isArray(value.content))
994
+ return [];
995
+ return value.content.flatMap((block) => (isRecord(block) && block.type === "text" && typeof block.text === "string" ? [block.text] : []));
996
+ }
894
997
  function assistantStreamVisibleTextForCompleteBlock(text, hasVisibleTextBeforeBlock) {
895
998
  let buffer = text;
896
999
  let visibleText = "";
@@ -641,7 +641,9 @@ export class AppTabsController {
641
641
  void this.saveTabs();
642
642
  this.scheduleTabPrewarm();
643
643
  const cachedView = this.sessionViewsByTabId.get(target.id);
644
- if (cachedView && this.host.restoreSessionView) {
644
+ const cachedViewNeedsHistoryReload = this.tabIdsNeedingHistoryReload.has(target.id)
645
+ && this.sessionActivity(targetRuntime.session) !== "running";
646
+ if (cachedView && this.host.restoreSessionView && !cachedViewNeedsHistoryReload) {
645
647
  this.host.restoreSessionView(cachedView);
646
648
  this.restoreDeferredUserMessages(target.id);
647
649
  this.host.setSessionStatus(targetRuntime.session);
@@ -13,7 +13,9 @@ export declare class AppSubagentsWidgetController {
13
13
  private pollTimer;
14
14
  private pollInFlight;
15
15
  private currentRunDir;
16
+ private currentRunDirs;
16
17
  private state;
18
+ private readonly runFreshnessByRunDir;
17
19
  private readonly taskPreviewsByRunDir;
18
20
  private readonly snapshotByRunDir;
19
21
  private refreshGeneration;
@@ -32,10 +34,16 @@ export declare class AppSubagentsWidgetController {
32
34
  private schedulePoll;
33
35
  private poll;
34
36
  private refreshFromFiles;
35
- private findActiveRegistryRunDirForCurrentSession;
37
+ private findActiveRegistryRunDirsForCurrentSession;
36
38
  private registryRunDirsNewestFirst;
37
- private clearMissingRunAndMaybeSelectReplacement;
38
39
  private clearCachedRun;
40
+ private buildStateFromRuns;
41
+ private orderRuns;
42
+ private orderRunDirs;
43
+ private rememberRunFreshness;
44
+ private runFreshness;
45
+ private runFreshnessFromAgents;
46
+ private parseRunTimestamp;
39
47
  private updateState;
40
48
  private isCurrentGeneration;
41
49
  private shouldContinuePolling;