pi-ui-extend 0.1.32 → 0.1.33

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 (93) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +2 -0
  3. package/dist/app/app.js +28 -0
  4. package/dist/app/commands/command-session-actions.js +29 -1
  5. package/dist/app/constants.d.ts +1 -1
  6. package/dist/app/constants.js +2 -2
  7. package/dist/app/icons.d.ts +4 -9
  8. package/dist/app/icons.js +11 -34
  9. package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
  10. package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
  11. package/dist/app/rendering/conversation-tool-renderer.js +12 -18
  12. package/dist/app/rendering/conversation-viewport.d.ts +4 -0
  13. package/dist/app/rendering/conversation-viewport.js +144 -13
  14. package/dist/app/rendering/dcp-stats.js +42 -16
  15. package/dist/app/rendering/render-controller.js +4 -0
  16. package/dist/app/rendering/status-line-renderer.d.ts +7 -1
  17. package/dist/app/rendering/status-line-renderer.js +21 -0
  18. package/dist/app/rendering/tab-line-renderer.js +2 -2
  19. package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
  20. package/dist/app/rendering/tool-block-renderer.js +37 -11
  21. package/dist/app/runtime.js +1 -1
  22. package/dist/app/screen/mouse-controller.d.ts +5 -1
  23. package/dist/app/screen/mouse-controller.js +16 -0
  24. package/dist/app/screen/scroll-controller.d.ts +20 -0
  25. package/dist/app/screen/scroll-controller.js +127 -10
  26. package/dist/app/session/lazy-session-manager.js +35 -5
  27. package/dist/app/session/pix-system-message.d.ts +1 -0
  28. package/dist/app/session/pix-system-message.js +14 -3
  29. package/dist/app/session/queued-message-controller.d.ts +11 -4
  30. package/dist/app/session/queued-message-controller.js +74 -59
  31. package/dist/app/session/queued-message-entries.d.ts +2 -1
  32. package/dist/app/session/queued-message-entries.js +12 -1
  33. package/dist/app/session/session-event-controller.d.ts +42 -1
  34. package/dist/app/session/session-event-controller.js +473 -29
  35. package/dist/app/session/session-history.js +23 -4
  36. package/dist/app/session/tabs-controller.d.ts +11 -1
  37. package/dist/app/session/tabs-controller.js +102 -21
  38. package/dist/app/types.d.ts +14 -1
  39. package/dist/bundled-extensions/question/contract.d.ts +25 -0
  40. package/dist/bundled-extensions/question/contract.js +94 -0
  41. package/dist/bundled-extensions/question/index.d.ts +7 -0
  42. package/dist/bundled-extensions/question/index.js +28 -0
  43. package/dist/bundled-extensions/question/render.d.ts +4 -0
  44. package/dist/bundled-extensions/question/render.js +27 -0
  45. package/dist/bundled-extensions/question/result.d.ts +6 -0
  46. package/dist/bundled-extensions/question/result.js +84 -0
  47. package/dist/bundled-extensions/question/tool-description.d.ts +7 -0
  48. package/dist/bundled-extensions/question/tool-description.js +11 -0
  49. package/dist/bundled-extensions/question/tui.d.ts +2 -0
  50. package/dist/bundled-extensions/question/tui.js +577 -0
  51. package/dist/bundled-extensions/question/types.d.ts +103 -0
  52. package/dist/bundled-extensions/question/types.js +1 -0
  53. package/dist/bundled-extensions/session-title/config.d.ts +17 -0
  54. package/dist/bundled-extensions/session-title/config.js +150 -0
  55. package/dist/bundled-extensions/session-title/index.d.ts +5 -0
  56. package/dist/bundled-extensions/session-title/index.js +384 -0
  57. package/dist/bundled-extensions/session-title/title-generation.d.ts +26 -0
  58. package/dist/bundled-extensions/session-title/title-generation.js +141 -0
  59. package/dist/bundled-extensions/terminal-bell/index.d.ts +14 -0
  60. package/dist/bundled-extensions/terminal-bell/index.js +491 -0
  61. package/dist/config.d.ts +1 -1
  62. package/dist/config.js +2 -1
  63. package/dist/default-pix-config.js +2 -1
  64. package/dist/icon-theme.d.ts +7 -0
  65. package/dist/icon-theme.js +36 -0
  66. package/dist/schemas/pi-tools-suite-schema.d.ts +4 -0
  67. package/dist/schemas/pi-tools-suite-schema.js +5 -0
  68. package/dist/schemas/pix-schema.d.ts +1 -0
  69. package/dist/schemas/pix-schema.js +1 -0
  70. package/external/pi-tools-suite/README.md +7 -7
  71. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +16 -16
  72. package/external/pi-tools-suite/src/async-subagents/core/state.ts +18 -4
  73. package/external/pi-tools-suite/src/async-subagents/core/types.ts +4 -0
  74. package/external/pi-tools-suite/src/async-subagents/tools/result.ts +14 -26
  75. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +0 -1
  76. package/external/pi-tools-suite/src/dcp/config.ts +14 -14
  77. package/external/pi-tools-suite/src/dcp/index.ts +31 -43
  78. package/external/pi-tools-suite/src/dcp/state-persistence.ts +151 -0
  79. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +25 -18
  80. package/external/pi-tools-suite/src/tool-descriptions.ts +6 -7
  81. package/package.json +3 -2
  82. package/schemas/pi-tools-suite.json +14 -0
  83. package/schemas/pix.json +7 -0
  84. package/extensions/question/contract.ts +0 -100
  85. package/extensions/question/index.ts +0 -34
  86. package/extensions/question/render.ts +0 -28
  87. package/extensions/question/result.ts +0 -86
  88. package/extensions/question/tool-description.ts +0 -11
  89. package/extensions/question/tui.ts +0 -629
  90. package/extensions/question/types.ts +0 -123
  91. package/extensions/session-title/config.ts +0 -164
  92. package/extensions/session-title/index.ts +0 -502
  93. package/extensions/terminal-bell/index.ts +0 -345
@@ -1,8 +1,11 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
1
3
  import { normalizeToolName, parseArgsText } from "../../tool-renderers/utils.js";
2
4
  const NUDGE_TYPES = ["turn", "iteration", "context-soft", "context-strong"];
3
5
  export function formatDcpStatsToast(session) {
4
- const stats = collectDcpSessionStats(session);
5
- const nudgeStats = collectDcpNudgeStats(session);
6
+ const latestState = resolveLatestDcpState(session);
7
+ const stats = collectDcpSessionStats(session, latestState);
8
+ const nudgeStats = collectDcpNudgeStats(session, latestState?.data);
6
9
  const activeBlocks = stats.activeBlocks ?? 0;
7
10
  const totalBlocks = stats.totalBlocks ?? stats.activeBlocks ?? 0;
8
11
  const totalNudgeEvents = nudgeStats.emitted + nudgeStats.upgraded;
@@ -12,7 +15,8 @@ export function formatDcpStatsToast(session) {
12
15
  ` Tokens saved (estimated): ${fmt(stats.tokensSaved)}`,
13
16
  ` Total pruning operations: ${fmt(stats.totalPruneCount)}`,
14
17
  ` Compression blocks active: ${activeBlocks} / ${totalBlocks} total`,
15
- " Manual mode: off",
18
+ ` Manual mode: ${stats.manualMode === true ? "on" : stats.manualMode === false ? "off" : "unknown"}`,
19
+ ` State source: ${formatStateSource(stats.stateSource)}`,
16
20
  "",
17
21
  "Nudge telemetry:",
18
22
  ` Sent: ${fmt(nudgeStats.emitted)} emitted, ${fmt(nudgeStats.upgraded)} upgraded`,
@@ -28,7 +32,7 @@ export function formatDcpStatsToast(session) {
28
32
  ];
29
33
  return lines.join("\n");
30
34
  }
31
- function collectDcpSessionStats(session) {
35
+ function collectDcpSessionStats(session, latestState) {
32
36
  const usage = session.getContextUsage();
33
37
  const stats = {
34
38
  runs: 0,
@@ -37,14 +41,14 @@ function collectDcpSessionStats(session) {
37
41
  items: 0,
38
42
  summaryTokens: 0,
39
43
  prunedTools: 0,
44
+ stateSource: latestState?.source ?? "tool-results",
40
45
  ...(usage?.tokens != null ? { contextTokens: usage.tokens } : {}),
41
46
  ...(usage?.contextWindow != null ? { contextWindow: usage.contextWindow } : {}),
42
47
  ...(usage?.percent != null ? { contextPercent: usage.percent } : {}),
43
48
  };
44
49
  const branch = session.sessionManager.getBranch();
45
- const latestState = latestCustomEntryData(branch, "dcp-state");
46
50
  if (latestState)
47
- applyDcpStateStats(stats, latestState);
51
+ applyDcpStateStats(stats, latestState.data);
48
52
  for (const entry of branch) {
49
53
  if (entry.type !== "message")
50
54
  continue;
@@ -94,8 +98,10 @@ function applyDcpStateStats(stats, data) {
94
98
  }
95
99
  if (Array.isArray(data.prunedToolIds))
96
100
  stats.prunedTools = data.prunedToolIds.length;
101
+ if (typeof data.manualMode === "boolean")
102
+ stats.manualMode = data.manualMode;
97
103
  }
98
- function collectDcpNudgeStats(session) {
104
+ function collectDcpNudgeStats(session, latestState) {
99
105
  const stats = {
100
106
  emitted: 0,
101
107
  upgraded: 0,
@@ -105,7 +111,6 @@ function collectDcpNudgeStats(session) {
105
111
  activeByType: { "turn": 0, "iteration": 0, "context-soft": 0, "context-strong": 0 },
106
112
  };
107
113
  const branch = session.sessionManager.getBranch();
108
- const latestState = latestCustomEntryData(branch, "dcp-state");
109
114
  if (latestState)
110
115
  applyActiveAnchorStats(stats, latestState);
111
116
  for (const entry of branch) {
@@ -156,19 +161,40 @@ function applyActiveAnchorStats(stats, data) {
156
161
  };
157
162
  }
158
163
  }
164
+ function resolveLatestDcpState(session) {
165
+ const sidecar = loadSidecarDcpState(session);
166
+ if (sidecar)
167
+ return { data: sidecar, source: "sidecar" };
168
+ return undefined;
169
+ }
170
+ function loadSidecarDcpState(session) {
171
+ const sessionManager = session.sessionManager;
172
+ const sessionDir = sessionManager.getSessionDir?.();
173
+ const sessionId = sessionManager.getSessionId?.();
174
+ if (!sessionDir || !sessionId)
175
+ return undefined;
176
+ try {
177
+ const statePath = join(sessionDir, "dcp-state", safeSessionFileName(sessionId));
178
+ const parsed = JSON.parse(readFileSync(statePath, "utf8"));
179
+ return isRecord(parsed) ? parsed : undefined;
180
+ }
181
+ catch {
182
+ return undefined;
183
+ }
184
+ }
185
+ function safeSessionFileName(sessionId) {
186
+ return `${sessionId.replace(/[^a-zA-Z0-9._-]/g, "_")}.json`;
187
+ }
188
+ function formatStateSource(source) {
189
+ if (source === "sidecar")
190
+ return "dcp-state sidecar";
191
+ return "compress tool results";
192
+ }
159
193
  function customEntryData(entry, customType) {
160
194
  if (!isRecord(entry) || entry.type !== "custom" || entry.customType !== customType)
161
195
  return undefined;
162
196
  return isRecord(entry.data) ? entry.data : undefined;
163
197
  }
164
- function latestCustomEntryData(entries, customType) {
165
- for (let i = entries.length - 1; i >= 0; i--) {
166
- const data = customEntryData(entries[i], customType);
167
- if (data)
168
- return data;
169
- }
170
- return undefined;
171
- }
172
198
  function parseToolResultText(content) {
173
199
  const parsed = parseArgsText(textContent(content));
174
200
  return isRecord(parsed) ? parsed : undefined;
@@ -67,6 +67,8 @@ export class AppRenderController {
67
67
  this.deps.mouseController.statusUserJumpTarget = undefined;
68
68
  this.deps.mouseController.statusThinkingExpandTarget = undefined;
69
69
  this.deps.mouseController.statusCompactToolsTarget = undefined;
70
+ this.deps.mouseController.statusQuickScrollUpTarget = undefined;
71
+ this.deps.mouseController.statusQuickScrollDownTarget = undefined;
70
72
  this.deps.mouseController.statusTerminalBellSoundTarget = undefined;
71
73
  this.deps.mouseController.statusSessionTarget = undefined;
72
74
  this.deps.mouseController.statusPromptEnhancerTarget = undefined;
@@ -289,6 +291,8 @@ export class AppRenderController {
289
291
  this.deps.mouseController.statusUserJumpTarget = this.deps.statusLineRenderer.userJumpTarget?.(widgetLayout, widgetRow);
290
292
  this.deps.mouseController.statusThinkingExpandTarget = this.deps.statusLineRenderer.thinkingExpandTarget?.(widgetLayout, widgetRow);
291
293
  this.deps.mouseController.statusCompactToolsTarget = this.deps.statusLineRenderer.compactToolsTarget?.(widgetLayout, widgetRow);
294
+ this.deps.mouseController.statusQuickScrollUpTarget = this.deps.statusLineRenderer.quickScrollUpTarget?.(widgetLayout, widgetRow);
295
+ this.deps.mouseController.statusQuickScrollDownTarget = this.deps.statusLineRenderer.quickScrollDownTarget?.(widgetLayout, widgetRow);
292
296
  this.deps.mouseController.statusTerminalBellSoundTarget = this.deps.statusLineRenderer.terminalBellSoundTarget?.(widgetLayout, widgetRow);
293
297
  }
294
298
  this.deps.mouseController.statusSessionTarget = this.deps.statusLineRenderer.sessionTarget(statusLayout.text, statusRow, statusLayout.sessionLabel, statusLayout.workspaceLabel);
@@ -1,6 +1,6 @@
1
1
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
2
2
  import type { Theme } from "../../theme.js";
3
- import type { SessionActivity, StatusCompactToolsTarget, StatusContextTarget, StatusDraftQueueTarget, StatusLineLayout, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
3
+ import type { SessionActivity, StatusCompactToolsTarget, StatusContextTarget, StatusDraftQueueTarget, StatusLineLayout, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusQuickScrollTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
4
4
  import type { ScreenStyler } from "../screen/screen-styler.js";
5
5
  import { type ModelColorsConfig } from "../../config.js";
6
6
  export type StatusLineRendererHost = {
@@ -26,6 +26,10 @@ export type StatusLineRendererHost = {
26
26
  terminalBellSoundStatusWidgetEnabled(): boolean;
27
27
  voiceStatusWidgetText(): string;
28
28
  voiceStatusWidgetActive(): boolean;
29
+ conversationQuickScrollDirections?(): {
30
+ up: boolean;
31
+ down: boolean;
32
+ };
29
33
  queueableInputActive?(): boolean;
30
34
  userMessageJumpMenuActive?(): boolean;
31
35
  allThinkingExpandedActive?(): boolean;
@@ -50,6 +54,8 @@ export declare class StatusLineRenderer {
50
54
  draftQueueTarget(layout: StatusLineLayout, row: number): StatusDraftQueueTarget | undefined;
51
55
  thinkingExpandTarget(layout: StatusLineLayout, row: number): StatusThinkingExpandTarget | undefined;
52
56
  compactToolsTarget(layout: StatusLineLayout, row: number): StatusCompactToolsTarget | undefined;
57
+ quickScrollUpTarget(layout: StatusLineLayout, row: number): StatusQuickScrollTarget | undefined;
58
+ quickScrollDownTarget(layout: StatusLineLayout, row: number): StatusQuickScrollTarget | undefined;
53
59
  terminalBellSoundTarget(layout: StatusLineLayout, row: number): StatusTerminalBellSoundTarget | undefined;
54
60
  sessionTarget(statusText: string, row: number, label: string, workspaceLabel: string): StatusSessionTarget | undefined;
55
61
  private segments;
@@ -58,6 +58,7 @@ export class StatusLineRenderer {
58
58
  widgets.push({ text, assign });
59
59
  hasParts = true;
60
60
  };
61
+ const quickScrollDirections = this.host.conversationQuickScrollDirections?.() ?? { up: false, down: false };
61
62
  const draftQueueButton = this.draftQueueWidgetText();
62
63
  appendWidget(draftQueueButton ? this.iconButtonText(draftQueueButton) : "", (column, text) => {
63
64
  layout.draftQueueWidget = this.widgetLayout(column, text);
@@ -79,6 +80,12 @@ export class StatusLineRenderer {
79
80
  appendWidget(this.iconButtonText(APP_ICONS.compactTools), (column, text) => {
80
81
  layout.compactToolsWidget = this.widgetLayout(column, text);
81
82
  });
83
+ appendWidget(quickScrollDirections.up ? this.iconButtonText(APP_ICONS.up) : "", (column, text) => {
84
+ layout.quickScrollUpWidget = this.widgetLayout(column, text);
85
+ });
86
+ appendWidget(quickScrollDirections.down ? this.iconButtonText(APP_ICONS.down) : "", (column, text) => {
87
+ layout.quickScrollDownWidget = this.widgetLayout(column, text);
88
+ });
82
89
  const voiceWidgetText = this.host.voiceStatusWidgetText();
83
90
  appendWidget(this.voiceBorderWidgetText(voiceWidgetText), (column, text) => {
84
91
  layout.voiceWidget = this.voiceWidgetLayout(column, voiceWidgetText, text);
@@ -137,6 +144,8 @@ export class StatusLineRenderer {
137
144
  : this.host.promptEnhancerStatusWidgetEnabled()
138
145
  ? colors.info
139
146
  : colors.muted);
147
+ pushWidgetSegment(layout.quickScrollUpWidget, colors.info);
148
+ pushWidgetSegment(layout.quickScrollDownWidget, colors.info);
140
149
  pushWidgetSegment(layout.userJumpWidget, this.host.userMessageJumpMenuActive?.() ? colors.info : colors.muted);
141
150
  pushWidgetSegment(layout.terminalBellSoundWidget, this.host.terminalBellSoundStatusWidgetEnabled() ? colors.info : colors.muted);
142
151
  pushWidgetSegment(layout.thinkingExpandWidget, this.host.allThinkingExpandedActive?.() ? colors.info : colors.muted);
@@ -252,6 +261,18 @@ export class StatusLineRenderer {
252
261
  return undefined;
253
262
  return { row, startColumn: widget.startColumn, endColumn: widget.endColumn };
254
263
  }
264
+ quickScrollUpTarget(layout, row) {
265
+ const widget = layout.quickScrollUpWidget;
266
+ if (!widget)
267
+ return undefined;
268
+ return { row, startColumn: widget.startColumn, endColumn: widget.endColumn, direction: "up" };
269
+ }
270
+ quickScrollDownTarget(layout, row) {
271
+ const widget = layout.quickScrollDownWidget;
272
+ if (!widget)
273
+ return undefined;
274
+ return { row, startColumn: widget.startColumn, endColumn: widget.endColumn, direction: "down" };
275
+ }
255
276
  terminalBellSoundTarget(layout, row) {
256
277
  const widget = layout.terminalBellSoundWidget;
257
278
  if (!widget)
@@ -197,9 +197,9 @@ export class TabLineRenderer {
197
197
  return { foreground: this.host.theme.colors.error, bold: true };
198
198
  }
199
199
  if (tab.activity === "running" || tab.activity === "thinking") {
200
- return { foreground: this.host.theme.colors.success, bold: true };
200
+ return { foreground: this.host.theme.colors.warning, bold: true };
201
201
  }
202
- return { foreground: this.host.theme.colors.statusDotBase };
202
+ return { foreground: this.host.theme.colors.success };
203
203
  }
204
204
  statusIndicatorIcon(tab) {
205
205
  if (tab.status !== "active" && tab.attention === "terminal-bell" && tab.attentionVisible !== false) {
@@ -26,5 +26,6 @@ export type ToolBlockRenderOptions = {
26
26
  backgroundOverride?: string;
27
27
  skipHeaderBackground?: boolean;
28
28
  showGutter?: boolean;
29
+ headerColorOverride?: string;
29
30
  };
30
31
  export declare function renderToolBlock(entry: ToolBlockEntry, rule: ResolvedToolRule, width: number, colors: Theme["colors"], options?: ToolBlockRenderOptions): RenderedLine[];
@@ -18,7 +18,7 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
18
18
  const hasLspDiagnostics = hasToolLspDiagnosticsAfterMutation(entry);
19
19
  const expanded = entry.expanded;
20
20
  const stateIcon = toolStatusIcon(entry);
21
- const toolColor = resolveColor(rule.color, colors);
21
+ const toolColor = options.headerColorOverride ?? resolveColor(rule.color, colors);
22
22
  const toolOutputColor = colors.statusForeground;
23
23
  const headerLabel = (entry.headerLabel ?? entry.toolName).toLowerCase();
24
24
  const bg = options.backgroundOverride;
@@ -87,18 +87,42 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
87
87
  return headerLines;
88
88
  }
89
89
  function renderCollapsedPreviewLines(entry, body, rule, width, target, color, colors, hasLspDiagnostics, showGutter) {
90
- const allLines = renderToolBodyLines(body, width, target, color, entry.bodyStyle, colors, undefined, entry.bodyWrap, hasLspDiagnostics, entry.bodyLineStyles, entry.preserveAnsi, showGutter);
91
- if (rule.previewLines >= allLines.length)
92
- return allLines;
93
- const previewLines = rule.direction === "tail" ? allLines.slice(-rule.previewLines) : allLines.slice(0, rule.previewLines);
90
+ const preview = previewBodyText(body, rule.direction, rule.previewLines);
91
+ const allPreviewLines = renderToolBodyLines(preview.text, width, target, color, entry.bodyStyle, colors, undefined, entry.bodyWrap, hasLspDiagnostics, entry.bodyLineStyles, entry.preserveAnsi, showGutter, { rawLineOffset: preview.rawLineOffset, bodyEndsAfterText: !preview.omittedAfter });
92
+ const overflow = preview.omittedBefore || preview.omittedAfter || allPreviewLines.length > rule.previewLines;
93
+ if (!overflow)
94
+ return allPreviewLines;
95
+ const previewLines = rule.direction === "tail" ? allPreviewLines.slice(-rule.previewLines) : allPreviewLines.slice(0, rule.previewLines);
94
96
  return markTruncatedPreviewLine(previewLines, rule.direction, colors.statusDotBase);
95
97
  }
96
98
  function collapsedInlinePreview(text, rule, preserveAnsi = false) {
97
- const rawLines = sanitizeToolBodyText(text, preserveAnsi).split("\n").map((line) => stripAnsi(line).trim()).filter(Boolean);
99
+ const preview = previewBodyText(text, rule.direction, rule.previewLines);
100
+ const rawLines = sanitizeToolBodyText(preview.text, preserveAnsi).split("\n").map((line) => stripAnsi(line).trim()).filter(Boolean);
98
101
  if (rawLines.length === 0)
99
102
  return { text: "", overflow: false };
100
103
  const selectedLines = rule.direction === "tail" ? rawLines.slice(-rule.previewLines) : rawLines.slice(0, rule.previewLines);
101
- return { text: selectedLines.join(" "), overflow: rawLines.length > rule.previewLines };
104
+ return { text: selectedLines.join(" "), overflow: preview.omittedBefore || preview.omittedAfter || rawLines.length > rule.previewLines };
105
+ }
106
+ function previewBodyText(text, direction, previewLines) {
107
+ const rawLines = text.split("\n");
108
+ const previewRawLines = Math.max(1, previewLines + 1);
109
+ if (rawLines.length <= previewRawLines)
110
+ return { text, rawLineOffset: 0, omittedBefore: false, omittedAfter: false };
111
+ if (direction === "tail") {
112
+ const start = Math.max(0, rawLines.length - previewRawLines);
113
+ return {
114
+ text: rawLines.slice(start).join("\n"),
115
+ rawLineOffset: start,
116
+ omittedBefore: start > 0,
117
+ omittedAfter: false,
118
+ };
119
+ }
120
+ return {
121
+ text: rawLines.slice(0, previewRawLines).join("\n"),
122
+ rawLineOffset: 0,
123
+ omittedBefore: false,
124
+ omittedAfter: rawLines.length > previewRawLines,
125
+ };
102
126
  }
103
127
  function markTruncatedPreviewLine(lines, direction, markerColor) {
104
128
  if (lines.length === 0)
@@ -122,7 +146,7 @@ function markTruncatedPreviewLine(lines, direction, markerColor) {
122
146
  };
123
147
  });
124
148
  }
125
- function renderToolBodyLines(text, width, target, color, style, colors, syntaxHighlight, bodyWrap = "char", hasLspDiagnostics = false, bodyLineStyles, preserveAnsi = false, showGutter = false) {
149
+ function renderToolBodyLines(text, width, target, color, style, colors, syntaxHighlight, bodyWrap = "char", hasLspDiagnostics = false, bodyLineStyles, preserveAnsi = false, showGutter = false, options = {}) {
126
150
  const displayPrefix = showGutter ? toolBodyGutterPrefix() : TOOL_BODY_PREFIX;
127
151
  const prefixWidth = stringDisplayWidth(displayPrefix);
128
152
  const bodyWidth = Math.max(1, width - prefixWidth);
@@ -131,13 +155,15 @@ function renderToolBodyLines(text, width, target, color, style, colors, syntaxHi
131
155
  const gutterSegment = showGutter
132
156
  ? { start: 0, end: 1, foreground: colors.statusDotBase }
133
157
  : undefined;
158
+ const rawLineOffset = options.rawLineOffset ?? 0;
134
159
  for (const [rawLineIndex, rawLine] of sanitizeToolBodyText(text, preserveAnsi).split("\n").entries()) {
160
+ const absoluteRawLineIndex = rawLineOffset + rawLineIndex;
135
161
  const ansiLine = preserveAnsi ? ansiStyledLine(rawLine) : undefined;
136
162
  const displayLine = ansiLine?.text ?? rawLine;
137
163
  const diffStyle = style === "diff" ? diffLineStyle(displayLine, colors) : undefined;
138
164
  const lspDiagnosticStyle = hasLspDiagnostics ? lspDiagnosticLineStyle(displayLine, colors) : undefined;
139
- const bodyLineStyle = bodyLineStyleForLine(bodyLineStyles, rawLineIndex, colors);
140
- const lineSyntaxHighlight = syntaxHighlightForLine(syntaxHighlight, rawLineIndex);
165
+ const bodyLineStyle = bodyLineStyleForLine(bodyLineStyles, absoluteRawLineIndex, colors);
166
+ const lineSyntaxHighlight = syntaxHighlightForLine(syntaxHighlight, absoluteRawLineIndex);
141
167
  const wrappedLines = ansiLine && !diffStyle && !lspDiagnosticStyle && !bodyLineStyle && !lineSyntaxHighlight
142
168
  ? wrapAnsiStyledDisplayLine(ansiLine, bodyWidth)
143
169
  : wrapBodyLine(displayLine, bodyWidth).map((wrapped) => ({ text: wrapped, segments: [] }));
@@ -195,7 +221,7 @@ function renderToolBodyLines(text, width, target, color, style, colors, syntaxHi
195
221
  lines.push(line);
196
222
  }
197
223
  }
198
- if (showGutter && lines.length > 0) {
224
+ if (showGutter && lines.length > 0 && options.bodyEndsAfterText !== false) {
199
225
  const lastLine = lines.at(-1);
200
226
  if (!lastLine)
201
227
  return lines;
@@ -10,7 +10,7 @@ import { isThinkingLevel, parseModelRef, parseScopedModelRef } from "./model/mod
10
10
  import { openLazySessionManager } from "./session/lazy-session-manager.js";
11
11
  const BUNDLED_QUESTION_EXTENSION_NAME = "question";
12
12
  const PI_TOOLS_SUITE_EXTENSION_NAME = "pi-tools-suite";
13
- const BUNDLED_EXTENSIONS_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "../..", "extensions");
13
+ const BUNDLED_EXTENSIONS_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "bundled-extensions");
14
14
  const BUNDLED_QUESTION_EXTENSION_DIR = resolve(BUNDLED_EXTENSIONS_DIR, BUNDLED_QUESTION_EXTENSION_NAME);
15
15
  const BUNDLED_SESSION_TITLE_EXTENSION_DIR = resolve(BUNDLED_EXTENSIONS_DIR, "session-title");
16
16
  const BUNDLED_TERMINAL_BELL_EXTENSION_DIR = resolve(BUNDLED_EXTENSIONS_DIR, "terminal-bell");
@@ -6,7 +6,7 @@ import type { ToastEntry, ToastVariant } from "../../ui.js";
6
6
  import type { AppPopupActionController } from "../popup/popup-action-controller.js";
7
7
  import type { AppPopupMenuController } from "../popup/popup-menu-controller.js";
8
8
  import type { AppScrollController } from "./scroll-controller.js";
9
- import type { Entry, ImageClickTarget, MouseEvent, MouseSelection, StatusContextTarget, StatusCompactToolsTarget, StatusDraftQueueTarget, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, TabLineMouseTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
9
+ import type { Entry, ImageClickTarget, MouseEvent, MouseSelection, StatusContextTarget, StatusCompactToolsTarget, StatusDraftQueueTarget, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusQuickScrollTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, TabLineMouseTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
10
10
  import { type RenderedLink } from "./file-links.js";
11
11
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
12
12
  type ClickFlash = {
@@ -56,6 +56,7 @@ export type AppMouseControllerHost = {
56
56
  refreshModelUsageStatus(): void | Promise<void>;
57
57
  refreshUserMessageJumpMenuItems?(): Promise<void>;
58
58
  queueInputFromStatus?(): void | Promise<void>;
59
+ scrollConversationQuick(direction: "up" | "down"): void | Promise<void>;
59
60
  toggleAllThinkingExpanded?(): void;
60
61
  toggleSuperCompactTools?(): void;
61
62
  toggleTerminalBellSound?(): void;
@@ -103,6 +104,8 @@ export declare class AppMouseController {
103
104
  statusDraftQueueTarget: StatusDraftQueueTarget | undefined;
104
105
  statusThinkingExpandTarget: StatusThinkingExpandTarget | undefined;
105
106
  statusCompactToolsTarget: StatusCompactToolsTarget | undefined;
107
+ statusQuickScrollUpTarget: StatusQuickScrollTarget | undefined;
108
+ statusQuickScrollDownTarget: StatusQuickScrollTarget | undefined;
106
109
  statusTerminalBellSoundTarget: StatusTerminalBellSoundTarget | undefined;
107
110
  statusSessionTarget: StatusSessionTarget | undefined;
108
111
  statusPromptEnhancerTarget: StatusPromptEnhancerTarget | undefined;
@@ -165,6 +168,7 @@ export declare class AppMouseController {
165
168
  private handleStatusPromptEnhancerClick;
166
169
  private handleStatusVoiceMicClick;
167
170
  private handleStatusVoiceLanguageClick;
171
+ private handleStatusQuickScrollClick;
168
172
  private handleInputClick;
169
173
  private handleExtensionInputClick;
170
174
  private openUserMessageMenu;
@@ -28,6 +28,8 @@ export class AppMouseController {
28
28
  statusDraftQueueTarget;
29
29
  statusThinkingExpandTarget;
30
30
  statusCompactToolsTarget;
31
+ statusQuickScrollUpTarget;
32
+ statusQuickScrollDownTarget;
31
33
  statusTerminalBellSoundTarget;
32
34
  statusSessionTarget;
33
35
  statusPromptEnhancerTarget;
@@ -78,6 +80,8 @@ export class AppMouseController {
78
80
  return;
79
81
  if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusModelUsageClick(event)))
80
82
  return;
83
+ if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusQuickScrollClick(event)))
84
+ return;
81
85
  if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusSessionClick(event)))
82
86
  return;
83
87
  if (event.button === 0 && this.withClickFlash(event, () => this.handleExtensionInputClick(event)))
@@ -316,6 +320,8 @@ export class AppMouseController {
316
320
  this.statusUserJumpTarget,
317
321
  this.statusThinkingExpandTarget,
318
322
  this.statusCompactToolsTarget,
323
+ this.statusQuickScrollUpTarget,
324
+ this.statusQuickScrollDownTarget,
319
325
  this.statusTerminalBellSoundTarget,
320
326
  this.statusSessionTarget,
321
327
  this.statusPromptEnhancerTarget,
@@ -600,6 +606,16 @@ export class AppMouseController {
600
606
  this.host.toggleVoiceLanguage();
601
607
  return true;
602
608
  }
609
+ handleStatusQuickScrollClick(event) {
610
+ const target = [this.statusQuickScrollUpTarget, this.statusQuickScrollDownTarget].find((candidate) => !!candidate
611
+ && event.y === candidate.row
612
+ && event.x >= candidate.startColumn
613
+ && event.x < candidate.endColumn);
614
+ if (!target)
615
+ return false;
616
+ this.host.scrollConversationQuick(target.direction);
617
+ return true;
618
+ }
603
619
  handleInputClick(event) {
604
620
  const columns = this.host.terminalColumns();
605
621
  const terminalRows = this.host.terminalRows();
@@ -12,6 +12,10 @@ export type ConversationTextScrollTarget = {
12
12
  entryId?: string;
13
13
  needles: readonly string[];
14
14
  };
15
+ export type AppScrollState = {
16
+ scrollFromBottom: number;
17
+ detachedScrollStart?: number;
18
+ };
15
19
  export type AppScrollControllerHost = {
16
20
  conversationViewport(): ConversationViewport;
17
21
  editorLayoutRenderer(): EditorLayoutRenderer;
@@ -24,6 +28,11 @@ export type AppScrollControllerHost = {
24
28
  render?: boolean;
25
29
  onPrependedEntries?: (entries: readonly Entry[]) => void;
26
30
  }): Promise<boolean>;
31
+ hasNewerSessionHistory?(): boolean;
32
+ isLoadingNewerSessionHistory?(): boolean;
33
+ loadNewerSessionHistory?(options?: {
34
+ render?: boolean;
35
+ }): Promise<boolean>;
27
36
  render(): void;
28
37
  };
29
38
  export declare class AppScrollController {
@@ -34,6 +43,15 @@ export declare class AppScrollController {
34
43
  constructor(host: AppScrollControllerHost);
35
44
  reset(): void;
36
45
  scrollToBottom(): boolean;
46
+ scrollToTop(): boolean;
47
+ scrollToAbsoluteTop(): Promise<boolean>;
48
+ scrollToAbsoluteBottom(): Promise<boolean>;
49
+ quickScrollDirections(columns: number, bodyHeight: number): {
50
+ up: boolean;
51
+ down: boolean;
52
+ };
53
+ captureState(): AppScrollState;
54
+ restoreState(state: AppScrollState): void;
37
55
  conversationView(columns: number, bodyHeight: number): {
38
56
  lines: RenderedLine[];
39
57
  metrics: AppScrollMetrics;
@@ -45,7 +63,9 @@ export declare class AppScrollController {
45
63
  render?: boolean;
46
64
  }): boolean;
47
65
  private shouldLoadOlderHistory;
66
+ private shouldLoadNewerHistory;
48
67
  private loadOlderHistoryAnchored;
68
+ private loadNewerHistoryAnchored;
49
69
  adjustForHistoryWindowPrune(edge: "top" | "bottom", lineCount: number): void;
50
70
  scrollToConversationEntry(entryId: string): boolean;
51
71
  scrollToConversationText(target: ConversationTextScrollTarget): boolean;