@oh-my-pi/pi-tui 18.11.2 → 18.12.0

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 (61) hide show
  1. package/CHANGELOG.md +37 -100
  2. package/README.md +36 -0
  3. package/dist/types/components/markdown.d.ts +4 -3
  4. package/dist/types/overlays/approval-dialog.d.ts +25 -15
  5. package/dist/types/overlays/model-picker.d.ts +2 -0
  6. package/dist/types/overlays/session-selector.d.ts +2 -0
  7. package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
  8. package/dist/types/render/hanging-wrap.d.ts +2 -0
  9. package/dist/types/render/render-utils.d.ts +9 -1
  10. package/dist/types/status-line/types.d.ts +3 -0
  11. package/dist/types/terminal-capabilities.d.ts +8 -1
  12. package/dist/types/terminal-multiplexer.d.ts +6 -0
  13. package/dist/types/theme/symbols.d.ts +1 -1
  14. package/dist/types/theme/theme.d.ts +11 -0
  15. package/dist/types/tools/ast-edit.d.ts +2 -2
  16. package/dist/types/tools/ast-grep.d.ts +2 -2
  17. package/dist/types/tools/edit.d.ts +2 -0
  18. package/dist/types/tools/find.d.ts +14 -4
  19. package/dist/types/tools/glob.d.ts +3 -3
  20. package/dist/types/tools/grep.d.ts +2 -2
  21. package/dist/types/tools/read.d.ts +13 -5
  22. package/dist/types/tools/task.d.ts +8 -0
  23. package/dist/types/tools/write.d.ts +2 -0
  24. package/package.json +9 -9
  25. package/src/chat/assistant-message.ts +3 -1
  26. package/src/chat/read-tool-group.ts +17 -9
  27. package/src/chat/tool-execution.ts +23 -6
  28. package/src/components/image.ts +31 -14
  29. package/src/components/markdown.ts +74 -5
  30. package/src/overlays/approval-dialog.ts +104 -36
  31. package/src/overlays/model-picker.ts +58 -21
  32. package/src/overlays/session-selector.ts +14 -9
  33. package/src/prompt/internal-url-autocomplete.ts +1 -0
  34. package/src/render/hanging-wrap.ts +28 -1
  35. package/src/render/render-utils.ts +16 -2
  36. package/src/status-line/component.ts +1 -0
  37. package/src/status-line/segments.ts +16 -5
  38. package/src/status-line/types.ts +1 -0
  39. package/src/terminal-capabilities.ts +35 -21
  40. package/src/terminal-multiplexer.ts +14 -0
  41. package/src/terminal.ts +11 -2
  42. package/src/theme/symbols.ts +1 -0
  43. package/src/theme/theme-class.ts +1 -0
  44. package/src/theme/theme.ts +46 -14
  45. package/src/tmux.ts +7 -1
  46. package/src/tools/ast-edit.ts +28 -4
  47. package/src/tools/ast-grep.ts +24 -4
  48. package/src/tools/debug.ts +2 -1
  49. package/src/tools/default-renderer.ts +3 -2
  50. package/src/tools/edit.ts +77 -16
  51. package/src/tools/find.ts +65 -15
  52. package/src/tools/glob.ts +51 -17
  53. package/src/tools/grep.ts +94 -11
  54. package/src/tools/lsp.ts +23 -18
  55. package/src/tools/mcp.ts +2 -2
  56. package/src/tools/read.ts +52 -22
  57. package/src/tools/task.ts +17 -7
  58. package/src/tools/web-search.ts +2 -1
  59. package/src/tools/write.ts +17 -5
  60. package/src/tools/yield.ts +2 -2
  61. package/src/tui.ts +36 -0
@@ -1,4 +1,5 @@
1
1
  import * as path from "node:path";
2
+ import { sanitizeText } from "@oh-my-pi/pi-utils";
2
3
  import type { HighlightStream } from "@oh-my-pi/pi-natives";
3
4
  import type { Component } from "../tui";
4
5
  import { fencedCode } from "../components/markdown";
@@ -434,6 +435,8 @@ function renderContentPreview(
434
435
  /** Render context for the write tool: resolves an `xd://`-mounted tool so its live renderer drives device dispatch previews. */
435
436
  export interface WriteRenderContext {
436
437
  resolveXdevMounted?: (name: string) => XdevMountedRenderer | undefined;
438
+ /** Original result metadata used only for filesystem link targets. */
439
+ rawDetails?: unknown;
437
440
  }
438
441
 
439
442
  /** Process id and operation named by the text after `proc://`. */
@@ -550,6 +553,15 @@ function describeWriteUrl(
550
553
  }
551
554
  }
552
555
 
556
+ function writeResolvedPath(rawDetails: unknown, fallback: string | undefined): string | undefined {
557
+ return rawDetails !== null &&
558
+ typeof rawDetails === "object" &&
559
+ "resolvedPath" in rawDetails &&
560
+ typeof rawDetails.resolvedPath === "string"
561
+ ? rawDetails.resolvedPath
562
+ : fallback;
563
+ }
564
+
553
565
  /** Native write head shared by the call and result: `Write · path · 22 lines`, executable/diagnostics chips. */
554
566
  function writeToolHead(
555
567
  rawPath: string,
@@ -563,7 +575,7 @@ function writeToolHead(
563
575
  if (diagnostics) badges.push(diagnostics);
564
576
  return {
565
577
  title: "Write",
566
- target: rawPath ? displayPath(rawPath) : undefined,
578
+ target: rawPath ? sanitizeText(displayPath(rawPath)) : undefined,
567
579
  targetKind: "path",
568
580
  href: fileHref(details?.resolvedPath),
569
581
  meta: lineCount > 0 ? [`${lineCount} line${lineCount === 1 ? "" : "s"}`] : undefined,
@@ -621,7 +633,7 @@ export const writeToolRenderer = {
621
633
  if (xdev.name === REPORT_ISSUE_DEVICE_NAME) return renderReportIssueDeviceCall(args.content, uiTheme);
622
634
  return renderXdevCall(xdev.name, args.content, options, uiTheme, options.renderContext?.resolveXdevMounted);
623
635
  }
624
- const filePath = shortenPath(rawPath);
636
+ const filePath = sanitizeText(shortenPath(rawPath));
625
637
  const lang = rawPath ? (getLanguageFromPath(rawPath) ?? "text") : "text";
626
638
  const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
627
639
  const styledPath = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
@@ -705,14 +717,14 @@ export const writeToolRenderer = {
705
717
  }
706
718
  const rawPath =
707
719
  typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
708
- const filePath = shortenPath(rawPath);
720
+ const filePath = sanitizeText(shortenPath(rawPath));
709
721
  const fileContent = normalizeDisplayText(args?.content);
710
722
  const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
711
723
  const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
712
724
  // The header shows the cwd-relative path but links to the absolute path the
713
725
  // write resolved to (args.path may be relative, which would yield a broken
714
726
  // `file://` URI). Falls back to plain text when the result lacks a path.
715
- const linkTarget = result.details?.resolvedPath;
727
+ const linkTarget = writeResolvedPath(options.renderContext?.rawDetails, result.details?.resolvedPath);
716
728
  const styledPath = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
717
729
  const pathDisplay = filePath && linkTarget ? fileHyperlink(linkTarget, styledPath) : styledPath;
718
730
 
@@ -835,7 +847,7 @@ export const writeToolRenderer = {
835
847
  const diagnostics = isPartial ? undefined : result.details?.diagnostics;
836
848
  const tool = writeToolHead(rawPath, fileContent, {
837
849
  madeExecutable: !isPartial && result.details?.madeExecutable === true,
838
- resolvedPath: result.details?.resolvedPath,
850
+ resolvedPath: writeResolvedPath(options.renderContext?.rawDetails, result.details?.resolvedPath),
839
851
  diagnostics,
840
852
  });
841
853
  if (result.isError) return { tool, tone: "error", body: [errorText(resultText(result))] };
@@ -9,7 +9,7 @@ import { compact, md } from "../native/describe";
9
9
  import { OwnerMemo } from "../native/memo";
10
10
  import type { NativeChild } from "../native/node";
11
11
  import { formatOutputPaneLines, styleToolOutputLine } from "../render/output-pane";
12
- import { formatExpandHint, sanitizeDisplayLines, truncateToWidth } from "../render/render-utils";
12
+ import { clipCollapsed, formatExpandHint, sanitizeDisplayLines } from "../render/render-utils";
13
13
  import type { StatusLineOptions } from "../render/status-line";
14
14
  import { plainToolCard, type ToolCardPhase } from "../render/tool-card";
15
15
  import type { Theme } from "../theme/theme";
@@ -134,7 +134,7 @@ function dataLines(data: unknown, expanded: boolean, width: number, theme: Theme
134
134
  lines: sanitizeText(data).trimEnd().split("\n"),
135
135
  expanded,
136
136
  collapsedMaxLines: TEXT_ROWS_COLLAPSED,
137
- styleLine: line => truncateToWidth(styleToolOutputLine(line, theme), width),
137
+ styleLine: line => clipCollapsed(styleToolOutputLine(line, theme), width, expanded),
138
138
  },
139
139
  theme,
140
140
  ).lines,
package/src/tui.ts CHANGED
@@ -36,6 +36,7 @@ import {
36
36
  encodeKittyDeleteImage,
37
37
  encodeKittyDeletePlacement,
38
38
  encodeKittyPlacementLine,
39
+ getTerminalBackgroundColor,
39
40
  ImageProtocol,
40
41
  isImageProtocolForced,
41
42
  isInsideTerminalMultiplexer,
@@ -46,6 +47,7 @@ import {
46
47
  synchronizedOutputUserOverride,
47
48
  TERMINAL,
48
49
  } from "./terminal-capabilities";
50
+ import { onTerminalBackgroundFallbackChange } from "./theme/theme";
49
51
  import { compositeLineAt } from "./render/composite";
50
52
  import { tmuxMultiplexer } from "./multiplexers/tmux";
51
53
  import {
@@ -1007,6 +1009,7 @@ export class TUI extends Container {
1007
1009
  #forceViewportRepaintOnNextRender = false;
1008
1010
  #hasEverRendered = false;
1009
1011
  #stopped = false;
1012
+ #appearanceReportUnsubscribe: (() => void) | undefined;
1010
1013
  #cancelPostmortemRestore?: () => void;
1011
1014
  /** True between a `deferInput` start() and enableInput(). */
1012
1015
  #inputDeferred = false;
@@ -1478,6 +1481,34 @@ export class TUI extends Container {
1478
1481
 
1479
1482
  start(options?: TUIStartOptions): void {
1480
1483
  this.#stopped = false;
1484
+ this.#appearanceReportUnsubscribe?.();
1485
+ let backgroundRgb: number | undefined;
1486
+ let subscribed = true;
1487
+ const repaintForBackgroundChange = () => {
1488
+ if (!subscribed || this.#stopped) return;
1489
+ const nextBackgroundRgb = getTerminalBackgroundColor();
1490
+ // Track RGB before the SIXEL guard: the startup OSC 11 reply usually
1491
+ // arrives before DA1 enables SIXEL, so revisiting the same color once
1492
+ // SIXEL is live must not replay identical rows.
1493
+ if (nextBackgroundRgb === backgroundRgb) return;
1494
+ backgroundRgb = nextBackgroundRgb;
1495
+ if (TERMINAL.imageProtocol !== ImageProtocol.Sixel) return;
1496
+ // Track RGB even during optimistic native ownership: fallback paints
1497
+ // with the latest color, so a later return to the startup RGB must repaint.
1498
+ // Never write rows while the native surface still owns rendering.
1499
+ if (this.#nativeLive) return;
1500
+ this.invalidate();
1501
+ // Match theme changes: replay owned history on direct terminals, but
1502
+ // leave multiplexer scrollback intact and repaint only the viewport.
1503
+ this.requestRender(true, { clearScrollback: !isInsideTerminalMultiplexer() });
1504
+ };
1505
+ const unsubscribe = this.terminal.onAppearanceReport?.(repaintForBackgroundChange);
1506
+ const unsubscribeFallback = onTerminalBackgroundFallbackChange(repaintForBackgroundChange);
1507
+ this.#appearanceReportUnsubscribe = () => {
1508
+ subscribed = false;
1509
+ unsubscribe?.();
1510
+ unsubscribeFallback();
1511
+ };
1481
1512
  this.#debugPaint = undefined;
1482
1513
  this.#debugServer?.stop();
1483
1514
  this.#debugServer = undefined;
@@ -1605,6 +1636,9 @@ export class TUI extends Container {
1605
1636
  () => this.stop(),
1606
1637
  { deferInput: this.#inputDeferred, isLoopStalled: () => this.#watchdog.isStalled() },
1607
1638
  );
1639
+ // ProcessTerminal.start() resets the previous owner's RGB. Seed only
1640
+ // after that reset so an identical OSC 11 reply still repaints fallback rows.
1641
+ backgroundRgb = TERMINAL.imageProtocol === ImageProtocol.Sixel ? getTerminalBackgroundColor() : undefined;
1608
1642
  if (this.#stopped) return;
1609
1643
  this.#cancelPostmortemRestore?.();
1610
1644
  this.#cancelPostmortemRestore = postmortem.register("tui-restore", () => this.stop());
@@ -2580,6 +2614,8 @@ export class TUI extends Container {
2580
2614
  }
2581
2615
 
2582
2616
  stop(): void {
2617
+ this.#appearanceReportUnsubscribe?.();
2618
+ this.#appearanceReportUnsubscribe = undefined;
2583
2619
  this.#cancelPostmortemRestore?.();
2584
2620
  this.#cancelPostmortemRestore = undefined;
2585
2621
  this.#debugServer?.stop();