opencode-add-dir 1.5.0 → 1.6.0
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/README.md +44 -49
- package/dist/index.js +28 -105
- package/dist/permissions.d.ts +1 -2
- package/dist/state.d.ts +2 -2
- package/dist/tui-plugin.d.ts +4 -3
- package/dist/tui.tsx +119 -71
- package/dist/types.d.ts +0 -3
- package/package.json +1 -1
- package/dist/validate.d.ts +0 -8
package/README.md
CHANGED
|
@@ -6,12 +6,8 @@ When you need an agent to read, edit, or search files outside the current projec
|
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```json
|
|
12
|
-
{
|
|
13
|
-
"plugin": ["opencode-add-dir"]
|
|
14
|
-
}
|
|
9
|
+
```bash
|
|
10
|
+
opencode plugin opencode-add-dir -g
|
|
15
11
|
```
|
|
16
12
|
|
|
17
13
|
Restart OpenCode. Done.
|
|
@@ -20,15 +16,13 @@ Restart OpenCode. Done.
|
|
|
20
16
|
<summary>Alternative: setup CLI</summary>
|
|
21
17
|
|
|
22
18
|
```bash
|
|
23
|
-
|
|
19
|
+
npx opencode-add-dir-setup
|
|
24
20
|
```
|
|
25
21
|
|
|
26
|
-
Automatically adds the plugin to your global `opencode.json`.
|
|
27
|
-
|
|
28
22
|
</details>
|
|
29
23
|
|
|
30
24
|
<details>
|
|
31
|
-
<summary>Alternative: local
|
|
25
|
+
<summary>Alternative: local development</summary>
|
|
32
26
|
|
|
33
27
|
```bash
|
|
34
28
|
git clone https://github.com/kuzeofficial/add-dir-opencode.git
|
|
@@ -36,77 +30,78 @@ cd add-dir-opencode
|
|
|
36
30
|
bun install && bun run deploy
|
|
37
31
|
```
|
|
38
32
|
|
|
39
|
-
|
|
33
|
+
Add the local path to both configs:
|
|
34
|
+
|
|
35
|
+
```jsonc
|
|
36
|
+
// ~/.config/opencode/opencode.json
|
|
37
|
+
{ "plugin": ["/path/to/add-dir-opencode"] }
|
|
38
|
+
|
|
39
|
+
// ~/.config/opencode/tui.json
|
|
40
|
+
{ "plugin": ["/path/to/add-dir-opencode"] }
|
|
41
|
+
```
|
|
40
42
|
|
|
41
43
|
</details>
|
|
42
44
|
|
|
43
45
|
## Commands
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
All commands are interactive TUI dialogs — type the command and select from autocomplete.
|
|
48
|
+
|
|
49
|
+
| Command | Dialog | Description |
|
|
50
|
+
|---------|--------|-------------|
|
|
51
|
+
| `/add-dir` | Text input + remember checkbox | Add a working directory. Toggle `[x] Remember` with tab to persist across sessions. |
|
|
52
|
+
| `/list-dir` | Alert | Shows all added directories. |
|
|
53
|
+
| `/remove-dir` | Select list + confirm | Pick a directory to remove, then confirm. |
|
|
51
54
|
|
|
52
55
|
## How It Works
|
|
53
56
|
|
|
54
|
-
The plugin
|
|
57
|
+
The plugin has two parts: a **TUI plugin** for the interactive dialogs and a **server plugin** for silent permission handling.
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|-------|------|-------|
|
|
58
|
-
| **Config hook** | Startup | Injects `external_directory: "allow"` rules for persisted dirs into all agents |
|
|
59
|
-
| **Session permission** | `/add-dir` | Sets `external_directory: true` on the current session |
|
|
60
|
-
| **tool.execute.before** | Every file tool | Detects subagent sessions accessing added dirs, grants permission before execution |
|
|
61
|
-
| **Event auto-approve** | Permission popup | Catches any remaining `external_directory` requests and auto-approves via SDK |
|
|
59
|
+
### TUI Plugin
|
|
62
60
|
|
|
63
|
-
|
|
61
|
+
Handles all three slash commands via dialogs. Writes persisted directories to `~/.local/share/opencode/add-dir/directories.json` and grants session permissions via the SDK.
|
|
64
62
|
|
|
65
|
-
|
|
63
|
+
### Server Plugin
|
|
66
64
|
|
|
67
|
-
|
|
65
|
+
Runs in the background — no commands, only hooks:
|
|
68
66
|
|
|
69
|
-
|
|
67
|
+
| Hook | What it does |
|
|
68
|
+
|------|-------------|
|
|
69
|
+
| `config` | Injects `external_directory: "allow"` permission rules for persisted dirs at startup |
|
|
70
|
+
| `tool.execute.before` | Auto-grants permissions when subagents access added directories |
|
|
71
|
+
| `event` | Auto-approves any remaining permission popups for added directories |
|
|
72
|
+
| `system.transform` | Injects `AGENTS.md` / `CLAUDE.md` content from added directories into the system prompt |
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
~/.local/share/opencode/add-dir/directories.json
|
|
73
|
-
```
|
|
74
|
+
### Context Injection
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
If an added directory contains `AGENTS.md`, `CLAUDE.md`, or `.agents/AGENTS.md`, the content is automatically injected into the system prompt.
|
|
76
77
|
|
|
77
78
|
## Development
|
|
78
79
|
|
|
79
80
|
```bash
|
|
80
81
|
bun install
|
|
81
|
-
bun test #
|
|
82
|
+
bun test # Run tests
|
|
82
83
|
bun run typecheck # Type check
|
|
83
84
|
bun run build # Build npm package
|
|
84
|
-
bun run deploy #
|
|
85
|
+
bun run deploy # Build server + TUI locally
|
|
85
86
|
```
|
|
86
87
|
|
|
87
88
|
### Project Structure
|
|
88
89
|
|
|
89
90
|
```
|
|
90
91
|
src/
|
|
91
|
-
├── index.ts
|
|
92
|
-
├── plugin.ts
|
|
93
|
-
├──
|
|
94
|
-
├──
|
|
95
|
-
├──
|
|
96
|
-
├──
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
## Debugging
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
opencode --print-logs 2>debug.log
|
|
104
|
-
grep "\[add-dir\]" debug.log
|
|
92
|
+
├── index.ts # Server plugin entry
|
|
93
|
+
├── plugin.ts # Server hooks (permissions, context injection)
|
|
94
|
+
├── tui-plugin.tsx # TUI plugin (dialogs for add/list/remove)
|
|
95
|
+
├── state.ts # Persistence, path utils, tui.json auto-config
|
|
96
|
+
├── validate.ts # Directory validation
|
|
97
|
+
├── permissions.ts # Session grants + auto-approve
|
|
98
|
+
├── context.ts # AGENTS.md injection
|
|
99
|
+
└── types.ts # Shared type definitions
|
|
105
100
|
```
|
|
106
101
|
|
|
107
102
|
## Limitations
|
|
108
103
|
|
|
109
|
-
- Directories added
|
|
104
|
+
- Directories added without "Remember" rely on session-level permissions. The first access by a subagent may briefly show a permission popup before auto-dismissing.
|
|
110
105
|
- The `permission.ask` plugin hook is defined in the OpenCode SDK but [not invoked](https://github.com/sst/opencode/blob/main/packages/opencode/src/permission/index.ts) in the source — this plugin works around it using `tool.execute.before` and event-based auto-approval.
|
|
111
106
|
|
|
112
107
|
## License
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// src/state.ts
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
function stateDir() {
|
|
5
5
|
return join(process.env["XDG_DATA_HOME"] || join(process.env["HOME"] || "~", ".local", "share"), "opencode", "add-dir");
|
|
6
6
|
}
|
|
7
|
+
var dirsFile = () => join(stateDir(), "directories.json");
|
|
7
8
|
function expandHome(p) {
|
|
8
9
|
return p.startsWith("~/") ? (process.env["HOME"] || "~") + p.slice(1) : p;
|
|
9
10
|
}
|
|
10
11
|
function loadDirs() {
|
|
11
12
|
const dirs = new Map;
|
|
12
|
-
const file =
|
|
13
|
+
const file = dirsFile();
|
|
13
14
|
if (!existsSync(file))
|
|
14
15
|
return dirs;
|
|
15
16
|
try {
|
|
@@ -18,12 +19,22 @@ function loadDirs() {
|
|
|
18
19
|
} catch {}
|
|
19
20
|
return dirs;
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
var cachedDirs;
|
|
23
|
+
var cachedMtime = 0;
|
|
24
|
+
function freshDirs() {
|
|
25
|
+
const file = dirsFile();
|
|
26
|
+
try {
|
|
27
|
+
const mtime = statSync(file).mtimeMs;
|
|
28
|
+
if (cachedDirs && mtime === cachedMtime)
|
|
29
|
+
return cachedDirs;
|
|
30
|
+
cachedMtime = mtime;
|
|
31
|
+
cachedDirs = loadDirs();
|
|
32
|
+
return cachedDirs;
|
|
33
|
+
} catch {
|
|
34
|
+
if (!cachedDirs)
|
|
35
|
+
cachedDirs = new Map;
|
|
36
|
+
return cachedDirs;
|
|
37
|
+
}
|
|
27
38
|
}
|
|
28
39
|
function isChildOf(parent, child) {
|
|
29
40
|
return child === parent || child.startsWith(parent + "/");
|
|
@@ -113,49 +124,13 @@ function ensureTuiConfig() {
|
|
|
113
124
|
} catch {}
|
|
114
125
|
}
|
|
115
126
|
|
|
116
|
-
// src/validate.ts
|
|
117
|
-
import { statSync } from "fs";
|
|
118
|
-
import { resolve } from "path";
|
|
119
|
-
function validateDir(input, worktree, existing) {
|
|
120
|
-
const trimmed = input.trim();
|
|
121
|
-
if (!trimmed)
|
|
122
|
-
return { ok: false, reason: "No directory path provided." };
|
|
123
|
-
const abs = resolve(expandHome(trimmed));
|
|
124
|
-
try {
|
|
125
|
-
if (!statSync(abs).isDirectory())
|
|
126
|
-
return { ok: false, reason: `${abs} is not a directory.` };
|
|
127
|
-
} catch (e) {
|
|
128
|
-
const code = e.code;
|
|
129
|
-
if (code && ["ENOENT", "ENOTDIR", "EACCES", "EPERM"].includes(code))
|
|
130
|
-
return { ok: false, reason: `Path ${abs} was not found.` };
|
|
131
|
-
throw e;
|
|
132
|
-
}
|
|
133
|
-
if (isChildOf(worktree, abs))
|
|
134
|
-
return { ok: false, reason: `${abs} is already within the project directory ${worktree}.` };
|
|
135
|
-
for (const dir of existing)
|
|
136
|
-
if (isChildOf(dir, abs))
|
|
137
|
-
return { ok: false, reason: `${abs} is already accessible within ${dir}.` };
|
|
138
|
-
return { ok: true, absolutePath: abs };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
127
|
// src/permissions.ts
|
|
142
|
-
import { join as join2, resolve
|
|
128
|
+
import { join as join2, resolve } from "path";
|
|
143
129
|
var FILE_TOOLS = new Set(["read", "write", "edit", "apply_patch", "multiedit", "glob", "grep", "list", "bash"]);
|
|
144
130
|
var grantedSessions = new Set;
|
|
145
131
|
function permissionGlob(dirPath) {
|
|
146
132
|
return join2(dirPath, "*");
|
|
147
133
|
}
|
|
148
|
-
function sendPrompt(sdk, sessionID, text, tools) {
|
|
149
|
-
const body = { noReply: true, ...tools && { tools }, parts: [{ type: "text", text }] };
|
|
150
|
-
return sdk.session.promptAsync({ path: { id: sessionID }, body })?.then?.(() => {})?.catch?.(() => {}) ?? Promise.resolve();
|
|
151
|
-
}
|
|
152
|
-
function notify(sdk, sessionID, text) {
|
|
153
|
-
sendPrompt(sdk, sessionID, text);
|
|
154
|
-
}
|
|
155
|
-
function grantSessionAsync(sdk, sessionID, text) {
|
|
156
|
-
grantedSessions.add(sessionID);
|
|
157
|
-
sendPrompt(sdk, sessionID, text, { external_directory: true });
|
|
158
|
-
}
|
|
159
134
|
async function grantSession(sdk, sessionID, text) {
|
|
160
135
|
if (grantedSessions.has(sessionID))
|
|
161
136
|
return;
|
|
@@ -167,7 +142,7 @@ function shouldGrantBeforeTool(dirs, tool, args) {
|
|
|
167
142
|
if (!dirs.size || !FILE_TOOLS.has(tool))
|
|
168
143
|
return false;
|
|
169
144
|
const p = extractPath(tool, args);
|
|
170
|
-
return !!p && matchesDirs(dirs,
|
|
145
|
+
return !!p && matchesDirs(dirs, resolve(expandHome(p)));
|
|
171
146
|
}
|
|
172
147
|
async function autoApprovePermission(sdk, props, dirs) {
|
|
173
148
|
if (props.permission !== "external_directory")
|
|
@@ -214,61 +189,12 @@ ${content}`);
|
|
|
214
189
|
}
|
|
215
190
|
|
|
216
191
|
// src/plugin.ts
|
|
217
|
-
var SENTINEL = Object.assign(new Error("__ADD_DIR_HANDLED__"), { stack: "" });
|
|
218
192
|
ensureTuiConfig();
|
|
219
|
-
var AddDirPlugin = async ({ client
|
|
220
|
-
const root = worktree || directory;
|
|
221
|
-
const dirs = loadDirs();
|
|
193
|
+
var AddDirPlugin = async ({ client }) => {
|
|
222
194
|
const sdk = client;
|
|
223
|
-
function add(dirPath, persist) {
|
|
224
|
-
const result = validateDir(dirPath, root, [...dirs.values()].map((d) => d.path));
|
|
225
|
-
if (!result.ok)
|
|
226
|
-
return { ok: false, message: result.reason };
|
|
227
|
-
dirs.set(result.absolutePath, { path: result.absolutePath, persist });
|
|
228
|
-
if (persist)
|
|
229
|
-
saveDirs(dirs);
|
|
230
|
-
const label = persist ? "persistent" : "session";
|
|
231
|
-
return { ok: true, message: `Added ${result.absolutePath} as a working directory (${label}).` };
|
|
232
|
-
}
|
|
233
|
-
function remove(path) {
|
|
234
|
-
if (!path?.trim())
|
|
235
|
-
return "Usage: /remove-dir <path>";
|
|
236
|
-
if (!dirs.has(path))
|
|
237
|
-
return `${path} is not in the directory list.`;
|
|
238
|
-
dirs.delete(path);
|
|
239
|
-
saveDirs(dirs);
|
|
240
|
-
return `Removed ${path} from working directories.`;
|
|
241
|
-
}
|
|
242
|
-
function list() {
|
|
243
|
-
if (!dirs.size)
|
|
244
|
-
return "No additional directories added.";
|
|
245
|
-
return [...dirs.values()].map((d) => `${d.path} (${d.persist ? "persistent" : "session"})`).join(`
|
|
246
|
-
`);
|
|
247
|
-
}
|
|
248
|
-
function handleAdd(args, sessionID) {
|
|
249
|
-
const tokens = args.trim().split(/\s+/);
|
|
250
|
-
const flags = new Set(tokens.filter((t) => t.startsWith("--")));
|
|
251
|
-
const pos = tokens.filter((t) => !t.startsWith("--"));
|
|
252
|
-
if (!pos[0])
|
|
253
|
-
return notify(sdk, sessionID, "Usage: /add-dir <path> [--remember]");
|
|
254
|
-
const result = add(pos[0], flags.has("--remember"));
|
|
255
|
-
if (result.ok)
|
|
256
|
-
grantSessionAsync(sdk, sessionID, result.message);
|
|
257
|
-
else
|
|
258
|
-
notify(sdk, sessionID, result.message);
|
|
259
|
-
}
|
|
260
|
-
const commands = {
|
|
261
|
-
__adddir: (args, sid) => handleAdd(args, sid),
|
|
262
|
-
"list-dir": (_, sid) => notify(sdk, sid, list()),
|
|
263
|
-
"remove-dir": (args, sid) => notify(sdk, sid, remove(args))
|
|
264
|
-
};
|
|
265
195
|
return {
|
|
266
196
|
config: async (cfg) => {
|
|
267
|
-
|
|
268
|
-
const cmd = cfg.command;
|
|
269
|
-
cmd["__adddir"] = { template: "/__adddir", description: "Internal: add a working directory" };
|
|
270
|
-
cmd["list-dir"] = { template: "/list-dir", description: "List added working directories" };
|
|
271
|
-
cmd["remove-dir"] = { template: "/remove-dir", description: "Remove a working directory" };
|
|
197
|
+
const dirs = freshDirs();
|
|
272
198
|
if (!dirs.size)
|
|
273
199
|
return;
|
|
274
200
|
const perm = cfg.permission ??= {};
|
|
@@ -276,23 +202,20 @@ var AddDirPlugin = async ({ client, worktree, directory }) => {
|
|
|
276
202
|
for (const entry of dirs.values())
|
|
277
203
|
extDir[permissionGlob(entry.path)] = "allow";
|
|
278
204
|
},
|
|
279
|
-
"command.execute.before": async (input) => {
|
|
280
|
-
const handler = commands[input.command];
|
|
281
|
-
if (!handler)
|
|
282
|
-
return;
|
|
283
|
-
handler(input.arguments || "", input.sessionID);
|
|
284
|
-
throw SENTINEL;
|
|
285
|
-
},
|
|
286
205
|
"tool.execute.before": async (input, output) => {
|
|
206
|
+
const dirs = freshDirs();
|
|
287
207
|
if (shouldGrantBeforeTool(dirs, input.tool, output.args))
|
|
288
208
|
await grantSession(sdk, input.sessionID, "Directory access granted by add-dir plugin.");
|
|
289
209
|
},
|
|
290
210
|
event: async ({ event }) => {
|
|
291
211
|
const e = event;
|
|
292
|
-
if (e.type === "permission.asked" && e.properties)
|
|
212
|
+
if (e.type === "permission.asked" && e.properties) {
|
|
213
|
+
const dirs = freshDirs();
|
|
293
214
|
await autoApprovePermission(sdk, e.properties, dirs);
|
|
215
|
+
}
|
|
294
216
|
},
|
|
295
217
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
218
|
+
const dirs = freshDirs();
|
|
296
219
|
output.system.push(...collectAgentContext(dirs));
|
|
297
220
|
}
|
|
298
221
|
};
|
package/dist/permissions.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { DirEntry } from "./state.js";
|
|
2
2
|
import type { SDK, PermissionEvent, ToolArgs } from "./types.js";
|
|
3
3
|
export declare function permissionGlob(dirPath: string): string;
|
|
4
|
-
export declare function notify(sdk: SDK, sessionID: string, text: string): void;
|
|
5
|
-
export declare function grantSessionAsync(sdk: SDK, sessionID: string, text: string): void;
|
|
6
4
|
export declare function grantSession(sdk: SDK, sessionID: string, text: string): Promise<void>;
|
|
7
5
|
export declare function shouldGrantBeforeTool(dirs: Map<string, DirEntry>, tool: string, args: ToolArgs): boolean;
|
|
8
6
|
export declare function autoApprovePermission(sdk: SDK, props: PermissionEvent, dirs: Map<string, DirEntry>): Promise<void>;
|
|
7
|
+
export declare function extractPath(tool: string, args: ToolArgs): string;
|
package/dist/state.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export interface DirEntry {
|
|
|
3
3
|
persist: boolean;
|
|
4
4
|
}
|
|
5
5
|
export declare function expandHome(p: string): string;
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function
|
|
6
|
+
export declare function freshDirs(): Map<string, DirEntry>;
|
|
7
|
+
export declare function invalidateCache(): void;
|
|
8
8
|
export declare function isChildOf(parent: string, child: string): boolean;
|
|
9
9
|
export declare function matchesDirs(dirs: Map<string, DirEntry>, filepath: string): boolean;
|
|
10
10
|
export declare function ensureTuiConfig(): void;
|
package/dist/tui-plugin.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
declare const
|
|
1
|
+
import type { TuiPlugin } from "@opencode-ai/plugin/tui";
|
|
2
|
+
declare const _default: {
|
|
3
3
|
id: string;
|
|
4
|
+
tui: TuiPlugin;
|
|
4
5
|
};
|
|
5
|
-
export default
|
|
6
|
+
export default _default;
|
package/dist/tui.tsx
CHANGED
|
@@ -1,110 +1,158 @@
|
|
|
1
1
|
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
2
|
+
import { useKeyboard } from "@opentui/solid"
|
|
2
3
|
import { createSignal } from "solid-js"
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "fs"
|
|
5
|
+
import { join, resolve } from "path"
|
|
3
6
|
|
|
4
|
-
const
|
|
5
|
-
const
|
|
7
|
+
const ID = "opencode-add-dir"
|
|
8
|
+
const DIRS_FILE = () => join(process.env["XDG_DATA_HOME"] || join(process.env["HOME"] || "~", ".local", "share"), "opencode", "add-dir", "directories.json")
|
|
6
9
|
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
if (route.name !== "session" || !route.params) return
|
|
10
|
-
return route.params.sessionID as string
|
|
10
|
+
function readDirs(): string[] {
|
|
11
|
+
try { return JSON.parse(readFileSync(DIRS_FILE(), "utf-8")) } catch { return [] }
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
14
|
+
function writeDirs(dirs: string[]) {
|
|
15
|
+
const dir = join(DIRS_FILE(), "..")
|
|
16
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
17
|
+
writeFileSync(DIRS_FILE(), JSON.stringify(dirs, null, 2))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolvePath(input: string) {
|
|
21
|
+
const p = input.trim()
|
|
22
|
+
return resolve(p.startsWith("~/") ? (process.env["HOME"] || "~") + p.slice(1) : p)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validate(input: string): string | undefined {
|
|
26
|
+
if (!input.trim()) return "No directory path provided."
|
|
27
|
+
const abs = resolvePath(input)
|
|
28
|
+
try { if (!statSync(abs).isDirectory()) return `${abs} is not a directory.` }
|
|
29
|
+
catch { return `Path ${abs} was not found.` }
|
|
30
|
+
if (readDirs().includes(abs)) return `${abs} is already added.`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sessionID(api: TuiPluginApi): string | undefined {
|
|
34
|
+
const r = api.route.current
|
|
35
|
+
return r.name === "session" && r.params ? r.params.sessionID as string : undefined
|
|
36
|
+
}
|
|
16
37
|
|
|
38
|
+
async function withSession(api: TuiPluginApi): Promise<string | undefined> {
|
|
39
|
+
const id = sessionID(api)
|
|
40
|
+
if (id) return id
|
|
17
41
|
const res = await api.client.session.create({})
|
|
18
42
|
if (res.error) return
|
|
19
|
-
|
|
20
43
|
api.route.navigate("session", { sessionID: res.data.id })
|
|
21
44
|
return res.data.id
|
|
22
45
|
}
|
|
23
46
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
sessionID,
|
|
36
|
-
command: INTERNAL_COMMAND,
|
|
37
|
-
arguments: dirPath,
|
|
47
|
+
type PromptAsyncFn = (params: {
|
|
48
|
+
sessionID: string
|
|
49
|
+
parts: { type: "text"; text: string }[]
|
|
50
|
+
noReply: boolean
|
|
51
|
+
tools: Record<string, boolean>
|
|
52
|
+
}) => Promise<unknown>
|
|
53
|
+
|
|
54
|
+
async function grant(api: TuiPluginApi, sid: string, msg: string) {
|
|
55
|
+
const promptAsync = (api.client.session as unknown as { promptAsync: PromptAsyncFn }).promptAsync
|
|
56
|
+
await promptAsync({
|
|
57
|
+
sessionID: sid, parts: [{ type: "text", text: msg }], noReply: true, tools: { external_directory: true },
|
|
38
58
|
}).catch(() => {})
|
|
39
59
|
}
|
|
40
60
|
|
|
41
61
|
function AddDirDialog(props: { api: TuiPluginApi }) {
|
|
42
62
|
const [busy, setBusy] = createSignal(false)
|
|
63
|
+
const [remember, setRemember] = createSignal(false)
|
|
64
|
+
const { api } = props
|
|
43
65
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
setBusy(true)
|
|
52
|
-
try {
|
|
53
|
-
await executeAddDir(props.api, dirPath)
|
|
54
|
-
props.api.ui.dialog.clear()
|
|
55
|
-
} finally {
|
|
56
|
-
setBusy(false)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
66
|
+
useKeyboard((e) => {
|
|
67
|
+
if (e.name !== "tab" || busy()) return
|
|
68
|
+
e.preventDefault(); e.stopPropagation()
|
|
69
|
+
setRemember((v) => !v)
|
|
70
|
+
})
|
|
59
71
|
|
|
60
72
|
return (
|
|
61
|
-
<
|
|
73
|
+
<api.ui.DialogPrompt
|
|
62
74
|
title="Add directory"
|
|
63
75
|
placeholder="/path/to/directory"
|
|
64
76
|
busy={busy()}
|
|
65
77
|
busyText="Adding directory..."
|
|
66
78
|
description={() => (
|
|
67
|
-
<box gap={
|
|
68
|
-
<
|
|
69
|
-
To get the full path of a project
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
</
|
|
74
|
-
<
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
{
|
|
79
|
-
</
|
|
79
|
+
<box gap={1}>
|
|
80
|
+
<box gap={0}>
|
|
81
|
+
<text fg={api.theme.current.textMuted}>To get the full path of a project:</text>
|
|
82
|
+
<text fg={api.theme.current.textMuted}> 1. Move to the project in your terminal</text>
|
|
83
|
+
<text fg={api.theme.current.textMuted}> 2. Run "pwd" and copy the output</text>
|
|
84
|
+
<text fg={api.theme.current.textMuted}> 3. Paste below</text>
|
|
85
|
+
</box>
|
|
86
|
+
<box flexDirection="row" gap={1}>
|
|
87
|
+
<text fg={remember() ? api.theme.current.text : api.theme.current.textMuted}>
|
|
88
|
+
{remember() ? "[x]" : "[ ]"} Remember across sessions
|
|
89
|
+
</text>
|
|
90
|
+
<text fg={api.theme.current.textMuted}>(tab toggle)</text>
|
|
91
|
+
</box>
|
|
80
92
|
</box>
|
|
81
93
|
)}
|
|
82
|
-
onConfirm={
|
|
83
|
-
|
|
94
|
+
onConfirm={async (value) => {
|
|
95
|
+
const err = validate(value)
|
|
96
|
+
if (err) return api.ui.toast({ variant: "error", message: err })
|
|
97
|
+
|
|
98
|
+
setBusy(true)
|
|
99
|
+
try {
|
|
100
|
+
const sid = await withSession(api)
|
|
101
|
+
if (!sid) return api.ui.toast({ variant: "error", message: "Failed to create session" })
|
|
102
|
+
const abs = resolvePath(value)
|
|
103
|
+
if (remember()) { const d = readDirs(); if (!d.includes(abs)) writeDirs([...d, abs]) }
|
|
104
|
+
await grant(api, sid, `Added ${abs} as a working directory (${remember() ? "persistent" : "session"}).`)
|
|
105
|
+
api.ui.dialog.clear()
|
|
106
|
+
} finally { setBusy(false) }
|
|
107
|
+
}}
|
|
108
|
+
onCancel={() => api.ui.dialog.clear()}
|
|
84
109
|
/>
|
|
85
110
|
)
|
|
86
111
|
}
|
|
87
112
|
|
|
88
|
-
function
|
|
89
|
-
|
|
113
|
+
function listDirs(api: TuiPluginApi) {
|
|
114
|
+
const dirs = readDirs()
|
|
115
|
+
if (!dirs.length) return api.ui.toast({ variant: "info", message: "No directories added." })
|
|
116
|
+
api.ui.dialog.replace(() => (
|
|
117
|
+
<api.ui.DialogAlert
|
|
118
|
+
title={`Directories (${dirs.length})`}
|
|
119
|
+
message={dirs.join("\n")}
|
|
120
|
+
onConfirm={() => api.ui.dialog.clear()}
|
|
121
|
+
/>
|
|
122
|
+
))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function removeDir(api: TuiPluginApi) {
|
|
126
|
+
const dirs = readDirs()
|
|
127
|
+
if (!dirs.length) return api.ui.toast({ variant: "info", message: "No directories to remove." })
|
|
128
|
+
api.ui.dialog.replace(() => (
|
|
129
|
+
<api.ui.DialogSelect
|
|
130
|
+
title="Remove directory"
|
|
131
|
+
options={dirs.map((d) => ({ title: d, value: d }))}
|
|
132
|
+
onSelect={(opt) => {
|
|
133
|
+
api.ui.dialog.replace(() => (
|
|
134
|
+
<api.ui.DialogConfirm
|
|
135
|
+
title="Remove directory"
|
|
136
|
+
message={`Remove ${opt.value}?`}
|
|
137
|
+
onConfirm={() => {
|
|
138
|
+
writeDirs(readDirs().filter((d) => d !== opt.value))
|
|
139
|
+
api.ui.toast({ variant: "success", message: `Removed ${opt.value}` })
|
|
140
|
+
api.ui.dialog.clear()
|
|
141
|
+
}}
|
|
142
|
+
onCancel={() => removeDir(api)}
|
|
143
|
+
/>
|
|
144
|
+
))
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
))
|
|
90
148
|
}
|
|
91
149
|
|
|
92
150
|
const tui: TuiPlugin = async (api) => {
|
|
93
151
|
api.command.register(() => [
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
description: "Add a working directory to the session",
|
|
98
|
-
category: "Directories",
|
|
99
|
-
slash: { name: "add-dir" },
|
|
100
|
-
onSelect: () => showDialog(api),
|
|
101
|
-
},
|
|
152
|
+
{ title: "Add directory", value: "add-dir", description: "Add a working directory", category: "Directories", slash: { name: "add-dir" }, onSelect: () => api.ui.dialog.replace(() => <AddDirDialog api={api} />) },
|
|
153
|
+
{ title: "List directories", value: "list-dir", description: "Show working directories", category: "Directories", slash: { name: "list-dir" }, onSelect: () => listDirs(api) },
|
|
154
|
+
{ title: "Remove directory", value: "remove-dir", description: "Remove a working directory", category: "Directories", slash: { name: "remove-dir" }, onSelect: () => removeDir(api) },
|
|
102
155
|
])
|
|
103
156
|
}
|
|
104
157
|
|
|
105
|
-
|
|
106
|
-
id: PLUGIN_ID,
|
|
107
|
-
tui,
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export default plugin
|
|
158
|
+
export default { id: ID, tui } satisfies TuiPluginModule & { id: string }
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED