pi-ui-extend 0.1.34 → 0.1.36

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 (73) hide show
  1. package/README.md +20 -0
  2. package/dist/app/app.d.ts +1 -0
  3. package/dist/app/app.js +12 -2
  4. package/dist/app/commands/command-host.d.ts +1 -0
  5. package/dist/app/commands/command-model-actions.d.ts +1 -0
  6. package/dist/app/commands/command-model-actions.js +32 -0
  7. package/dist/app/commands/command-navigation-actions.js +3 -0
  8. package/dist/app/commands/command-session-actions.js +2 -0
  9. package/dist/app/constants.d.ts +2 -1
  10. package/dist/app/constants.js +6 -1
  11. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  12. package/dist/app/extensions/extension-actions-controller.js +4 -0
  13. package/dist/app/input/input-controller.d.ts +5 -1
  14. package/dist/app/input/input-controller.js +122 -16
  15. package/dist/app/input/input-paste-handler.js +3 -1
  16. package/dist/app/input/terminal-edit-shortcuts.d.ts +21 -0
  17. package/dist/app/input/terminal-edit-shortcuts.js +92 -16
  18. package/dist/app/popup/popup-action-controller.d.ts +1 -0
  19. package/dist/app/popup/popup-action-controller.js +1 -0
  20. package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
  21. package/dist/app/rendering/conversation-entry-renderer.js +1 -1
  22. package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
  23. package/dist/app/rendering/conversation-tool-renderer.js +21 -0
  24. package/dist/app/rendering/conversation-viewport.d.ts +3 -0
  25. package/dist/app/rendering/conversation-viewport.js +41 -5
  26. package/dist/app/rendering/editor-layout-renderer.js +3 -2
  27. package/dist/app/rendering/editor-panels.js +27 -10
  28. package/dist/app/runtime.d.ts +1 -0
  29. package/dist/app/runtime.js +33 -14
  30. package/dist/app/session/session-event-controller.d.ts +7 -0
  31. package/dist/app/session/session-event-controller.js +78 -0
  32. package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
  33. package/dist/app/session/session-lifecycle-controller.js +7 -0
  34. package/dist/app/session/tabs-controller.d.ts +1 -0
  35. package/dist/app/session/tabs-controller.js +4 -1
  36. package/dist/app/subagents/subagents-widget-controller.d.ts +10 -2
  37. package/dist/app/subagents/subagents-widget-controller.js +141 -70
  38. package/dist/app/terminal/terminal-controller.d.ts +10 -0
  39. package/dist/app/terminal/terminal-controller.js +91 -2
  40. package/dist/app/todo/todo-model.js +2 -0
  41. package/dist/app/todo/todo-widget-controller.d.ts +2 -0
  42. package/dist/app/todo/todo-widget-controller.js +17 -7
  43. package/dist/app/types.d.ts +4 -0
  44. package/dist/app/workspace/workspace-actions-controller.d.ts +1 -0
  45. package/dist/app/workspace/workspace-actions-controller.js +1 -0
  46. package/dist/bundled-extensions/question/tui.js +8 -1
  47. package/dist/bundled-extensions/session-title/index.js +65 -14
  48. package/dist/input-editor-files.js +23 -4
  49. package/dist/markdown-format.d.ts +4 -1
  50. package/dist/markdown-format.js +76 -9
  51. package/external/pi-tools-suite/README.md +71 -1
  52. package/external/pi-tools-suite/package.json +5 -5
  53. package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -6
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +133 -37
  55. package/external/pi-tools-suite/src/context-usage.ts +6 -1
  56. package/external/pi-tools-suite/src/dcp/commands.ts +3 -2
  57. package/external/pi-tools-suite/src/dcp/compress-tool.ts +9 -4
  58. package/external/pi-tools-suite/src/dcp/config.ts +142 -6
  59. package/external/pi-tools-suite/src/dcp/index.ts +20 -8
  60. package/external/pi-tools-suite/src/dcp/prompts.ts +17 -9
  61. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +59 -15
  62. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +6 -8
  63. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
  64. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +51 -1
  65. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +16 -11
  66. package/external/pi-tools-suite/src/model-tools/index.ts +24 -12
  67. package/external/pi-tools-suite/src/prompt-commands/index.ts +11 -2
  68. package/external/pi-tools-suite/src/telegram-mirror/index.ts +66 -27
  69. package/external/pi-tools-suite/src/todo/index.ts +87 -16
  70. package/external/pi-tools-suite/src/todo/state/store.ts +41 -10
  71. package/external/pi-tools-suite/src/todo/todo.ts +49 -6
  72. package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
  73. package/package.json +7 -5
@@ -3,34 +3,84 @@ const CONTROL_MODIFIER_FLAG = 4;
3
3
  const COMMAND_MODIFIER_FLAG = 8;
4
4
  const LOCK_MODIFIER_MASK = 64 + 128;
5
5
  const KEY_CODE_C = 99;
6
+ const KEY_CODE_ENTER = 13;
7
+ const KEY_CODE_V = 118;
6
8
  const KEY_CODE_Y = 121;
7
9
  const KEY_CODE_Z = 122;
8
10
  const CYRILLIC_SMALL_ES_CODE = 1089;
9
11
  const CYRILLIC_CAPITAL_ES_CODE = 1057;
12
+ const CYRILLIC_SMALL_EM_CODE = 1084;
13
+ const CYRILLIC_CAPITAL_EM_CODE = 1052;
14
+ const KITTY_ARROW_CODEPOINTS = {
15
+ A: -1,
16
+ B: -2,
17
+ C: -3,
18
+ D: -4,
19
+ };
10
20
  const KITTY_CSI_U_SEQUENCE = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u/;
21
+ const KITTY_ARROW_SEQUENCE = /^\x1b\[1;(\d+)(?::(\d+))?([ABCD])/;
11
22
  const XTERM_MODIFY_OTHER_KEYS_SEQUENCE = /^\x1b\[27;(\d+);(\d+)~/;
12
- export function parseTerminalEditShortcutSequence(input) {
23
+ export function parseTerminalModifiedKeySequence(input) {
13
24
  const kitty = parseKittyCsiUSequence(input);
14
25
  if (kitty)
15
- return terminalEditShortcutResult(kitty);
26
+ return { kind: "key", key: kitty };
27
+ const kittyArrow = parseKittyArrowSequence(input);
28
+ if (kittyArrow)
29
+ return { kind: "key", key: kittyArrow };
16
30
  const xterm = parseXtermModifyOtherKeysSequence(input);
17
31
  if (xterm)
18
- return terminalEditShortcutResult(xterm);
19
- if (isPotentialEditShortcutPrefix(input))
32
+ return { kind: "key", key: xterm };
33
+ if (isPotentialModifiedKeyPrefix(input))
20
34
  return { kind: "pending" };
21
35
  return { kind: "none" };
22
36
  }
37
+ export function parseTerminalEditShortcutSequence(input) {
38
+ const result = parseTerminalModifiedKeySequence(input);
39
+ if (result.kind === "pending")
40
+ return { kind: "pending" };
41
+ if (result.kind === "none")
42
+ return { kind: "none" };
43
+ return terminalEditShortcutResult(result.key);
44
+ }
23
45
  export function parseTerminalInterruptSequence(input) {
24
- const kitty = parseKittyCsiUSequence(input);
25
- if (kitty && terminalKeyIsControlC(kitty))
26
- return { kind: "interrupt", length: kitty.length };
27
- const xterm = parseXtermModifyOtherKeysSequence(input);
28
- if (xterm && terminalKeyIsControlC(xterm))
29
- return { kind: "interrupt", length: xterm.length };
30
- if (isPotentialInterruptPrefix(input))
46
+ const result = parseTerminalModifiedKeySequence(input);
47
+ if (result.kind === "pending")
31
48
  return { kind: "pending" };
49
+ if (result.kind === "key" && terminalKeyIsControlC(result.key))
50
+ return { kind: "interrupt", length: result.key.length };
51
+ if (result.kind === "key" || isPotentialInterruptPrefix(input))
52
+ return { kind: "none" };
32
53
  return { kind: "none" };
33
54
  }
55
+ export function terminalKeyIsShiftEnter(key) {
56
+ const effectiveModifier = key.modifier & ~LOCK_MODIFIER_MASK;
57
+ if ((effectiveModifier & SHIFT_MODIFIER_FLAG) === 0)
58
+ return false;
59
+ return terminalKeyMatchesCodepoint(key, KEY_CODE_ENTER);
60
+ }
61
+ export function terminalKeyIsClipboardImagePaste(key) {
62
+ const effectiveModifier = key.modifier & ~LOCK_MODIFIER_MASK;
63
+ if ((effectiveModifier & (CONTROL_MODIFIER_FLAG | COMMAND_MODIFIER_FLAG)) === 0)
64
+ return false;
65
+ return terminalKeyMatchesCodepoint(key, KEY_CODE_V, CYRILLIC_SMALL_EM_CODE, CYRILLIC_CAPITAL_EM_CODE);
66
+ }
67
+ export function terminalKeyShouldIgnore(key) {
68
+ return key.eventType === 3;
69
+ }
70
+ export function terminalKeyArrowDirection(key) {
71
+ const effectiveModifier = key.modifier & ~LOCK_MODIFIER_MASK;
72
+ if (effectiveModifier !== 0)
73
+ return undefined;
74
+ if (key.codepoint === KITTY_ARROW_CODEPOINTS.A)
75
+ return "up";
76
+ if (key.codepoint === KITTY_ARROW_CODEPOINTS.B)
77
+ return "down";
78
+ if (key.codepoint === KITTY_ARROW_CODEPOINTS.C)
79
+ return "right";
80
+ if (key.codepoint === KITTY_ARROW_CODEPOINTS.D)
81
+ return "left";
82
+ return undefined;
83
+ }
34
84
  export function terminalEditShortcutForControlChar(char, shiftPressed) {
35
85
  if (char === "\u001a")
36
86
  return shiftPressed ? "redo" : "undo";
@@ -56,6 +106,24 @@ function parseKittyCsiUSequence(input) {
56
106
  length: match[0].length,
57
107
  };
58
108
  }
109
+ function parseKittyArrowSequence(input) {
110
+ const match = KITTY_ARROW_SEQUENCE.exec(input);
111
+ if (!match)
112
+ return undefined;
113
+ const modifierValue = Number.parseInt(match[1] ?? "", 10);
114
+ const eventType = match[2] ? Number.parseInt(match[2], 10) : undefined;
115
+ const arrow = match[3];
116
+ const codepoint = arrow ? KITTY_ARROW_CODEPOINTS[arrow] : undefined;
117
+ if (!Number.isFinite(modifierValue) || codepoint === undefined)
118
+ return undefined;
119
+ return {
120
+ codepoint,
121
+ baseLayoutKey: undefined,
122
+ modifier: modifierValue - 1,
123
+ eventType: Number.isFinite(eventType) ? eventType : undefined,
124
+ length: match[0].length,
125
+ };
126
+ }
59
127
  function parseXtermModifyOtherKeysSequence(input) {
60
128
  const match = XTERM_MODIFY_OTHER_KEYS_SEQUENCE.exec(input);
61
129
  if (!match)
@@ -95,6 +163,17 @@ function interruptCodepointIsC(codepoint) {
95
163
  const normalized = normalizeLetterCodepoint(codepoint);
96
164
  return normalized === KEY_CODE_C || normalized === CYRILLIC_SMALL_ES_CODE || normalized === CYRILLIC_CAPITAL_ES_CODE;
97
165
  }
166
+ function terminalKeyMatchesCodepoint(key, ...expectedCodepoints) {
167
+ return expectedCodepoints.some((codepoint) => keyMatchesCodepoint(key, codepoint));
168
+ }
169
+ function keyMatchesCodepoint(key, expectedCodepoint) {
170
+ const normalizedExpected = normalizeLetterCodepoint(expectedCodepoint);
171
+ const primary = normalizeLetterCodepoint(key.codepoint);
172
+ if (primary === normalizedExpected)
173
+ return true;
174
+ const baseLayout = key.baseLayoutKey;
175
+ return baseLayout !== undefined && normalizeLetterCodepoint(baseLayout) === normalizedExpected;
176
+ }
98
177
  function terminalEditShortcutForKey(key, effectiveModifier) {
99
178
  const codepoint = editShortcutCodepoint(key);
100
179
  if (codepoint === KEY_CODE_Y)
@@ -114,16 +193,13 @@ function normalizeLetterCodepoint(codepoint) {
114
193
  return codepoint + 32;
115
194
  return codepoint;
116
195
  }
117
- function isPotentialEditShortcutPrefix(input) {
196
+ function isPotentialModifiedKeyPrefix(input) {
118
197
  if (!input.startsWith("\x1b["))
119
198
  return false;
120
199
  if (input.includes("u") || input.includes("~"))
121
200
  return false;
122
201
  const body = input.slice(2);
123
- if (!/^[\d:;]*$/.test(body))
124
- return false;
125
- const possibleStarts = ["122", "121", "90", "27;"];
126
- return possibleStarts.some((start) => start.startsWith(body) || body.startsWith(start));
202
+ return /^[\d:;]*$/.test(body);
127
203
  }
128
204
  function isPotentialInterruptPrefix(input) {
129
205
  if (!input.startsWith("\x1b["))
@@ -7,6 +7,7 @@ import type { Entry, SlashCommand, UserMessageJumpMenuValue } from "../types.js"
7
7
  import type { AppWorkspaceActionsController } from "../workspace/workspace-actions-controller.js";
8
8
  export type AppPopupActionControllerHost = {
9
9
  runtime(): AgentSessionRuntime | undefined;
10
+ awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime): Promise<void>;
10
11
  getBuiltinSlashCommands(): readonly SlashCommand[];
11
12
  isRunning(): boolean;
12
13
  setInput(value: string): void;
@@ -228,6 +228,7 @@ export class AppPopupActionController {
228
228
  this.host.setStatus("switching session");
229
229
  this.host.render();
230
230
  try {
231
+ await this.host.awaitCurrentSessionExtensions(runtime);
231
232
  const result = await runtime.switchSession(session.path);
232
233
  if (result.cancelled) {
233
234
  this.host.addEntry({ id: createId("system"), kind: "system", text: "Resume cancelled." });
@@ -14,6 +14,7 @@ export type ConversationEntryRenderOptions = {
14
14
  availableThinkingLevels?: readonly string[];
15
15
  superCompactTools?: boolean;
16
16
  allThinkingExpanded?: boolean;
17
+ currentTimeMs?: number;
17
18
  renderInlineUserMessageMenu: (entry: Extract<Entry, {
18
19
  kind: "user";
19
20
  }>, context: InlineUserMessageMenuContext) => RenderedLine[];
@@ -75,7 +75,7 @@ function renderAssistantLines(text, width, options) {
75
75
  if (!displayText)
76
76
  return [];
77
77
  const { left: contentLeft, contentWidth } = horizontalPaddingLayout(width);
78
- const contentLines = renderMarkdownTextLines(displayText, contentWidth, contentLeft);
78
+ const contentLines = renderMarkdownTextLines(displayText, contentWidth, contentLeft, { preserveWrappedWordSeparator: true });
79
79
  if (contentLines.length === 0)
80
80
  return [];
81
81
  const lines = [];
@@ -8,6 +8,7 @@ export type ConversationToolRenderOptions = {
8
8
  availableThinkingLevels?: readonly string[];
9
9
  superCompactTools?: boolean;
10
10
  allThinkingExpanded?: boolean;
11
+ currentTimeMs?: number;
11
12
  };
12
13
  export declare function renderConversationToolEntry(entry: Extract<Entry, {
13
14
  kind: "tool";
@@ -56,9 +56,11 @@ export function renderThinkingEntry(entry, width, options) {
56
56
  const headerColorOverride = entry.level
57
57
  ? thinkingLevelThemeColor(entry.level, options.colors, options.availableThinkingLevels)
58
58
  : undefined;
59
+ const elapsed = thinkingElapsedText(entry, options.currentTimeMs ?? Date.now());
59
60
  return renderToolBlock({
60
61
  id: entry.id,
61
62
  toolName: THINKING_TOOL_NAME,
63
+ ...(elapsed === undefined ? {} : { headerArgs: elapsed }),
62
64
  expanded,
63
65
  status: entry.status,
64
66
  isError: false,
@@ -74,6 +76,25 @@ export function renderThinkingEntry(entry, width, options) {
74
76
  ...(headerColorOverride === undefined ? {} : { headerColorOverride }),
75
77
  });
76
78
  }
79
+ function thinkingElapsedText(entry, currentTimeMs) {
80
+ if (entry.startedAt === undefined)
81
+ return undefined;
82
+ const endTimeMs = entry.finishedAt ?? currentTimeMs;
83
+ const elapsedMs = Math.max(0, endTimeMs - entry.startedAt);
84
+ return formatThinkingElapsed(elapsedMs);
85
+ }
86
+ function formatThinkingElapsed(elapsedMs) {
87
+ const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000));
88
+ const seconds = totalSeconds % 60;
89
+ const totalMinutes = Math.floor(totalSeconds / 60);
90
+ if (totalMinutes < 1)
91
+ return `${seconds}s`;
92
+ const minutes = totalMinutes % 60;
93
+ const hours = Math.floor(totalMinutes / 60);
94
+ if (hours < 1)
95
+ return `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
96
+ return `${hours}h ${minutes.toString().padStart(2, "0")}m`;
97
+ }
77
98
  function trimTrailingBlankLines(text) {
78
99
  return text.replace(/(?:\r?\n[ \t]*)+$/u, "");
79
100
  }
@@ -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 },
@@ -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;