@oh-my-pi/pi-coding-agent 3.1.1337 → 3.4.1337

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.
@@ -67,6 +67,7 @@ import { SessionSelectorComponent } from "./components/session-selector";
67
67
  import { SettingsSelectorComponent } from "./components/settings-selector";
68
68
  import { ToolExecutionComponent } from "./components/tool-execution";
69
69
  import { TreeSelectorComponent } from "./components/tree-selector";
70
+ import { TtsrNotificationComponent } from "./components/ttsr-notification";
70
71
  import { UserMessageComponent } from "./components/user-message";
71
72
  import { UserMessageSelectorComponent } from "./components/user-message-selector";
72
73
  import { WelcomeComponent } from "./components/welcome";
@@ -1007,18 +1008,28 @@ export class InteractiveMode {
1007
1008
  if (event.message.role === "user") break;
1008
1009
  if (this.streamingComponent && event.message.role === "assistant") {
1009
1010
  this.streamingMessage = event.message;
1010
- this.streamingComponent.updateContent(this.streamingMessage);
1011
+ // Don't show "Aborted" text for TTSR aborts - we'll show a nicer message
1012
+ if (this.session.isTtsrAbortPending && this.streamingMessage.stopReason === "aborted") {
1013
+ // TTSR abort - suppress the "Aborted" rendering in the component
1014
+ const msgWithoutAbort = { ...this.streamingMessage, stopReason: "stop" as const };
1015
+ this.streamingComponent.updateContent(msgWithoutAbort);
1016
+ } else {
1017
+ this.streamingComponent.updateContent(this.streamingMessage);
1018
+ }
1011
1019
 
1012
1020
  if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
1013
- const errorMessage =
1014
- this.streamingMessage.stopReason === "aborted"
1015
- ? "Operation aborted"
1016
- : this.streamingMessage.errorMessage || "Error";
1017
- for (const [, component] of this.pendingTools.entries()) {
1018
- component.updateResult({
1019
- content: [{ type: "text", text: errorMessage }],
1020
- isError: true,
1021
- });
1021
+ // Skip error handling for TTSR aborts
1022
+ if (!this.session.isTtsrAbortPending) {
1023
+ const errorMessage =
1024
+ this.streamingMessage.stopReason === "aborted"
1025
+ ? "Operation aborted"
1026
+ : this.streamingMessage.errorMessage || "Error";
1027
+ for (const [, component] of this.pendingTools.entries()) {
1028
+ component.updateResult({
1029
+ content: [{ type: "text", text: errorMessage }],
1030
+ isError: true,
1031
+ });
1032
+ }
1022
1033
  }
1023
1034
  this.pendingTools.clear();
1024
1035
  } else {
@@ -1188,6 +1199,15 @@ export class InteractiveMode {
1188
1199
  this.ui.requestRender();
1189
1200
  break;
1190
1201
  }
1202
+
1203
+ case "ttsr_triggered": {
1204
+ // Show a fancy notification when TTSR rules are triggered
1205
+ const component = new TtsrNotificationComponent(event.rules);
1206
+ component.setExpanded(this.toolOutputExpanded);
1207
+ this.chatContainer.addChild(component);
1208
+ this.ui.requestRender();
1209
+ break;
1210
+ }
1191
1211
  }
1192
1212
  }
1193
1213
 
@@ -1617,7 +1637,7 @@ export class InteractiveMode {
1617
1637
  theme.bold(theme.fg("warning", "Update Available")) +
1618
1638
  "\n" +
1619
1639
  theme.fg("muted", `New version ${newVersion} is available. Run: `) +
1620
- theme.fg("accent", "npm install -g @oh-my-pi/pi-coding-agent"),
1640
+ theme.fg("accent", "omp update"),
1621
1641
  1,
1622
1642
  0,
1623
1643
  ),
@@ -2391,14 +2411,26 @@ export class InteractiveMode {
2391
2411
  }
2392
2412
 
2393
2413
  private handleStatusCommand(): void {
2394
- const sections: string[] = [];
2395
-
2396
2414
  type StatusSource =
2397
2415
  | { provider: string; level: string }
2398
2416
  | { mcpServer: string; provider?: string }
2399
2417
  | "builtin"
2400
2418
  | "unknown";
2401
2419
 
2420
+ type StatusLine = {
2421
+ name: string;
2422
+ sourceText: string;
2423
+ nameWithSource: string;
2424
+ desc?: string;
2425
+ };
2426
+
2427
+ type LineSection = {
2428
+ title: string;
2429
+ lines: StatusLine[];
2430
+ };
2431
+
2432
+ type Section = { kind: "lines"; section: LineSection } | { kind: "text"; text: string };
2433
+
2402
2434
  const capitalize = (value: string): string => value.charAt(0).toUpperCase() + value.slice(1);
2403
2435
 
2404
2436
  const resolveSourceText = (source: StatusSource): string => {
@@ -2440,29 +2472,28 @@ export class InteractiveMode {
2440
2472
  return `${acc}...`;
2441
2473
  };
2442
2474
 
2443
- // Helper to format a section with consistent column alignment
2444
- const formatSection = <T>(
2475
+ const buildLineSection = <T>(
2445
2476
  title: string,
2446
2477
  items: readonly T[],
2447
2478
  getName: (item: T) => string,
2448
2479
  getDesc: (item: T) => string | undefined,
2449
2480
  getSource: (item: T) => StatusSource,
2450
- ): string => {
2451
- if (items.length === 0) return "";
2481
+ ): LineSection | null => {
2482
+ if (items.length === 0) return null;
2452
2483
 
2453
- const lineItems = items.map((item) => {
2484
+ const lines = items.map((item) => {
2454
2485
  const name = getName(item);
2455
- const desc = getDesc(item);
2486
+ const desc = getDesc(item)?.trim();
2456
2487
  const sourceText = resolveSourceText(getSource(item));
2457
2488
  const nameWithSource = sourceText ? `${name} ${sourceText}` : name;
2458
2489
  return { name, sourceText, nameWithSource, desc };
2459
2490
  });
2460
2491
 
2461
- const maxNameWidth = Math.min(
2462
- 60,
2463
- Math.max(...lineItems.map((line) => visibleWidth(line.nameWithSource))),
2464
- );
2465
- const formattedLines = lineItems.map((line) => {
2492
+ return { title, lines };
2493
+ };
2494
+
2495
+ const renderLineSection = (section: LineSection, maxNameWidth: number): string => {
2496
+ const formattedLines = section.lines.map((line) => {
2466
2497
  let nameText = line.name;
2467
2498
  let sourceText = line.sourceText;
2468
2499
 
@@ -2471,103 +2502,97 @@ export class InteractiveMode {
2471
2502
  sourceText = truncateText(sourceText, maxSourceWidth);
2472
2503
  }
2473
2504
  const sourceWidth = sourceText ? visibleWidth(sourceText) : 0;
2474
- const availableForName = sourceText
2475
- ? Math.max(1, maxNameWidth - sourceWidth - 1)
2476
- : maxNameWidth;
2505
+ const availableForName = sourceText ? Math.max(1, maxNameWidth - sourceWidth - 1) : maxNameWidth;
2477
2506
  nameText = truncateText(nameText, availableForName);
2478
2507
 
2479
2508
  const nameWithSourcePlain = sourceText ? `${nameText} ${sourceText}` : nameText;
2480
2509
  const sourceRendered = sourceText ? renderSourceText(sourceText) : "";
2481
2510
  const nameRendered = sourceText ? `${theme.bold(nameText)} ${sourceRendered}` : theme.bold(nameText);
2482
2511
  const pad = Math.max(0, maxNameWidth - visibleWidth(nameWithSourcePlain));
2483
- const desc = line.desc?.trim();
2512
+ const desc = line.desc;
2484
2513
  const descPart = desc ? ` ${theme.fg("dim", desc.slice(0, 50) + (desc.length > 50 ? "..." : ""))}` : "";
2485
2514
  return ` ${nameRendered}${" ".repeat(pad)}${descPart}`;
2486
2515
  });
2487
2516
 
2488
- return `${theme.bold(theme.fg("accent", title))}\n${formattedLines.join("\n")}`;
2517
+ return `${theme.bold(theme.fg("accent", section.title))}\n${formattedLines.join("\n")}`;
2518
+ };
2519
+
2520
+ const sections: Section[] = [];
2521
+ const pushLineSection = <T>(
2522
+ title: string,
2523
+ items: readonly T[],
2524
+ getName: (item: T) => string,
2525
+ getDesc: (item: T) => string | undefined,
2526
+ getSource: (item: T) => StatusSource,
2527
+ ): void => {
2528
+ const section = buildLineSection(title, items, getName, getDesc, getSource);
2529
+ if (section) {
2530
+ sections.push({ kind: "lines", section });
2531
+ }
2489
2532
  };
2490
2533
 
2491
2534
  // Loaded context files
2492
2535
  const contextFilesResult = loadSync(contextFileCapability.id, { cwd: process.cwd() });
2493
2536
  const contextFiles = contextFilesResult.items as ContextFile[];
2494
- if (contextFiles.length > 0) {
2495
- sections.push(
2496
- formatSection(
2497
- "Context Files",
2498
- contextFiles,
2499
- (f) => basename(f.path),
2500
- () => undefined,
2501
- (f) => ({ provider: f._source.providerName, level: f.level }),
2502
- ),
2503
- );
2504
- }
2537
+ pushLineSection(
2538
+ "Context Files",
2539
+ contextFiles,
2540
+ (f) => basename(f.path),
2541
+ () => undefined,
2542
+ (f) => ({ provider: f._source.providerName, level: f.level }),
2543
+ );
2505
2544
 
2506
2545
  // Loaded skills
2507
2546
  const skillsSettings = this.session.skillsSettings;
2508
2547
  if (skillsSettings?.enabled !== false) {
2509
2548
  const { skills, warnings: skillWarnings } = loadSkills(skillsSettings ?? {});
2510
- if (skills.length > 0) {
2511
- sections.push(
2512
- formatSection(
2513
- "Skills",
2514
- skills,
2515
- (s) => s.name,
2516
- (s) => s.description,
2517
- (s) => (s._source ? { provider: s._source.providerName, level: s._source.level } : "unknown"),
2518
- ),
2519
- );
2520
- }
2549
+ pushLineSection(
2550
+ "Skills",
2551
+ skills,
2552
+ (s) => s.name,
2553
+ (s) => s.description,
2554
+ (s) => (s._source ? { provider: s._source.providerName, level: s._source.level } : "unknown"),
2555
+ );
2521
2556
  if (skillWarnings.length > 0) {
2522
- sections.push(
2523
- theme.bold(theme.fg("warning", "Skill Warnings")) +
2557
+ sections.push({
2558
+ kind: "text",
2559
+ text:
2560
+ theme.bold(theme.fg("warning", "Skill Warnings")) +
2524
2561
  "\n" +
2525
2562
  skillWarnings.map((w) => theme.fg("warning", ` ${w.skillPath}: ${w.message}`)).join("\n"),
2526
- );
2563
+ });
2527
2564
  }
2528
2565
  }
2529
2566
 
2530
2567
  // Loaded rules
2531
2568
  const rulesResult = loadSync<Rule>(ruleCapability.id, { cwd: process.cwd() });
2532
- if (rulesResult.items.length > 0) {
2533
- sections.push(
2534
- formatSection(
2535
- "Rules",
2536
- rulesResult.items,
2537
- (r) => r.name,
2538
- (r) => r.description,
2539
- (r) => ({ provider: r._source.providerName, level: r._source.level }),
2540
- ),
2541
- );
2542
- }
2569
+ pushLineSection(
2570
+ "Rules",
2571
+ rulesResult.items,
2572
+ (r) => r.name,
2573
+ (r) => r.description,
2574
+ (r) => ({ provider: r._source.providerName, level: r._source.level }),
2575
+ );
2543
2576
 
2544
2577
  // Loaded prompts
2545
2578
  const promptsResult = loadSync<Prompt>(promptCapability.id, { cwd: process.cwd() });
2546
- if (promptsResult.items.length > 0) {
2547
- sections.push(
2548
- formatSection(
2549
- "Prompts",
2550
- promptsResult.items,
2551
- (p) => p.name,
2552
- () => undefined,
2553
- (p) => ({ provider: p._source.providerName, level: p._source.level }),
2554
- ),
2555
- );
2556
- }
2579
+ pushLineSection(
2580
+ "Prompts",
2581
+ promptsResult.items,
2582
+ (p) => p.name,
2583
+ () => undefined,
2584
+ (p) => ({ provider: p._source.providerName, level: p._source.level }),
2585
+ );
2557
2586
 
2558
2587
  // Loaded instructions
2559
2588
  const instructionsResult = loadSync<Instruction>(instructionCapability.id, { cwd: process.cwd() });
2560
- if (instructionsResult.items.length > 0) {
2561
- sections.push(
2562
- formatSection(
2563
- "Instructions",
2564
- instructionsResult.items,
2565
- (i) => i.name,
2566
- (i) => (i.applyTo ? `applies to: ${i.applyTo}` : undefined),
2567
- (i) => ({ provider: i._source.providerName, level: i._source.level }),
2568
- ),
2569
- );
2570
- }
2589
+ pushLineSection(
2590
+ "Instructions",
2591
+ instructionsResult.items,
2592
+ (i) => i.name,
2593
+ (i) => (i.applyTo ? `applies to: ${i.applyTo}` : undefined),
2594
+ (i) => ({ provider: i._source.providerName, level: i._source.level }),
2595
+ );
2571
2596
 
2572
2597
  // Loaded custom tools - split MCP from non-MCP
2573
2598
  if (this.customTools.size > 0) {
@@ -2577,68 +2602,74 @@ export class InteractiveMode {
2577
2602
 
2578
2603
  // MCP Tools section
2579
2604
  if (mcpTools.length > 0) {
2580
- sections.push(
2581
- formatSection(
2582
- "MCP Tools",
2583
- mcpTools,
2584
- (ct) => ct.tool.label || ct.tool.name,
2585
- () => undefined,
2586
- (ct) => {
2587
- const match = ct.path.match(/^mcp:(.+?) via (.+)$/);
2588
- if (match) {
2589
- const [, serverName, providerName] = match;
2590
- return { mcpServer: serverName, provider: providerName };
2591
- }
2592
- return ct.path.startsWith("mcp:") ? { mcpServer: ct.path.slice(4) } : "unknown";
2593
- },
2594
- ),
2605
+ pushLineSection(
2606
+ "MCP Tools",
2607
+ mcpTools,
2608
+ (ct) => ct.tool.label || ct.tool.name,
2609
+ () => undefined,
2610
+ (ct) => {
2611
+ const match = ct.path.match(/^mcp:(.+?) via (.+)$/);
2612
+ if (match) {
2613
+ const [, serverName, providerName] = match;
2614
+ return { mcpServer: serverName, provider: providerName };
2615
+ }
2616
+ return ct.path.startsWith("mcp:") ? { mcpServer: ct.path.slice(4) } : "unknown";
2617
+ },
2595
2618
  );
2596
2619
  }
2597
2620
 
2598
2621
  // Custom Tools section
2599
2622
  if (customTools.length > 0) {
2600
- sections.push(
2601
- formatSection(
2602
- "Custom Tools",
2603
- customTools,
2604
- (ct) => ct.tool.label || ct.tool.name,
2605
- (ct) => ct.tool.description,
2606
- (ct) => {
2607
- if (ct.source?.provider === "builtin") return "builtin";
2608
- if (ct.path === "<exa>") return "builtin";
2609
- return ct.source ? { provider: ct.source.providerName, level: ct.source.level } : "unknown";
2610
- },
2611
- ),
2623
+ pushLineSection(
2624
+ "Custom Tools",
2625
+ customTools,
2626
+ (ct) => ct.tool.label || ct.tool.name,
2627
+ (ct) => ct.tool.description,
2628
+ (ct) => {
2629
+ if (ct.source?.provider === "builtin") return "builtin";
2630
+ if (ct.path === "<exa>") return "builtin";
2631
+ return ct.source ? { provider: ct.source.providerName, level: ct.source.level } : "unknown";
2632
+ },
2612
2633
  );
2613
2634
  }
2614
2635
  }
2615
2636
 
2616
2637
  // Loaded slash commands (file-based)
2617
2638
  const fileCommands = this.session.fileCommands;
2618
- if (fileCommands.length > 0) {
2619
- sections.push(
2620
- formatSection(
2621
- "Slash Commands",
2622
- fileCommands,
2623
- (cmd) => `/${cmd.name}`,
2624
- (cmd) => cmd.description,
2625
- (cmd) => (cmd._source ? { provider: cmd._source.providerName, level: cmd._source.level } : "unknown"),
2626
- ),
2627
- );
2628
- }
2639
+ pushLineSection(
2640
+ "Slash Commands",
2641
+ fileCommands,
2642
+ (cmd) => `/${cmd.name}`,
2643
+ (cmd) => cmd.description,
2644
+ (cmd) => (cmd._source ? { provider: cmd._source.providerName, level: cmd._source.level } : "unknown"),
2645
+ );
2629
2646
 
2630
2647
  // Loaded hooks
2631
2648
  const hookRunner = this.session.hookRunner;
2632
2649
  if (hookRunner) {
2633
2650
  const hookPaths = hookRunner.getHookPaths();
2634
2651
  if (hookPaths.length > 0) {
2635
- sections.push(
2636
- `${theme.bold(theme.fg("accent", "Hooks"))}\n${hookPaths.map((p) => ` ${theme.bold(basename(p))} ${theme.fg("dim", "hook")}`).join("\n")}`,
2637
- );
2652
+ sections.push({
2653
+ kind: "text",
2654
+ text:
2655
+ `${theme.bold(theme.fg("accent", "Hooks"))}\n` +
2656
+ hookPaths.map((p) => ` ${theme.bold(basename(p))} ${theme.fg("dim", "hook")}`).join("\n"),
2657
+ });
2638
2658
  }
2639
2659
  }
2640
2660
 
2641
- if (sections.length === 0) {
2661
+ const lineSections = sections.filter((section): section is { kind: "lines"; section: LineSection } => {
2662
+ return section.kind === "lines";
2663
+ });
2664
+ const allLines = lineSections.flatMap((section) => section.section.lines);
2665
+ const maxNameWidth = allLines.length
2666
+ ? Math.min(60, Math.max(...allLines.map((line) => visibleWidth(line.nameWithSource))))
2667
+ : 0;
2668
+ const renderedSections = sections
2669
+ .map((section) => (section.kind === "lines" ? renderLineSection(section.section, maxNameWidth) : section.text))
2670
+ .filter((section) => section.length > 0);
2671
+
2672
+ if (renderedSections.length === 0) {
2642
2673
  this.chatContainer.addChild(new Spacer(1));
2643
2674
  this.chatContainer.addChild(new Text(theme.fg("muted", "No extensions loaded."), 1, 0));
2644
2675
  } else {
@@ -2646,7 +2677,7 @@ export class InteractiveMode {
2646
2677
  this.chatContainer.addChild(new DynamicBorder());
2647
2678
  this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Loaded Extensions")), 1, 0));
2648
2679
  this.chatContainer.addChild(new Spacer(1));
2649
- for (const section of sections) {
2680
+ for (const section of renderedSections) {
2650
2681
  this.chatContainer.addChild(new Text(section, 1, 0));
2651
2682
  this.chatContainer.addChild(new Spacer(1));
2652
2683
  }
@@ -2,8 +2,8 @@
2
2
  * Print mode (single-shot): Send prompts, output result, exit.
3
3
  *
4
4
  * Used for:
5
- * - `pi -p "prompt"` - text output
6
- * - `pi --mode json "prompt"` - JSON event stream
5
+ * - `omp -p "prompt"` - text output
6
+ * - `omp --mode json "prompt"` - JSON event stream
7
7
  */
8
8
 
9
9
  import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";