@oh-my-pi/pi-tui 18.13.0 → 18.13.2

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 (46) hide show
  1. package/CHANGELOG.md +36 -32
  2. package/dist/types/chrome/context-map.d.ts +15 -0
  3. package/dist/types/chrome/context-presentation.d.ts +48 -0
  4. package/dist/types/chrome/context-view-model.d.ts +77 -0
  5. package/dist/types/overlays/advisor-config.d.ts +6 -0
  6. package/dist/types/overlays/ask-dialog.d.ts +12 -3
  7. package/dist/types/overlays/context-viewer-format.d.ts +42 -0
  8. package/dist/types/overlays/context-viewer-overview.d.ts +70 -0
  9. package/dist/types/overlays/context-viewer.d.ts +25 -0
  10. package/dist/types/overlays/hook-input.d.ts +1 -0
  11. package/dist/types/overlays/pause-screen.d.ts +13 -3
  12. package/dist/types/overlays/running-subagent-badge.d.ts +7 -1
  13. package/dist/types/status-line/component.d.ts +2 -2
  14. package/dist/types/status-line/context-usage.d.ts +15 -30
  15. package/dist/types/theme/symbols.d.ts +1 -1
  16. package/dist/types/tools/ask.d.ts +4 -0
  17. package/dist/types/tools/fetch.d.ts +1 -0
  18. package/dist/types/tools/github.d.ts +3 -0
  19. package/dist/types/tools/read.d.ts +3 -0
  20. package/dist/types/tools/wait.d.ts +6 -0
  21. package/dist/types/tui.d.ts +2 -0
  22. package/package.json +9 -9
  23. package/src/chrome/context-map.ts +62 -0
  24. package/src/chrome/context-presentation.ts +26 -0
  25. package/src/chrome/context-view-model.ts +73 -0
  26. package/src/components/editor.ts +23 -0
  27. package/src/components/markdown.ts +14 -2
  28. package/src/overlays/advisor-config.ts +9 -2
  29. package/src/overlays/ask-dialog.ts +122 -35
  30. package/src/overlays/context-viewer-format.ts +145 -0
  31. package/src/overlays/context-viewer-overview.ts +381 -0
  32. package/src/overlays/context-viewer.ts +728 -0
  33. package/src/overlays/hook-input.ts +2 -0
  34. package/src/overlays/pause-screen.ts +166 -37
  35. package/src/overlays/running-subagent-badge.ts +12 -1
  36. package/src/render/output-pane.ts +3 -3
  37. package/src/status-line/component.ts +6 -4
  38. package/src/status-line/context-usage.ts +56 -580
  39. package/src/theme/symbols.ts +16 -0
  40. package/src/tools/ask.ts +4 -0
  41. package/src/tools/bash.ts +5 -23
  42. package/src/tools/fetch.ts +1 -0
  43. package/src/tools/github.ts +23 -3
  44. package/src/tools/read.ts +3 -1
  45. package/src/tools/wait.ts +6 -0
  46. package/src/tui.ts +23 -3
@@ -9,6 +9,7 @@ export interface ReadUrlToolDetails {
9
9
  finalUrl: string;
10
10
  contentType: string;
11
11
  method: string;
12
+ raw?: boolean;
12
13
  truncated: boolean;
13
14
  notes: string[];
14
15
  meta?: OutputMeta;
@@ -85,6 +85,7 @@ export interface GhRunWatchFailedLogDetails {
85
85
  conclusion?: string;
86
86
  tail?: string;
87
87
  available: boolean;
88
+ unavailableReason?: string;
88
89
  }
89
90
  /** Live workflow watcher display snapshot. */
90
91
  export interface GhRunWatchViewDetails {
@@ -130,6 +131,8 @@ export interface GhFailedJobLog {
130
131
  full?: string;
131
132
  tail?: string;
132
133
  available: boolean;
134
+ unavailableReason?: string;
135
+ truncated?: boolean;
133
136
  }
134
137
  /** Choose the terminal conclusion or current job state. */
135
138
  export declare function formatJobState(job: GhRunJobSnapshot): string;
@@ -29,7 +29,10 @@ export interface ReadToolDetails {
29
29
  suffixResolution?: {
30
30
  from: string;
31
31
  to: string;
32
+ raw?: boolean;
32
33
  };
34
+ /** Raw source output keeps descriptive diagnostics in details, not in the body. */
35
+ raw?: boolean;
33
36
  url?: string;
34
37
  finalUrl?: string;
35
38
  contentType?: string;
@@ -55,6 +55,12 @@ export interface JobSnapshot {
55
55
  * on collision, else the job id itself. See {@link AsyncJob.agentId}.
56
56
  */
57
57
  agentUrlId?: string;
58
+ /**
59
+ * Whether this job's turn published `<agentUrlId>.md`. False where the job
60
+ * body reported that it wrote none, so `agent://` still holds an earlier
61
+ * turn's output; undefined where the body does not report it.
62
+ */
63
+ agentOutputPublished?: boolean;
58
64
  }
59
65
  /** Outcome of cancelling one background job. */
60
66
  export type CancelStatus = "cancelled" | "not_found" | "already_completed";
@@ -363,6 +363,8 @@ export declare class TUI extends Container {
363
363
  * observers (live stream publisher, session recorder) coexist.
364
364
  */
365
365
  addPaintListener(listener: PaintListener): () => void;
366
+ /** Observe actual ANSI frame writes, including transient resize paints; returns the unsubscribe. */
367
+ addFrameWriteListener(listener: (data: string) => void): () => void;
366
368
  /** Install the product-owned bounded frame provider. */
367
369
  setFrameProvider(provider: TerminalFrameProvider | undefined): void;
368
370
  get fullRedraws(): number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "18.13.0",
4
+ "version": "18.13.2",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": {
@@ -41,14 +41,14 @@
41
41
  "gen:glyphs": "bun scripts/gen-glyphs.ts"
42
42
  },
43
43
  "dependencies": {
44
- "@oh-my-pi/omptype": "18.13.0",
45
- "@oh-my-pi/pi-agent-core": "18.13.0",
46
- "@oh-my-pi/pi-ai": "18.13.0",
47
- "@oh-my-pi/pi-catalog": "18.13.0",
48
- "@oh-my-pi/pi-natives": "18.13.0",
49
- "@oh-my-pi/pi-utils": "18.13.0",
50
- "@oh-my-pi/pi-wire": "18.13.0",
51
- "@oh-my-pi/snapcompact": "18.13.0"
44
+ "@oh-my-pi/omptype": "18.13.2",
45
+ "@oh-my-pi/pi-agent-core": "18.13.2",
46
+ "@oh-my-pi/pi-ai": "18.13.2",
47
+ "@oh-my-pi/pi-catalog": "18.13.2",
48
+ "@oh-my-pi/pi-natives": "18.13.2",
49
+ "@oh-my-pi/pi-utils": "18.13.2",
50
+ "@oh-my-pi/pi-wire": "18.13.2",
51
+ "@oh-my-pi/snapcompact": "18.13.2"
52
52
  },
53
53
  "devDependencies": {
54
54
  "kitty-vt-wasm": "^0.2.0"
@@ -0,0 +1,62 @@
1
+ /** Pure occupancy calculations for contiguous token segments, independent of themes and renderers. */
2
+ export interface ContextMapSegment<T> {
3
+ readonly id: T;
4
+ readonly tokens: number;
5
+ }
6
+
7
+ export interface ContextMapCell<T> {
8
+ readonly id: T;
9
+ readonly full: boolean;
10
+ }
11
+
12
+ /**
13
+ * Lays `segments` out in order over a `columns` × `rows` grid representing `windowTokens`; each cell takes the
14
+ * segment covering most of it and is `full` at 70% coverage or more. Returns `[]` when the window or grid
15
+ * size is unusable, and omits cells no segment touches.
16
+ */
17
+ export function planContextMap<T>(
18
+ segments: readonly ContextMapSegment<T>[],
19
+ windowTokens: number,
20
+ columns: number,
21
+ rows: number,
22
+ ): ContextMapCell<T>[] {
23
+ if (
24
+ !Number.isFinite(windowTokens) ||
25
+ windowTokens <= 0 ||
26
+ !Number.isInteger(columns) ||
27
+ !Number.isInteger(rows) ||
28
+ columns <= 0 ||
29
+ rows <= 0
30
+ )
31
+ return [];
32
+ const cellCount = columns * rows;
33
+ const tokensPerCell = windowTokens / cellCount;
34
+ const ranges: { id: T; start: number; end: number }[] = [];
35
+ let cursor = 0;
36
+ for (const segment of segments) {
37
+ if (!Number.isFinite(segment.tokens) || segment.tokens <= 0) continue;
38
+ const end = Math.min(windowTokens, cursor + segment.tokens);
39
+ if (end > cursor) ranges.push({ id: segment.id, start: cursor, end });
40
+ cursor = end;
41
+ if (cursor >= windowTokens) break;
42
+ }
43
+ const cells: ContextMapCell<T>[] = [];
44
+ let first = 0;
45
+ for (let index = 0; index < cellCount; index++) {
46
+ const start = index * tokensPerCell;
47
+ const end = (index + 1) * tokensPerCell;
48
+ while (first < ranges.length && ranges[first]!.end <= start) first++;
49
+ let winner: T | undefined;
50
+ let overlap = 0;
51
+ for (let part = first; part < ranges.length && ranges[part]!.start < end; part++) {
52
+ const range = ranges[part]!;
53
+ const covered = Math.min(end, range.end) - Math.max(start, range.start);
54
+ if (covered > overlap) {
55
+ overlap = covered;
56
+ winner = range.id;
57
+ }
58
+ }
59
+ if (winner !== undefined) cells.push({ id: winner, full: overlap / tokensPerCell >= 0.7 });
60
+ }
61
+ return cells;
62
+ }
@@ -0,0 +1,26 @@
1
+ import type { ThemeColor } from "../theme";
2
+ import type { SymbolKey } from "../theme/symbols";
3
+ import type { ContextCategoryId } from "./context-view-model";
4
+
5
+ /** Shared category styles for maps and interactive legends; resolve glyphs at render time. */
6
+ const SYSTEM = { color: "accent", symbol: "context.content" } as const;
7
+ const DETAILS = { color: "customMessageLabel", symbol: "context.content" } as const;
8
+ const TOOLS = { color: "warning", symbol: "context.content" } as const;
9
+ const SKILLS = { color: "success", symbol: "context.content" } as const;
10
+ const MESSAGES = { color: "userMessageText", symbol: "context.messages" } as const;
11
+ const OUTPUT = { color: "muted", symbol: "context.messages" } as const;
12
+ export const CONTEXT_PRESENTATION = {
13
+ "system-prompt": SYSTEM,
14
+ skills: SKILLS,
15
+ "built-in-tools": TOOLS,
16
+ "custom-tools": DETAILS,
17
+ // Shares the System Prompt color; the message glyph tells them apart in the map.
18
+ "user-messages": { color: "accent", symbol: "context.messages" },
19
+ "assistant-messages": MESSAGES,
20
+ "assistant-thinking": DETAILS,
21
+ "tool-calls": TOOLS,
22
+ "tool-output": OUTPUT,
23
+ buffer: { color: "warning", symbol: "context.buffer" },
24
+ free: { color: "dim", symbol: "context.free" },
25
+ } as const satisfies Record<ContextPresentationId, { color: ThemeColor; symbol: SymbolKey }>;
26
+ export type ContextPresentationId = ContextCategoryId | "buffer" | "free";
@@ -0,0 +1,73 @@
1
+ /** Context snapshot shapes shared by the session snapshot builder and the context viewer. */
2
+ import type { ContextBreakdown } from "../status-line/context-usage";
3
+
4
+ interface ContextCategoryDescriptor {
5
+ readonly id: string;
6
+ readonly label: string;
7
+ /** Overview row label when the Overview weight differs from the category's content. */
8
+ readonly overviewLabel?: string;
9
+ /** Listed with its weight but never opened. */
10
+ readonly openable?: false;
11
+ }
12
+
13
+ /** Context categories in display order. */
14
+ export const CONTEXT_CATEGORIES = [
15
+ // The Overview weight excludes the Skills listing; content views keep complete system blocks.
16
+ { id: "system-prompt", label: "System Prompt", overviewLabel: "System Prompt (excluding Skills)" },
17
+ // An estimate of the prompt's skill listing, without entries of its own.
18
+ { id: "skills", label: "Skills", openable: false },
19
+ { id: "built-in-tools", label: "Built-in Tools" },
20
+ { id: "custom-tools", label: "Custom Tools" },
21
+ { id: "user-messages", label: "User / Developer Messages" },
22
+ { id: "assistant-messages", label: "Assistant Messages" },
23
+ { id: "assistant-thinking", label: "Assistant Thinking" },
24
+ { id: "tool-calls", label: "Tool Calls" },
25
+ { id: "tool-output", label: "Tool Output" },
26
+ ] as const satisfies readonly ContextCategoryDescriptor[];
27
+
28
+ export type ContextCategoryId = (typeof CONTEXT_CATEGORIES)[number]["id"];
29
+
30
+ const DESCRIPTORS_BY_ID = new Map<ContextCategoryId, ContextCategoryDescriptor>(
31
+ CONTEXT_CATEGORIES.map(descriptor => [descriptor.id, descriptor]),
32
+ );
33
+
34
+ /** Descriptor of a category id: its label, optional Overview label, and whether it can be opened. */
35
+ export function contextCategoryDescriptor(id: ContextCategoryId): ContextCategoryDescriptor {
36
+ return DESCRIPTORS_BY_ID.get(id)!;
37
+ }
38
+
39
+ export interface ContextThinkingGroup {
40
+ readonly id: string;
41
+ readonly label: string;
42
+ readonly tokens: number;
43
+ readonly interrupted?: boolean;
44
+ }
45
+
46
+ export interface ContextViewEntry {
47
+ readonly id: string;
48
+ readonly label: string;
49
+ readonly text: string;
50
+ /** Aggregation weight for the unscaled replay estimate; shared blocks contribute through their group. */
51
+ readonly tokens: number;
52
+ readonly interrupted?: boolean;
53
+ readonly thinkingGroup?: ContextThinkingGroup;
54
+ readonly path?: string;
55
+ readonly info?: string;
56
+ readonly timestamp?: number;
57
+ readonly callId?: string;
58
+ }
59
+
60
+ export interface ContextViewCategory {
61
+ id: ContextCategoryId;
62
+ key?: string;
63
+ label: string;
64
+ /** Overview attribution weight, not reported usage; may differ from the entry sum (e.g. Skills subtraction). */
65
+ tokens: number;
66
+ entries: ContextViewEntry[];
67
+ children?: ContextViewCategory[];
68
+ }
69
+
70
+ export interface ContextViewData {
71
+ readonly breakdown: ContextBreakdown;
72
+ readonly categories: readonly ContextViewCategory[];
73
+ }
@@ -2431,10 +2431,12 @@ export class Editor implements Component, Focusable {
2431
2431
  // Word left
2432
2432
  this.#resetKillSequence();
2433
2433
  this.#moveWordBackwards();
2434
+ this.#dismissAutocomplete();
2434
2435
  } else if (kb.matchesCanonical(canonical, "tui.editor.cursorWordRight")) {
2435
2436
  // Word right
2436
2437
  this.#resetKillSequence();
2437
2438
  this.#moveWordForwards();
2439
+ this.#dismissAutocomplete();
2438
2440
  }
2439
2441
  // Arrow keys
2440
2442
  else if (kb.matchesCanonical(canonical, "tui.editor.cursorUp")) {
@@ -3900,6 +3902,9 @@ export class Editor implements Component, Focusable {
3900
3902
  const dropSpace = this.#hasProvisionalSpace();
3901
3903
  this.#historyIndex = -1; // Exit history browsing mode
3902
3904
  this.#resetKillSequence();
3905
+ // The cursor leaves the trigger token's line, so any open popup is stale:
3906
+ // keeping it would float on the new line and Enter would submit past it.
3907
+ this.#dismissAutocomplete();
3903
3908
  this.#recordUndoState();
3904
3909
 
3905
3910
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
@@ -4201,23 +4206,27 @@ export class Editor implements Component, Focusable {
4201
4206
 
4202
4207
  #moveToLineStart(): void {
4203
4208
  this.#resetKillSequence();
4209
+ this.#dismissAutocomplete();
4204
4210
  this.#setCursorCol(0);
4205
4211
  }
4206
4212
 
4207
4213
  #moveToLineEnd(): void {
4208
4214
  this.#resetKillSequence();
4215
+ this.#dismissAutocomplete();
4209
4216
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
4210
4217
  this.#setCursorCol(currentLine.length);
4211
4218
  }
4212
4219
 
4213
4220
  #moveToMessageStart(): void {
4214
4221
  this.#resetKillSequence();
4222
+ this.#dismissAutocomplete();
4215
4223
  this.#state.cursorLine = 0;
4216
4224
  this.#setCursorCol(0);
4217
4225
  }
4218
4226
 
4219
4227
  #moveToMessageEnd(): void {
4220
4228
  this.#resetKillSequence();
4229
+ this.#dismissAutocomplete();
4221
4230
  this.#state.cursorLine = this.#state.lines.length - 1;
4222
4231
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
4223
4232
  this.#setCursorCol(currentLine.length);
@@ -4697,6 +4706,7 @@ export class Editor implements Component, Focusable {
4697
4706
 
4698
4707
  #moveCursor(deltaLine: number, deltaCol: number): void {
4699
4708
  this.#resetKillSequence();
4709
+ this.#dismissAutocomplete();
4700
4710
  const visualLines = this.#buildVisualLineMap(this.#caretRowWidth);
4701
4711
  const currentVisualLine = this.#findCurrentVisualLine(visualLines);
4702
4712
 
@@ -4749,6 +4759,7 @@ export class Editor implements Component, Focusable {
4749
4759
 
4750
4760
  #pageScroll(direction: -1 | 1): void {
4751
4761
  this.#resetKillSequence();
4762
+ this.#dismissAutocomplete();
4752
4763
  const visualLines = this.#buildVisualLineMap(this.#caretRowWidth);
4753
4764
  const currentVisualLine = this.#findCurrentVisualLine(visualLines);
4754
4765
  const step = this.#getPageScrollStep(visualLines.length);
@@ -4779,6 +4790,7 @@ export class Editor implements Component, Focusable {
4779
4790
  */
4780
4791
  #jumpToChar(char: string, direction: "forward" | "backward"): void {
4781
4792
  this.#resetKillSequence();
4793
+ this.#dismissAutocomplete();
4782
4794
  const isForward = direction === "forward";
4783
4795
  const lines = this.#state.lines;
4784
4796
 
@@ -5195,6 +5207,17 @@ export class Editor implements Component, Focusable {
5195
5207
  }
5196
5208
  }
5197
5209
 
5210
+ /**
5211
+ * Close the popup when the cursor leaves the token it was opened for (newline,
5212
+ * pure cursor motion). Its prefix no longer matches the text before the cursor,
5213
+ * so it could neither accept nor refresh and would linger stale.
5214
+ */
5215
+ #dismissAutocomplete(): void {
5216
+ if (this.#autocompleteState === null) return;
5217
+ this.#cancelAutocomplete();
5218
+ this.onAutocompleteUpdate?.();
5219
+ }
5220
+
5198
5221
  /**
5199
5222
  * Whether an autocomplete popup is on screen. An `@` list narrowed to no match
5200
5223
  * while its refresh is pending stays open internally but is hidden, so it does
@@ -4346,7 +4346,13 @@ export class Markdown implements Component {
4346
4346
 
4347
4347
  // Process item tokens; nested-list lines arrive structurally tagged and
4348
4348
  // already carry their own full indent.
4349
- const itemLines = this.#renderListItem(item.tokens || [], depth, width, styleContext);
4349
+ const itemLines = this.#renderListItem(
4350
+ item.tokens || [],
4351
+ depth,
4352
+ width,
4353
+ Math.max(1, width - visibleWidth(continuationIndent)),
4354
+ styleContext,
4355
+ );
4350
4356
 
4351
4357
  if (itemLines.length > 0) {
4352
4358
  const firstLine = itemLines[0]!;
@@ -4398,11 +4404,13 @@ export class Markdown implements Component {
4398
4404
  tokens: Token[],
4399
4405
  parentDepth: number,
4400
4406
  width: number,
4407
+ contentWidth: number,
4401
4408
  styleContext?: InlineStyleContext,
4402
4409
  ): RenderedListItemLine[] {
4403
4410
  const lines: RenderedListItemLine[] = [];
4404
4411
 
4405
- for (const token of tokens) {
4412
+ for (let i = 0; i < tokens.length; i++) {
4413
+ const token = tokens[i]!;
4406
4414
  if (token.type === "list") {
4407
4415
  // Nested list - render with one additional indent level
4408
4416
  // These lines carry their own indent, so tag them for pass-through
@@ -4442,6 +4450,10 @@ export class Markdown implements Component {
4442
4450
  lines.push({ ...bodyLine, nested: false });
4443
4451
  }
4444
4452
  lines.push({ text: this.#theme.codeBlockBorder("```"), nested: false });
4453
+ } else if (token.type === "table") {
4454
+ for (const row of this.#renderTable(token as TableToken, contentWidth, tokens[i + 1]?.type, styleContext)) {
4455
+ lines.push({ text: row, nested: false });
4456
+ }
4445
4457
  } else if (isMathToken(token)) {
4446
4458
  // Display math block inside a list item: stack fractions / matrix rows.
4447
4459
  const apply = styleContext?.applyText ?? ((t: string) => this.#applyDefaultStyle(t));
@@ -64,10 +64,11 @@ import { getThinkingLevelMetadata, parseThinkingLevel } from "../thinking";
64
64
 
65
65
  export const ADVISOR_REVIEW_MODES = ["turn", "agent-end"] as const;
66
66
  export const ADVISOR_SYNC_BACKLOG_MODES = ["off", "1", "3", "5", "strict"] as const;
67
+ export const ADVISOR_SESSION_SCOPES = ["main", "subagent", "all"] as const;
67
68
 
68
69
  export type AdvisorReviewMode = (typeof ADVISOR_REVIEW_MODES)[number];
69
70
  export type AdvisorSyncBacklog = (typeof ADVISOR_SYNC_BACKLOG_MODES)[number];
70
-
71
+ export type AdvisorSessionScope = (typeof ADVISOR_SESSION_SCOPES)[number];
71
72
  /** One advisor declared in `WATCHDOG.yml`; its instructions specialize the shared baseline. */
72
73
  export interface AdvisorConfig {
73
74
  name: string;
@@ -83,6 +84,10 @@ export interface AdvisorConfig {
83
84
  reviewMode?: AdvisorReviewMode;
84
85
  reviewInterval?: number;
85
86
  syncBacklog?: AdvisorSyncBacklog;
87
+ /** Which session kinds this advisor attaches to (`main`, `subagent`, or `all`; defaults to `all`). */
88
+ scope?: AdvisorSessionScope;
89
+ /** Whether primary assistant thinking blocks are included in updates sent to this advisor (defaults to `true`). */
90
+ includeReasoning?: boolean;
86
91
  }
87
92
 
88
93
  /** Which level a `WATCHDOG.yml` lives at: the project root or the user agent dir. */
@@ -1202,7 +1207,9 @@ export class AdvisorConfigOverlayComponent implements Component {
1202
1207
  advisor.syncBacklog === undefined &&
1203
1208
  !advisor.instructions?.trim() &&
1204
1209
  advisor.enabled !== false &&
1205
- advisor.maxNotesPerUpdate === undefined
1210
+ advisor.maxNotesPerUpdate === undefined &&
1211
+ advisor.scope === undefined &&
1212
+ advisor.includeReasoning === undefined
1206
1213
  );
1207
1214
  }
1208
1215