@docyrus/docyrus 0.0.20 → 0.0.21
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/agent-loader.js +32 -1
- package/agent-loader.js.map +2 -2
- package/main.js +321 -70
- package/main.js.map +4 -4
- package/package.json +12 -2
- package/resources/chrome-tools/browser-content.js +103 -0
- package/resources/chrome-tools/browser-cookies.js +35 -0
- package/resources/chrome-tools/browser-eval.js +53 -0
- package/resources/chrome-tools/browser-hn-scraper.js +108 -0
- package/resources/chrome-tools/browser-nav.js +44 -0
- package/resources/chrome-tools/browser-pick.js +162 -0
- package/resources/chrome-tools/browser-screenshot.js +34 -0
- package/resources/chrome-tools/browser-start.js +86 -0
- package/resources/pi-agent/extensions/answer.ts +532 -0
- package/resources/pi-agent/extensions/context.ts +578 -0
- package/resources/pi-agent/extensions/control.ts +1779 -0
- package/resources/pi-agent/extensions/diff.ts +218 -0
- package/resources/pi-agent/extensions/files.ts +199 -0
- package/resources/pi-agent/extensions/loop.ts +446 -0
- package/resources/pi-agent/extensions/multi-edit.ts +835 -0
- package/resources/pi-agent/extensions/notify.ts +88 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/CHANGELOG.md +192 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/LICENSE +21 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/README.md +296 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +67 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/cli.js +108 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +211 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +227 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +64 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +301 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/errors.ts +219 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +80 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +427 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +232 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +319 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +93 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +169 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +713 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +191 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +419 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +56 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/package.json +85 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/paths.ts +29 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +635 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/resource-tools.ts +17 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +330 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/state.ts +41 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +144 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/tool-registrar.ts +46 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +367 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +145 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +623 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +384 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-stream-types.ts +89 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +75 -0
- package/resources/pi-agent/extensions/prompt-editor.ts +1315 -0
- package/resources/pi-agent/extensions/prompt-url-widget.ts +158 -0
- package/resources/pi-agent/extensions/redraws.ts +24 -0
- package/resources/pi-agent/extensions/review.ts +2160 -0
- package/resources/pi-agent/extensions/todos.ts +2076 -0
- package/resources/pi-agent/extensions/tps.ts +47 -0
- package/resources/pi-agent/extensions/whimsical.ts +474 -0
- package/resources/pi-agent/skills/changelog-generator/SKILL.md +425 -0
- package/resources/pi-agent/skills/docyrus-chrome-devtools-cli/SKILL.md +80 -0
- package/resources/pi-agent/skills/docyrus-platform/references/docyrus-cli-usage.md +51 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff Extension
|
|
3
|
+
*
|
|
4
|
+
* /diff command shows modified/deleted/new files from git status and opens
|
|
5
|
+
* the selected file in VS Code's diff view.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { Container, Key, matchesKey, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
|
|
11
|
+
|
|
12
|
+
interface FileInfo {
|
|
13
|
+
status: string;
|
|
14
|
+
statusLabel: string;
|
|
15
|
+
file: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function (pi: ExtensionAPI) {
|
|
19
|
+
pi.registerCommand("diff", {
|
|
20
|
+
description: "Show git changes and open in VS Code diff view",
|
|
21
|
+
handler: async (_args, ctx) => {
|
|
22
|
+
if (!ctx.hasUI) {
|
|
23
|
+
ctx.ui.notify("No UI available", "error");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Get changed files from git status
|
|
28
|
+
const result = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
|
|
29
|
+
|
|
30
|
+
if (result.code !== 0) {
|
|
31
|
+
ctx.ui.notify(`git status failed: ${result.stderr}`, "error");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!result.stdout || !result.stdout.trim()) {
|
|
36
|
+
ctx.ui.notify("No changes in working tree", "info");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Parse git status output
|
|
41
|
+
// Format: XY filename (where XY is two-letter status, then space, then filename)
|
|
42
|
+
const lines = result.stdout.split("\n");
|
|
43
|
+
const files: FileInfo[] = [];
|
|
44
|
+
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
if (line.length < 4) continue; // Need at least "XY f"
|
|
47
|
+
|
|
48
|
+
const status = line.slice(0, 2);
|
|
49
|
+
const file = line.slice(2).trimStart();
|
|
50
|
+
|
|
51
|
+
// Translate status codes to short labels
|
|
52
|
+
let statusLabel: string;
|
|
53
|
+
if (status.includes("M")) statusLabel = "M";
|
|
54
|
+
else if (status.includes("A")) statusLabel = "A";
|
|
55
|
+
else if (status.includes("D")) statusLabel = "D";
|
|
56
|
+
else if (status.includes("?")) statusLabel = "?";
|
|
57
|
+
else if (status.includes("R")) statusLabel = "R";
|
|
58
|
+
else if (status.includes("C")) statusLabel = "C";
|
|
59
|
+
else statusLabel = status.trim() || "~";
|
|
60
|
+
|
|
61
|
+
files.push({ status: statusLabel, statusLabel, file });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (files.length === 0) {
|
|
65
|
+
ctx.ui.notify("No changes found", "info");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>^%\r\n]/;
|
|
70
|
+
const quoteCmdArg = (value: string) => `"${value.replace(/"/g, '""')}"`;
|
|
71
|
+
|
|
72
|
+
const openWithCode = async (file: string) => {
|
|
73
|
+
if (process.platform === "win32") {
|
|
74
|
+
if (WINDOWS_UNSAFE_CMD_CHARS_RE.test(file)) {
|
|
75
|
+
ctx.ui.notify(
|
|
76
|
+
`Refusing to open ${file}: path contains Windows cmd metacharacters (& | < > ^ % or newline).`,
|
|
77
|
+
"error",
|
|
78
|
+
);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const commandLine = `code -g ${quoteCmdArg(file)}`;
|
|
82
|
+
return pi.exec("cmd", ["/d", "/s", "/c", commandLine], { cwd: ctx.cwd });
|
|
83
|
+
}
|
|
84
|
+
return pi.exec("code", ["-g", file], { cwd: ctx.cwd });
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const openSelected = async (fileInfo: FileInfo): Promise<void> => {
|
|
88
|
+
try {
|
|
89
|
+
// Open in VS Code diff view.
|
|
90
|
+
// For untracked files, git difftool won't work, so fall back to just opening the file.
|
|
91
|
+
if (fileInfo.status === "?") {
|
|
92
|
+
const openResult = await openWithCode(fileInfo.file);
|
|
93
|
+
if (!openResult) return;
|
|
94
|
+
if (openResult.code !== 0) {
|
|
95
|
+
const openStderr = openResult.stderr.trim();
|
|
96
|
+
ctx.ui.notify(
|
|
97
|
+
`Failed to open ${fileInfo.file} (exit ${openResult.code})${openStderr ? `: ${openStderr}` : ""}`,
|
|
98
|
+
"error",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const diffResult = await pi.exec("git", ["difftool", "-y", "--tool=vscode", fileInfo.file], {
|
|
105
|
+
cwd: ctx.cwd,
|
|
106
|
+
});
|
|
107
|
+
if (diffResult.code !== 0) {
|
|
108
|
+
const diffStderr = diffResult.stderr.trim();
|
|
109
|
+
ctx.ui.notify(
|
|
110
|
+
`Failed to show diff with vscode for ${fileInfo.file} (exit ${diffResult.code})${diffStderr ? `: ${diffStderr}` : ""}`,
|
|
111
|
+
"error",
|
|
112
|
+
);
|
|
113
|
+
ctx.ui.notify(
|
|
114
|
+
"Troubleshooting: check git difftool config (e.g. `git config --get difftool.vscode.cmd`).",
|
|
115
|
+
"info",
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const openResult = await openWithCode(fileInfo.file);
|
|
119
|
+
if (!openResult) return;
|
|
120
|
+
if (openResult.code !== 0) {
|
|
121
|
+
const openStderr = openResult.stderr.trim();
|
|
122
|
+
ctx.ui.notify(
|
|
123
|
+
`Failed to open ${fileInfo.file} (exit ${openResult.code})${openStderr ? `: ${openStderr}` : ""}`,
|
|
124
|
+
"error",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
130
|
+
ctx.ui.notify(`Failed to open ${fileInfo.file}: ${message}`, "error");
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Show file picker with SelectList
|
|
135
|
+
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
136
|
+
const container = new Container();
|
|
137
|
+
|
|
138
|
+
// Top border
|
|
139
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
140
|
+
|
|
141
|
+
// Title
|
|
142
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to diff")), 0, 0));
|
|
143
|
+
|
|
144
|
+
// Build select items with colored status
|
|
145
|
+
const items: SelectItem[] = files.map((f) => {
|
|
146
|
+
let statusColor: string;
|
|
147
|
+
switch (f.status) {
|
|
148
|
+
case "M":
|
|
149
|
+
statusColor = theme.fg("warning", f.status);
|
|
150
|
+
break;
|
|
151
|
+
case "A":
|
|
152
|
+
statusColor = theme.fg("success", f.status);
|
|
153
|
+
break;
|
|
154
|
+
case "D":
|
|
155
|
+
statusColor = theme.fg("error", f.status);
|
|
156
|
+
break;
|
|
157
|
+
case "?":
|
|
158
|
+
statusColor = theme.fg("muted", f.status);
|
|
159
|
+
break;
|
|
160
|
+
default:
|
|
161
|
+
statusColor = theme.fg("dim", f.status);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
value: f,
|
|
165
|
+
label: `${statusColor} ${f.file}`,
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const visibleRows = Math.min(files.length, 15);
|
|
170
|
+
let currentIndex = 0;
|
|
171
|
+
|
|
172
|
+
const selectList = new SelectList(items, visibleRows, {
|
|
173
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
174
|
+
selectedText: (t) => t, // Keep existing colors
|
|
175
|
+
description: (t) => theme.fg("muted", t),
|
|
176
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
177
|
+
noMatch: (t) => theme.fg("warning", t),
|
|
178
|
+
});
|
|
179
|
+
selectList.onSelect = (item) => {
|
|
180
|
+
void openSelected(item.value as FileInfo);
|
|
181
|
+
};
|
|
182
|
+
selectList.onCancel = () => done();
|
|
183
|
+
selectList.onSelectionChange = (item) => {
|
|
184
|
+
currentIndex = items.indexOf(item);
|
|
185
|
+
};
|
|
186
|
+
container.addChild(selectList);
|
|
187
|
+
|
|
188
|
+
// Help text
|
|
189
|
+
container.addChild(
|
|
190
|
+
new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Bottom border
|
|
194
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
render: (w) => container.render(w),
|
|
198
|
+
invalidate: () => container.invalidate(),
|
|
199
|
+
handleInput: (data) => {
|
|
200
|
+
// Add paging with left/right
|
|
201
|
+
if (matchesKey(data, Key.left)) {
|
|
202
|
+
// Page up - clamp to 0
|
|
203
|
+
currentIndex = Math.max(0, currentIndex - visibleRows);
|
|
204
|
+
selectList.setSelectedIndex(currentIndex);
|
|
205
|
+
} else if (matchesKey(data, Key.right)) {
|
|
206
|
+
// Page down - clamp to last
|
|
207
|
+
currentIndex = Math.min(items.length - 1, currentIndex + visibleRows);
|
|
208
|
+
selectList.setSelectedIndex(currentIndex);
|
|
209
|
+
} else {
|
|
210
|
+
selectList.handleInput(data);
|
|
211
|
+
}
|
|
212
|
+
tui.requestRender();
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Files Extension
|
|
3
|
+
*
|
|
4
|
+
* /files command lists all files the model has read/written/edited in the active session branch,
|
|
5
|
+
* coalesced by path and sorted newest first. Selecting a file opens it in VS Code.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { Container, Key, matchesKey, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
|
|
11
|
+
|
|
12
|
+
interface FileEntry {
|
|
13
|
+
path: string;
|
|
14
|
+
operations: Set<"read" | "write" | "edit">;
|
|
15
|
+
lastTimestamp: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type FileToolName = "read" | "write" | "edit";
|
|
19
|
+
|
|
20
|
+
export default function (pi: ExtensionAPI) {
|
|
21
|
+
pi.registerCommand("files", {
|
|
22
|
+
description: "Show files read/written/edited in this session",
|
|
23
|
+
handler: async (_args, ctx) => {
|
|
24
|
+
if (!ctx.hasUI) {
|
|
25
|
+
ctx.ui.notify("No UI available", "error");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Get the current branch (path from leaf to root)
|
|
30
|
+
const branch = ctx.sessionManager.getBranch();
|
|
31
|
+
|
|
32
|
+
// First pass: collect tool calls (id -> {path, name}) from assistant messages
|
|
33
|
+
const toolCalls = new Map<string, { path: string; name: FileToolName; timestamp: number }>();
|
|
34
|
+
|
|
35
|
+
for (const entry of branch) {
|
|
36
|
+
if (entry.type !== "message") continue;
|
|
37
|
+
const msg = entry.message;
|
|
38
|
+
|
|
39
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
40
|
+
for (const block of msg.content) {
|
|
41
|
+
if (block.type === "toolCall") {
|
|
42
|
+
const name = block.name;
|
|
43
|
+
if (name === "read" || name === "write" || name === "edit") {
|
|
44
|
+
const path = block.arguments?.path;
|
|
45
|
+
if (path && typeof path === "string") {
|
|
46
|
+
toolCalls.set(block.id, { path, name, timestamp: msg.timestamp });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Second pass: match tool results to get the actual execution timestamp
|
|
55
|
+
const fileMap = new Map<string, FileEntry>();
|
|
56
|
+
|
|
57
|
+
for (const entry of branch) {
|
|
58
|
+
if (entry.type !== "message") continue;
|
|
59
|
+
const msg = entry.message;
|
|
60
|
+
|
|
61
|
+
if (msg.role === "toolResult") {
|
|
62
|
+
const toolCall = toolCalls.get(msg.toolCallId);
|
|
63
|
+
if (!toolCall) continue;
|
|
64
|
+
|
|
65
|
+
const { path, name } = toolCall;
|
|
66
|
+
const timestamp = msg.timestamp;
|
|
67
|
+
|
|
68
|
+
const existing = fileMap.get(path);
|
|
69
|
+
if (existing) {
|
|
70
|
+
existing.operations.add(name);
|
|
71
|
+
if (timestamp > existing.lastTimestamp) {
|
|
72
|
+
existing.lastTimestamp = timestamp;
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
fileMap.set(path, {
|
|
76
|
+
path,
|
|
77
|
+
operations: new Set([name]),
|
|
78
|
+
lastTimestamp: timestamp,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (fileMap.size === 0) {
|
|
85
|
+
ctx.ui.notify("No files read/written/edited in this session", "info");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Sort by most recent first
|
|
90
|
+
const files = Array.from(fileMap.values()).sort((a, b) => b.lastTimestamp - a.lastTimestamp);
|
|
91
|
+
|
|
92
|
+
const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>^%\r\n]/;
|
|
93
|
+
const quoteCmdArg = (value: string) => `"${value.replace(/"/g, '""')}"`;
|
|
94
|
+
|
|
95
|
+
const openWithCode = async (path: string) => {
|
|
96
|
+
if (process.platform === "win32") {
|
|
97
|
+
if (WINDOWS_UNSAFE_CMD_CHARS_RE.test(path)) {
|
|
98
|
+
ctx.ui.notify(
|
|
99
|
+
`Refusing to open ${path}: path contains Windows cmd metacharacters (& | < > ^ % or newline).`,
|
|
100
|
+
"error",
|
|
101
|
+
);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const commandLine = `code -g ${quoteCmdArg(path)}`;
|
|
105
|
+
return pi.exec("cmd", ["/d", "/s", "/c", commandLine], { cwd: ctx.cwd });
|
|
106
|
+
}
|
|
107
|
+
return pi.exec("code", ["-g", path], { cwd: ctx.cwd });
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const openSelected = async (file: FileEntry): Promise<void> => {
|
|
111
|
+
try {
|
|
112
|
+
const openResult = await openWithCode(file.path);
|
|
113
|
+
if (!openResult) return;
|
|
114
|
+
if (openResult.code !== 0) {
|
|
115
|
+
const openStderr = openResult.stderr.trim();
|
|
116
|
+
ctx.ui.notify(
|
|
117
|
+
`Failed to open ${file.path} (exit ${openResult.code})${openStderr ? `: ${openStderr}` : ""}`,
|
|
118
|
+
"error",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
+
ctx.ui.notify(`Failed to open ${file.path}: ${message}`, "error");
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Show file picker with SelectList
|
|
128
|
+
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
129
|
+
const container = new Container();
|
|
130
|
+
|
|
131
|
+
// Top border
|
|
132
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
133
|
+
|
|
134
|
+
// Title
|
|
135
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to open")), 0, 0));
|
|
136
|
+
|
|
137
|
+
// Build select items with colored operations
|
|
138
|
+
const items: SelectItem[] = files.map((f) => {
|
|
139
|
+
const ops: string[] = [];
|
|
140
|
+
if (f.operations.has("read")) ops.push(theme.fg("muted", "R"));
|
|
141
|
+
if (f.operations.has("write")) ops.push(theme.fg("success", "W"));
|
|
142
|
+
if (f.operations.has("edit")) ops.push(theme.fg("warning", "E"));
|
|
143
|
+
const opsLabel = ops.join("");
|
|
144
|
+
return {
|
|
145
|
+
value: f,
|
|
146
|
+
label: `${opsLabel} ${f.path}`,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const visibleRows = Math.min(files.length, 15);
|
|
151
|
+
let currentIndex = 0;
|
|
152
|
+
|
|
153
|
+
const selectList = new SelectList(items, visibleRows, {
|
|
154
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
155
|
+
selectedText: (t) => t, // Keep existing colors
|
|
156
|
+
description: (t) => theme.fg("muted", t),
|
|
157
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
158
|
+
noMatch: (t) => theme.fg("warning", t),
|
|
159
|
+
});
|
|
160
|
+
selectList.onSelect = (item) => {
|
|
161
|
+
void openSelected(item.value as FileEntry);
|
|
162
|
+
};
|
|
163
|
+
selectList.onCancel = () => done();
|
|
164
|
+
selectList.onSelectionChange = (item) => {
|
|
165
|
+
currentIndex = items.indexOf(item);
|
|
166
|
+
};
|
|
167
|
+
container.addChild(selectList);
|
|
168
|
+
|
|
169
|
+
// Help text
|
|
170
|
+
container.addChild(
|
|
171
|
+
new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter open • esc close"), 0, 0),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Bottom border
|
|
175
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
render: (w) => container.render(w),
|
|
179
|
+
invalidate: () => container.invalidate(),
|
|
180
|
+
handleInput: (data) => {
|
|
181
|
+
// Add paging with left/right
|
|
182
|
+
if (matchesKey(data, Key.left)) {
|
|
183
|
+
// Page up - clamp to 0
|
|
184
|
+
currentIndex = Math.max(0, currentIndex - visibleRows);
|
|
185
|
+
selectList.setSelectedIndex(currentIndex);
|
|
186
|
+
} else if (matchesKey(data, Key.right)) {
|
|
187
|
+
// Page down - clamp to last
|
|
188
|
+
currentIndex = Math.min(items.length - 1, currentIndex + visibleRows);
|
|
189
|
+
selectList.setSelectedIndex(currentIndex);
|
|
190
|
+
} else {
|
|
191
|
+
selectList.handleInput(data);
|
|
192
|
+
}
|
|
193
|
+
tui.requestRender();
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|