pi-ui-extend 0.1.50 → 0.1.53

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 (47) hide show
  1. package/dist/app/icons.d.ts +1 -0
  2. package/dist/app/icons.js +2 -0
  3. package/dist/app/input/autocomplete-controller.js +1 -1
  4. package/dist/app/input/input-controller.d.ts +1 -0
  5. package/dist/app/input/input-controller.js +7 -24
  6. package/dist/app/input/input-paste-handler.d.ts +1 -0
  7. package/dist/app/input/input-paste-handler.js +13 -16
  8. package/dist/app/rendering/conversation-tool-renderer.js +1 -0
  9. package/dist/app/rendering/popup-menu-renderer.js +2 -2
  10. package/dist/app/rendering/render-controller.js +10 -16
  11. package/dist/app/rendering/status-line-renderer.d.ts +0 -7
  12. package/dist/app/rendering/status-line-renderer.js +7 -79
  13. package/dist/app/rendering/tab-line-renderer.js +1 -1
  14. package/dist/app/rendering/toast-renderer.js +40 -15
  15. package/dist/app/rendering/tool-block-renderer.d.ts +2 -1
  16. package/dist/app/rendering/tool-block-renderer.js +43 -3
  17. package/dist/app/screen/mouse-controller.d.ts +1 -0
  18. package/dist/app/screen/mouse-controller.js +30 -76
  19. package/dist/app/session/lazy-session-manager.js +0 -7
  20. package/dist/app/session/queued-message-controller.d.ts +0 -1
  21. package/dist/app/session/queued-message-controller.js +5 -13
  22. package/dist/app/session/queued-message-entries.d.ts +1 -0
  23. package/dist/app/session/queued-message-entries.js +8 -0
  24. package/dist/app/session/tabs-controller.d.ts +2 -1
  25. package/dist/app/session/tabs-controller.js +67 -24
  26. package/dist/app/types.d.ts +1 -0
  27. package/dist/bundled-extensions/question/tool-description.js +2 -3
  28. package/dist/bundled-extensions/session-title/title-generation.js +1 -1
  29. package/dist/tool-renderers/compress.js +28 -7
  30. package/dist/tool-renderers/patch-normalize.js +4 -4
  31. package/dist/tool-renderers/read.js +1 -1
  32. package/dist/tool-renderers/types.d.ts +10 -0
  33. package/external/pi-tools-suite/package.json +3 -3
  34. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +6 -10
  35. package/external/pi-tools-suite/src/async-subagents/core/cleanup.ts +2 -2
  36. package/external/pi-tools-suite/src/async-subagents/core/paths.ts +12 -0
  37. package/external/pi-tools-suite/src/async-subagents/core/registry.ts +3 -4
  38. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +1 -1
  39. package/external/pi-tools-suite/src/async-subagents/core/sessions.ts +2 -2
  40. package/external/pi-tools-suite/src/async-subagents/core/state.ts +3 -3
  41. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +1 -1
  42. package/external/pi-tools-suite/src/async-subagents/lib.ts +1 -1
  43. package/external/pi-tools-suite/src/coding-discipline/index.ts +13 -23
  44. package/external/pi-tools-suite/src/dcp/auto-compress.ts +1 -1
  45. package/external/pi-tools-suite/src/dcp/prompts.ts +9 -49
  46. package/external/pi-tools-suite/src/tool-descriptions.ts +34 -61
  47. package/package.json +4 -4
@@ -12,6 +12,7 @@ declare const NERD_FONT_ICONS: {
12
12
  readonly deleted: "󰅙";
13
13
  readonly deferred: "󰍷";
14
14
  readonly info: "󰋼";
15
+ readonly fork: "└";
15
16
  readonly lightbulb: "󰌵";
16
17
  readonly microphone: "󰍬";
17
18
  readonly plus: "󰐕";
package/dist/app/icons.js CHANGED
@@ -23,6 +23,7 @@ const NERD_FONT_ICONS = {
23
23
  deleted: "\u{f0159}",
24
24
  deferred: "\u{f0377}",
25
25
  info: "\u{f02fc}",
26
+ fork: "└",
26
27
  lightbulb: "\u{f0335}",
27
28
  microphone: "\u{f036c}",
28
29
  plus: "\u{f0415}",
@@ -54,6 +55,7 @@ const FALLBACK_ICONS = {
54
55
  deleted: "×",
55
56
  deferred: "↷",
56
57
  info: "i",
58
+ fork: "└",
57
59
  lightbulb: "💡",
58
60
  microphone: "m",
59
61
  plus: "+",
@@ -1,4 +1,4 @@
1
- import { streamSimple } from "@earendil-works/pi-ai";
1
+ import { streamSimple } from "@earendil-works/pi-ai/compat";
2
2
  import { isRecord } from "../guards.js";
3
3
  import { parseModelRef } from "../model/model-ref.js";
4
4
  const AUTOCOMPLETE_DEBOUNCE_MS = 350;
@@ -51,6 +51,7 @@ export declare class AppInputController {
51
51
  private consumeEscapeKeySequence;
52
52
  private consumeClipboardImagePasteSequence;
53
53
  private consumeShiftEnterSequence;
54
+ private consumeModifiedKeySequence;
54
55
  private consumeTerminalInterruptSequence;
55
56
  private handleArrowUp;
56
57
  private handleArrowDown;
@@ -341,42 +341,25 @@ export class AppInputController {
341
341
  return "consumed";
342
342
  }
343
343
  consumeEscapeKeySequence() {
344
- const result = parseTerminalModifiedKeySequence(this.inputBuffer);
345
- if (result.kind === "pending")
346
- return "pending";
347
- if (result.kind === "none")
348
- return "none";
349
- if (!terminalKeyIsEscape(result.key))
350
- return "none";
351
- this.inputBuffer = this.inputBuffer.slice(result.key.length);
352
- if (!terminalKeyShouldIgnore(result.key))
353
- void this.host.handleEscape();
354
- return "consumed";
344
+ return this.consumeModifiedKeySequence(terminalKeyIsEscape, () => { void this.host.handleEscape(); });
355
345
  }
356
346
  consumeClipboardImagePasteSequence() {
357
- const result = parseTerminalModifiedKeySequence(this.inputBuffer);
358
- if (result.kind === "pending")
359
- return "pending";
360
- if (result.kind === "none")
361
- return "none";
362
- if (!terminalKeyIsClipboardImagePaste(result.key))
363
- return "none";
364
- this.inputBuffer = this.inputBuffer.slice(result.key.length);
365
- if (!terminalKeyShouldIgnore(result.key))
366
- void this.pasteHandler.handleClipboardImagePaste();
367
- return "consumed";
347
+ return this.consumeModifiedKeySequence(terminalKeyIsClipboardImagePaste, () => { void this.pasteHandler.handleClipboardImagePaste(); });
368
348
  }
369
349
  consumeShiftEnterSequence() {
350
+ return this.consumeModifiedKeySequence(terminalKeyIsShiftEnter, () => this.insertInputNewline());
351
+ }
352
+ consumeModifiedKeySequence(matches, handle) {
370
353
  const result = parseTerminalModifiedKeySequence(this.inputBuffer);
371
354
  if (result.kind === "pending")
372
355
  return "pending";
373
356
  if (result.kind === "none")
374
357
  return "none";
375
- if (!terminalKeyIsShiftEnter(result.key))
358
+ if (!matches(result.key))
376
359
  return "none";
377
360
  this.inputBuffer = this.inputBuffer.slice(result.key.length);
378
361
  if (!terminalKeyShouldIgnore(result.key))
379
- this.insertInputNewline();
362
+ handle();
380
363
  return "consumed";
381
364
  }
382
365
  consumeTerminalInterruptSequence() {
@@ -20,6 +20,7 @@ export declare class InputPasteHandler {
20
20
  private plainPasteFilePath;
21
21
  private isDuplicatePaste;
22
22
  private handlePasteEnd;
23
+ private handlePastedFilePath;
23
24
  private schedulePastedText;
24
25
  private handleFilePaste;
25
26
  private filePathForInput;
@@ -14,15 +14,8 @@ export class InputPasteHandler {
14
14
  this.host = host;
15
15
  }
16
16
  handlePlainData(data) {
17
- const plainFilePath = !this.host.inputEditor.isInBracketedPaste && this.plainPasteFilePath(data);
18
- if (plainFilePath) {
19
- if (isImagePath(plainFilePath) && Date.now() < this.suppressImagePathPasteUntil) {
20
- this.host.render();
21
- return true;
22
- }
23
- void this.handleFilePaste(plainFilePath);
17
+ if (!this.host.inputEditor.isInBracketedPaste && this.handlePastedFilePath(data))
24
18
  return true;
25
- }
26
19
  if (!this.host.inputEditor.isInBracketedPaste && this.isPlainMultilinePasteChunk(data)) {
27
20
  this.schedulePastedText(data);
28
21
  return true;
@@ -96,17 +89,21 @@ export class InputPasteHandler {
96
89
  void this.handleClipboardImagePaste();
97
90
  return;
98
91
  }
99
- const filePath = this.plainPasteFilePath(text);
100
- if (filePath) {
101
- if (isImagePath(filePath) && Date.now() < this.suppressImagePathPasteUntil) {
102
- this.host.render();
103
- return;
104
- }
105
- void this.handleFilePaste(filePath);
92
+ if (this.handlePastedFilePath(text))
106
93
  return;
107
- }
108
94
  this.schedulePastedText(text);
109
95
  }
96
+ handlePastedFilePath(text) {
97
+ const filePath = this.plainPasteFilePath(text);
98
+ if (!filePath)
99
+ return false;
100
+ if (isImagePath(filePath) && Date.now() < this.suppressImagePathPasteUntil) {
101
+ this.host.render();
102
+ return true;
103
+ }
104
+ void this.handleFilePaste(filePath);
105
+ return true;
106
+ }
110
107
  schedulePastedText(text) {
111
108
  const timer = setTimeout(() => {
112
109
  if (this.isDuplicatePaste("text", text)) {
@@ -31,6 +31,7 @@ export function renderConversationToolEntry(entry, width, options) {
31
31
  id: entry.id,
32
32
  toolName,
33
33
  headerArgs: display.headerArgs,
34
+ headerArgSegments: display.headerArgSegments,
34
35
  bodyLineStyles: display.bodyLineStyles,
35
36
  bodyStyle: display.bodyStyle,
36
37
  preserveAnsi: display.preserveAnsi,
@@ -1,7 +1,7 @@
1
1
  import { colorLine } from "../../theme.js";
2
2
  import { stringDisplayWidth } from "../../terminal-width.js";
3
3
  import { resolveColor, resolveModelColor } from "../../config.js";
4
- import { SLASH_COMMAND_DESCRIPTION_COLUMN, } from "../constants.js";
4
+ import { SLASH_COMMAND_DESCRIPTION_COLUMN, THINKING_LEVELS, } from "../constants.js";
5
5
  import { APP_ICONS } from "../icons.js";
6
6
  import { ellipsizeDisplay, padOrTrimPlain, sanitizeText } from "./render-text.js";
7
7
  import { modelProviderThemeColor, thinkingLevelThemeColor } from "./status-line-renderer.js";
@@ -287,7 +287,7 @@ export class PopupMenuRenderer {
287
287
  }
288
288
  availableThinkingLevels() {
289
289
  const levels = this.host.session?.getAvailableThinkingLevels();
290
- return Array.isArray(levels) && levels.length > 0 ? levels.map(String) : ["off", "minimal", "low", "medium", "high", "xhigh"];
290
+ return Array.isArray(levels) && levels.length > 0 ? levels.map(String) : [...THINKING_LEVELS];
291
291
  }
292
292
  queueMessageItemVariant(value) {
293
293
  return value === "cancel" ? "error" : "normal";
@@ -263,27 +263,21 @@ export class AppRenderController {
263
263
  return `\x1b[${flash.y};${flash.startColumn}H\x1b[7m${text}${ANSI_RESET}`;
264
264
  }
265
265
  updateStatusMouseState(statusLayout, statusRow) {
266
- const widgetLayout = statusLayout;
267
- const widgetRow = statusRow;
268
266
  this.deps.mouseController.statusModelTarget = this.deps.statusLineRenderer.modelTarget(statusLayout.text, statusRow);
269
267
  this.deps.mouseController.statusThinkingTarget = this.deps.statusLineRenderer.thinkingTarget(statusLayout.text, statusRow);
270
268
  this.deps.mouseController.statusContextTarget = this.deps.statusLineRenderer.contextTarget(statusLayout.text, statusRow, statusLayout);
271
269
  this.deps.mouseController.statusModelUsageTarget = this.deps.statusLineRenderer.modelUsageTarget(statusLayout.text, statusRow, statusLayout);
272
- if (widgetLayout) {
273
- this.deps.mouseController.statusDraftQueueTarget = this.deps.statusLineRenderer.draftQueueTarget?.(widgetLayout, widgetRow);
274
- this.deps.mouseController.statusUserJumpTarget = this.deps.statusLineRenderer.userJumpTarget?.(widgetLayout, widgetRow);
275
- this.deps.mouseController.statusThinkingExpandTarget = this.deps.statusLineRenderer.thinkingExpandTarget?.(widgetLayout, widgetRow);
276
- this.deps.mouseController.statusCompactToolsTarget = this.deps.statusLineRenderer.compactToolsTarget?.(widgetLayout, widgetRow);
277
- this.deps.mouseController.statusQuickScrollUpTarget = this.deps.statusLineRenderer.quickScrollUpTarget?.(widgetLayout, widgetRow);
278
- this.deps.mouseController.statusQuickScrollDownTarget = this.deps.statusLineRenderer.quickScrollDownTarget?.(widgetLayout, widgetRow);
279
- this.deps.mouseController.statusTerminalBellSoundTarget = this.deps.statusLineRenderer.terminalBellSoundTarget?.(widgetLayout, widgetRow);
280
- }
270
+ this.deps.mouseController.statusDraftQueueTarget = this.deps.statusLineRenderer.draftQueueTarget?.(statusLayout, statusRow);
271
+ this.deps.mouseController.statusUserJumpTarget = this.deps.statusLineRenderer.userJumpTarget?.(statusLayout, statusRow);
272
+ this.deps.mouseController.statusThinkingExpandTarget = this.deps.statusLineRenderer.thinkingExpandTarget?.(statusLayout, statusRow);
273
+ this.deps.mouseController.statusCompactToolsTarget = this.deps.statusLineRenderer.compactToolsTarget?.(statusLayout, statusRow);
274
+ this.deps.mouseController.statusQuickScrollUpTarget = this.deps.statusLineRenderer.quickScrollUpTarget?.(statusLayout, statusRow);
275
+ this.deps.mouseController.statusQuickScrollDownTarget = this.deps.statusLineRenderer.quickScrollDownTarget?.(statusLayout, statusRow);
276
+ this.deps.mouseController.statusTerminalBellSoundTarget = this.deps.statusLineRenderer.terminalBellSoundTarget?.(statusLayout, statusRow);
281
277
  this.deps.mouseController.statusSessionTarget = this.deps.statusLineRenderer.sessionTarget(statusLayout.text, statusRow, statusLayout.sessionLabel, statusLayout.workspaceLabel);
282
- if (widgetLayout) {
283
- this.deps.mouseController.statusPromptEnhancerTarget = this.deps.statusLineRenderer.promptEnhancerTarget(widgetLayout, widgetRow);
284
- this.deps.mouseController.statusVoiceMicTarget = this.deps.statusLineRenderer.voiceMicTarget(widgetLayout, widgetRow);
285
- this.deps.mouseController.statusVoiceLanguageTarget = this.deps.statusLineRenderer.voiceLanguageTarget(widgetLayout, widgetRow);
286
- }
278
+ this.deps.mouseController.statusPromptEnhancerTarget = this.deps.statusLineRenderer.promptEnhancerTarget(statusLayout, statusRow);
279
+ this.deps.mouseController.statusVoiceMicTarget = this.deps.statusLineRenderer.voiceMicTarget(statusLayout, statusRow);
280
+ this.deps.mouseController.statusVoiceLanguageTarget = this.deps.statusLineRenderer.voiceLanguageTarget(statusLayout, statusRow);
287
281
  this.deps.mouseController.renderedRowTexts.set(statusRow, statusLayout.text);
288
282
  }
289
283
  renderVoiceProgressOverlay(message, width, rows) {
@@ -61,14 +61,7 @@ export declare class StatusLineRenderer {
61
61
  private segments;
62
62
  private fitWorkspaceLabel;
63
63
  private pushStatusWidgetSegments;
64
- private pushPromptEnhancerWidgetSegment;
65
- private pushUserJumpWidgetSegment;
66
- private pushDraftQueueWidgetSegment;
67
64
  private draftQueueWidgetText;
68
- private pushThinkingExpandWidgetSegment;
69
- private pushCompactToolsWidgetSegment;
70
- private pushTerminalBellSoundWidgetSegment;
71
- private pushVoiceWidgetSegment;
72
65
  private pushWorkspaceSegments;
73
66
  private pushModelUsageSegments;
74
67
  private pushResetDurationSegments;
@@ -1,10 +1,12 @@
1
1
  import { compactProgressBarSegments, formatCompactProgressBar } from "../../context-progress-bar.js";
2
2
  import { ellipsizeDisplay, padOrTrimPlain } from "./render-text.js";
3
3
  import { displayIndexForColumn, stringDisplayWidth } from "../../terminal-width.js";
4
+ import { THINKING_LEVELS } from "../constants.js";
4
5
  import { APP_ICONS } from "../icons.js";
5
6
  import { resolveColor, resolveModelColor } from "../../config.js";
6
7
  const MODEL_USAGE_PROGRESS_BAR_WIDTH = stringDisplayWidth(formatCompactProgressBar(100));
7
8
  const STATUS_WIDGET_GAP = " ";
9
+ const DEFAULT_THINKING_LEVEL_LABELS = THINKING_LEVELS;
8
10
  export class StatusLineRenderer {
9
11
  host;
10
12
  constructor(host) {
@@ -370,81 +372,9 @@ export class StatusLineRenderer {
370
372
  }
371
373
  }
372
374
  }
373
- pushPromptEnhancerWidgetSegment(segments, statusText) {
374
- const widgetText = this.host.promptEnhancerStatusWidgetText();
375
- const start = statusText.lastIndexOf(widgetText);
376
- if (start < 0 || widgetText.length <= 0)
377
- return;
378
- const foreground = this.host.promptEnhancerStatusWidgetActive()
379
- ? this.host.theme.colors.warning
380
- : this.host.promptEnhancerStatusWidgetEnabled()
381
- ? this.host.theme.colors.info
382
- : this.host.theme.colors.muted;
383
- this.pushSegment(segments, start, widgetText.length, foreground);
384
- }
385
- pushUserJumpWidgetSegment(segments, statusText) {
386
- const buttonText = APP_ICONS.user;
387
- const start = statusText.indexOf(buttonText);
388
- if (start < 0)
389
- return;
390
- const foreground = this.host.userMessageJumpMenuActive?.()
391
- ? this.host.theme.colors.info
392
- : this.host.theme.colors.muted;
393
- this.pushSegment(segments, start, buttonText.length, foreground);
394
- }
395
- pushDraftQueueWidgetSegment(segments, statusText) {
396
- const buttonText = this.draftQueueWidgetText();
397
- const start = statusText.indexOf(buttonText);
398
- if (start < 0 || buttonText.length <= 0)
399
- return;
400
- this.pushSegment(segments, start, buttonText.length, this.host.theme.colors.info);
401
- }
402
375
  draftQueueWidgetText() {
403
376
  return this.host.queueableInputActive?.() ? APP_ICONS.timerSand : "";
404
377
  }
405
- pushThinkingExpandWidgetSegment(segments, statusText, layout) {
406
- if (!layout.thinkingExpandWidget)
407
- return;
408
- const buttonText = APP_ICONS.thinkingExpanded;
409
- const buttonMarker = ` ${buttonText} `;
410
- const markerStart = statusText.indexOf(buttonMarker);
411
- const start = markerStart >= 0 ? markerStart + 1 : -1;
412
- if (start < 0)
413
- return;
414
- this.pushSegment(segments, start, buttonText.length, this.host.allThinkingExpandedActive?.() ? this.host.theme.colors.info : this.host.theme.colors.muted);
415
- }
416
- pushCompactToolsWidgetSegment(segments, statusText) {
417
- const buttonText = APP_ICONS.compactTools;
418
- const start = statusText.indexOf(buttonText);
419
- if (start < 0)
420
- return;
421
- this.pushSegment(segments, start, buttonText.length, this.host.superCompactToolsActive?.() ? this.host.theme.colors.info : this.host.theme.colors.muted);
422
- }
423
- pushTerminalBellSoundWidgetSegment(segments, statusText) {
424
- const widgetText = this.host.terminalBellSoundStatusWidgetText();
425
- const start = statusText.lastIndexOf(widgetText);
426
- if (start < 0 || widgetText.length <= 0)
427
- return;
428
- this.pushSegment(segments, start, widgetText.length, this.host.terminalBellSoundStatusWidgetEnabled() ? this.host.theme.colors.info : this.host.theme.colors.muted);
429
- }
430
- pushVoiceWidgetSegment(segments, statusText) {
431
- const widgetText = this.host.voiceStatusWidgetText();
432
- if (widgetText.length <= 0)
433
- return;
434
- const start = statusText.lastIndexOf(widgetText);
435
- const micStart = statusText.lastIndexOf(APP_ICONS.microphone);
436
- if (start < 0 && micStart < 0)
437
- return;
438
- if (this.host.voiceStatusWidgetActive()) {
439
- const separatorIndex = widgetText.indexOf(" ");
440
- const micLength = separatorIndex >= 0 ? separatorIndex : widgetText.length;
441
- this.pushSegment(segments, micStart >= 0 ? micStart : start, micLength, this.host.theme.colors.error);
442
- return;
443
- }
444
- const separatorIndex = widgetText.indexOf(" ");
445
- const micLength = separatorIndex >= 0 ? separatorIndex : widgetText.length;
446
- this.pushSegment(segments, micStart >= 0 ? micStart : start, micLength, this.host.theme.colors.muted);
447
- }
448
378
  pushWorkspaceSegments(segments, statusText, workspaceLabel) {
449
379
  const start = statusText.lastIndexOf(workspaceLabel);
450
380
  if (start < 0 || workspaceLabel.length <= 0)
@@ -566,7 +496,7 @@ export class StatusLineRenderer {
566
496
  }
567
497
  availableThinkingLevels() {
568
498
  const levels = this.host.session?.getAvailableThinkingLevels();
569
- return Array.isArray(levels) && levels.length > 0 ? levels.map(String) : ["off", "minimal", "low", "medium", "high", "xhigh"];
499
+ return Array.isArray(levels) && levels.length > 0 ? levels.map(String) : [...DEFAULT_THINKING_LEVEL_LABELS];
570
500
  }
571
501
  contextBarLabel(status, width, workspaceLabel) {
572
502
  const session = this.host.session;
@@ -651,14 +581,13 @@ function overlayText(text, startColumn, overlay) {
651
581
  return `${padded.slice(0, startIndex)}${overlay}${padded.slice(endIndex)}`;
652
582
  }
653
583
  export function thinkingLevelThemeColor(label, colors, availableLevels) {
654
- const levels = availableLevels && availableLevels.length > 0 ? availableLevels.map(String) : ["off", "minimal", "low", "medium", "high", "xhigh"];
584
+ const levels = availableLevels && availableLevels.length > 0 ? availableLevels.map(String) : DEFAULT_THINKING_LEVEL_LABELS;
655
585
  const rank = levels.indexOf(label);
656
586
  if (rank >= 0)
657
587
  return thinkingRankThemeColor(label, rank, levels.length, colors);
658
- const fallbackLevels = ["off", "minimal", "low", "medium", "high", "xhigh"];
659
- const fallbackRank = fallbackLevels.indexOf(label);
588
+ const fallbackRank = DEFAULT_THINKING_LEVEL_LABELS.indexOf(label);
660
589
  return fallbackRank >= 0
661
- ? thinkingRankThemeColor(label, fallbackRank, fallbackLevels.length, colors)
590
+ ? thinkingRankThemeColor(label, fallbackRank, DEFAULT_THINKING_LEVEL_LABELS.length, colors)
662
591
  : colors.info;
663
592
  }
664
593
  function thinkingRankThemeColor(label, rank, count, colors) {
@@ -671,8 +600,7 @@ function thinkingRankThemeColor(label, rank, count, colors) {
671
600
  colors.thinkingXHigh,
672
601
  ];
673
602
  const palette = count > baseColors.length ? [colors.statusForeground, ...baseColors] : baseColors;
674
- const fallbackLevels = ["off", "minimal", "low", "medium", "high", "xhigh"];
675
- const fallbackRank = fallbackLevels.indexOf(label);
603
+ const fallbackRank = DEFAULT_THINKING_LEVEL_LABELS.indexOf(label);
676
604
  const colorIndex = count <= baseColors.length && fallbackRank >= 0 ? fallbackRank : rank;
677
605
  return palette[Math.max(0, Math.min(palette.length - 1, colorIndex))] ?? colors.info;
678
606
  }
@@ -157,7 +157,7 @@ export class TabLineRenderer {
157
157
  return this.buttonLayoutFromText(`${prefix}${ellipsizeDisplay(title, titleWidth)}${suffix}`, 0, statusText.length);
158
158
  }
159
159
  displayTitle(tab) {
160
- return tab.title;
160
+ return tab.isFork ? `${APP_ICONS.fork}${tab.title}` : tab.title;
161
161
  }
162
162
  buttonLayoutFromText(text, statusStart, statusLength) {
163
163
  const closeStart = Math.max(0, text.lastIndexOf(APP_ICONS.close));
@@ -1,7 +1,7 @@
1
- import { colorLine } from "../../theme.js";
2
- import { stringDisplayWidth, wrapDisplayLine } from "../../terminal-width.js";
1
+ import { ANSI_RESET, ansiStylePrefix } from "../../theme.js";
2
+ import { expandTabs, stringDisplayWidth, wrapDisplayLine } from "../../terminal-width.js";
3
3
  import { APP_ICONS } from "../icons.js";
4
- import { padOrTrimPlain, sanitizeText } from "./render-text.js";
4
+ import { padOrTrimPlain } from "./render-text.js";
5
5
  export function renderToastOverlays(states, width, maxRows, theme) {
6
6
  if (maxRows <= 0)
7
7
  return [];
@@ -22,11 +22,12 @@ export function renderToastOverlays(states, width, maxRows, theme) {
22
22
  const toastWidth = Math.min(Math.max(12, contentWidth + 2), Math.max(1, width - 4));
23
23
  const leftWidth = Math.max(0, width - toastWidth - 2);
24
24
  const column = leftWidth + 1;
25
+ const style = toastStyle(state, theme);
25
26
  for (const line of visibleLines) {
26
27
  const message = ` ${padOrTrimPlain(line, Math.max(0, toastWidth - 2))} `;
27
- const text = padOrTrimPlain(message, toastWidth);
28
- const output = colorLine(message, toastWidth, {
29
- ...toastKindStyle(state.kind, theme),
28
+ const text = padOrTrimPlain(` ${padOrTrimPlain(stripToastAnsi(line), Math.max(0, toastWidth - 2))} `, toastWidth);
29
+ const output = colorToastLine(message, toastWidth, {
30
+ ...style,
30
31
  bold: true,
31
32
  });
32
33
  overlays.push({
@@ -47,7 +48,7 @@ function toastMessageLines(message, icon, maxWidth) {
47
48
  const safeMaxWidth = Math.max(1, maxWidth);
48
49
  const safeContinuationPrefix = stringDisplayWidth(continuationPrefix) < safeMaxWidth ? continuationPrefix : "";
49
50
  const lines = [];
50
- const logicalLines = sanitizeText(message).split("\n");
51
+ const logicalLines = sanitizeToastText(message).split("\n");
51
52
  for (const [index, logicalLine] of logicalLines.entries()) {
52
53
  const prefix = index === 0 ? firstPrefix : continuationPrefix;
53
54
  const prefixWidth = stringDisplayWidth(prefix);
@@ -74,21 +75,21 @@ function renderDialogToastOverlay(state, width, maxRows, theme, rowOffset) {
74
75
  const visibleBodyLines = bodyLines.slice(0, bodyRows);
75
76
  const includeBottom = maxRows > 1;
76
77
  const dialogRows = [
77
- dialogTopLine(closeLabel, dialogWidth),
78
- ...visibleBodyLines.map((line) => `│ ${padOrTrimPlain(line, bodyWidth)} │`),
79
- ...(includeBottom ? [`╰${"─".repeat(Math.max(0, dialogWidth - 2))}╯`] : []),
78
+ toastRow(dialogTopLine(closeLabel, dialogWidth)),
79
+ ...visibleBodyLines.map((line) => toastRow(`│ ${padOrTrimPlain(line, bodyWidth)} │`, `│ ${padOrTrimPlain(stripToastAnsi(line), bodyWidth)} │`)),
80
+ ...(includeBottom ? [toastRow(`╰${"─".repeat(Math.max(0, dialogWidth - 2))}╯`)] : []),
80
81
  ].slice(0, maxRows);
81
82
  const leftWidth = Math.max(0, width - dialogWidth - 2);
82
83
  const column = leftWidth + 1;
83
- const style = toastKindStyle(state.kind, theme);
84
+ const style = toastStyle(state, theme);
84
85
  const closeStartColumn = column + 1 + dialogTopCloseOffset(closeLabel, dialogWidth);
85
86
  const closeEndColumn = closeStartColumn + stringDisplayWidth(closeLabel);
86
- return dialogRows.map((text, index) => ({
87
+ return dialogRows.map((row, index) => ({
87
88
  id: state.id,
88
89
  row: rowOffset + index + 1,
89
90
  column,
90
- text,
91
- output: colorLine(text, dialogWidth, { ...style, bold: true }),
91
+ text: row.text,
92
+ output: colorToastLine(row.output, dialogWidth, { ...style, bold: true }),
92
93
  target: index === 0
93
94
  ? { kind: "toast", id: state.id, action: "close", startColumn: closeStartColumn, endColumn: closeEndColumn }
94
95
  : { kind: "toast", id: state.id, action: "body", startColumn: column, endColumn: column + dialogWidth },
@@ -96,9 +97,29 @@ function renderDialogToastOverlay(state, width, maxRows, theme, rowOffset) {
96
97
  }
97
98
  function dialogMessageLines(message, maxWidth) {
98
99
  const safeMaxWidth = Math.max(1, maxWidth);
99
- const lines = sanitizeText(message).split("\n").flatMap((line) => wrapDisplayLine(line, safeMaxWidth));
100
+ const lines = sanitizeToastText(message).split("\n").flatMap((line) => wrapDisplayLine(line, safeMaxWidth));
100
101
  return lines.length > 0 ? lines : [""];
101
102
  }
103
+ function toastRow(output, text = output) {
104
+ return { output, text };
105
+ }
106
+ function sanitizeToastText(text) {
107
+ return expandTabs(text.replace(/⚠️?|\u{f0026}/gu, APP_ICONS.alert).replace(/\r/g, ""))
108
+ .replace(/\x1b(?!\[[\d;:]*m)/gu, "␛");
109
+ }
110
+ function stripToastAnsi(text) {
111
+ return text.replace(/\x1b\[[\d;:]*m/gu, "");
112
+ }
113
+ function hasToastAnsiColor(text) {
114
+ return /\x1b\[[\d;:]*m/u.test(text);
115
+ }
116
+ function colorToastLine(text, width, options) {
117
+ const line = padOrTrimPlain(text, width);
118
+ const prefix = ansiStylePrefix(options);
119
+ if (!prefix)
120
+ return line;
121
+ return `${prefix}${line.replaceAll(ANSI_RESET, `${ANSI_RESET}${prefix}`)}${ANSI_RESET}`;
122
+ }
102
123
  function dialogTopLine(closeLabel, width) {
103
124
  const innerWidth = Math.max(0, width - 2);
104
125
  const closeOffset = dialogTopCloseOffset(closeLabel, width);
@@ -135,3 +156,7 @@ function toastKindStyle(kind, theme) {
135
156
  return { foreground: theme.colors.background, background: theme.colors.info };
136
157
  }
137
158
  }
159
+ function toastStyle(state, theme) {
160
+ const style = toastKindStyle(state.kind, theme);
161
+ return hasToastAnsiColor(state.message) ? { ...style, background: "#000000" } : style;
162
+ }
@@ -1,13 +1,14 @@
1
1
  import { type ResolvedToolRule } from "../../config.js";
2
2
  import type { ToolBodySyntaxHighlights } from "../../syntax-highlight.js";
3
3
  import type { Theme } from "../../theme.js";
4
- import type { RenderedLine } from "../types.js";
4
+ import type { RenderedLine, StyledSegment } from "../types.js";
5
5
  import type { ToolBodyLineStyle } from "../../tool-renderers/types.js";
6
6
  export type ToolBlockEntry = {
7
7
  id: string;
8
8
  toolName: string;
9
9
  headerLabel?: string | undefined;
10
10
  headerArgs?: string | undefined;
11
+ headerArgSegments?: readonly StyledSegment[] | undefined;
11
12
  bodyLineStyles?: readonly ToolBodyLineStyle[] | undefined;
12
13
  bodyStyle?: "diff" | undefined;
13
14
  preserveAnsi?: boolean | undefined;
@@ -28,9 +28,16 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
28
28
  const headerArgs = formatToolHeaderArgs(entry.headerArgs);
29
29
  const headerArgsWidth = width - stringDisplayWidth(headerPrefix) - 1;
30
30
  const clippedHeaderArgs = headerArgsWidth > 0 ? sliceByDisplayWidth(headerArgs, headerArgsWidth) : "";
31
+ const header = clippedHeaderArgs ? `${headerPrefix} ${clippedHeaderArgs}` : headerPrefix;
32
+ const headerArgsStart = headerPrefix.length + 1;
33
+ const clippedHeaderArgSegments = clippedHeaderArgs
34
+ ? clipAndShiftSegments(entry.headerArgSegments, headerArgsStart, clippedHeaderArgs.length)
35
+ : [];
36
+ const headerArgBaseSegments = clippedHeaderArgs
37
+ ? uncoveredSegments(headerArgsStart, header.length, toolOutputColor, clippedHeaderArgSegments)
38
+ : [];
31
39
  const target = { kind: "tool", id: entry.id };
32
40
  const showGutter = options.showGutter ?? true;
33
- const header = clippedHeaderArgs ? `${headerPrefix} ${clippedHeaderArgs}` : headerPrefix;
34
41
  const headerLine = {
35
42
  text: header,
36
43
  target,
@@ -39,7 +46,8 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
39
46
  segments: [
40
47
  { start: 0, end: stateIcon.length, foreground: toolStatusIconColor(entry, colors), bold: true },
41
48
  { start: stateIcon.length, end: headerPrefix.length, bold: true },
42
- ...(clippedHeaderArgs ? [{ start: headerPrefix.length + 1, end: header.length, foreground: toolOutputColor }] : []),
49
+ ...headerArgBaseSegments,
50
+ ...clippedHeaderArgSegments,
43
51
  ],
44
52
  };
45
53
  const headerLines = [headerLine];
@@ -85,6 +93,35 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
85
93
  ];
86
94
  return headerLines;
87
95
  }
96
+ function clipAndShiftSegments(segments, offset, length) {
97
+ if (!segments || length <= 0)
98
+ return [];
99
+ return segments.flatMap((segment) => {
100
+ const start = Math.max(0, segment.start);
101
+ const end = Math.min(length, segment.end);
102
+ if (end <= start)
103
+ return [];
104
+ return [{ ...segment, start: offset + start, end: offset + end }];
105
+ });
106
+ }
107
+ function uncoveredSegments(start, end, foreground, coveredSegments) {
108
+ if (end <= start)
109
+ return [];
110
+ const result = [];
111
+ let offset = start;
112
+ for (const segment of [...coveredSegments].sort((a, b) => a.start - b.start || a.end - b.end)) {
113
+ const segmentStart = Math.max(start, Math.min(end, segment.start));
114
+ const segmentEnd = Math.max(segmentStart, Math.min(end, segment.end));
115
+ if (segmentEnd <= offset)
116
+ continue;
117
+ if (segmentStart > offset)
118
+ result.push({ start: offset, end: segmentStart, foreground });
119
+ offset = segmentEnd;
120
+ }
121
+ if (offset < end)
122
+ result.push({ start: offset, end, foreground });
123
+ return result;
124
+ }
88
125
  function renderCollapsedPreviewLines(entry, body, rule, width, target, color, colors, hasLspDiagnostics, showGutter) {
89
126
  const preview = previewBodyText(body, rule.direction, rule.previewLines);
90
127
  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 });
@@ -467,7 +504,10 @@ function syntaxHighlightForLine(highlights, lineIndex) {
467
504
  return list.find((highlight) => lineIndex >= highlight.startLine && (highlight.endLine == null || lineIndex < highlight.endLine));
468
505
  }
469
506
  function diffLineStyle(line, colors) {
470
- const content = line.trimStart();
507
+ // Diff markers are only markers at column 0. Context lines in unified/apply_patch
508
+ // output start with a leading space, so trimming would incorrectly color
509
+ // markdown bullets like ` - item` as deletions and ` + item` as additions.
510
+ const content = line;
471
511
  if (/^(?:diff --git|index |\*\*\* (?:Begin|End) Patch)/.test(content))
472
512
  return { foreground: colors.muted, bold: true };
473
513
  if (/^(?:---|\+\+\+|\*\*\* (?:Update|Add|Delete) File:)/.test(content))
@@ -169,6 +169,7 @@ export declare class AppMouseController {
169
169
  private handleStatusVoiceMicClick;
170
170
  private handleStatusVoiceLanguageClick;
171
171
  private handleStatusQuickScrollClick;
172
+ private statusTargetContains;
172
173
  private handleInputClick;
173
174
  private handleExtensionInputClick;
174
175
  private openUserMessageMenu;