@oh-my-pi/pi-tui 18.2.11 → 18.3.1

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 (101) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/THIRD-PARTY-NOTICES.txt +3 -3
  3. package/dist/types/apps/ps-data.d.ts +1 -1
  4. package/dist/types/apps/ps-top.d.ts +1 -1
  5. package/dist/types/chat/chat-transcript-builder.d.ts +1 -1
  6. package/dist/types/chat/read-tool-group.d.ts +3 -3
  7. package/dist/types/chat/user-message.d.ts +4 -1
  8. package/dist/types/components/editor.d.ts +0 -2
  9. package/dist/types/components/input.d.ts +16 -0
  10. package/dist/types/overlays/annotation-overlay.d.ts +30 -0
  11. package/dist/types/overlays/annotation-types.d.ts +84 -0
  12. package/dist/types/overlays/btw-history-panel.d.ts +4 -1
  13. package/dist/types/overlays/copy-selector.d.ts +30 -2
  14. package/dist/types/overlays/copy-targets.d.ts +2 -0
  15. package/dist/types/overlays/model-hub.d.ts +2 -2
  16. package/dist/types/overlays/oauth-selector.d.ts +3 -6
  17. package/dist/types/overlays/settings-defs.d.ts +5 -0
  18. package/dist/types/overlays/usage-dashboard.d.ts +2 -0
  19. package/dist/types/prompt/composer.d.ts +12 -1
  20. package/dist/types/prompt/custom-editor.d.ts +5 -26
  21. package/dist/types/prompt/image-source.d.ts +9 -8
  22. package/dist/types/space-hold.d.ts +45 -0
  23. package/dist/types/status-line/context-usage.d.ts +3 -1
  24. package/dist/types/status-line/host.d.ts +2 -0
  25. package/dist/types/theme/mermaid-cache.d.ts +4 -6
  26. package/dist/types/tools/bash.d.ts +7 -0
  27. package/dist/types/tools/cfg-render.d.ts +37 -0
  28. package/dist/types/tools/cfg-url.d.ts +16 -0
  29. package/dist/types/tools/daemon.d.ts +46 -0
  30. package/dist/types/tools/find.d.ts +2 -2
  31. package/dist/types/tools/irc.d.ts +30 -0
  32. package/dist/types/tools/line-ranges.d.ts +1 -1
  33. package/dist/types/tools/output-meta.d.ts +2 -0
  34. package/dist/types/tools/proc-render.d.ts +27 -0
  35. package/dist/types/tools/read.d.ts +18 -4
  36. package/dist/types/tools/result-card.d.ts +20 -0
  37. package/dist/types/tools/url-scheme-host.d.ts +29 -0
  38. package/dist/types/tools/wait.d.ts +131 -0
  39. package/dist/types/tools/write.d.ts +6 -0
  40. package/dist/types/tools/xd-url.d.ts +8 -0
  41. package/dist/types/tools/xdev.d.ts +9 -1
  42. package/dist/types/windows-altgr.d.ts +54 -0
  43. package/package.json +13 -10
  44. package/src/apps/debug/protocol-probe.ts +1 -1
  45. package/src/apps/ps-data.ts +1 -1
  46. package/src/apps/ps-top.ts +1 -1
  47. package/src/chat/chat-transcript-builder.ts +9 -5
  48. package/src/chat/read-tool-group.ts +8 -9
  49. package/src/chat/tool-execution.ts +15 -13
  50. package/src/chat/transcript-render-helpers.ts +3 -3
  51. package/src/chat/user-message.ts +16 -6
  52. package/src/components/editor.ts +2 -4
  53. package/src/components/input.ts +103 -30
  54. package/src/components/markdown.ts +19 -7
  55. package/src/desktop-notify.ts +1 -1
  56. package/src/overlays/annotation-overlay.ts +1144 -0
  57. package/src/overlays/annotation-types.ts +78 -0
  58. package/src/overlays/btw-history-panel.ts +4 -0
  59. package/src/overlays/copy-selector.ts +45 -19
  60. package/src/overlays/copy-targets.ts +3 -3
  61. package/src/overlays/model-hub.ts +5 -4
  62. package/src/overlays/oauth-selector.ts +8 -12
  63. package/src/overlays/plan-review-overlay.ts +213 -40
  64. package/src/overlays/settings-defs.ts +6 -0
  65. package/src/overlays/settings-selector.ts +4 -3
  66. package/src/overlays/usage-dashboard.ts +22 -1
  67. package/src/prompt/composer.ts +83 -56
  68. package/src/prompt/custom-editor.ts +15 -137
  69. package/src/prompt/image-source.ts +9 -8
  70. package/src/prompt/tips.txt +1 -0
  71. package/src/setup/scenes/sign-in.ts +1 -1
  72. package/src/space-hold.ts +143 -0
  73. package/src/status-line/component.ts +6 -0
  74. package/src/status-line/context-usage.ts +15 -3
  75. package/src/status-line/host.ts +2 -0
  76. package/src/status-line/segments.ts +4 -0
  77. package/src/terminal-capabilities.ts +2 -2
  78. package/src/terminal.ts +8 -1
  79. package/src/theme/mermaid-cache.ts +83 -25
  80. package/src/tools/bash.ts +14 -1
  81. package/src/tools/cfg-render.ts +117 -0
  82. package/src/tools/cfg-url.ts +30 -0
  83. package/src/tools/daemon.ts +51 -0
  84. package/src/tools/find.ts +7 -6
  85. package/src/tools/index.ts +2 -2
  86. package/src/tools/irc.ts +33 -0
  87. package/src/tools/line-ranges.ts +13 -3
  88. package/src/tools/output-meta.ts +2 -0
  89. package/src/tools/proc-render.ts +270 -0
  90. package/src/tools/read.ts +118 -56
  91. package/src/tools/result-card.ts +36 -0
  92. package/src/tools/url-scheme-host.ts +42 -0
  93. package/src/tools/wait.ts +595 -0
  94. package/src/tools/write.ts +120 -1
  95. package/src/tools/xd-url.ts +9 -0
  96. package/src/tools/xdev.ts +9 -1
  97. package/src/windows-altgr.ts +227 -0
  98. package/dist/types/chat/read-target.d.ts +0 -4
  99. package/dist/types/tools/hub.d.ts +0 -342
  100. package/src/chat/read-target.ts +0 -7
  101. package/src/tools/hub.ts +0 -1492
@@ -1,5 +1,5 @@
1
1
  import type { MermaidRenderOptions } from "@oh-my-pi/pi-natives";
2
- import { renderMermaidAsciiSafe } from "@oh-my-pi/pi-utils/mermaid-ascii";
2
+ import * as mermaidAscii from "@oh-my-pi/pi-utils/mermaid-ascii";
3
3
 
4
4
  /**
5
5
  * Options controlling how fenced Mermaid source is resolved to terminal ASCII.
@@ -8,12 +8,10 @@ import { renderMermaidAsciiSafe } from "@oh-my-pi/pi-utils/mermaid-ascii";
8
8
  */
9
9
  export interface MermaidResolveOptions extends MermaidRenderOptions {
10
10
  /**
11
- * Maximum display width (terminal columns) the diagram should occupy. A
12
- * layout that overflows this width is re-rendered in the perpendicular
13
- * orientation — a wide horizontal chain collapses to a tall vertical column
14
- * (which the terminal can scroll), and a wide vertical fan-out collapses to a
15
- * tall horizontal column. Omit to keep the source's own layout regardless of
16
- * width.
11
+ * Maximum display width (terminal columns) the diagram should occupy.
12
+ * Flowcharts are also rendered top-down and left-to-right. A resize picks
13
+ * the shortest variant that fits; if none fit, the narrowest, which the
14
+ * caller may still clip. Omit to keep the source's own layout.
17
15
  */
18
16
  maxWidth?: number;
19
17
  }
@@ -23,16 +21,29 @@ export interface MermaidResolveOptions extends MermaidRenderOptions {
23
21
  // the cached renders, so a terminal resize re-decides without re-rendering.
24
22
  const cache = new Map<string, string | null>();
25
23
 
26
- /** Widest rendered row in display columns (ANSI- and CJK-aware). */
24
+ /** Display columns, ignoring ANSI so themed diagrams are measured as drawn. */
25
+ const DISPLAY_WIDTH = { countAnsiEscapeCodes: false } as const;
26
+
27
27
  function asciiDisplayWidth(ascii: string): number {
28
28
  let max = 0;
29
29
  for (const line of ascii.split("\n")) {
30
- const width = Bun.stringWidth(line);
30
+ const width = Bun.stringWidth(line, DISPLAY_WIDTH);
31
31
  if (width > max) max = width;
32
32
  }
33
33
  return max;
34
34
  }
35
35
 
36
+ interface LayoutCandidate {
37
+ ascii: string;
38
+ width: number;
39
+ height: number;
40
+ authored: boolean;
41
+ }
42
+
43
+ function measureLayout(ascii: string, authored: boolean): LayoutCandidate {
44
+ return { ascii, width: asciiDisplayWidth(ascii), height: ascii.split("\n").length, authored };
45
+ }
46
+
36
47
  function renderVariant(
37
48
  source: string,
38
49
  baseOptions: MermaidRenderOptions,
@@ -43,11 +54,39 @@ function renderVariant(
43
54
  const cached = cache.get(key);
44
55
  if (cached !== undefined) return cached;
45
56
 
46
- const ascii = renderMermaidAsciiSafe(source, direction ? { ...baseOptions, direction } : baseOptions);
57
+ const ascii = mermaidAscii.renderMermaidAsciiSafe(source, direction ? { ...baseOptions, direction } : baseOptions);
47
58
  cache.set(key, ascii);
48
59
  return ascii;
49
60
  }
50
61
 
62
+ const FORCED_DIRECTIONS = ["TD", "LR"] as const;
63
+ type ForcedDirection = (typeof FORCED_DIRECTIONS)[number];
64
+
65
+ /**
66
+ * Forced TD/LR renders that can differ from the authored one. Sequence, class,
67
+ * ER, and xychart ignore direction, so they get none; header match mirrors
68
+ * native `detect_diagram_kind`, including its JavaScript `\w` boundary
69
+ * (`[A-Za-z0-9_]`). A `graph`/`flowchart` header already authored in one of
70
+ * the forced layouts (`TD`/`TB` lay out as TD, `LR`/`RL` as LR) skips that
71
+ * variant: the native override replaces the header direction, so the render
72
+ * would be byte-identical to the authored one.
73
+ */
74
+ function forcedDirections(source: string): readonly ForcedDirection[] {
75
+ const header = (source.split("\n", 1)[0] ?? "").trim().toLowerCase();
76
+ for (const prefix of ["xychart-beta", "xychart"] as const) {
77
+ if (!header.startsWith(prefix)) continue;
78
+ const next = header.charCodeAt(prefix.length);
79
+ const wordChar =
80
+ (next >= 48 && next <= 57) || (next >= 65 && next <= 90) || (next >= 97 && next <= 122) || next === 95;
81
+ if (!wordChar) return [];
82
+ }
83
+ if (header === "sequencediagram" || header === "classdiagram" || header === "erdiagram") return [];
84
+ const authored = /^(?:graph|flowchart)\s+(td|tb|lr|rl)$/.exec(header)?.[1];
85
+ if (authored === "td" || authored === "tb") return ["LR"];
86
+ if (authored === "lr" || authored === "rl") return ["TD"];
87
+ return FORCED_DIRECTIONS;
88
+ }
89
+
51
90
  /**
52
91
  * Resolve mermaid ASCII from fenced block source text.
53
92
  * Returns null when rendering fails, while memoizing failures to avoid repeated work.
@@ -65,24 +104,43 @@ export function resolveMermaidAscii(source: string, options?: MermaidResolveOpti
65
104
  if (base === null) return null;
66
105
  if (maxWidth === undefined) return base;
67
106
 
68
- let best = base;
69
- let bestWidth = asciiDisplayWidth(base);
70
- if (bestWidth <= maxWidth) return base;
71
-
72
- // The as-authored layout overflows. Render both forced orientations and keep
73
- // the narrowest (clipping at the call site handles any residual overflow).
74
- // Re-rendering the already-authored orientation is a cache hit, so this stays
75
- // cheap, and one of the two will be the perpendicular fit.
76
- for (const direction of ["TD", "LR"] as const) {
107
+ // Width selection is against cached renders, so a later resize re-decides
108
+ // without drawing again. Only forced layouts that can differ from the
109
+ // authored one are rendered (see `forcedDirections`).
110
+ const candidates = [measureLayout(base, true)];
111
+ for (const direction of forcedDirections(normalizedSource)) {
77
112
  const variant = renderVariant(normalizedSource, baseOptions, baseKey, direction);
78
- if (variant === null) continue;
79
- const variantWidth = asciiDisplayWidth(variant);
80
- if (variantWidth < bestWidth) {
81
- best = variant;
82
- bestWidth = variantWidth;
113
+ if (variant !== null) candidates.push(measureLayout(variant, false));
114
+ }
115
+
116
+ const fitting = candidates.filter(candidate => candidate.width <= maxWidth);
117
+ const pool = fitting.length > 0 ? fitting : candidates;
118
+ let best = pool[0]!;
119
+ for (let i = 1; i < pool.length; i++) {
120
+ const next = pool[i]!;
121
+ if (fitting.length > 0) {
122
+ if (next.height !== best.height) {
123
+ if (next.height < best.height) best = next;
124
+ continue;
125
+ }
126
+ if (next.authored !== best.authored) {
127
+ if (next.authored) best = next;
128
+ continue;
129
+ }
130
+ if (next.width < best.width) best = next;
131
+ continue;
132
+ }
133
+ if (next.width !== best.width) {
134
+ if (next.width < best.width) best = next;
135
+ continue;
136
+ }
137
+ if (next.height !== best.height) {
138
+ if (next.height < best.height) best = next;
139
+ continue;
83
140
  }
141
+ if (next.authored) best = next;
84
142
  }
85
- return best;
143
+ return best.ascii;
86
144
  }
87
145
 
88
146
  /**
package/src/tools/bash.ts CHANGED
@@ -41,6 +41,13 @@ export interface BashToolDetails {
41
41
  timedOut?: boolean;
42
42
  /** Live ACP update only; completed results refer to released terminals. */
43
43
  terminalId?: string;
44
+ service?: {
45
+ name: string;
46
+ state: string;
47
+ ready: boolean;
48
+ timedOut: boolean;
49
+ pid?: number;
50
+ };
44
51
  async?: {
45
52
  state: "running" | "completed" | "failed";
46
53
  jobId: string;
@@ -362,6 +369,12 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
362
369
  if (details?.async?.state === "running") {
363
370
  statsParts.push(`Backgrounded: ${details.async.jobId}`);
364
371
  }
372
+ if (details?.service) {
373
+ const service = details.service;
374
+ statsParts.push(`Service: ${service.name}`, `State: ${service.state}`);
375
+ statsParts.push(`Ready: ${service.ready ? "yes" : service.timedOut ? "timed out" : "no"}`);
376
+ if (service.pid !== undefined) statsParts.push(`PID: ${service.pid}`);
377
+ }
365
378
  if (wallTimeMs !== undefined) {
366
379
  statsParts.push(`Wall: ${formatWallTimeSeconds(wallTimeMs)}s`);
367
380
  }
@@ -460,7 +473,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
460
473
 
461
474
  /** Renders bash command previews and output. */
462
475
  export const bashToolRenderer = createShellRenderer<BashRenderArgs>({
463
- resolveTitle: () => "Bash",
476
+ resolveTitle: args => (args?.name ? `Bash · ${String(args.name)}` : "Bash"),
464
477
  resolveCommand: args => args?.command,
465
478
  resolveCwd: args => args?.cwd,
466
479
  resolveEnv: args => args?.env,
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Transcript renderers for `cfg://` settings reads and writes.
3
+ *
4
+ * Reads show the YAML-ish settings listing the model received, highlighted as
5
+ * YAML. Writes show the `previous → value` change, its scope (session or
6
+ * saved), and whether the user approved it.
7
+ */
8
+ import type { Component } from "../tui";
9
+ import { renderStatusLine } from "../render";
10
+ import {
11
+ cappedHeadLines,
12
+ formatBadge,
13
+ formatErrorDetail,
14
+ formatMoreItems,
15
+ PREVIEW_LIMITS,
16
+ } from "../render/render-utils";
17
+ import { highlightCode, type Theme } from "../theme/theme";
18
+ import { parseCfgUrl } from "./cfg-url";
19
+ import type { RenderResultOptions } from "./renderer";
20
+ import { card, type CardToolResult, firstText, safe } from "./result-card";
21
+
22
+ /** Summary of a `cfg://` read, attached to read tool details. */
23
+ export interface CfgReadDetails {
24
+ /** Dotted setting path or namespace that was read; empty for the whole tree. */
25
+ path: string;
26
+ /** Settings in the listing. */
27
+ count: number;
28
+ /** Listed settings whose effective value differs from the schema default. */
29
+ modified: number;
30
+ }
31
+
32
+ /** How a `cfg://` write ended. */
33
+ export type CfgWriteOutcome = "applied" | "declined" | "unchanged";
34
+
35
+ /** A `cfg://` write, attached to write tool details. Values are display-formatted (credentials redacted). */
36
+ export interface CfgWriteDetails {
37
+ path: string;
38
+ /** Persisted to config.yml rather than scoped to the session. */
39
+ save: boolean;
40
+ previous: string;
41
+ value: string;
42
+ outcome: CfgWriteOutcome;
43
+ /** Effective value after saving when a higher-precedence layer still shadows the saved one. */
44
+ effective?: string;
45
+ }
46
+
47
+ /** Render `read cfg://…`: pending header, or the highlighted listing with counts. */
48
+ export function renderCfgRead(
49
+ url: string,
50
+ result: CardToolResult | undefined,
51
+ details: CfgReadDetails | undefined,
52
+ options: RenderResultOptions,
53
+ theme: Theme,
54
+ ): Component {
55
+ return card((_width, expanded) => {
56
+ const meta: string[] = [];
57
+ if (details) {
58
+ meta.push(details.count === 1 ? "1 setting" : `${details.count} settings`);
59
+ if (details.modified) meta.push(`${details.modified} modified`);
60
+ }
61
+ const header = renderStatusLine(
62
+ {
63
+ icon: result === undefined ? "pending" : result.isError ? "error" : "info",
64
+ title: "Config",
65
+ description: (details?.path ?? parseCfgUrl(url)?.segments.join(".")) || "all settings",
66
+ meta,
67
+ },
68
+ theme,
69
+ );
70
+ if (!result) return [header];
71
+ if (result.isError) return [header, formatErrorDetail(firstText(result) || "Settings read failed.", theme)];
72
+ const body = firstText(result).trimEnd().split("\n");
73
+ const shown = cappedHeadLines(body, expanded ? body.length : PREVIEW_LIMITS.COLLAPSED_ITEMS);
74
+ const lines = [header, ...highlightCode(safe(shown.lines.join("\n")), "yaml", theme).map(line => ` ${line}`)];
75
+ if (shown.hidden) lines.push(theme.fg("dim", ` ${formatMoreItems(shown.hidden, "line")}`));
76
+ return lines;
77
+ }, options);
78
+ }
79
+
80
+ /** Render `write cfg://…[/save]`: the proposed value while pending, then the approved/declined change. */
81
+ export function renderCfgWrite(
82
+ url: string,
83
+ content: string | undefined,
84
+ result: CardToolResult | undefined,
85
+ details: CfgWriteDetails | undefined,
86
+ options: RenderResultOptions,
87
+ theme: Theme,
88
+ ): Component {
89
+ return card(() => {
90
+ const target = parseCfgUrl(url);
91
+ const save = details?.save ?? target?.save ?? false;
92
+ const icon =
93
+ result === undefined
94
+ ? "pending"
95
+ : result.isError
96
+ ? "error"
97
+ : details?.outcome === "declined"
98
+ ? "warning"
99
+ : "success";
100
+ const badges = [formatBadge(save ? "persist" : "session", save ? "accent" : "muted", theme)];
101
+ if (details?.outcome === "declined") badges.push(formatBadge("declined", "warning", theme));
102
+ if (details?.outcome === "unchanged") badges.push(formatBadge("unchanged", "muted", theme));
103
+ const header = `${renderStatusLine({ icon, title: "Config", description: details?.path ?? target?.segments.join(".") }, theme)} ${badges.join(" ")}`;
104
+ if (result?.isError) return [header, formatErrorDetail(firstText(result) || "Settings write failed.", theme)];
105
+ const arrow = theme.fg("dim", "→");
106
+ const next = theme.fg("toolOutput", details?.value ?? content?.trim() ?? "…");
107
+ const lines = [
108
+ header,
109
+ details ? ` ${theme.fg("dim", details.previous)} ${arrow} ${next}` : ` ${arrow} ${next}`,
110
+ ];
111
+ if (details?.effective !== undefined)
112
+ lines.push(
113
+ theme.fg("warning", ` still ${details.effective}: a higher-precedence layer overrides the saved value`),
114
+ );
115
+ return lines;
116
+ }, options);
117
+ }
@@ -0,0 +1,30 @@
1
+ /** Canonical prefix for settings URLs. */
2
+ export const CFG_URL_PREFIX = "cfg://";
3
+
4
+ /** Trailing write segment that persists a `cfg://` change instead of scoping it to the session. */
5
+ export const CFG_SAVE_SEGMENT = "save";
6
+
7
+ /** Setting addressed by a `cfg://` URL. */
8
+ export interface CfgTarget {
9
+ /** Setting path segments; empty for the root. `/` and `.` both separate segments. */
10
+ segments: string[];
11
+ /** Whether the URL ends in the `/save` suffix, which is removed from {@link segments}. */
12
+ save: boolean;
13
+ }
14
+
15
+ /**
16
+ * Parse a `cfg://` URL (`cfg://advisor/enabled`, `cfg://advisor.enabled/save`).
17
+ * Returns `null` for other schemes; query and fragment are ignored.
18
+ */
19
+ export function parseCfgUrl(input: string): CfgTarget | null {
20
+ const trimmed = input.trim();
21
+ if (!trimmed.toLowerCase().startsWith(CFG_URL_PREFIX)) return null;
22
+ const segments = trimmed
23
+ .slice(CFG_URL_PREFIX.length)
24
+ .replace(/[?#].*$/s, "")
25
+ .split(/[/.]/)
26
+ .filter(Boolean);
27
+ const save = segments.length > 1 && segments.at(-1) === CFG_SAVE_SEGMENT;
28
+ if (save) segments.pop();
29
+ return { segments, save };
30
+ }
@@ -0,0 +1,51 @@
1
+ /** Launch-broker daemon types shared by services, `proc://`, and `omp ps`. */
2
+
3
+ /** Stable lifecycle states exposed by the launch broker. */
4
+ export type DaemonState = "starting" | "running" | "ready" | "restarting" | "stopping" | "exited" | "failed";
5
+
6
+ /** Restart behavior applied after an unexpected daemon exit. */
7
+ export type DaemonRestartPolicy = "no" | "on-failure" | "always";
8
+
9
+ /** Readiness conditions; every configured condition must pass. */
10
+ export interface DaemonReadySpec {
11
+ log?: string;
12
+ port?: number;
13
+ host?: string;
14
+ timeoutMs: number;
15
+ }
16
+
17
+ /** Immutable launch specification retained for restart and inspection. */
18
+ export interface DaemonSpec {
19
+ name: string;
20
+ application: string;
21
+ args: string[];
22
+ env: Record<string, string>;
23
+ cwd: string;
24
+ pty: boolean;
25
+ ready?: DaemonReadySpec;
26
+ restart: DaemonRestartPolicy;
27
+ persist: boolean;
28
+ detached: boolean;
29
+ }
30
+
31
+ /** Serializable daemon state visible to every client in one broker scope. */
32
+ export interface DaemonSnapshot {
33
+ name: string;
34
+ id: string;
35
+ state: DaemonState;
36
+ pid?: number;
37
+ createdAt: number;
38
+ startedAt: number;
39
+ readyAt?: number;
40
+ exitedAt?: number;
41
+ exitCode?: number;
42
+ exitReason?: string;
43
+ restartCount: number;
44
+ outputBytes: number;
45
+ owner?: string;
46
+ readyMatch?: string;
47
+ /** Readiness conditions still unmet while `state` is `starting`; absent once ready or without a ready spec. */
48
+ readyPending?: ("log" | "port")[];
49
+ persist: boolean;
50
+ detached: boolean;
51
+ }
package/src/tools/find.ts CHANGED
@@ -28,6 +28,7 @@ import type { Theme, ThemeColor } from "../theme/theme";
28
28
  import type { Component } from "../tui";
29
29
  import type { OutputMeta } from "./output-meta";
30
30
  import type { RenderResultOptions, ToolRenderer } from "./renderer";
31
+ import { splitUrlScheme } from "./url-scheme-host";
31
32
 
32
33
  /** A verified line range with its yes-probability and a one-line preview. */
33
34
  export interface FindRange {
@@ -39,7 +40,7 @@ export interface FindRange {
39
40
 
40
41
  /** A file whose verified passages cleared the threshold; `ranges` are merged positive spans, strongest first. */
41
42
  export interface FindHit {
42
- /** Display path relative to {@link FindToolDetails.cwd}, or an `omp://` doc URL for docs scopes. */
43
+ /** Display path relative to {@link FindToolDetails.cwd}, or an internal URL under URL scopes. */
43
44
  rel: string;
44
45
  /** Filename judgment, when the name batch answered. */
45
46
  nameScore?: number;
@@ -85,7 +86,7 @@ export interface FindToolDetails {
85
86
  elapsedMs: number;
86
87
  /** Session cwd; hit paths are relative to it. */
87
88
  cwd: string;
88
- /** Display form of the searched directory when narrower than cwd. */
89
+ /** Display form of the searched directory or file when narrower than cwd. */
89
90
  scopePath?: string;
90
91
  meta?: OutputMeta;
91
92
  }
@@ -124,11 +125,11 @@ function gauge(p: number, theme: Theme): string {
124
125
  }
125
126
 
126
127
  function renderHit(hit: FindHit, rangeLimit: number, cwd: string | undefined, theme: Theme): string[] {
127
- // `omp://` hits are virtual docs, not files under `cwd`: link the URL
128
- // itself instead of joining it onto a filesystem base.
129
- const isOmpHit = /^omp:\/\//i.test(hit.rel);
128
+ // `scheme://` hits (e.g. virtual docs) are not files under `cwd`: link the
129
+ // URL itself instead of joining it onto a filesystem base.
130
+ const isUrlHit = splitUrlScheme(hit.rel) !== undefined;
130
131
  const link = (text: string, line?: number): string => {
131
- if (isOmpHit) return uriHyperlink(line === undefined ? hit.rel : `${hit.rel}:${line}`, text);
132
+ if (isUrlHit) return uriHyperlink(line === undefined ? hit.rel : `${hit.rel}:${line}`, text);
132
133
  const absPath = cwd === undefined ? undefined : path.join(cwd, hit.rel);
133
134
  return absPath === undefined ? text : fileHyperlink(absPath, text, { line });
134
135
  };
@@ -15,7 +15,7 @@ import { githubToolRenderer } from "./github";
15
15
  import { globToolRenderer } from "./glob";
16
16
  import { goalToolRenderer } from "./goal";
17
17
  import { grepToolRenderer } from "./grep";
18
- import { hubToolRenderer } from "./hub";
18
+ import { waitToolRenderer } from "./wait";
19
19
  import { lspToolRenderer } from "./lsp";
20
20
  import { recallToolRenderer, reflectToolRenderer, retainToolRenderer } from "./memory";
21
21
  import { readToolRenderer } from "./read";
@@ -45,7 +45,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
45
45
  glob: globToolRenderer,
46
46
  grep: grepToolRenderer,
47
47
  lsp: lspToolRenderer,
48
- hub: hubToolRenderer,
48
+ wait: waitToolRenderer,
49
49
  read: readToolRenderer,
50
50
  // Keyed by xd:// resolution-device names: the write dispatch delegates here
51
51
  // by dispatch tool, and historical `resolve` tool transcripts still render
@@ -0,0 +1,33 @@
1
+ /** Peer-message types and roster constants shared by messaging, `wait`, and child prompts. */
2
+
3
+ /** Maximum live peer rows embedded in child prompts. */
4
+ export const DEFAULT_PEER_ROSTER_LIMIT = 32;
5
+
6
+ /** Serializable peer message retained in coordination result snapshots. */
7
+ export interface IrcMessage {
8
+ id: string;
9
+ /** Sender agent id. */
10
+ from: string;
11
+ /** Recipient agent id (resolved; "all" is expanded by the tool, not stored). */
12
+ to: string;
13
+ body: string;
14
+ ts: number;
15
+ /** Message id being answered. */
16
+ replyTo?: string;
17
+ /**
18
+ * Automated wake-turn relay of a woken subagent's stop output (task executor
19
+ * `relayWakeTurnOutput`). Relays are answers, never wake sources: the
20
+ * recipient's own wake-turn relay must skip them or two idle peers
21
+ * ping-pong forever.
22
+ */
23
+ wakeRelay?: boolean;
24
+ }
25
+
26
+ /** Delivery outcome for one peer recipient. */
27
+ export interface IrcDeliveryReceipt {
28
+ to: string;
29
+ outcome: "injected" | "woken" | "revived" | "failed";
30
+ error?: string;
31
+ }
32
+ /** Status ordering for peer rosters in child prompts. */
33
+ export const LIST_STATUS_ORDER: Record<string, number> = { running: 0, idle: 1, parked: 2 };
@@ -15,6 +15,11 @@ const LINE_RANGE_CHUNK_RE = new RegExp(`^${LINE_RANGE_CHUNK_SOURCE}$`, "i");
15
15
 
16
16
  /** Parse a single `N`, `N-M`, `N-`, `N+K`, or `..`-aliased (`N..M`, `N..`) chunk. Throws via {@link ToolError} on invalid bounds. */
17
17
  export function parseLineRangeChunk(sel: string): LineRange | null {
18
+ return parseChunk(sel, false);
19
+ }
20
+
21
+ /** `pinBare` makes a separator-less `N` the single line `N` instead of "from `N` onward". */
22
+ function parseChunk(sel: string, pinBare: boolean): LineRange | null {
18
23
  const lineMatch = LINE_RANGE_CHUNK_RE.exec(sel);
19
24
  if (!lineMatch) return null;
20
25
  const rawStart = Number.parseInt(lineMatch[1]!, 10);
@@ -31,27 +36,32 @@ export function parseLineRangeChunk(sel: string): LineRange | null {
31
36
  }
32
37
  rawEnd = rawStart + rhs - 1;
33
38
  } else if (sep === "-") {
34
- // `301-` is shorthand for "from 301 onward" — equivalent to bare `301`.
39
+ // `301-` is shorthand for "from 301 onward" — equivalent to a lone bare `301`.
35
40
  if (rhs !== undefined) {
36
41
  if (rhs < rawStart) {
37
42
  throw new ToolError(`Invalid range ${rawStart}-${rhs}: end must be >= start.`);
38
43
  }
39
44
  rawEnd = rhs;
40
45
  }
46
+ } else if (pinBare) {
47
+ rawEnd = rawStart;
41
48
  }
42
49
  return { startLine: rawStart, endLine: rawEnd };
43
50
  }
44
51
 
45
52
  /**
46
- * Parse a comma-separated list of line ranges (e.g. `5-16,960-973`). Returns
53
+ * Parse a comma-separated list of line ranges (e.g. `5-16,960-973` or `19,59`). Returns
47
54
  * the ranges in ascending order with overlapping/adjacent ranges merged so
48
55
  * downstream consumers can stream the file in a single forward pass per range.
49
56
  */
50
57
  export function parseLineRanges(sel: string): [LineRange, ...LineRange[]] | null {
51
58
  const chunks = sel.split(",");
59
+ // A lone `:50` means "from line 50". Inside a comma list, a bare number is
60
+ // that one line; otherwise `:19,59` collapses to "from 19 through EOF".
61
+ const pinBare = chunks.length > 1;
52
62
  const parsed: LineRange[] = [];
53
63
  for (const chunk of chunks) {
54
- const range = parseLineRangeChunk(chunk);
64
+ const range = parseChunk(chunk, pinBare);
55
65
  if (!range) return null;
56
66
  parsed.push(range);
57
67
  }
@@ -72,6 +72,8 @@ export interface OutputMeta {
72
72
  /** Capture failure of this output itself; aggregate reports keep source failures on their entries. */
73
73
  artifactError?: OutputArtifactError;
74
74
  source?: SourceMeta;
75
+ /** The output is a bounded page of session artifact storage its `source` re-reads with line selectors; spill-to-artifact skips it. */
76
+ pagedSource?: true;
75
77
  diagnostics?: DiagnosticMeta;
76
78
  limits?: LimitsMeta;
77
79
  }