@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.
- package/CHANGELOG.md +22 -0
- package/package.json +5 -4
- package/src/core/sdk.ts +14 -1
- package/src/core/session-manager.ts +98 -69
- package/src/core/settings-manager.ts +33 -0
- package/src/core/system-prompt.ts +15 -0
- package/src/core/title-generator.ts +28 -6
- package/src/core/tools/index.ts +6 -0
- package/src/core/tools/rulebook.ts +124 -0
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +297 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +477 -0
- package/src/modes/interactive/components/extensions/index.ts +9 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
- package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
- package/src/modes/interactive/components/extensions/types.ts +191 -0
- package/src/modes/interactive/components/settings-defs.ts +2 -31
- package/src/modes/interactive/components/settings-selector.ts +0 -1
- package/src/modes/interactive/interactive-mode.ts +24 -296
- package/src/modes/print-mode.ts +34 -0
- package/src/modes/rpc/rpc-mode.ts +8 -7
|
@@ -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
|
-
*
|
|
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
|
|
302
|
+
return SETTINGS_DEFS;
|
|
332
303
|
}
|
|
333
304
|
|
|
334
305
|
/** Get settings for a specific tab */
|
|
@@ -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: "
|
|
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.
|
|
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(`
|
|
1666
|
+
setTerminalTitle(`omp: ${title}`);
|
|
1678
1667
|
}
|
|
1679
1668
|
})
|
|
1680
1669
|
.catch(() => {
|
|
1681
|
-
//
|
|
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) {
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -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
|