@oh-my-pi/pi-tui 18.2.10 → 18.3.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 (69) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/THIRD-PARTY-NOTICES.txt +1 -1
  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/overlays/annotation-overlay.d.ts +30 -0
  7. package/dist/types/overlays/annotation-types.d.ts +84 -0
  8. package/dist/types/overlays/copy-selector.d.ts +30 -2
  9. package/dist/types/overlays/copy-targets.d.ts +2 -0
  10. package/dist/types/overlays/model-hub.d.ts +2 -2
  11. package/dist/types/overlays/oauth-selector.d.ts +3 -6
  12. package/dist/types/overlays/usage-dashboard.d.ts +2 -0
  13. package/dist/types/prompt/image-source.d.ts +9 -8
  14. package/dist/types/render/render-utils.d.ts +2 -0
  15. package/dist/types/status-line/context-usage.d.ts +3 -1
  16. package/dist/types/theme/mermaid-cache.d.ts +4 -6
  17. package/dist/types/tools/bash.d.ts +7 -0
  18. package/dist/types/tools/daemon.d.ts +46 -0
  19. package/dist/types/tools/irc.d.ts +30 -0
  20. package/dist/types/tools/line-ranges.d.ts +1 -1
  21. package/dist/types/tools/proc-render.d.ts +34 -0
  22. package/dist/types/tools/read.d.ts +4 -1
  23. package/dist/types/tools/task.d.ts +4 -0
  24. package/dist/types/tools/wait.d.ts +131 -0
  25. package/dist/types/tools/write.d.ts +4 -0
  26. package/dist/types/tools/xd-url.d.ts +8 -0
  27. package/dist/types/windows-altgr.d.ts +54 -0
  28. package/package.json +13 -10
  29. package/src/apps/debug/protocol-probe.ts +1 -1
  30. package/src/apps/ps-data.ts +1 -1
  31. package/src/apps/ps-top.ts +1 -1
  32. package/src/chat/chat-transcript-builder.ts +4 -4
  33. package/src/chat/tool-execution.ts +8 -8
  34. package/src/chat/transcript-render-helpers.ts +3 -3
  35. package/src/components/markdown.ts +1 -1
  36. package/src/desktop-notify.ts +1 -1
  37. package/src/overlays/annotation-overlay.ts +1145 -0
  38. package/src/overlays/annotation-types.ts +78 -0
  39. package/src/overlays/copy-selector.ts +45 -19
  40. package/src/overlays/copy-targets.ts +3 -3
  41. package/src/overlays/model-hub.ts +5 -4
  42. package/src/overlays/oauth-selector.ts +8 -12
  43. package/src/overlays/plan-review-overlay.ts +213 -40
  44. package/src/overlays/settings-defs.ts +1 -0
  45. package/src/overlays/usage-dashboard.ts +120 -25
  46. package/src/prompt/image-source.ts +9 -8
  47. package/src/prompt/welcome.ts +4 -1
  48. package/src/render/render-utils.ts +1 -1
  49. package/src/setup/scenes/sign-in.ts +1 -1
  50. package/src/status-line/component.ts +15 -4
  51. package/src/status-line/context-usage.ts +15 -3
  52. package/src/status-line/segments.ts +3 -3
  53. package/src/terminal-capabilities.ts +16 -7
  54. package/src/terminal.ts +8 -1
  55. package/src/theme/mermaid-cache.ts +83 -25
  56. package/src/tools/bash.ts +14 -1
  57. package/src/tools/daemon.ts +51 -0
  58. package/src/tools/index.ts +2 -2
  59. package/src/tools/irc.ts +33 -0
  60. package/src/tools/line-ranges.ts +13 -3
  61. package/src/tools/proc-render.ts +294 -0
  62. package/src/tools/read.ts +22 -1
  63. package/src/tools/task.ts +18 -0
  64. package/src/tools/wait.ts +595 -0
  65. package/src/tools/write.ts +76 -0
  66. package/src/tools/xd-url.ts +9 -0
  67. package/src/windows-altgr.ts +227 -0
  68. package/dist/types/tools/hub.d.ts +0 -342
  69. package/src/tools/hub.ts +0 -1492
@@ -230,7 +230,7 @@ export class SignInTab implements SetupTab {
230
230
  this.#host.restoreFocus();
231
231
  this.#host.requestRender();
232
232
  try {
233
- await this.#authStorage.login(providerId as OAuthProvider, {
233
+ await this.#authStorage.oauth.login(providerId as OAuthProvider, {
234
234
  signal: this.#loginAbort.signal,
235
235
  onBrowserSession: (request, signal) => this.#host.ctx.captureBrowserSession(request, signal),
236
236
  onAuth: info => {
@@ -55,6 +55,18 @@ const BRAND_FADE_MS = 450;
55
55
  /** Repaint cadence while the brand fade is in flight (rust omp's `FADE_FRAME`). */
56
56
  const BRAND_FADE_FRAME_MS = 40;
57
57
 
58
+ /**
59
+ * Providers whose subscription quota is a single monthly bucket, so their
60
+ * `monthly`/`30d` window is the one the usage segment must show. Providers that
61
+ * merely report a monthly side-counter (GitHub Copilot's premium requests) stay
62
+ * out: their monthly row is not the session quota.
63
+ */
64
+ const MONTHLY_SUBSCRIPTION_PROVIDERS: Record<string, true> = {
65
+ "alibaba-token-plan": true,
66
+ cursor: true,
67
+ "opencode-go": true,
68
+ };
69
+
58
70
  /** A displayable limit after provider, account, model, and window filtering. */
59
71
  interface UsageWindowCandidate {
60
72
  id?: string;
@@ -1842,6 +1854,8 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1842
1854
  const activeModelId = normalizeUsageScopeValue(context.modelId);
1843
1855
  const activeAntigravityCounter =
1844
1856
  context.provider === "google-antigravity" ? getAntigravityCounterKeyForModel(context.modelId) : undefined;
1857
+ const monthlySubscriptionProvider =
1858
+ context.provider !== undefined && MONTHLY_SUBSCRIPTION_PROVIDERS[context.provider] === true;
1845
1859
  const scopeGroups = new Map<string, UsageScopeGroup>();
1846
1860
  for (const report of reports) {
1847
1861
  if (!report || typeof report !== "object") continue;
@@ -1904,10 +1918,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
1904
1918
  : undefined;
1905
1919
  const windowClass =
1906
1920
  subscriptionWindow ??
1907
- ((context.provider === "cursor" || context.provider === "opencode-go") &&
1908
- (windowId === "monthly" || windowId === "30d")
1909
- ? "monthly"
1910
- : undefined);
1921
+ (monthlySubscriptionProvider && (windowId === "monthly" || windowId === "30d") ? "monthly" : undefined);
1911
1922
  if (!windowClass) continue;
1912
1923
 
1913
1924
  const modelId = normalizeUsageScopeValue("modelId" in scope ? scope.modelId : undefined);
@@ -2,6 +2,7 @@ import type { AgentMessage, Tokenizer } from "@oh-my-pi/pi-agent-core";
2
2
  import type { CompactionSettings } from "@oh-my-pi/pi-agent-core/compaction";
3
3
  import { effectiveReserveTokens, resolveThresholdTokens } from "@oh-my-pi/pi-agent-core/compaction";
4
4
  import type { Tool as AiTool, Model } from "@oh-my-pi/pi-ai";
5
+ import { renderToolExamples } from "@oh-my-pi/pi-ai/dialect";
5
6
  import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
6
7
  import { formatNumber } from "@oh-my-pi/pi-utils";
7
8
  import type { Theme } from "../theme";
@@ -12,7 +13,7 @@ interface ContextSkill {
12
13
  readonly hide?: boolean;
13
14
  }
14
15
 
15
- type ContextTool = Pick<AiTool, "name" | "description" | "parameters">;
16
+ type ContextTool = Pick<AiTool, "name" | "description" | "parameters" | "examples">;
16
17
 
17
18
  /** Savings computed by the host's inline-image planner, not by the renderer. */
18
19
  export interface ContextSavingsEstimate {
@@ -140,6 +141,8 @@ export interface NonMessageTokenSource {
140
141
  };
141
142
  };
142
143
  readonly skills?: readonly ContextSkill[];
144
+ /** Provider-facing, session-frozen descriptions when available. */
145
+ readonly renderedSkills?: readonly ContextSkill[];
143
146
  }
144
147
 
145
148
  /** Shared empty system-prompt part list, avoiding an allocation per render. */
@@ -244,9 +247,13 @@ export function estimateToolSchemaTokens(tools: ToolSchemaSource, tokenizer: Tok
244
247
  name,
245
248
  description,
246
249
  parameters: parameters as AiTool["parameters"],
250
+ examples: tool.examples,
247
251
  };
248
252
  const wireJson = JSON.stringify(toolWireSchema(wireTool) ?? {});
249
253
  if (typeof wireJson === "string") fragments.push(wireJson);
254
+ // The agent loop appends rendered examples to the wire description.
255
+ const examplesBlock = renderToolExamples(wireTool);
256
+ if (examplesBlock) fragments.push(examplesBlock);
250
257
  } catch {
251
258
  // Schema may contain functions or cycles; ignore.
252
259
  }
@@ -313,7 +320,7 @@ function nonMessageTokenCacheEntry(
313
320
  const systemPromptRef = session.systemPrompt ?? EMPTY_STRING_PARTS;
314
321
  const toolsRef = session.agent?.state?.tools ?? EMPTY_TOOLS;
315
322
  const toolsRevision = getToolSchemaMetadataRevision(toolsRef);
316
- const skillsRef = session.skills ?? EMPTY_SKILLS;
323
+ const skillsRef = session.renderedSkills ?? session.skills ?? EMPTY_SKILLS;
317
324
  let entry = cachedSession[NON_MESSAGE_TOKEN_CACHE];
318
325
  if (
319
326
  entry &&
@@ -378,7 +385,12 @@ export function computeNonMessageBreakdown(
378
385
  if (entry.breakdown && entry.skillful === skillful) return entry.breakdown;
379
386
  const tools = session.agent?.state?.tools ?? EMPTY_TOOLS;
380
387
  const skillsTokens =
381
- skillful === false ? 0 : estimateSkillsTokens(renderedSkills(session.skills ?? EMPTY_SKILLS, tools), tokenizer);
388
+ skillful === false
389
+ ? 0
390
+ : estimateSkillsTokens(
391
+ renderedSkills(session.renderedSkills ?? session.skills ?? EMPTY_SKILLS, tools),
392
+ tokenizer,
393
+ );
382
394
  const toolsTokens = estimateToolSchemaTokens(tools, tokenizer, sourceRevision);
383
395
  const systemPromptParts = session.systemPrompt ?? EMPTY_STRING_PARTS;
384
396
  const systemContextTokens = tokenizer.countTokens(Array.from(systemPromptParts.slice(1), part => part ?? ""));
@@ -881,9 +881,9 @@ const usageSegment: StatusLineSegment = {
881
881
  parts.push(formatQuotaWindow(ctx, "7d", u.sevenDay.percent, u.sevenDay.resetHours, "h", "round"));
882
882
  }
883
883
  if (u.monthly) {
884
- // Cursor and OpenCode Go (normalize gates monthly to those providers).
885
- // Both floor used percents upstream (Cursor's dashboard shows 1.88 →
886
- // "1% used"; OpenCode's endpoint already emits floored integers).
884
+ // Monthly-subscription providers only (the normalizer gates the class).
885
+ // Cursor and QwenCloud floor used percents upstream (Cursor's dashboard
886
+ // shows 1.88 → "1% used"; OpenCode's endpoint emits floored integers).
887
887
  parts.push(formatQuotaWindow(ctx, "mo", u.monthly.percent, u.monthly.resetHours, "h", "floor"));
888
888
  }
889
889
  if (u.resetCredits) {
@@ -42,7 +42,7 @@ export type TerminalId =
42
42
  | "base"
43
43
  | "trueColor";
44
44
 
45
- const CMUX_NOTIFICATION_TITLE = "Oh My Pi";
45
+ const CMUX_NOTIFICATION_TITLE = "omp";
46
46
  const CMUX_SURFACE_ID_PATTERN = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu;
47
47
 
48
48
  /** Title and body for an out-of-band multiplexer notification (cmux, Herdr). */
@@ -678,11 +678,20 @@ const KNOWN_TERMINALS = Object.freeze({
678
678
  // OSC 66 text sizing are unverified, so those stay on conservative defaults;
679
679
  // synchronized output is left to the runtime DECRQM probe.
680
680
  otty: new TerminalInfo("otty", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc99),
681
- // rio ships the Kitty graphics protocol — direct placement plus U=1 Unicode
682
- // placeholders verified by the reporter (#12205). Everything unproven stays
683
- // conservative: hyperlinks, DECCARA, screen-to-scrollback, and notifications
684
- // keep the base defaults until verified in that terminal.
685
- rio: new TerminalInfo("rio", ImageProtocol.Kitty, true, false),
681
+ // rio publishes its supported escape sequences at
682
+ // rioterm.com/docs/escape-sequence-support: OSC 8 hyperlinks are opened via
683
+ // the hints system (Alt+click on Windows/Linux) — live-verified on
684
+ // rio 0.5.28/Win11 26100 where an OSC 8 span Alt+click opened the target
685
+ // URL in the browser, and an OSC 52 clipboard query round-tripped. Kitty
686
+ // graphics direct placement plus U=1 Unicode placeholders verified by the
687
+ // reporter (#12205). Notifications stay on BEL: rio parses OSC 9/777 into
688
+ // its own notifier, but its Windows path raises an unpackaged WinRT toast
689
+ // with AppId "Rio" that Windows silently drops without a registered AUMID
690
+ // shortcut — observed as a no-op live — so flipping it would only remove
691
+ // the D-Bus fallback for Linux rio users. DECCARA, screen-to-scrollback,
692
+ // OSC 99, and OSC 66 text sizing are outside rio's supported set and keep
693
+ // the conservative defaults.
694
+ rio: new TerminalInfo("rio", ImageProtocol.Kitty, true, true),
686
695
  });
687
696
 
688
697
  /** Resolve terminal identity from environment markers used by common emulators. */
@@ -1424,7 +1433,7 @@ function notificationToLine(n: TerminalNotification): string {
1424
1433
  // C0/C1 control characters that are unsafe inside an OSC payload (must base64).
1425
1434
  const OSC99_UNSAFE = /[\x00-\x1f\x7f\x80-\x9f]/u;
1426
1435
  const OSC99_MAX_PAYLOAD_BYTES = 2048;
1427
- const OSC99_APP_NAME = "Oh My Pi";
1436
+ const OSC99_APP_NAME = "omp";
1428
1437
  let nextOsc99NotificationId = 1;
1429
1438
 
1430
1439
  function base64Utf8(value: string): string {
package/src/terminal.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  } from "./terminal-capabilities";
26
26
  import { isInsideTmux, wrapTmuxPassthrough } from "./tmux";
27
27
  import { setHangulCompatibilityJamoWidth } from "./utils";
28
+ import { translateWindowsAltGrSequence } from "./windows-altgr";
28
29
 
29
30
  const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000;
30
31
  const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07";
@@ -1463,7 +1464,13 @@ export class ProcessTerminal implements Terminal {
1463
1464
  return;
1464
1465
  }
1465
1466
  if (this.#inputHandler) {
1466
- this.#inputHandler(sequence);
1467
+ // Windows console hosts drop AltGr text under kitty (AltGr+F → `CSI 102;3u`);
1468
+ // recover it from the active layout before any keybinding sees an Alt chord.
1469
+ const altGrText =
1470
+ this.#kittyProtocolActive && this.#conpty && process.platform === "win32"
1471
+ ? translateWindowsAltGrSequence(sequence)
1472
+ : undefined;
1473
+ this.#inputHandler(altGrText ?? sequence);
1467
1474
  }
1468
1475
  });
1469
1476
 
@@ -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,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
+ }
@@ -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
  }