@oh-my-pi/pi-coding-agent 3.4.1337 → 3.6.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.
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Types for the Extension Control Center dashboard.
3
+ */
4
+
5
+ import type { SourceMeta } from "../../../../capability/types";
6
+
7
+ /**
8
+ * Extension kinds matching capability types.
9
+ */
10
+ export type ExtensionKind =
11
+ | "skill"
12
+ | "rule"
13
+ | "tool"
14
+ | "mcp"
15
+ | "prompt"
16
+ | "instruction"
17
+ | "context-file"
18
+ | "hook"
19
+ | "slash-command";
20
+
21
+ /**
22
+ * Extension state (active, disabled, or shadowed).
23
+ */
24
+ export type ExtensionState = "active" | "disabled" | "shadowed";
25
+
26
+ /**
27
+ * Reason why an extension is disabled.
28
+ */
29
+ export type DisabledReason = "provider-disabled" | "item-disabled" | "shadowed";
30
+
31
+ /**
32
+ * Unified extension representation for the dashboard.
33
+ * Normalizes all capability types into a common shape.
34
+ */
35
+ export interface Extension {
36
+ /** Unique ID: `${kind}:${name}` */
37
+ id: string;
38
+ /** Extension kind */
39
+ kind: ExtensionKind;
40
+ /** Extension name */
41
+ name: string;
42
+ /** Display name (may differ from name) */
43
+ displayName: string;
44
+ /** Description if available */
45
+ description?: string;
46
+ /** Trigger pattern (slash command, glob, regex) */
47
+ trigger?: string;
48
+ /** Absolute path to source file */
49
+ path: string;
50
+ /** Source metadata */
51
+ source: {
52
+ provider: string;
53
+ providerName: string;
54
+ level: "user" | "project" | "native";
55
+ };
56
+ /** Current state */
57
+ state: ExtensionState;
58
+ /** Reason for disabled state */
59
+ disabledReason?: DisabledReason;
60
+ /** If shadowed, what shadows it */
61
+ shadowedBy?: string;
62
+ /** Raw item data for inspector */
63
+ raw: unknown;
64
+ }
65
+
66
+ /**
67
+ * Tree node types for sidebar hierarchy.
68
+ */
69
+ export type TreeNodeType = "provider" | "kind" | "item";
70
+
71
+ /**
72
+ * Sidebar tree node.
73
+ */
74
+ export interface TreeNode {
75
+ /** Unique ID */
76
+ id: string;
77
+ /** Display label */
78
+ label: string;
79
+ /** Node type (provider can be toggled, kind groups items) */
80
+ type: TreeNodeType;
81
+ /** Whether this node/provider is enabled */
82
+ enabled: boolean;
83
+ /** Whether collapsed */
84
+ collapsed: boolean;
85
+ /** Child nodes */
86
+ children: TreeNode[];
87
+ /** Extension count (for display) */
88
+ count?: number;
89
+ }
90
+
91
+ /**
92
+ * Flattened tree item for navigation.
93
+ */
94
+ export interface FlatTreeItem {
95
+ node: TreeNode;
96
+ depth: number;
97
+ index: number;
98
+ }
99
+
100
+ /**
101
+ * Focus region in the tabbed dashboard.
102
+ */
103
+ export type FocusRegion = "tabs" | "list";
104
+
105
+ /**
106
+ * Provider tab representation.
107
+ */
108
+ export interface ProviderTab {
109
+ /** Provider ID (or "all" for the ALL tab) */
110
+ id: string;
111
+ /** Display label */
112
+ label: string;
113
+ /** Whether provider is enabled (always true for "all") */
114
+ enabled: boolean;
115
+ /** Extension count for this provider */
116
+ count: number;
117
+ }
118
+
119
+ /**
120
+ * Tabbed dashboard state.
121
+ */
122
+ export interface DashboardState {
123
+ /** Provider tabs */
124
+ tabs: ProviderTab[];
125
+ /** Active tab index */
126
+ activeTabIndex: number;
127
+
128
+ /** All extensions (unfiltered) */
129
+ extensions: Extension[];
130
+ /** Extensions filtered by active tab */
131
+ tabFiltered: Extension[];
132
+ /** Extensions filtered by search (applied after tab filter) */
133
+ searchFiltered: Extension[];
134
+ /** Current search query */
135
+ searchQuery: string;
136
+
137
+ /** Selected index in main list */
138
+ listIndex: number;
139
+ /** Scroll offset for main list */
140
+ scrollOffset: number;
141
+
142
+ /** Currently selected extension for inspector */
143
+ selected: Extension | null;
144
+ }
145
+
146
+ /**
147
+ * @deprecated Use FocusRegion instead
148
+ */
149
+ export type FocusPane = "sidebar" | "main" | "inspector";
150
+
151
+ /**
152
+ * Callbacks from dashboard to parent.
153
+ */
154
+ export interface DashboardCallbacks {
155
+ /** Called when provider is toggled */
156
+ onProviderToggle: (providerId: string, enabled: boolean) => void;
157
+ /** Called when extension item is toggled */
158
+ onExtensionToggle: (extensionId: string, enabled: boolean) => void;
159
+ /** Called when dashboard is closed */
160
+ onClose: () => void;
161
+ }
162
+
163
+ /**
164
+ * Create extension ID from kind and name.
165
+ */
166
+ export function makeExtensionId(kind: ExtensionKind, name: string): string {
167
+ return `${kind}:${name}`;
168
+ }
169
+
170
+ /**
171
+ * Parse extension ID into kind and name.
172
+ */
173
+ export function parseExtensionId(id: string): { kind: ExtensionKind; name: string } | null {
174
+ const colonIdx = id.indexOf(":");
175
+ if (colonIdx === -1) return null;
176
+ return {
177
+ kind: id.slice(0, colonIdx) as ExtensionKind,
178
+ name: id.slice(colonIdx + 1),
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Map SourceMeta to extension source shape.
184
+ */
185
+ export function sourceFromMeta(meta: SourceMeta): Extension["source"] {
186
+ return {
187
+ provider: meta.provider,
188
+ providerName: meta.providerName,
189
+ level: meta.level,
190
+ };
191
+ }
@@ -11,7 +11,6 @@
11
11
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
12
12
  import { getCapabilities } from "@oh-my-pi/pi-tui";
13
13
  import type { SettingsManager } from "../../../core/settings-manager";
14
- import { getAllProvidersInfo, isProviderEnabled } from "../../../discovery";
15
14
 
16
15
  // Setting value types
17
16
  export type SettingValue = boolean | string;
@@ -297,38 +296,10 @@ export const SETTINGS_DEFS: SettingDef[] = [
297
296
  ];
298
297
 
299
298
  /**
300
- * Get discovery provider settings dynamically.
301
- * These are generated at runtime from getAllProvidersInfo().
302
- */
303
- function getDiscoverySettings(): SettingDef[] {
304
- const providers = getAllProvidersInfo();
305
- const settings: SettingDef[] = [];
306
-
307
- for (const provider of providers) {
308
- // Skip native provider - it can't be disabled
309
- if (provider.id === "native") {
310
- continue;
311
- }
312
-
313
- settings.push({
314
- id: `discovery.${provider.id}`,
315
- tab: "discovery",
316
- type: "boolean",
317
- label: provider.displayName,
318
- description: provider.description,
319
- get: () => isProviderEnabled(provider.id),
320
- set: () => {}, // Handled in interactive-mode.ts
321
- });
322
- }
323
-
324
- return settings;
325
- }
326
-
327
- /**
328
- * All settings with dynamic discovery settings merged in.
299
+ * All settings. Discovery settings have been moved to /extensions dashboard.
329
300
  */
330
301
  function getAllSettings(): SettingDef[] {
331
- return [...SETTINGS_DEFS, ...getDiscoverySettings()];
302
+ return SETTINGS_DEFS;
332
303
  }
333
304
 
334
305
  /** Get settings for a specific tab */
@@ -99,7 +99,6 @@ const SETTINGS_TABS: Tab[] = [
99
99
  { id: "config", label: "Config" },
100
100
  { id: "lsp", label: "LSP" },
101
101
  { id: "exa", label: "Exa" },
102
- { id: "discovery", label: "Discovery" },
103
102
  { id: "plugins", label: "Plugins" },
104
103
  ];
105
104
 
@@ -6,7 +6,6 @@
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
- import { basename } from "node:path";
10
9
  import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
11
10
  import type { AssistantMessage, ImageContent, Message, OAuthProvider } from "@oh-my-pi/pi-ai";
12
11
  import type { SlashCommand } from "@oh-my-pi/pi-tui";
@@ -24,28 +23,15 @@ import {
24
23
  TUI,
25
24
  visibleWidth,
26
25
  } from "@oh-my-pi/pi-tui";
27
- import { contextFileCapability } from "../../capability/context-file";
28
- import { instructionCapability } from "../../capability/instruction";
29
- import { promptCapability } from "../../capability/prompt";
30
- import { ruleCapability } from "../../capability/rule";
31
26
  import { getAuthPath, getDebugLogPath } from "../../config";
32
27
  import type { AgentSession, AgentSessionEvent } from "../../core/agent-session";
33
28
  import type { CustomToolSessionEvent, LoadedCustomTool } from "../../core/custom-tools/index";
34
29
  import type { HookUIContext } from "../../core/hooks/index";
35
30
  import { createCompactionSummaryMessage } from "../../core/messages";
36
31
  import { getRecentSessions, type SessionContext, SessionManager } from "../../core/session-manager";
37
- import { loadSkills } from "../../core/skills";
38
32
  import { generateSessionTitle, setTerminalTitle } from "../../core/title-generator";
39
33
  import type { TruncationResult } from "../../core/tools/truncate";
40
- import {
41
- type ContextFile,
42
- disableProvider,
43
- enableProvider,
44
- type Instruction,
45
- loadSync,
46
- type Prompt,
47
- type Rule,
48
- } from "../../discovery";
34
+ import { disableProvider, enableProvider } from "../../discovery";
49
35
  import { getChangelogPath, parseChangelog } from "../../utils/changelog";
50
36
  import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
51
37
  import { ArminComponent } from "./components/armin";
@@ -56,6 +42,7 @@ import { BranchSummaryMessageComponent } from "./components/branch-summary-messa
56
42
  import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message";
57
43
  import { CustomEditor } from "./components/custom-editor";
58
44
  import { DynamicBorder } from "./components/dynamic-border";
45
+ import { ExtensionDashboard } from "./components/extensions";
59
46
  import { FooterComponent } from "./components/footer";
60
47
  import { HookEditorComponent } from "./components/hook-editor";
61
48
  import { HookInputComponent } from "./components/hook-input";
@@ -203,7 +190,8 @@ export class InteractiveMode {
203
190
  { name: "share", description: "Share session as a secret GitHub gist" },
204
191
  { name: "copy", description: "Copy last agent message to clipboard" },
205
192
  { name: "session", description: "Show session info and stats" },
206
- { name: "status", description: "Show loaded extensions (context, skills, tools, hooks)" },
193
+ { name: "extensions", description: "Open Extension Control Center dashboard" },
194
+ { name: "status", description: "Alias for /extensions" },
207
195
  { name: "changelog", description: "Show changelog entries" },
208
196
  { name: "hotkeys", description: "Show all keyboard shortcuts" },
209
197
  { name: "branch", description: "Create a new branch from a previous message" },
@@ -413,6 +401,7 @@ export class InteractiveMode {
413
401
  this.streamingComponent = undefined;
414
402
  this.streamingMessage = undefined;
415
403
  this.pendingTools.clear();
404
+ this.titleGenerationAttempted = false;
416
405
 
417
406
  this.chatContainer.addChild(new Spacer(1));
418
407
  this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
@@ -794,8 +783,8 @@ export class InteractiveMode {
794
783
  this.editor.setText("");
795
784
  return;
796
785
  }
797
- if (text === "/status") {
798
- this.handleStatusCommand();
786
+ if (text === "/extensions" || text === "/status") {
787
+ this.showExtensionsDashboard();
799
788
  this.editor.setText("");
800
789
  return;
801
790
  }
@@ -1674,11 +1663,11 @@ export class InteractiveMode {
1674
1663
  .then((title) => {
1675
1664
  if (title) {
1676
1665
  this.sessionManager.setSessionTitle(title);
1677
- setTerminalTitle(`pi: ${title}`);
1666
+ setTerminalTitle(`omp: ${title}`);
1678
1667
  }
1679
1668
  })
1680
1669
  .catch(() => {
1681
- // Ignore title generation errors
1670
+ // Errors logged via logger in title-generator
1682
1671
  });
1683
1672
  }
1684
1673
 
@@ -1756,6 +1745,21 @@ export class InteractiveMode {
1756
1745
  });
1757
1746
  }
1758
1747
 
1748
+ /**
1749
+ * Show the Extension Control Center dashboard.
1750
+ * Replaces /status with a unified view of all providers and extensions.
1751
+ */
1752
+ private showExtensionsDashboard(): void {
1753
+ this.showSelector((done) => {
1754
+ const dashboard = new ExtensionDashboard(process.cwd(), this.settingsManager);
1755
+ dashboard.onClose = () => {
1756
+ done();
1757
+ this.ui.requestRender();
1758
+ };
1759
+ return { component: dashboard, focus: dashboard };
1760
+ });
1761
+ }
1762
+
1759
1763
  /**
1760
1764
  * Handle setting changes from the settings selector.
1761
1765
  * Most settings are saved directly via SettingsManager in the definitions.
@@ -2410,282 +2414,6 @@ export class InteractiveMode {
2410
2414
  this.ui.requestRender();
2411
2415
  }
2412
2416
 
2413
- private handleStatusCommand(): void {
2414
- type StatusSource =
2415
- | { provider: string; level: string }
2416
- | { mcpServer: string; provider?: string }
2417
- | "builtin"
2418
- | "unknown";
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
-
2434
- const capitalize = (value: string): string => value.charAt(0).toUpperCase() + value.slice(1);
2435
-
2436
- const resolveSourceText = (source: StatusSource): string => {
2437
- if (source === "builtin") return "builtin";
2438
- if (source === "unknown") return "unknown";
2439
- if ("mcpServer" in source) {
2440
- if (!source.provider) return `mcp:${source.mcpServer}`;
2441
- return `${source.mcpServer} via ${source.provider}`;
2442
- }
2443
- const levelLabel = capitalize(source.level);
2444
- return `via ${source.provider} (${levelLabel})`;
2445
- };
2446
-
2447
- const renderSourceText = (text: string): string => text.replace(/\bvia\b/, theme.italic("via"));
2448
-
2449
- const truncateText = (text: string, maxWidth: number): string => {
2450
- const textWidth = visibleWidth(text);
2451
- if (textWidth <= maxWidth) return text;
2452
- if (maxWidth <= 3) {
2453
- let acc = "";
2454
- let width = 0;
2455
- for (const char of text) {
2456
- const charWidth = visibleWidth(char);
2457
- if (width + charWidth > maxWidth) break;
2458
- width += charWidth;
2459
- acc += char;
2460
- }
2461
- return acc;
2462
- }
2463
- const targetWidth = maxWidth - 3;
2464
- let acc = "";
2465
- let width = 0;
2466
- for (const char of text) {
2467
- const charWidth = visibleWidth(char);
2468
- if (width + charWidth > targetWidth) break;
2469
- width += charWidth;
2470
- acc += char;
2471
- }
2472
- return `${acc}...`;
2473
- };
2474
-
2475
- const buildLineSection = <T>(
2476
- title: string,
2477
- items: readonly T[],
2478
- getName: (item: T) => string,
2479
- getDesc: (item: T) => string | undefined,
2480
- getSource: (item: T) => StatusSource,
2481
- ): LineSection | null => {
2482
- if (items.length === 0) return null;
2483
-
2484
- const lines = items.map((item) => {
2485
- const name = getName(item);
2486
- const desc = getDesc(item)?.trim();
2487
- const sourceText = resolveSourceText(getSource(item));
2488
- const nameWithSource = sourceText ? `${name} ${sourceText}` : name;
2489
- return { name, sourceText, nameWithSource, desc };
2490
- });
2491
-
2492
- return { title, lines };
2493
- };
2494
-
2495
- const renderLineSection = (section: LineSection, maxNameWidth: number): string => {
2496
- const formattedLines = section.lines.map((line) => {
2497
- let nameText = line.name;
2498
- let sourceText = line.sourceText;
2499
-
2500
- if (sourceText) {
2501
- const maxSourceWidth = Math.max(0, maxNameWidth - 2);
2502
- sourceText = truncateText(sourceText, maxSourceWidth);
2503
- }
2504
- const sourceWidth = sourceText ? visibleWidth(sourceText) : 0;
2505
- const availableForName = sourceText ? Math.max(1, maxNameWidth - sourceWidth - 1) : maxNameWidth;
2506
- nameText = truncateText(nameText, availableForName);
2507
-
2508
- const nameWithSourcePlain = sourceText ? `${nameText} ${sourceText}` : nameText;
2509
- const sourceRendered = sourceText ? renderSourceText(sourceText) : "";
2510
- const nameRendered = sourceText ? `${theme.bold(nameText)} ${sourceRendered}` : theme.bold(nameText);
2511
- const pad = Math.max(0, maxNameWidth - visibleWidth(nameWithSourcePlain));
2512
- const desc = line.desc;
2513
- const descPart = desc ? ` ${theme.fg("dim", desc.slice(0, 50) + (desc.length > 50 ? "..." : ""))}` : "";
2514
- return ` ${nameRendered}${" ".repeat(pad)}${descPart}`;
2515
- });
2516
-
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
- }
2532
- };
2533
-
2534
- // Loaded context files
2535
- const contextFilesResult = loadSync(contextFileCapability.id, { cwd: process.cwd() });
2536
- const contextFiles = contextFilesResult.items as ContextFile[];
2537
- pushLineSection(
2538
- "Context Files",
2539
- contextFiles,
2540
- (f) => basename(f.path),
2541
- () => undefined,
2542
- (f) => ({ provider: f._source.providerName, level: f.level }),
2543
- );
2544
-
2545
- // Loaded skills
2546
- const skillsSettings = this.session.skillsSettings;
2547
- if (skillsSettings?.enabled !== false) {
2548
- const { skills, warnings: skillWarnings } = loadSkills(skillsSettings ?? {});
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
- );
2556
- if (skillWarnings.length > 0) {
2557
- sections.push({
2558
- kind: "text",
2559
- text:
2560
- theme.bold(theme.fg("warning", "Skill Warnings")) +
2561
- "\n" +
2562
- skillWarnings.map((w) => theme.fg("warning", ` ${w.skillPath}: ${w.message}`)).join("\n"),
2563
- });
2564
- }
2565
- }
2566
-
2567
- // Loaded rules
2568
- const rulesResult = loadSync<Rule>(ruleCapability.id, { cwd: process.cwd() });
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
- );
2576
-
2577
- // Loaded prompts
2578
- const promptsResult = loadSync<Prompt>(promptCapability.id, { cwd: process.cwd() });
2579
- pushLineSection(
2580
- "Prompts",
2581
- promptsResult.items,
2582
- (p) => p.name,
2583
- () => undefined,
2584
- (p) => ({ provider: p._source.providerName, level: p._source.level }),
2585
- );
2586
-
2587
- // Loaded instructions
2588
- const instructionsResult = loadSync<Instruction>(instructionCapability.id, { cwd: process.cwd() });
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
- );
2596
-
2597
- // Loaded custom tools - split MCP from non-MCP
2598
- if (this.customTools.size > 0) {
2599
- const allTools = Array.from(this.customTools.values());
2600
- const mcpTools = allTools.filter((ct) => ct.path.startsWith("mcp:"));
2601
- const customTools = allTools.filter((ct) => !ct.path.startsWith("mcp:"));
2602
-
2603
- // MCP Tools section
2604
- if (mcpTools.length > 0) {
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
- },
2618
- );
2619
- }
2620
-
2621
- // Custom Tools section
2622
- if (customTools.length > 0) {
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
- },
2633
- );
2634
- }
2635
- }
2636
-
2637
- // Loaded slash commands (file-based)
2638
- const fileCommands = this.session.fileCommands;
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
- );
2646
-
2647
- // Loaded hooks
2648
- const hookRunner = this.session.hookRunner;
2649
- if (hookRunner) {
2650
- const hookPaths = hookRunner.getHookPaths();
2651
- if (hookPaths.length > 0) {
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
- });
2658
- }
2659
- }
2660
-
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) {
2673
- this.chatContainer.addChild(new Spacer(1));
2674
- this.chatContainer.addChild(new Text(theme.fg("muted", "No extensions loaded."), 1, 0));
2675
- } else {
2676
- this.chatContainer.addChild(new Spacer(1));
2677
- this.chatContainer.addChild(new DynamicBorder());
2678
- this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Loaded Extensions")), 1, 0));
2679
- this.chatContainer.addChild(new Spacer(1));
2680
- for (const section of renderedSections) {
2681
- this.chatContainer.addChild(new Text(section, 1, 0));
2682
- this.chatContainer.addChild(new Spacer(1));
2683
- }
2684
- this.chatContainer.addChild(new DynamicBorder());
2685
- }
2686
- this.ui.requestRender();
2687
- }
2688
-
2689
2417
  private async handleClearCommand(): Promise<void> {
2690
2418
  // Stop loading animation
2691
2419
  if (this.loadingAnimation) {
@@ -7,9 +7,35 @@
7
7
  */
8
8
 
9
9
  import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
10
+ import { APP_NAME, VERSION } from "../config";
10
11
  import type { AgentSession } from "../core/agent-session";
11
12
  import { logger } from "../core/logger";
12
13
 
14
+ /**
15
+ * Print session header to stderr (text mode only).
16
+ */
17
+ function printHeader(session: AgentSession): void {
18
+ const model = session.model;
19
+ const lines = [
20
+ `${APP_NAME} v${VERSION}`,
21
+ "--------",
22
+ `workdir: ${process.cwd()}`,
23
+ `model: ${model?.id ?? "unknown"}`,
24
+ `provider: ${model?.provider ?? "unknown"}`,
25
+ `thinking: ${session.thinkingLevel}`,
26
+ `session: ${session.sessionId}`,
27
+ "--------",
28
+ ];
29
+ console.error(lines.join("\n"));
30
+ }
31
+
32
+ /**
33
+ * Print session footer to stderr (text mode only).
34
+ */
35
+ function printFooter(): void {
36
+ console.error("--------");
37
+ }
38
+
13
39
  /**
14
40
  * Run in print (single-shot) mode.
15
41
  * Sends prompts to the agent and outputs the result.
@@ -27,6 +53,11 @@ export async function runPrintMode(
27
53
  initialMessage?: string,
28
54
  initialImages?: ImageContent[],
29
55
  ): Promise<void> {
56
+ // Print header to stderr (text mode only)
57
+ if (mode === "text") {
58
+ printHeader(session);
59
+ }
60
+
30
61
  // Hook runner already has no-op UI context by default (set in main.ts)
31
62
  // Set up hooks for print mode (no UI)
32
63
  const hookRunner = session.hookRunner;
@@ -116,6 +147,9 @@ export async function runPrintMode(
116
147
  }
117
148
  }
118
149
  }
150
+
151
+ // Print footer to stderr
152
+ printFooter();
119
153
  }
120
154
 
121
155
  // Ensure stdout is fully flushed before returning