@oh-my-pi/pi-tui 18.2.11 → 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 (61) hide show
  1. package/CHANGELOG.md +20 -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/status-line/context-usage.d.ts +3 -1
  15. package/dist/types/theme/mermaid-cache.d.ts +4 -6
  16. package/dist/types/tools/bash.d.ts +7 -0
  17. package/dist/types/tools/daemon.d.ts +46 -0
  18. package/dist/types/tools/irc.d.ts +30 -0
  19. package/dist/types/tools/line-ranges.d.ts +1 -1
  20. package/dist/types/tools/proc-render.d.ts +34 -0
  21. package/dist/types/tools/read.d.ts +4 -1
  22. package/dist/types/tools/wait.d.ts +131 -0
  23. package/dist/types/tools/write.d.ts +4 -0
  24. package/dist/types/tools/xd-url.d.ts +8 -0
  25. package/dist/types/windows-altgr.d.ts +54 -0
  26. package/package.json +13 -10
  27. package/src/apps/debug/protocol-probe.ts +1 -1
  28. package/src/apps/ps-data.ts +1 -1
  29. package/src/apps/ps-top.ts +1 -1
  30. package/src/chat/chat-transcript-builder.ts +4 -4
  31. package/src/chat/tool-execution.ts +8 -8
  32. package/src/chat/transcript-render-helpers.ts +3 -3
  33. package/src/components/markdown.ts +1 -1
  34. package/src/desktop-notify.ts +1 -1
  35. package/src/overlays/annotation-overlay.ts +1145 -0
  36. package/src/overlays/annotation-types.ts +78 -0
  37. package/src/overlays/copy-selector.ts +45 -19
  38. package/src/overlays/copy-targets.ts +3 -3
  39. package/src/overlays/model-hub.ts +5 -4
  40. package/src/overlays/oauth-selector.ts +8 -12
  41. package/src/overlays/plan-review-overlay.ts +213 -40
  42. package/src/overlays/usage-dashboard.ts +22 -1
  43. package/src/prompt/image-source.ts +9 -8
  44. package/src/setup/scenes/sign-in.ts +1 -1
  45. package/src/status-line/context-usage.ts +15 -3
  46. package/src/terminal-capabilities.ts +2 -2
  47. package/src/terminal.ts +8 -1
  48. package/src/theme/mermaid-cache.ts +83 -25
  49. package/src/tools/bash.ts +14 -1
  50. package/src/tools/daemon.ts +51 -0
  51. package/src/tools/index.ts +2 -2
  52. package/src/tools/irc.ts +33 -0
  53. package/src/tools/line-ranges.ts +13 -3
  54. package/src/tools/proc-render.ts +294 -0
  55. package/src/tools/read.ts +22 -1
  56. package/src/tools/wait.ts +595 -0
  57. package/src/tools/write.ts +76 -0
  58. package/src/tools/xd-url.ts +9 -0
  59. package/src/windows-altgr.ts +227 -0
  60. package/dist/types/tools/hub.d.ts +0 -342
  61. package/src/tools/hub.ts +0 -1492
@@ -0,0 +1,78 @@
1
+ export type ReviewSourceRowKind = "context" | "added" | "removed";
2
+
3
+ export interface ReviewSourceRow {
4
+ kind: ReviewSourceRowKind;
5
+ raw: string;
6
+ content: string;
7
+ oldLine?: number;
8
+ newLine?: number;
9
+ hunkHeader: string;
10
+ }
11
+
12
+ export type ReviewDiffRow =
13
+ | ReviewSourceRow
14
+ | { kind: "hunk"; raw: string; hunkHeader: string }
15
+ | { kind: "no-newline"; raw: string; hunkHeader: string };
16
+
17
+ export interface ReviewDiffFile {
18
+ path: string;
19
+ oldPath?: string;
20
+ newPath?: string;
21
+ occurrence: number;
22
+ rawDiff: string;
23
+ rows: ReviewDiffRow[];
24
+ linesAdded: number;
25
+ linesRemoved: number;
26
+ isBinary: boolean;
27
+ }
28
+
29
+ export interface CodeReviewAnnotationBase {
30
+ path: string;
31
+ oldPath?: string;
32
+ newPath?: string;
33
+ occurrence: number;
34
+ note: string;
35
+ }
36
+
37
+ export type CodeReviewAnnotation =
38
+ | (CodeReviewAnnotationBase & {
39
+ scope: "file";
40
+ })
41
+ | (CodeReviewAnnotationBase & {
42
+ scope: "line";
43
+ hunkHeader: string;
44
+ oldLine?: number;
45
+ newLine?: number;
46
+ rawLine: string;
47
+ });
48
+
49
+ export interface CodeReviewOverlayResult {
50
+ action: "review" | "paste";
51
+ annotations: CodeReviewAnnotation[];
52
+ }
53
+
54
+ export type TextReviewSourceProvenance =
55
+ | { kind: "latest-assistant"; entryId: string }
56
+ | { kind: "session"; entryId: string }
57
+ | { kind: "file"; path: string }
58
+ | { kind: "prompt" };
59
+
60
+ export interface TextReviewSource {
61
+ /** Identifies the source within this annotation run. */
62
+ id: string;
63
+ kind: "message" | "code" | "quote" | "command" | "file" | "prompt";
64
+ label: string;
65
+ text: string;
66
+ /** Identifies the active session entry that supplied this source, when applicable. */
67
+ provenance?: TextReviewSourceProvenance;
68
+ sessionId?: string;
69
+ }
70
+
71
+ export type TextReviewAnnotation =
72
+ | { scope: "text"; note: string }
73
+ | { scope: "line"; line: number; quote: string; note: string };
74
+
75
+ export interface TextReviewOverlayResult {
76
+ action: "paste";
77
+ annotations: TextReviewAnnotation[];
78
+ }
@@ -49,19 +49,37 @@ export interface CopySelectorDeps {
49
49
  proseOnlyThinking?: () => boolean;
50
50
  linkTargets?: ReadonlyMap<string, string>;
51
51
  requestRender: () => void;
52
- /** The outlined content was chosen — copy it. `label` feeds the status line. */
53
- onPick: (content: string, label: string) => void;
52
+ /** Replaces the "Copy" header when the picker is reused for another purpose. */
53
+ title?: string;
54
+ /** Verb shown for the pick action in hints and block controls (default "copy"). */
55
+ actionLabel?: string;
56
+ /**
57
+ * The outlined content was chosen — copy it. `label` feeds the status line;
58
+ * `source` names the transcript entry (and inner block, when descended) it came from.
59
+ */
60
+ onPick: (content: string, label: string, source: CopyPickSource) => void;
54
61
  /** `o` on a link block — open `href` with the system opener. Absent: `o` is ignored. */
55
62
  onOpen?: (href: string, label: string) => void;
56
63
  onCancel: () => void;
57
64
  }
58
65
 
66
+ /** Where picked content came from in the transcript. */
67
+ export interface CopyPickSource {
68
+ entry: TranscriptEntry;
69
+ /** The inner block, when the pick happened in the descended block view. */
70
+ block?: CopyBlock;
71
+ }
72
+
59
73
  /** One copyable inner block of a transcript turn. */
60
- interface CopyBlock {
74
+ export interface CopyBlock {
61
75
  /** Short kind label ("code · ts", "bash command", "read result", …). */
62
76
  label: string;
63
77
  /** Exact text placed on the clipboard. */
64
78
  content: string;
79
+ /** Transcript entry that produced this block. */
80
+ entry: TranscriptEntry;
81
+ /** Markdown code/quote block, or a bash/eval tool-call command. */
82
+ kind?: "code" | "quote" | "command";
65
83
  /** Highlight language for the block preview. */
66
84
  language?: string;
67
85
  /** Set for link blocks: the URL `o` opens. `content` is the same URL. */
@@ -245,13 +263,13 @@ export class CopySelectorComponent implements Component {
245
263
  if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
246
264
  if (this.#blocks) {
247
265
  const block = this.#blocks[this.#blockSelected];
248
- if (block) this.deps.onPick(block.content, block.label);
266
+ if (block) this.deps.onPick(block.content, block.label, { entry: block.entry, block });
249
267
  return;
250
268
  }
251
269
  const target = this.#targets[this.#selected];
252
270
  if (!target) return;
253
271
  const item = targetCopy(target, this.#blocksFor(target));
254
- this.deps.onPick(item.content, item.label);
272
+ this.deps.onPick(item.content, item.label, { entry: target.entries[0]! });
255
273
  return;
256
274
  }
257
275
  // Page/home/end/shift+arrow scrolling without moving the selection.
@@ -283,7 +301,7 @@ export class CopySelectorComponent implements Component {
283
301
  if (block.href && this.deps.onOpen) this.deps.onOpen(block.href, block.label);
284
302
  return;
285
303
  }
286
- this.deps.onPick(block.content, block.label);
304
+ this.deps.onPick(block.content, block.label, { entry: block.entry, block });
287
305
  }
288
306
 
289
307
  #moveVertical(delta: -1 | 1): void {
@@ -363,9 +381,10 @@ export class CopySelectorComponent implements Component {
363
381
 
364
382
  const selectedBlock = this.#blocks?.[this.#blockSelected];
365
383
  const openHint = selectedBlock?.href && this.deps.onOpen ? " o open" : "";
384
+ const action = this.deps.actionLabel ?? "copy";
366
385
  const hint = this.#blocks
367
- ? `${this.#blockSelected + 1}/${this.#blocks.length} ↑/↓ block ←/esc back enter copy${openHint} click ${theme.cmd.copy}/${theme.cmd.share}`
368
- : `${this.#targets.length > 0 ? `${this.#selected + 1}/${this.#targets.length} ` : ""}↑/↓ step ${blocks.length > 0 ? "→ blocks " : ""}enter copy ${this.#truncated ? "a earlier turns " : ""}ctrl+o expand esc close`;
386
+ ? `${this.#blockSelected + 1}/${this.#blocks.length} ↑/↓ block ←/esc back enter ${action}${openHint} click ${theme.cmd.copy}/${theme.cmd.share}`
387
+ : `${this.#targets.length > 0 ? `${this.#selected + 1}/${this.#targets.length} ` : ""}↑/↓ step ${blocks.length > 0 ? "→ blocks " : ""}enter ${action} ${this.#truncated ? "a earlier turns " : ""}ctrl+o expand esc close`;
369
388
  const anchorId = target
370
389
  ? this.#blocks
371
390
  ? `copy:${target.turnId}:block:${this.#blockSelected}`
@@ -373,7 +392,9 @@ export class CopySelectorComponent implements Component {
373
392
  : undefined;
374
393
  return {
375
394
  header: [
376
- `${theme.cmd.copy} ${theme.bold("Copy")}${theme.sep.dot}${theme.fg("dim", "pick what to put on the clipboard")}`,
395
+ this.deps.title
396
+ ? theme.bold(this.deps.title)
397
+ : `${theme.cmd.copy} ${theme.bold("Copy")}${theme.sep.dot}${theme.fg("dim", "pick what to put on the clipboard")}`,
377
398
  ],
378
399
  body: {
379
400
  lines: composed.lines,
@@ -409,7 +430,7 @@ export class CopySelectorComponent implements Component {
409
430
  const selected = index === this.#blockSelected;
410
431
  const captionColor: ThemeColor = selected ? OUTLINE_COLOR : "dim";
411
432
  const controls: Array<{ action: ControlRegion["action"]; text: string }> = [
412
- { action: "copy", text: `${theme.cmd.copy} copy` },
433
+ { action: "copy", text: `${theme.cmd.copy} ${this.deps.actionLabel ?? "copy"}` },
413
434
  ];
414
435
  if (block.href && this.deps.onOpen) controls.push({ action: "open", text: `${theme.cmd.share} open` });
415
436
  const controlsWidth = controls.reduce((sum, control) => sum + visibleWidth(control.text) + 2, 0);
@@ -497,16 +518,18 @@ function toolResultText(message: Extract<SessionMessageEntry["message"], { role:
497
518
  .trim();
498
519
  }
499
520
 
500
- function pushMarkdownBlocks(blocks: CopyBlock[], text: string): void {
521
+ function pushMarkdownBlocks(blocks: CopyBlock[], text: string, entry: TranscriptEntry): void {
501
522
  for (const block of extractBlocks(text)) {
502
523
  if (block.kind === "code") {
503
524
  blocks.push({
504
525
  label: block.lang ? `${block.lang} code` : "code",
505
526
  content: block.code,
527
+ entry,
528
+ kind: "code",
506
529
  language: block.lang || undefined,
507
530
  });
508
531
  } else {
509
- blocks.push({ label: "quote", content: block.text });
532
+ blocks.push({ label: "quote", content: block.text, entry, kind: "quote" });
510
533
  }
511
534
  }
512
535
  // Links follow the message's blocks. The preview shows the whole URL on one
@@ -515,6 +538,7 @@ function pushMarkdownBlocks(blocks: CopyBlock[], text: string): void {
515
538
  blocks.push({
516
539
  label: link.text !== link.href ? `link${theme.sep.dot}${link.text}` : "link",
517
540
  content: link.href,
541
+ entry,
518
542
  href: link.href,
519
543
  });
520
544
  }
@@ -528,10 +552,10 @@ function collectBlocks(entries: readonly TranscriptEntry[]): CopyBlock[] {
528
552
  if (!message) continue;
529
553
  switch (message.role) {
530
554
  case "user":
531
- pushMarkdownBlocks(blocks, rawUserText(message));
555
+ pushMarkdownBlocks(blocks, rawUserText(message), entry);
532
556
  break;
533
557
  case "assistant": {
534
- pushMarkdownBlocks(blocks, assistantVisibleText(message));
558
+ pushMarkdownBlocks(blocks, assistantVisibleText(message), entry);
535
559
  for (const content of message.content) {
536
560
  if (content.type !== "toolCall") continue;
537
561
  const command = commandFromToolCall(content);
@@ -539,6 +563,8 @@ function collectBlocks(entries: readonly TranscriptEntry[]): CopyBlock[] {
539
563
  blocks.push({
540
564
  label: command.kind === "bash" ? "bash command" : "eval code",
541
565
  content: command.code,
566
+ entry,
567
+ kind: "command",
542
568
  language: command.language,
543
569
  });
544
570
  }
@@ -547,16 +573,16 @@ function collectBlocks(entries: readonly TranscriptEntry[]): CopyBlock[] {
547
573
  }
548
574
  case "toolResult": {
549
575
  const text = toolResultText(message);
550
- if (text) blocks.push({ label: `${message.toolName} result`, content: text });
576
+ if (text) blocks.push({ label: `${message.toolName} result`, content: text, entry });
551
577
  break;
552
578
  }
553
579
  case "bashExecution":
554
- blocks.push({ label: "command", content: message.command, language: "bash" });
555
- if (message.output.trim()) blocks.push({ label: "output", content: message.output });
580
+ blocks.push({ label: "command", content: message.command, entry, language: "bash" });
581
+ if (message.output.trim()) blocks.push({ label: "output", content: message.output, entry });
556
582
  break;
557
583
  case "pythonExecution":
558
- blocks.push({ label: "eval code", content: message.code, language: "python" });
559
- if (message.output.trim()) blocks.push({ label: "output", content: message.output });
584
+ blocks.push({ label: "eval code", content: message.code, entry, language: "python" });
585
+ if (message.output.trim()) blocks.push({ label: "output", content: message.output, entry });
560
586
  break;
561
587
  default:
562
588
  break;
@@ -199,12 +199,12 @@ export function extractLastCommand(messages: readonly AgentMessage[]): LastComma
199
199
  return undefined;
200
200
  }
201
201
 
202
- /** Concatenated visible text of an assistant message, or undefined when empty. */
203
- function assistantText(msg: AgentMessage): string | undefined {
202
+ /** Concatenated visible text of an assistant message, verbatim; undefined when blank. */
203
+ export function assistantText(msg: AgentMessage): string | undefined {
204
204
  if (msg.role !== "assistant") return undefined;
205
205
  let text = "";
206
206
  for (const content of msg.content) {
207
207
  if (content.type === "text") text += content.text;
208
208
  }
209
- return text.trim() || undefined;
209
+ return text.trim() ? text : undefined;
210
210
  }
@@ -11,7 +11,7 @@ import { parseModelString, splitUpstreamRouting, formatModelSelectorValue } from
11
11
  * in the compact alt+p picker ({@link ./model-picker}).
12
12
  */
13
13
  import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
14
- import type { Model } from "@oh-my-pi/pi-ai";
14
+ import type { KeysApi, Model } from "@oh-my-pi/pi-ai";
15
15
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
16
16
  import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
17
17
  import { providerEntry } from "@oh-my-pi/pi-catalog/compat/providers";
@@ -89,7 +89,7 @@ export interface ModelHubSource extends ModelBrowserSource {
89
89
 
90
90
  /** Catalog capabilities required by the model hub. */
91
91
  export interface ModelHubRegistry extends ModelBrowserRegistry {
92
- readonly authStorage: { hasAuth(provider: string): boolean };
92
+ readonly authStorage: { readonly keys: Pick<KeysApi, "source"> };
93
93
  getDiscoverableProviders(): string[];
94
94
  getProviderDiscoveryState(provider: string):
95
95
  | {
@@ -451,7 +451,8 @@ export class ModelHubComponent implements Component {
451
451
  // Discoverable without stored auth: catalog-backed providers stay
452
452
  // locked; keyless/custom endpoints (ollama, vllm, …) surface as
453
453
  // selectable so discovery can populate them.
454
- if (authStorage.hasAuth(provider) || !locked.has(provider)) {
454
+ const authenticated = authStorage.keys.source(provider) !== undefined;
455
+ if (authenticated || !locked.has(provider)) {
455
456
  // #2761: implicit local endpoints (optional: true) stay hidden
456
457
  // until discovery actually reaches a server. "idle" means never
457
458
  // probed; "unavailable" means the endpoint is unreachable; both
@@ -459,7 +460,7 @@ export class ModelHubComponent implements Component {
459
460
  // configured. models.yml discovery providers (optional: false)
460
461
  // and providers with stored auth keep their entry so
461
462
  // misconfigurations stay visible and diagnosable.
462
- if (!authStorage.hasAuth(provider)) {
463
+ if (!authenticated) {
463
464
  const discovery = this.#registry.getProviderDiscoveryState(provider);
464
465
  if (discovery?.optional && (discovery.status === "idle" || discovery.status === "unavailable")) {
465
466
  continue;
@@ -1,3 +1,4 @@
1
+ import type { CredentialsApi, KeysApi } from "@oh-my-pi/pi-ai";
1
2
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
2
3
  import type { OAuthProviderInfo } from "@oh-my-pi/pi-ai/oauth/types";
3
4
  import {
@@ -19,14 +20,8 @@ const OAUTH_SELECTOR_MAX_VISIBLE = 10;
19
20
 
20
21
  /** Credential presence and provenance needed by the provider picker. */
21
22
  export interface OAuthSelectorAuthSource {
22
- has(providerId: string): boolean;
23
- hasAuth(providerId: string): boolean;
24
- getCredentialOrigin(providerId: string):
25
- | {
26
- kind: "runtime" | "config" | "oauth" | "api_key" | "env" | "fallback";
27
- envVar?: string;
28
- }
29
- | undefined;
23
+ readonly credentials: Pick<CredentialsApi, "has">;
24
+ readonly keys: Pick<KeysApi, "source">;
30
25
  }
31
26
 
32
27
  /**
@@ -42,7 +37,6 @@ const ORIGIN_LABELS = {
42
37
  oauth: "login",
43
38
  api_key: "api key",
44
39
  env: "env",
45
- fallback: "custom provider",
46
40
  };
47
41
  /**
48
42
  * Component that renders an OAuth provider selector.
@@ -122,7 +116,9 @@ export class OAuthSelectorComponent extends OverlayPanel {
122
116
  this.#updateList();
123
117
  }
124
118
  #hasSelectableAuth(providerId: string): boolean {
125
- return this.#mode === "logout" ? this.#authStorage.has(providerId) : this.#authStorage.hasAuth(providerId);
119
+ return this.#mode === "logout"
120
+ ? this.#authStorage.credentials.has(providerId)
121
+ : this.#authStorage.keys.source(providerId) !== undefined;
126
122
  }
127
123
 
128
124
  #loadProviders(disabledProviders: readonly string[] = []): void {
@@ -212,7 +208,7 @@ export class OAuthSelectorComponent extends OverlayPanel {
212
208
  * the list distinguishes a real login from an env var aliasing the provider.
213
209
  */
214
210
  #getSourceLabel(providerId: string): string {
215
- const origin = this.#authStorage.getCredentialOrigin(providerId);
211
+ const origin = this.#authStorage.keys.source(providerId);
216
212
  if (!origin) return "";
217
213
  const detail = origin.kind === "env" && origin.envVar ? `env: ${origin.envVar}` : ORIGIN_LABELS[origin.kind];
218
214
  return theme.fg("muted", ` (${detail})`);
@@ -253,7 +249,7 @@ export class OAuthSelectorComponent extends OverlayPanel {
253
249
 
254
250
  #getProviderSearchText(provider: OAuthProviderInfo): string {
255
251
  let text = `${provider.name} ${provider.id}`;
256
- const origin = this.#authStorage.getCredentialOrigin(provider.id);
252
+ const origin = this.#authStorage.keys.source(provider.id);
257
253
  if (origin) {
258
254
  text += ` logged in authenticated ${ORIGIN_LABELS[origin.kind]}`;
259
255
  if (origin.envVar) text += ` ${origin.envVar}`;