@oh-my-pi/pi-coding-agent 3.4.1337 → 3.5.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,313 @@
1
+ /**
2
+ * InspectorPanel - Detail view for selected extension.
3
+ *
4
+ * Shows name, description, origin, status, and kind-specific preview.
5
+ */
6
+
7
+ import { readFileSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { type Component, truncateToWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
10
+ import { theme } from "../../theme/theme";
11
+ import type { Extension, ExtensionState } from "./types";
12
+
13
+ export class InspectorPanel implements Component {
14
+ private extension: Extension | null = null;
15
+
16
+ setExtension(extension: Extension | null): void {
17
+ this.extension = extension;
18
+ }
19
+
20
+ invalidate(): void {}
21
+
22
+ render(width: number): string[] {
23
+ if (!this.extension) {
24
+ return [theme.fg("muted", "Select an extension"), theme.fg("dim", "to view details")];
25
+ }
26
+
27
+ const ext = this.extension;
28
+ const lines: string[] = [];
29
+
30
+ // Name header
31
+ lines.push(theme.bold(theme.fg("accent", ext.displayName)));
32
+ lines.push("");
33
+
34
+ // Kind badge
35
+ lines.push(theme.fg("muted", "Type: ") + this.getKindBadge(ext.kind));
36
+ lines.push("");
37
+
38
+ // Description (wrapped)
39
+ if (ext.description) {
40
+ const wrapped = wrapTextWithAnsi(ext.description, width - 2);
41
+ for (const line of wrapped) {
42
+ lines.push(truncateToWidth(line, width));
43
+ }
44
+ lines.push("");
45
+ }
46
+
47
+ // Origin
48
+ lines.push(theme.fg("muted", "Origin:"));
49
+ const levelLabel = ext.source.level === "user" ? "User" : ext.source.level === "project" ? "Project" : "Native";
50
+ lines.push(` ${theme.italic(`via ${ext.source.providerName} (${levelLabel})`)}`);
51
+ lines.push(` ${theme.fg("dim", this.shortenPath(ext.path))}`);
52
+ lines.push("");
53
+
54
+ // Status badge
55
+ lines.push(theme.fg("muted", "Status:"));
56
+ lines.push(` ${this.getStatusBadge(ext.state, ext.disabledReason, ext.shadowedBy)}`);
57
+ lines.push("");
58
+
59
+ // Preview section (routed based on kind)
60
+ const previewLines = this.renderPreview(ext, width);
61
+ lines.push(...previewLines);
62
+
63
+ return lines;
64
+ }
65
+
66
+ private renderPreview(ext: Extension, width: number): string[] {
67
+ const lines: string[] = [];
68
+ let content: string[] = [];
69
+
70
+ switch (ext.kind) {
71
+ case "context-file":
72
+ content = this.renderFilePreview(ext.path, width);
73
+ break;
74
+ case "tool":
75
+ content = this.renderToolArgs(ext.raw, width);
76
+ break;
77
+ case "skill":
78
+ content = this.renderSkillContent(ext.raw, width);
79
+ break;
80
+ case "mcp":
81
+ content = this.renderMcpDetails(ext.raw, width);
82
+ break;
83
+ default:
84
+ content = this.renderDefaultPreview(ext, width);
85
+ break;
86
+ }
87
+
88
+ if (content.length > 0) {
89
+ lines.push(...content);
90
+ }
91
+
92
+ return lines;
93
+ }
94
+
95
+ private renderFilePreview(path: string, width: number): string[] {
96
+ const lines: string[] = [];
97
+ lines.push(theme.fg("muted", "Preview:"));
98
+ lines.push(theme.fg("dim", "─".repeat(Math.min(width - 2, 40))));
99
+
100
+ try {
101
+ const content = readFileSync(path, "utf-8");
102
+ const fileLines = content.split("\n").slice(0, 20);
103
+
104
+ for (const line of fileLines) {
105
+ const highlighted = this.highlightMarkdown(line);
106
+ lines.push(truncateToWidth(highlighted, width - 2));
107
+ }
108
+
109
+ if (content.split("\n").length > 20) {
110
+ lines.push(theme.fg("dim", "(truncated at line 20)"));
111
+ }
112
+ } catch (err) {
113
+ lines.push(theme.fg("error", `Failed to read file: ${err instanceof Error ? err.message : String(err)}`));
114
+ }
115
+
116
+ lines.push("");
117
+ return lines;
118
+ }
119
+
120
+ private highlightMarkdown(line: string): string {
121
+ // Basic markdown syntax highlighting
122
+ let highlighted = line;
123
+
124
+ // Headers
125
+ if (/^#{1,6}\s/.test(highlighted)) {
126
+ highlighted = theme.bold(theme.fg("accent", highlighted));
127
+ }
128
+ // Code blocks
129
+ else if (/^```/.test(highlighted)) {
130
+ highlighted = theme.fg("dim", highlighted);
131
+ }
132
+ // Lists
133
+ else if (/^[\s]*[-*+]\s/.test(highlighted)) {
134
+ highlighted = highlighted.replace(/^([\s]*[-*+]\s)/, theme.fg("accent", "$1"));
135
+ }
136
+ // Numbered lists
137
+ else if (/^[\s]*\d+\.\s/.test(highlighted)) {
138
+ highlighted = highlighted.replace(/^([\s]*\d+\.\s)/, theme.fg("accent", "$1"));
139
+ }
140
+
141
+ return highlighted;
142
+ }
143
+
144
+ private renderToolArgs(raw: unknown, width: number): string[] {
145
+ const lines: string[] = [];
146
+ lines.push(theme.fg("muted", "Arguments:"));
147
+ lines.push(theme.fg("dim", "─".repeat(Math.min(width - 2, 40))));
148
+
149
+ try {
150
+ const tool = raw as any;
151
+ const params = tool?.parameters?.properties || tool?.inputSchema?.properties || {};
152
+
153
+ if (Object.keys(params).length === 0) {
154
+ lines.push(theme.fg("dim", " (no arguments)"));
155
+ } else {
156
+ const required = new Set(tool?.parameters?.required || tool?.inputSchema?.required || []);
157
+
158
+ for (const [name, spec] of Object.entries(params)) {
159
+ const param = spec as any;
160
+ const type = param.type || "any";
161
+ const isRequired = required.has(name);
162
+ const defaultVal = param.default !== undefined ? `Default: ${param.default}` : null;
163
+
164
+ const nameCol = theme.fg("accent", name.padEnd(12));
165
+ const typeCol = theme.fg("muted", type.padEnd(10));
166
+ const reqCol = isRequired
167
+ ? theme.fg("warning", "Required")
168
+ : defaultVal
169
+ ? theme.fg("dim", defaultVal)
170
+ : theme.fg("dim", "Optional");
171
+
172
+ lines.push(` ${nameCol} ${typeCol} ${reqCol}`);
173
+ }
174
+ }
175
+ } catch {
176
+ lines.push(theme.fg("dim", " (unable to parse tool definition)"));
177
+ }
178
+
179
+ lines.push("");
180
+ return lines;
181
+ }
182
+
183
+ private renderSkillContent(raw: unknown, width: number): string[] {
184
+ const lines: string[] = [];
185
+ lines.push(theme.fg("muted", "Instruction:"));
186
+ lines.push(theme.fg("dim", "─".repeat(Math.min(width - 2, 40))));
187
+
188
+ try {
189
+ const skill = raw as any;
190
+ const instruction = skill?.prompt || skill?.instruction || skill?.content || "";
191
+
192
+ if (!instruction) {
193
+ lines.push(theme.fg("dim", " (no instruction text)"));
194
+ } else {
195
+ const instructionLines = instruction.split("\n").slice(0, 15);
196
+ for (const line of instructionLines) {
197
+ lines.push(truncateToWidth(line, width - 2));
198
+ }
199
+
200
+ if (instruction.split("\n").length > 15) {
201
+ lines.push(theme.fg("dim", "(truncated at line 15)"));
202
+ }
203
+ }
204
+ } catch {
205
+ lines.push(theme.fg("dim", " (unable to parse skill content)"));
206
+ }
207
+
208
+ lines.push("");
209
+ return lines;
210
+ }
211
+
212
+ private renderMcpDetails(raw: unknown, width: number): string[] {
213
+ const lines: string[] = [];
214
+ lines.push(theme.fg("muted", "Connection:"));
215
+ lines.push(theme.fg("dim", "─".repeat(Math.min(width - 2, 40))));
216
+
217
+ try {
218
+ const mcp = raw as any;
219
+ const transport = mcp?.transport || mcp?.type || "unknown";
220
+ const command = mcp?.command || mcp?.cmd || "";
221
+ const args = mcp?.args || mcp?.arguments || [];
222
+
223
+ lines.push(` ${theme.fg("muted", "Transport:")} ${theme.fg("accent", transport)}`);
224
+
225
+ if (command) {
226
+ lines.push(` ${theme.fg("muted", "Command:")} ${theme.fg("success", command)}`);
227
+ }
228
+
229
+ if (Array.isArray(args) && args.length > 0) {
230
+ lines.push(` ${theme.fg("muted", "Args:")} ${theme.fg("dim", args.join(" "))}`);
231
+ }
232
+
233
+ // Environment variables if present
234
+ if (mcp?.env && typeof mcp.env === "object") {
235
+ const envCount = Object.keys(mcp.env).length;
236
+ if (envCount > 0) {
237
+ lines.push(` ${theme.fg("muted", "Env vars:")} ${theme.fg("dim", `${envCount} defined`)}`);
238
+ }
239
+ }
240
+ } catch {
241
+ lines.push(theme.fg("dim", " (unable to parse MCP configuration)"));
242
+ }
243
+
244
+ lines.push("");
245
+ return lines;
246
+ }
247
+
248
+ private renderDefaultPreview(ext: Extension, width: number): string[] {
249
+ const lines: string[] = [];
250
+
251
+ // Show trigger pattern if present
252
+ if (ext.trigger) {
253
+ lines.push(theme.fg("muted", "Trigger:"));
254
+ lines.push(theme.fg("dim", "─".repeat(Math.min(width - 2, 40))));
255
+ lines.push(` ${theme.fg("accent", ext.trigger)}`);
256
+ lines.push("");
257
+ }
258
+
259
+ return lines;
260
+ }
261
+
262
+ private getKindBadge(kind: string): string {
263
+ const kindColors: Record<string, string> = {
264
+ skill: "accent",
265
+ rule: "success",
266
+ tool: "warning",
267
+ mcp: "accent",
268
+ prompt: "muted",
269
+ hook: "warning",
270
+ "context-file": "dim",
271
+ instruction: "muted",
272
+ "slash-command": "accent",
273
+ };
274
+
275
+ const color = kindColors[kind] || "muted";
276
+ return theme.fg(color as any, kind);
277
+ }
278
+
279
+ private getStatusBadge(state: ExtensionState, reason?: string, shadowedBy?: string): string {
280
+ switch (state) {
281
+ case "active":
282
+ return theme.fg("success", "● Active");
283
+ case "disabled": {
284
+ const reasonText =
285
+ reason === "provider-disabled"
286
+ ? "provider disabled"
287
+ : reason === "item-disabled"
288
+ ? "manually disabled"
289
+ : "unknown";
290
+ return theme.fg("dim", `○ Disabled (${reasonText})`);
291
+ }
292
+ case "shadowed":
293
+ return theme.fg("warning", `◐ Shadowed${shadowedBy ? ` by ${shadowedBy}` : ""}`);
294
+ }
295
+ }
296
+
297
+ private shortenPath(path: string): string {
298
+ const home = homedir();
299
+ if (path.startsWith(home)) {
300
+ return `~${path.slice(home.length)}`;
301
+ }
302
+
303
+ // If path is very long, show just the last parts
304
+ if (path.length > 40) {
305
+ const parts = path.split("/");
306
+ if (parts.length > 3) {
307
+ return `.../${parts.slice(-3).join("/")}`;
308
+ }
309
+ }
310
+
311
+ return path;
312
+ }
313
+ }