@cdoing/cli 0.1.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/.cdoing/permissions.json +8 -0
- package/dist/callbacks.d.ts +17 -0
- package/dist/callbacks.d.ts.map +1 -0
- package/dist/callbacks.js +265 -0
- package/dist/callbacks.js.map +1 -0
- package/dist/chat.d.ts +27 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +57 -0
- package/dist/chat.js.map +1 -0
- package/dist/commands.d.ts +22 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +452 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +84 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +427 -0
- package/dist/config.js.map +1 -0
- package/dist/help.d.ts +9 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +167 -0
- package/dist/help.js.map +1 -0
- package/dist/history.d.ts +51 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +207 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +220 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth.d.ts +13 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +182 -0
- package/dist/oauth.js.map +1 -0
- package/dist/review.d.ts +26 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +198 -0
- package/dist/review.js.map +1 -0
- package/dist/serve.d.ts +23 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +293 -0
- package/dist/serve.js.map +1 -0
- package/dist/tools.d.ts +14 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +57 -0
- package/dist/tools.js.map +1 -0
- package/dist/ui/App.d.ts +24 -0
- package/dist/ui/App.d.ts.map +1 -0
- package/dist/ui/App.js +321 -0
- package/dist/ui/App.js.map +1 -0
- package/dist/ui/MessageList.d.ts +14 -0
- package/dist/ui/MessageList.d.ts.map +1 -0
- package/dist/ui/MessageList.js +147 -0
- package/dist/ui/MessageList.js.map +1 -0
- package/dist/ui/SessionBrowser.d.ts +18 -0
- package/dist/ui/SessionBrowser.d.ts.map +1 -0
- package/dist/ui/SessionBrowser.js +149 -0
- package/dist/ui/SessionBrowser.js.map +1 -0
- package/dist/ui/SetupWizard.d.ts +23 -0
- package/dist/ui/SetupWizard.d.ts.map +1 -0
- package/dist/ui/SetupWizard.js +402 -0
- package/dist/ui/SetupWizard.js.map +1 -0
- package/dist/ui/Spinner.d.ts +15 -0
- package/dist/ui/Spinner.d.ts.map +1 -0
- package/dist/ui/Spinner.js +111 -0
- package/dist/ui/Spinner.js.map +1 -0
- package/dist/ui/StatusBar.d.ts +16 -0
- package/dist/ui/StatusBar.d.ts.map +1 -0
- package/dist/ui/StatusBar.js +56 -0
- package/dist/ui/StatusBar.js.map +1 -0
- package/dist/ui/UserInput.d.ts +13 -0
- package/dist/ui/UserInput.d.ts.map +1 -0
- package/dist/ui/UserInput.js +872 -0
- package/dist/ui/UserInput.js.map +1 -0
- package/dist/ui/hooks/helpers.d.ts +55 -0
- package/dist/ui/hooks/helpers.d.ts.map +1 -0
- package/dist/ui/hooks/helpers.js +304 -0
- package/dist/ui/hooks/helpers.js.map +1 -0
- package/dist/ui/hooks/useAgent.d.ts +60 -0
- package/dist/ui/hooks/useAgent.d.ts.map +1 -0
- package/dist/ui/hooks/useAgent.js +213 -0
- package/dist/ui/hooks/useAgent.js.map +1 -0
- package/dist/ui/hooks/useChat.d.ts +74 -0
- package/dist/ui/hooks/useChat.d.ts.map +1 -0
- package/dist/ui/hooks/useChat.js +819 -0
- package/dist/ui/hooks/useChat.js.map +1 -0
- package/dist/ui/theme.d.ts +73 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +214 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/ui/types.d.ts +37 -0
- package/dist/ui/types.d.ts.map +1 -0
- package/dist/ui/types.js +3 -0
- package/dist/ui/types.js.map +1 -0
- package/package.json +33 -0
- package/src/callbacks.ts +294 -0
- package/src/chat.ts +72 -0
- package/src/commands.ts +425 -0
- package/src/config.ts +462 -0
- package/src/help.ts +182 -0
- package/src/history.ts +205 -0
- package/src/index.ts +248 -0
- package/src/oauth.ts +164 -0
- package/src/review.ts +233 -0
- package/src/serve.ts +290 -0
- package/src/tools.ts +104 -0
- package/src/ui/App.tsx +426 -0
- package/src/ui/MessageList.tsx +222 -0
- package/src/ui/SessionBrowser.tsx +161 -0
- package/src/ui/SetupWizard.tsx +412 -0
- package/src/ui/Spinner.tsx +103 -0
- package/src/ui/StatusBar.tsx +106 -0
- package/src/ui/UserInput.tsx +954 -0
- package/src/ui/hooks/helpers.ts +271 -0
- package/src/ui/hooks/useAgent.ts +270 -0
- package/src/ui/hooks/useChat.ts +943 -0
- package/src/ui/theme.ts +326 -0
- package/src/ui/types.ts +41 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
import React, { useState, useCallback, useRef } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { getTheme } from "./theme";
|
|
6
|
+
|
|
7
|
+
const SLASH_COMMANDS = [
|
|
8
|
+
{ cmd: "/help", desc: "Show help" },
|
|
9
|
+
{ cmd: "/clear", desc: "Clear conversation" },
|
|
10
|
+
{ cmd: "/new", desc: "New conversation" },
|
|
11
|
+
{ cmd: "/ls", desc: "Browse sessions (interactive TUI)" },
|
|
12
|
+
{ cmd: "/history", desc: "List saved conversations (text)" },
|
|
13
|
+
{ cmd: "/resume", desc: "Resume a conversation" },
|
|
14
|
+
{ cmd: "/fork", desc: "Fork current or given conversation" },
|
|
15
|
+
{ cmd: "/delete", desc: "Delete a conversation" },
|
|
16
|
+
{ cmd: "/config", desc: "View/update config" },
|
|
17
|
+
{ cmd: "/model", desc: "Switch model" },
|
|
18
|
+
{ cmd: "/provider", desc: "Switch provider" },
|
|
19
|
+
{ cmd: "/mode", desc: "Change permission mode" },
|
|
20
|
+
{ cmd: "/dir", desc: "Change working directory" },
|
|
21
|
+
{ cmd: "/permissions", desc: "View/clear permissions" },
|
|
22
|
+
{ cmd: "/memory", desc: "View/manage memory" },
|
|
23
|
+
{ cmd: "/hooks", desc: "View configured hooks" },
|
|
24
|
+
{ cmd: "/usage", desc: "Token usage" },
|
|
25
|
+
{ cmd: "/compact", desc: "Compact context" },
|
|
26
|
+
{ cmd: "/tasks", desc: "Show task list" },
|
|
27
|
+
{ cmd: "/plan", desc: "Toggle plan mode" },
|
|
28
|
+
{ cmd: "/effort", desc: "Set analysis depth" },
|
|
29
|
+
{ cmd: "/btw", desc: "Ask without adding to history" },
|
|
30
|
+
{ cmd: "/bg", desc: "Run prompt as background job" },
|
|
31
|
+
{ cmd: "/jobs", desc: "Show background jobs" },
|
|
32
|
+
{ cmd: "/rules", desc: "View project rules" },
|
|
33
|
+
{ cmd: "/mcp", desc: "MCP server status / interactive picker" },
|
|
34
|
+
{ cmd: "/context", desc: "List context providers" },
|
|
35
|
+
{ cmd: "/queue", desc: "Show message queue" },
|
|
36
|
+
{ cmd: "/theme", desc: "Switch theme (dark/light/auto)" },
|
|
37
|
+
{ cmd: "/setup", desc: "View & change provider / model / API key" },
|
|
38
|
+
{ cmd: "/doctor", desc: "Check system health" },
|
|
39
|
+
{ cmd: "/init", desc: "Initialize project" },
|
|
40
|
+
{ cmd: "/login", desc: "Open setup wizard to authenticate" },
|
|
41
|
+
{ cmd: "/logout", desc: "Clear OAuth tokens" },
|
|
42
|
+
{ cmd: "/auth-status", desc: "Show auth status" },
|
|
43
|
+
{ cmd: "/exit", desc: "Quit" },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const AT_PROVIDERS = [
|
|
47
|
+
{ cmd: "@terminal", desc: "Recent terminal output" },
|
|
48
|
+
{ cmd: "@url", desc: "Fetch a URL (@url https://...)" },
|
|
49
|
+
{ cmd: "@tree", desc: "Project file tree" },
|
|
50
|
+
{ cmd: "@codebase", desc: "Full codebase context" },
|
|
51
|
+
{ cmd: "@clip", desc: "Paste clipboard content" },
|
|
52
|
+
{ cmd: "@file", desc: "Include a file's contents (@file src/foo.ts)" },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// Shell commands that always take a path argument (hardcoded core set)
|
|
56
|
+
const PATH_COMMANDS_CORE = new Set([
|
|
57
|
+
"cd", "ls", "ll", "la", "cat", "less", "more", "head", "tail",
|
|
58
|
+
"vim", "vi", "nvim", "nano", "code", "open",
|
|
59
|
+
"cp", "mv", "rm", "mkdir", "rmdir", "touch", "ln", "chmod", "chown",
|
|
60
|
+
"diff", "wc", "file", "stat", "find",
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
// Lazily resolved: all executable names found on $PATH
|
|
64
|
+
let _pathBinaries: Set<string> | null = null;
|
|
65
|
+
function getPathBinaries(): Set<string> {
|
|
66
|
+
if (_pathBinaries) return _pathBinaries;
|
|
67
|
+
_pathBinaries = new Set(PATH_COMMANDS_CORE);
|
|
68
|
+
try {
|
|
69
|
+
const dirs = (process.env.PATH || "").split(path.delimiter).filter(Boolean);
|
|
70
|
+
for (const dir of dirs) {
|
|
71
|
+
try {
|
|
72
|
+
const entries = fs.readdirSync(dir);
|
|
73
|
+
for (const e of entries) _pathBinaries.add(e);
|
|
74
|
+
} catch { /* skip unreadable dir */ }
|
|
75
|
+
}
|
|
76
|
+
} catch { /* ignore */ }
|
|
77
|
+
return _pathBinaries;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Tool / subcommand suggestions ────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
interface SubcmdSuggestion { cmd: string; desc: string; }
|
|
83
|
+
|
|
84
|
+
const TOOL_SUBCOMMANDS: Record<string, SubcmdSuggestion[]> = {
|
|
85
|
+
npm: [
|
|
86
|
+
{ cmd: "install", desc: "Install dependencies" },
|
|
87
|
+
{ cmd: "install -g",desc: "Install package globally" },
|
|
88
|
+
{ cmd: "run", desc: "Run a script" },
|
|
89
|
+
{ cmd: "run dev", desc: "Start dev server" },
|
|
90
|
+
{ cmd: "run build", desc: "Build project" },
|
|
91
|
+
{ cmd: "run test", desc: "Run tests" },
|
|
92
|
+
{ cmd: "run lint", desc: "Lint code" },
|
|
93
|
+
{ cmd: "start", desc: "Start application" },
|
|
94
|
+
{ cmd: "test", desc: "Run tests" },
|
|
95
|
+
{ cmd: "publish", desc: "Publish package" },
|
|
96
|
+
{ cmd: "update", desc: "Update dependencies" },
|
|
97
|
+
{ cmd: "uninstall", desc: "Remove a package" },
|
|
98
|
+
{ cmd: "ls", desc: "List installed packages" },
|
|
99
|
+
{ cmd: "audit", desc: "Security audit" },
|
|
100
|
+
{ cmd: "outdated", desc: "Show outdated packages" },
|
|
101
|
+
{ cmd: "init", desc: "Create package.json" },
|
|
102
|
+
{ cmd: "ci", desc: "Clean install" },
|
|
103
|
+
],
|
|
104
|
+
npx: [
|
|
105
|
+
{ cmd: "create-react-app", desc: "Create React app" },
|
|
106
|
+
{ cmd: "create-next-app", desc: "Create Next.js app" },
|
|
107
|
+
{ cmd: "tsc", desc: "TypeScript compiler" },
|
|
108
|
+
{ cmd: "eslint", desc: "Run ESLint" },
|
|
109
|
+
{ cmd: "prettier", desc: "Format code" },
|
|
110
|
+
],
|
|
111
|
+
yarn: [
|
|
112
|
+
{ cmd: "install", desc: "Install dependencies" },
|
|
113
|
+
{ cmd: "add", desc: "Add a package" },
|
|
114
|
+
{ cmd: "add -D", desc: "Add dev dependency" },
|
|
115
|
+
{ cmd: "remove", desc: "Remove a package" },
|
|
116
|
+
{ cmd: "run dev", desc: "Start dev server" },
|
|
117
|
+
{ cmd: "run build", desc: "Build project" },
|
|
118
|
+
{ cmd: "run test", desc: "Run tests" },
|
|
119
|
+
{ cmd: "upgrade", desc: "Upgrade packages" },
|
|
120
|
+
{ cmd: "workspace", desc: "Run command in workspace" },
|
|
121
|
+
{ cmd: "workspaces",desc: "Run command in all workspaces" },
|
|
122
|
+
],
|
|
123
|
+
pnpm: [
|
|
124
|
+
{ cmd: "install", desc: "Install dependencies" },
|
|
125
|
+
{ cmd: "add", desc: "Add a package" },
|
|
126
|
+
{ cmd: "add -D", desc: "Add dev dependency" },
|
|
127
|
+
{ cmd: "remove", desc: "Remove a package" },
|
|
128
|
+
{ cmd: "run dev", desc: "Start dev server" },
|
|
129
|
+
{ cmd: "run build", desc: "Build project" },
|
|
130
|
+
{ cmd: "run test", desc: "Run tests" },
|
|
131
|
+
],
|
|
132
|
+
git: [
|
|
133
|
+
{ cmd: "status", desc: "Show working tree status" },
|
|
134
|
+
{ cmd: "add .", desc: "Stage all changes" },
|
|
135
|
+
{ cmd: "add -p", desc: "Interactive staging" },
|
|
136
|
+
{ cmd: "commit -m", desc: "Commit with message" },
|
|
137
|
+
{ cmd: "push", desc: "Push to remote" },
|
|
138
|
+
{ cmd: "pull", desc: "Pull from remote" },
|
|
139
|
+
{ cmd: "checkout", desc: "Switch branch / restore file" },
|
|
140
|
+
{ cmd: "checkout -b", desc: "Create and switch branch" },
|
|
141
|
+
{ cmd: "branch", desc: "List / create branches" },
|
|
142
|
+
{ cmd: "branch -d", desc: "Delete branch" },
|
|
143
|
+
{ cmd: "merge", desc: "Merge branch" },
|
|
144
|
+
{ cmd: "rebase", desc: "Rebase onto branch" },
|
|
145
|
+
{ cmd: "log --oneline", desc: "Compact commit log" },
|
|
146
|
+
{ cmd: "diff", desc: "Show unstaged diff" },
|
|
147
|
+
{ cmd: "diff --staged", desc: "Show staged diff" },
|
|
148
|
+
{ cmd: "stash", desc: "Stash changes" },
|
|
149
|
+
{ cmd: "stash pop", desc: "Apply stashed changes" },
|
|
150
|
+
{ cmd: "fetch", desc: "Fetch from remote" },
|
|
151
|
+
{ cmd: "remote -v", desc: "List remotes" },
|
|
152
|
+
{ cmd: "clone", desc: "Clone repository" },
|
|
153
|
+
{ cmd: "init", desc: "Initialize repository" },
|
|
154
|
+
{ cmd: "reset --hard", desc: "Discard all local changes" },
|
|
155
|
+
{ cmd: "cherry-pick", desc: "Apply specific commit" },
|
|
156
|
+
{ cmd: "tag", desc: "Create / list tags" },
|
|
157
|
+
],
|
|
158
|
+
gh: [
|
|
159
|
+
{ cmd: "pr create", desc: "Create pull request" },
|
|
160
|
+
{ cmd: "pr list", desc: "List pull requests" },
|
|
161
|
+
{ cmd: "pr checkout", desc: "Check out pull request" },
|
|
162
|
+
{ cmd: "pr merge", desc: "Merge pull request" },
|
|
163
|
+
{ cmd: "issue create", desc: "Create issue" },
|
|
164
|
+
{ cmd: "issue list", desc: "List issues" },
|
|
165
|
+
{ cmd: "repo clone", desc: "Clone repository" },
|
|
166
|
+
{ cmd: "run list", desc: "List workflow runs" },
|
|
167
|
+
{ cmd: "run view", desc: "View workflow run" },
|
|
168
|
+
{ cmd: "auth login", desc: "Authenticate with GitHub" },
|
|
169
|
+
],
|
|
170
|
+
python: [
|
|
171
|
+
{ cmd: "-m venv", desc: "Create virtual environment" },
|
|
172
|
+
{ cmd: "-m pip install", desc: "Install packages" },
|
|
173
|
+
{ cmd: "-m pip freeze", desc: "List installed packages" },
|
|
174
|
+
{ cmd: "-m pytest", desc: "Run tests" },
|
|
175
|
+
{ cmd: "-m http.server", desc: "Start HTTP server" },
|
|
176
|
+
{ cmd: "-c", desc: "Execute inline code" },
|
|
177
|
+
],
|
|
178
|
+
python3: [
|
|
179
|
+
{ cmd: "-m venv", desc: "Create virtual environment" },
|
|
180
|
+
{ cmd: "-m pip install", desc: "Install packages" },
|
|
181
|
+
{ cmd: "-m pip freeze", desc: "List installed packages" },
|
|
182
|
+
{ cmd: "-m pytest", desc: "Run tests" },
|
|
183
|
+
{ cmd: "-m http.server", desc: "Start HTTP server" },
|
|
184
|
+
{ cmd: "-c", desc: "Execute inline code" },
|
|
185
|
+
],
|
|
186
|
+
pip: [
|
|
187
|
+
{ cmd: "install", desc: "Install package" },
|
|
188
|
+
{ cmd: "install -r requirements.txt", desc: "Install from requirements" },
|
|
189
|
+
{ cmd: "uninstall", desc: "Uninstall package" },
|
|
190
|
+
{ cmd: "list", desc: "List packages" },
|
|
191
|
+
{ cmd: "freeze", desc: "Output installed packages" },
|
|
192
|
+
{ cmd: "show", desc: "Show package info" },
|
|
193
|
+
{ cmd: "search", desc: "Search PyPI" },
|
|
194
|
+
],
|
|
195
|
+
pip3: [
|
|
196
|
+
{ cmd: "install", desc: "Install package" },
|
|
197
|
+
{ cmd: "install -r requirements.txt", desc: "Install from requirements" },
|
|
198
|
+
{ cmd: "uninstall", desc: "Uninstall package" },
|
|
199
|
+
{ cmd: "list", desc: "List packages" },
|
|
200
|
+
{ cmd: "freeze", desc: "Output installed packages" },
|
|
201
|
+
],
|
|
202
|
+
docker: [
|
|
203
|
+
{ cmd: "ps", desc: "List running containers" },
|
|
204
|
+
{ cmd: "ps -a", desc: "List all containers" },
|
|
205
|
+
{ cmd: "images", desc: "List images" },
|
|
206
|
+
{ cmd: "build -t", desc: "Build image" },
|
|
207
|
+
{ cmd: "run", desc: "Run container" },
|
|
208
|
+
{ cmd: "run -it", desc: "Run interactive container" },
|
|
209
|
+
{ cmd: "exec -it", desc: "Exec into container" },
|
|
210
|
+
{ cmd: "stop", desc: "Stop container" },
|
|
211
|
+
{ cmd: "rm", desc: "Remove container" },
|
|
212
|
+
{ cmd: "rmi", desc: "Remove image" },
|
|
213
|
+
{ cmd: "pull", desc: "Pull image" },
|
|
214
|
+
{ cmd: "push", desc: "Push image" },
|
|
215
|
+
{ cmd: "logs", desc: "Show container logs" },
|
|
216
|
+
{ cmd: "compose up", desc: "Start compose services" },
|
|
217
|
+
{ cmd: "compose down", desc: "Stop compose services" },
|
|
218
|
+
{ cmd: "compose build", desc: "Build compose services" },
|
|
219
|
+
],
|
|
220
|
+
kubectl: [
|
|
221
|
+
{ cmd: "get pods", desc: "List pods" },
|
|
222
|
+
{ cmd: "get services", desc: "List services" },
|
|
223
|
+
{ cmd: "get deployments", desc: "List deployments" },
|
|
224
|
+
{ cmd: "describe pod", desc: "Describe pod" },
|
|
225
|
+
{ cmd: "apply -f", desc: "Apply config file" },
|
|
226
|
+
{ cmd: "delete -f", desc: "Delete from config file" },
|
|
227
|
+
{ cmd: "logs", desc: "Print pod logs" },
|
|
228
|
+
{ cmd: "exec -it", desc: "Exec into pod" },
|
|
229
|
+
{ cmd: "port-forward", desc: "Forward port" },
|
|
230
|
+
{ cmd: "rollout status", desc: "Check rollout status" },
|
|
231
|
+
{ cmd: "scale", desc: "Scale deployment" },
|
|
232
|
+
],
|
|
233
|
+
cargo: [
|
|
234
|
+
{ cmd: "build", desc: "Build package" },
|
|
235
|
+
{ cmd: "build --release", desc: "Build release" },
|
|
236
|
+
{ cmd: "run", desc: "Run binary" },
|
|
237
|
+
{ cmd: "test", desc: "Run tests" },
|
|
238
|
+
{ cmd: "add", desc: "Add dependency" },
|
|
239
|
+
{ cmd: "check", desc: "Type-check without build" },
|
|
240
|
+
{ cmd: "fmt", desc: "Format code" },
|
|
241
|
+
{ cmd: "clippy", desc: "Run linter" },
|
|
242
|
+
{ cmd: "update", desc: "Update dependencies" },
|
|
243
|
+
{ cmd: "publish", desc: "Publish crate" },
|
|
244
|
+
],
|
|
245
|
+
go: [
|
|
246
|
+
{ cmd: "run", desc: "Run Go program" },
|
|
247
|
+
{ cmd: "build", desc: "Build package" },
|
|
248
|
+
{ cmd: "test", desc: "Run tests" },
|
|
249
|
+
{ cmd: "test ./...", desc: "Run all tests" },
|
|
250
|
+
{ cmd: "mod tidy", desc: "Clean up modules" },
|
|
251
|
+
{ cmd: "mod init", desc: "Initialize module" },
|
|
252
|
+
{ cmd: "get", desc: "Add dependency" },
|
|
253
|
+
{ cmd: "fmt", desc: "Format code" },
|
|
254
|
+
{ cmd: "vet", desc: "Vet code" },
|
|
255
|
+
{ cmd: "install", desc: "Install package" },
|
|
256
|
+
],
|
|
257
|
+
turbo: [
|
|
258
|
+
{ cmd: "run build", desc: "Build all packages" },
|
|
259
|
+
{ cmd: "run dev", desc: "Dev all packages" },
|
|
260
|
+
{ cmd: "run test", desc: "Test all packages" },
|
|
261
|
+
{ cmd: "run lint", desc: "Lint all packages" },
|
|
262
|
+
{ cmd: "prune", desc: "Prune for deployment" },
|
|
263
|
+
],
|
|
264
|
+
bun: [
|
|
265
|
+
{ cmd: "install", desc: "Install dependencies" },
|
|
266
|
+
{ cmd: "add", desc: "Add a package" },
|
|
267
|
+
{ cmd: "add -d", desc: "Add dev dependency" },
|
|
268
|
+
{ cmd: "remove", desc: "Remove a package" },
|
|
269
|
+
{ cmd: "run dev", desc: "Start dev server" },
|
|
270
|
+
{ cmd: "run build", desc: "Build project" },
|
|
271
|
+
{ cmd: "run test", desc: "Run tests" },
|
|
272
|
+
{ cmd: "test", desc: "Run tests (built-in)" },
|
|
273
|
+
{ cmd: "init", desc: "Create package.json" },
|
|
274
|
+
{ cmd: "create", desc: "Create from template" },
|
|
275
|
+
{ cmd: "upgrade", desc: "Upgrade packages" },
|
|
276
|
+
],
|
|
277
|
+
deno: [
|
|
278
|
+
{ cmd: "run", desc: "Run a script" },
|
|
279
|
+
{ cmd: "test", desc: "Run tests" },
|
|
280
|
+
{ cmd: "fmt", desc: "Format code" },
|
|
281
|
+
{ cmd: "lint", desc: "Lint code" },
|
|
282
|
+
{ cmd: "compile", desc: "Compile to executable" },
|
|
283
|
+
{ cmd: "task", desc: "Run a task" },
|
|
284
|
+
{ cmd: "install", desc: "Install dependency" },
|
|
285
|
+
{ cmd: "check", desc: "Type-check" },
|
|
286
|
+
],
|
|
287
|
+
make: [
|
|
288
|
+
{ cmd: "all", desc: "Build all targets" },
|
|
289
|
+
{ cmd: "clean", desc: "Clean build artifacts" },
|
|
290
|
+
{ cmd: "install", desc: "Install" },
|
|
291
|
+
{ cmd: "test", desc: "Run tests" },
|
|
292
|
+
{ cmd: "build", desc: "Build" },
|
|
293
|
+
],
|
|
294
|
+
cat: [
|
|
295
|
+
{ cmd: "-n", desc: "Show line numbers" },
|
|
296
|
+
],
|
|
297
|
+
ls: [
|
|
298
|
+
{ cmd: "-la", desc: "Long format, show hidden" },
|
|
299
|
+
{ cmd: "-lh", desc: "Long format, human sizes" },
|
|
300
|
+
{ cmd: "-R", desc: "Recursive listing" },
|
|
301
|
+
{ cmd: "-t", desc: "Sort by time" },
|
|
302
|
+
],
|
|
303
|
+
grep: [
|
|
304
|
+
{ cmd: "-r", desc: "Recursive search" },
|
|
305
|
+
{ cmd: "-rn", desc: "Recursive with line numbers" },
|
|
306
|
+
{ cmd: "-i", desc: "Case insensitive" },
|
|
307
|
+
{ cmd: "-l", desc: "Files with matches only" },
|
|
308
|
+
{ cmd: "-c", desc: "Count matches" },
|
|
309
|
+
],
|
|
310
|
+
find: [
|
|
311
|
+
{ cmd: ". -name", desc: "Find by name" },
|
|
312
|
+
{ cmd: ". -type f", desc: "Find files only" },
|
|
313
|
+
{ cmd: ". -type d", desc: "Find directories only" },
|
|
314
|
+
{ cmd: ". -mtime", desc: "Find by modification time" },
|
|
315
|
+
],
|
|
316
|
+
curl: [
|
|
317
|
+
{ cmd: "-X GET", desc: "GET request" },
|
|
318
|
+
{ cmd: "-X POST", desc: "POST request" },
|
|
319
|
+
{ cmd: "-H", desc: "Add header" },
|
|
320
|
+
{ cmd: "-d", desc: "Send data" },
|
|
321
|
+
{ cmd: "-o", desc: "Save to file" },
|
|
322
|
+
{ cmd: "-s", desc: "Silent mode" },
|
|
323
|
+
{ cmd: "-v", desc: "Verbose output" },
|
|
324
|
+
],
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// ── Path helpers ─────────────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
interface PathEntry {
|
|
330
|
+
name: string;
|
|
331
|
+
isDir: boolean;
|
|
332
|
+
/** The full replacement value for the input (e.g. "cd packages/") */
|
|
333
|
+
full: string;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function readPathEntries(partial: string, workingDir: string, cmdPrefix: string): PathEntry[] {
|
|
337
|
+
try {
|
|
338
|
+
const resolved = path.resolve(workingDir, partial);
|
|
339
|
+
const isDir = partial.endsWith("/") || partial === "";
|
|
340
|
+
const dir = isDir ? resolved : path.dirname(resolved);
|
|
341
|
+
const prefix = isDir ? "" : path.basename(resolved).toLowerCase();
|
|
342
|
+
|
|
343
|
+
if (!fs.existsSync(dir)) return [];
|
|
344
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
345
|
+
return entries
|
|
346
|
+
.filter((e) => !e.name.startsWith(".") || prefix.startsWith("."))
|
|
347
|
+
.filter((e) => prefix === "" || e.name.toLowerCase().startsWith(prefix))
|
|
348
|
+
.slice(0, 40)
|
|
349
|
+
.map((e) => {
|
|
350
|
+
const rel = path.join(path.relative(workingDir, dir), e.name);
|
|
351
|
+
const suffix = e.isDirectory() ? "/" : "";
|
|
352
|
+
return {
|
|
353
|
+
name: e.name + suffix,
|
|
354
|
+
isDir: e.isDirectory(),
|
|
355
|
+
full: cmdPrefix + rel + suffix,
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
} catch {
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
function getProjectFiles(workingDir: string, partial: string): { cmd: string; desc: string }[] {
|
|
365
|
+
const results: { cmd: string; desc: string }[] = [];
|
|
366
|
+
const IGNORE = new Set(["node_modules", "dist", ".git", ".next", "build", ".turbo", "coverage"]);
|
|
367
|
+
|
|
368
|
+
function walk(dir: string, prefix: string, depth: number) {
|
|
369
|
+
if (depth > 2) return;
|
|
370
|
+
try {
|
|
371
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
372
|
+
for (const e of entries) {
|
|
373
|
+
if (IGNORE.has(e.name)) continue;
|
|
374
|
+
if (e.name.startsWith(".") && !partial.startsWith(".")) continue;
|
|
375
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
376
|
+
const matchStr = rel.toLowerCase();
|
|
377
|
+
const partialLower = partial.toLowerCase();
|
|
378
|
+
if (!partial || matchStr.includes(partialLower) || e.name.toLowerCase().startsWith(partialLower)) {
|
|
379
|
+
results.push({
|
|
380
|
+
cmd: "@file " + rel + (e.isDirectory() ? "/" : ""),
|
|
381
|
+
desc: e.isDirectory() ? "dir" : "file",
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (e.isDirectory()) walk(path.join(dir, e.name), rel, depth + 1);
|
|
385
|
+
if (results.length >= 30) return;
|
|
386
|
+
}
|
|
387
|
+
} catch {}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
walk(workingDir, "", 0);
|
|
391
|
+
return results.slice(0, 20);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function getFirstPathCompletion(partial: string, workingDir: string): string {
|
|
395
|
+
try {
|
|
396
|
+
const resolved = path.resolve(workingDir, partial);
|
|
397
|
+
const isDir = partial.endsWith("/") || partial === "";
|
|
398
|
+
const dir = isDir ? resolved : path.dirname(resolved);
|
|
399
|
+
const prefix = isDir ? "" : path.basename(resolved).toLowerCase();
|
|
400
|
+
if (!fs.existsSync(dir)) return "";
|
|
401
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
402
|
+
const match = entries.find(
|
|
403
|
+
(e) => !e.name.startsWith(".") && (prefix === "" || e.name.toLowerCase().startsWith(prefix)),
|
|
404
|
+
);
|
|
405
|
+
if (!match) return "";
|
|
406
|
+
const rel = path.join(path.relative(workingDir, dir), match.name);
|
|
407
|
+
return rel + (match.isDirectory() ? "/" : "");
|
|
408
|
+
} catch {
|
|
409
|
+
return "";
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── Ghost suggestion ──────────────────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
interface GhostResult { suffix: string; full: string; }
|
|
416
|
+
|
|
417
|
+
function computeGhost(line: string, history: string[], workingDir: string): GhostResult | null {
|
|
418
|
+
if (!line) return null;
|
|
419
|
+
|
|
420
|
+
for (const h of history) {
|
|
421
|
+
if (h.startsWith(line) && h !== line) return { suffix: h.slice(line.length), full: h };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (line.startsWith("/")) {
|
|
425
|
+
const match = SLASH_COMMANDS.find((c) => c.cmd.startsWith(line) && c.cmd !== line);
|
|
426
|
+
if (match) return { suffix: match.cmd.slice(line.length), full: match.cmd };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const atMatch = line.match(/@(\S*)$/);
|
|
430
|
+
if (atMatch) {
|
|
431
|
+
const atToken = "@" + atMatch[1];
|
|
432
|
+
const fileArgMatch = line.match(/@file\s+(\S*)$/);
|
|
433
|
+
if (fileArgMatch) {
|
|
434
|
+
const completed = getFirstPathCompletion(fileArgMatch[1], workingDir);
|
|
435
|
+
if (completed) {
|
|
436
|
+
const full = line.replace(/@file\s+\S*$/, "@file " + completed);
|
|
437
|
+
return { suffix: full.slice(line.length), full };
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
const match = AT_PROVIDERS.find((p) => p.cmd.startsWith(atToken) && p.cmd !== atToken);
|
|
442
|
+
if (match) {
|
|
443
|
+
const full = line.replace(/@\S*$/, match.cmd);
|
|
444
|
+
return { suffix: full.slice(line.length), full };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const shellPathMatch = line.match(/^(!?\w[\w\-]*\s+)(\S+)$/);
|
|
449
|
+
if (shellPathMatch) {
|
|
450
|
+
const prefix = shellPathMatch[1];
|
|
451
|
+
const partial = shellPathMatch[2];
|
|
452
|
+
const completed = getFirstPathCompletion(partial, workingDir);
|
|
453
|
+
if (completed && completed !== partial) {
|
|
454
|
+
const full = prefix + completed;
|
|
455
|
+
return { suffix: full.slice(line.length), full };
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Detect if the current input is a shell command that wants path completions.
|
|
464
|
+
* Returns { cmdPrefix, partial } or null.
|
|
465
|
+
*/
|
|
466
|
+
function detectPathContext(line: string): { cmdPrefix: string; partial: string } | null {
|
|
467
|
+
// Match: (optional !) + command + space + optional partial path
|
|
468
|
+
const m = line.match(/^(!?)(\w[\w\-]*)(\s+)(\S*)$/);
|
|
469
|
+
if (!m) {
|
|
470
|
+
// "cd " with trailing space, no path yet
|
|
471
|
+
const m2 = line.match(/^(!?)(\w[\w\-]*)(\s+)$/);
|
|
472
|
+
if (m2) {
|
|
473
|
+
const cmd = m2[2].toLowerCase();
|
|
474
|
+
if (PATH_COMMANDS_CORE.has(cmd) || getPathBinaries().has(cmd)) {
|
|
475
|
+
return { cmdPrefix: m2[1] + m2[2] + m2[3], partial: "" };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
const cmd = m[2].toLowerCase();
|
|
481
|
+
// Only show path completions for core path commands or binaries on $PATH
|
|
482
|
+
// that are NOT tools with their own subcommand suggestions
|
|
483
|
+
if (TOOL_SUBCOMMANDS[cmd]) return null;
|
|
484
|
+
if (!PATH_COMMANDS_CORE.has(cmd) && !getPathBinaries().has(cmd)) return null;
|
|
485
|
+
return { cmdPrefix: m[1] + m[2] + m[3], partial: m[4] };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ── PathMenu component ────────────────────────────────────────────────────────
|
|
489
|
+
|
|
490
|
+
interface PathMenuProps {
|
|
491
|
+
entries: PathEntry[];
|
|
492
|
+
selectedIdx: number;
|
|
493
|
+
label: string;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const PathMenu: React.FC<PathMenuProps> = ({ entries, selectedIdx, label }) => {
|
|
497
|
+
const t = getTheme();
|
|
498
|
+
const termWidth = process.stdout.columns || 100;
|
|
499
|
+
const padWidth = 2;
|
|
500
|
+
|
|
501
|
+
const maxName = Math.max(...entries.map((e) => e.name.length), 4);
|
|
502
|
+
const colWidth = Math.min(maxName + padWidth, Math.floor(termWidth / 2));
|
|
503
|
+
const numCols = Math.max(1, Math.floor((termWidth - 4) / colWidth));
|
|
504
|
+
|
|
505
|
+
const rows: PathEntry[][] = [];
|
|
506
|
+
for (let i = 0; i < entries.length; i += numCols) {
|
|
507
|
+
rows.push(entries.slice(i, i + numCols));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return (
|
|
511
|
+
<Box flexDirection="column" paddingLeft={2}>
|
|
512
|
+
<Text dimColor={t.useDim} color={t.textDim}>{label}</Text>
|
|
513
|
+
{rows.map((row, rowIdx) => (
|
|
514
|
+
<Box key={rowIdx} flexDirection="row">
|
|
515
|
+
{row.map((e, colIdx) => {
|
|
516
|
+
const globalIdx = rowIdx * numCols + colIdx;
|
|
517
|
+
const isSelected = globalIdx === selectedIdx;
|
|
518
|
+
const padded = e.name.padEnd(colWidth);
|
|
519
|
+
return isSelected ? (
|
|
520
|
+
<Text key={e.full} backgroundColor={t.selectedBg} color={t.selected === "white" ? "black" : t.selected}>{padded}</Text>
|
|
521
|
+
) : (
|
|
522
|
+
<Text key={e.full} color={e.isDir ? t.accent : t.text}>{padded}</Text>
|
|
523
|
+
);
|
|
524
|
+
})}
|
|
525
|
+
</Box>
|
|
526
|
+
))}
|
|
527
|
+
<Text dimColor={t.useDim} color={t.textDim}>{"Tab=cycle →=accept ESC=close"}</Text>
|
|
528
|
+
</Box>
|
|
529
|
+
);
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
// ── Suggestion icon helpers ───────────────────────────────────────────────────
|
|
533
|
+
|
|
534
|
+
function getSuggestionColor(s: { cmd: string; desc: string }): string {
|
|
535
|
+
const t = getTheme();
|
|
536
|
+
if (s.cmd.startsWith("@file")) return t.suggestionFile;
|
|
537
|
+
if (s.cmd.startsWith("@")) return t.suggestionProvider;
|
|
538
|
+
const toolMatch = s.cmd.match(/^!?(\w[\w-]*)\s/);
|
|
539
|
+
if (toolMatch && TOOL_SUBCOMMANDS[toolMatch[1].toLowerCase()]) return t.suggestionTool;
|
|
540
|
+
return t.suggestionDefault;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ── Component ─────────────────────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
interface UserInputProps {
|
|
546
|
+
isProcessing: boolean;
|
|
547
|
+
queueLength: number;
|
|
548
|
+
workingDir: string;
|
|
549
|
+
permissionMode: string;
|
|
550
|
+
onSubmit: (value: string) => void;
|
|
551
|
+
onCancel: () => void;
|
|
552
|
+
onModeChange: (mode: string) => void;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export const UserInput: React.FC<UserInputProps> = ({
|
|
556
|
+
isProcessing,
|
|
557
|
+
queueLength: _queueLength,
|
|
558
|
+
workingDir,
|
|
559
|
+
permissionMode,
|
|
560
|
+
onSubmit,
|
|
561
|
+
onCancel,
|
|
562
|
+
onModeChange,
|
|
563
|
+
}) => {
|
|
564
|
+
const [input, setInput] = useState("");
|
|
565
|
+
const [history, setHistory] = useState<string[]>([]);
|
|
566
|
+
const [historyIdx, setHistoryIdx] = useState(-1);
|
|
567
|
+
|
|
568
|
+
// Vertical dropdown for /commands and @providers
|
|
569
|
+
const [suggestions, setSuggestions] = useState<{ cmd: string; desc: string }[]>([]);
|
|
570
|
+
const [selectedSuggestion, setSelectedSuggestion] = useState(-1);
|
|
571
|
+
|
|
572
|
+
// Horizontal zsh-style path menu
|
|
573
|
+
const [pathEntries, setPathEntries] = useState<PathEntry[]>([]);
|
|
574
|
+
const [selectedPath, setSelectedPath] = useState(0);
|
|
575
|
+
const [pathContext, setPathContext] = useState<{ cmdPrefix: string; partial: string } | null>(null);
|
|
576
|
+
|
|
577
|
+
// Inline ghost
|
|
578
|
+
const [ghost, setGhost] = useState<GhostResult | null>(null);
|
|
579
|
+
|
|
580
|
+
// Counter for clipboard image placeholders
|
|
581
|
+
const imageCountRef = useRef(0);
|
|
582
|
+
|
|
583
|
+
const clearAll = () => {
|
|
584
|
+
setSuggestions([]);
|
|
585
|
+
setSelectedSuggestion(-1);
|
|
586
|
+
setPathEntries([]);
|
|
587
|
+
setSelectedPath(0);
|
|
588
|
+
setPathContext(null);
|
|
589
|
+
setGhost(null);
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const updateAll = useCallback((line: string, hist: string[]) => {
|
|
593
|
+
// ── /commands dropdown ──
|
|
594
|
+
if (line.startsWith("/")) {
|
|
595
|
+
const matches = SLASH_COMMANDS.filter((c) => c.cmd.startsWith(line) && c.cmd !== line);
|
|
596
|
+
setSuggestions(matches);
|
|
597
|
+
setSelectedSuggestion(0);
|
|
598
|
+
setPathEntries([]);
|
|
599
|
+
setPathContext(null);
|
|
600
|
+
setGhost(computeGhost(line, hist, workingDir));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ── @provider / file dropdown ──
|
|
605
|
+
const atMatch = line.match(/@(\S*)$/);
|
|
606
|
+
if (atMatch) {
|
|
607
|
+
const partial = atMatch[1]; // everything after @
|
|
608
|
+
|
|
609
|
+
// Match providers
|
|
610
|
+
const providerMatches = AT_PROVIDERS.filter((p) =>
|
|
611
|
+
partial === "" || p.cmd.slice(1).toLowerCase().startsWith(partial.toLowerCase())
|
|
612
|
+
);
|
|
613
|
+
// Project files — displayed as @path (inserted as @file path)
|
|
614
|
+
const fileMatches = getProjectFiles(workingDir, partial);
|
|
615
|
+
|
|
616
|
+
const combined = [...providerMatches, ...fileMatches].slice(0, 50);
|
|
617
|
+
setSuggestions(combined);
|
|
618
|
+
setSelectedSuggestion(0);
|
|
619
|
+
setPathEntries([]);
|
|
620
|
+
setPathContext(null);
|
|
621
|
+
setGhost(null);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ── Tool subcommand suggestions (npm, git, python, docker…) ──
|
|
626
|
+
// Matches: "npm " or "!git " or "npm ins" etc.
|
|
627
|
+
const toolMatch = line.match(/^!?(\w[\w-]*)(\s+)(\S*)$/);
|
|
628
|
+
if (toolMatch) {
|
|
629
|
+
const tool = toolMatch[1].toLowerCase();
|
|
630
|
+
const subcmdPartial = toolMatch[3].toLowerCase();
|
|
631
|
+
const subcmds = TOOL_SUBCOMMANDS[tool];
|
|
632
|
+
if (subcmds) {
|
|
633
|
+
const matches = subcmds.filter((s) =>
|
|
634
|
+
subcmdPartial === "" || s.cmd.toLowerCase().startsWith(subcmdPartial)
|
|
635
|
+
);
|
|
636
|
+
if (matches.length > 0) {
|
|
637
|
+
// Prefix each suggestion with the tool name so acceptSuggestion inserts correctly
|
|
638
|
+
const prefixed = matches.map((s) => ({
|
|
639
|
+
cmd: (line.startsWith("!") ? "!" : "") + tool + " " + s.cmd,
|
|
640
|
+
desc: s.desc,
|
|
641
|
+
}));
|
|
642
|
+
setSuggestions(prefixed.slice(0, 10));
|
|
643
|
+
setSelectedSuggestion(0);
|
|
644
|
+
setPathEntries([]);
|
|
645
|
+
setPathContext(null);
|
|
646
|
+
setGhost(null);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ── Path completion for shell commands (cd, ls, vim…) ──
|
|
653
|
+
const pc = detectPathContext(line);
|
|
654
|
+
if (pc) {
|
|
655
|
+
const entries = readPathEntries(pc.partial, workingDir, pc.cmdPrefix);
|
|
656
|
+
setSuggestions([]);
|
|
657
|
+
setSelectedSuggestion(-1);
|
|
658
|
+
setPathEntries(entries);
|
|
659
|
+
setSelectedPath(0);
|
|
660
|
+
setPathContext(pc);
|
|
661
|
+
setGhost(computeGhost(line, hist, workingDir));
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
setSuggestions([]);
|
|
666
|
+
setSelectedSuggestion(-1);
|
|
667
|
+
setPathEntries([]);
|
|
668
|
+
setPathContext(null);
|
|
669
|
+
setGhost(computeGhost(line, hist, workingDir));
|
|
670
|
+
}, [workingDir]);
|
|
671
|
+
|
|
672
|
+
const acceptSuggestion = useCallback((chosen: { cmd: string; desc: string }, submit = false) => {
|
|
673
|
+
let newVal: string;
|
|
674
|
+
if (chosen.cmd.startsWith("@")) {
|
|
675
|
+
newVal = input.replace(/@\S*$/, chosen.cmd);
|
|
676
|
+
// directories keep the "/" but no space — continue drilling in
|
|
677
|
+
if (!newVal.endsWith("/")) newVal += " ";
|
|
678
|
+
} else {
|
|
679
|
+
newVal = chosen.cmd + " ";
|
|
680
|
+
}
|
|
681
|
+
setInput(newVal);
|
|
682
|
+
if (submit) {
|
|
683
|
+
setHistory((h) => [newVal.trim(), ...h].slice(0, 200));
|
|
684
|
+
setHistoryIdx(-1);
|
|
685
|
+
setInput("");
|
|
686
|
+
clearAll();
|
|
687
|
+
onSubmit(newVal.trim());
|
|
688
|
+
} else {
|
|
689
|
+
// re-run updateAll so directory drilldown populates new suggestions
|
|
690
|
+
updateAll(newVal, history);
|
|
691
|
+
}
|
|
692
|
+
}, [input, history, workingDir, onSubmit, updateAll]);
|
|
693
|
+
|
|
694
|
+
useInput((char, key) => {
|
|
695
|
+
// Ctrl+L — clear screen
|
|
696
|
+
if (key.ctrl && char === "l") {
|
|
697
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Ctrl+V — paste from clipboard (text or image placeholder)
|
|
702
|
+
if (key.ctrl && char === "v") {
|
|
703
|
+
try {
|
|
704
|
+
const { execSync } = require("child_process") as typeof import("child_process");
|
|
705
|
+
// macOS: pbpaste, Linux: xclip -o or xsel -ob
|
|
706
|
+
let pasted = "";
|
|
707
|
+
try {
|
|
708
|
+
pasted = execSync("pbpaste", { encoding: "utf-8", timeout: 500 }).trim();
|
|
709
|
+
} catch {
|
|
710
|
+
try {
|
|
711
|
+
pasted = execSync("xclip -o -selection clipboard", { encoding: "utf-8", timeout: 500 }).trim();
|
|
712
|
+
} catch {
|
|
713
|
+
try {
|
|
714
|
+
pasted = execSync("xsel -ob", { encoding: "utf-8", timeout: 500 }).trim();
|
|
715
|
+
} catch { /* no clipboard tool */ }
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (pasted) {
|
|
720
|
+
// Replace newlines with spaces for single-line input
|
|
721
|
+
const cleaned = pasted.replace(/\n/g, " ").replace(/\r/g, "");
|
|
722
|
+
const next = input + cleaned;
|
|
723
|
+
setInput(next);
|
|
724
|
+
updateAll(next, history);
|
|
725
|
+
} else {
|
|
726
|
+
// Clipboard might contain an image — insert placeholder
|
|
727
|
+
imageCountRef.current += 1;
|
|
728
|
+
const placeholder = `[Image #${imageCountRef.current}]`;
|
|
729
|
+
const next = input + placeholder;
|
|
730
|
+
setInput(next);
|
|
731
|
+
updateAll(next, history);
|
|
732
|
+
}
|
|
733
|
+
} catch { /* skip on any error */ }
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Shift+Tab — cycle permission mode
|
|
738
|
+
if (char === "\x1b[Z") {
|
|
739
|
+
const modes = ["ask", "auto-edit", "auto"];
|
|
740
|
+
const idx = modes.indexOf(permissionMode);
|
|
741
|
+
const next = modes[(idx + 1) % modes.length];
|
|
742
|
+
onModeChange(next);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// ESC
|
|
747
|
+
if (key.escape) {
|
|
748
|
+
if (suggestions.length > 0 || pathEntries.length > 0) { clearAll(); return; }
|
|
749
|
+
if (ghost) { setGhost(null); return; }
|
|
750
|
+
if (isProcessing) onCancel();
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// ── Path menu navigation — Tab cycles, → accepts, Enter always submits ──
|
|
755
|
+
if (pathEntries.length > 0) {
|
|
756
|
+
if (key.tab) {
|
|
757
|
+
const next = (selectedPath + 1) % pathEntries.length;
|
|
758
|
+
setSelectedPath(next);
|
|
759
|
+
const entry = pathEntries[next];
|
|
760
|
+
setGhost({ suffix: entry.full.slice(input.length), full: entry.full });
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (key.rightArrow || (char === "\x05")) {
|
|
764
|
+
// → accepts the highlighted entry into the input
|
|
765
|
+
const entry = pathEntries[selectedPath];
|
|
766
|
+
if (entry) {
|
|
767
|
+
setInput(entry.full);
|
|
768
|
+
setPathEntries([]);
|
|
769
|
+
setPathContext(null);
|
|
770
|
+
setGhost(null);
|
|
771
|
+
if (entry.isDir) updateAll(entry.full, history);
|
|
772
|
+
}
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (key.upArrow) {
|
|
776
|
+
setSelectedPath((p) => (p - 1 + pathEntries.length) % pathEntries.length);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (key.downArrow) {
|
|
780
|
+
setSelectedPath((p) => (p + 1) % pathEntries.length);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
// Enter falls through to the normal submit logic below
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// → accept ghost
|
|
787
|
+
if ((key.rightArrow || (char === "\x05")) && ghost && suggestions.length === 0 && pathEntries.length === 0) {
|
|
788
|
+
setInput(ghost.full);
|
|
789
|
+
setGhost(computeGhost(ghost.full, history, workingDir));
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Tab — dropdown or ghost
|
|
794
|
+
if (key.tab) {
|
|
795
|
+
if (suggestions.length > 0) {
|
|
796
|
+
const idx = selectedSuggestion >= 0 ? selectedSuggestion : 0;
|
|
797
|
+
const chosen = suggestions[idx];
|
|
798
|
+
if (chosen) acceptSuggestion(chosen);
|
|
799
|
+
} else if (ghost) {
|
|
800
|
+
setInput(ghost.full);
|
|
801
|
+
setSuggestions([]);
|
|
802
|
+
setGhost(computeGhost(ghost.full, history, workingDir));
|
|
803
|
+
}
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Dropdown navigation
|
|
808
|
+
if (suggestions.length > 0) {
|
|
809
|
+
if (key.upArrow) {
|
|
810
|
+
setSelectedSuggestion((s) => (s <= 0 ? suggestions.length - 1 : s - 1));
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
if (key.downArrow) {
|
|
814
|
+
setSelectedSuggestion((s) => (s >= suggestions.length - 1 ? 0 : s + 1));
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (key.return) {
|
|
818
|
+
const chosen = suggestions[selectedSuggestion >= 0 ? selectedSuggestion : 0];
|
|
819
|
+
if (chosen) acceptSuggestion(chosen, chosen.cmd.startsWith("/"));
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
} else {
|
|
823
|
+
// History navigation
|
|
824
|
+
if (key.upArrow) {
|
|
825
|
+
const nextIdx = Math.min(historyIdx + 1, history.length - 1);
|
|
826
|
+
setHistoryIdx(nextIdx);
|
|
827
|
+
const val = history[nextIdx] || "";
|
|
828
|
+
setInput(val);
|
|
829
|
+
updateAll(val, history);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (key.downArrow) {
|
|
833
|
+
const nextIdx = Math.max(historyIdx - 1, -1);
|
|
834
|
+
setHistoryIdx(nextIdx);
|
|
835
|
+
const val = nextIdx >= 0 ? history[nextIdx] : "";
|
|
836
|
+
setInput(val);
|
|
837
|
+
updateAll(val, history);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Enter — submit
|
|
843
|
+
if (key.return) {
|
|
844
|
+
const trimmed = input.trim();
|
|
845
|
+
if (!trimmed) return;
|
|
846
|
+
const newHistory = [trimmed, ...history].slice(0, 200);
|
|
847
|
+
clearAll();
|
|
848
|
+
setHistory(newHistory);
|
|
849
|
+
setHistoryIdx(-1);
|
|
850
|
+
setInput("");
|
|
851
|
+
onSubmit(trimmed);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Backspace
|
|
856
|
+
if (key.backspace || key.delete) {
|
|
857
|
+
const next = input.slice(0, -1);
|
|
858
|
+
setInput(next);
|
|
859
|
+
updateAll(next, history);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Regular character
|
|
864
|
+
if (char && !key.ctrl && !key.meta) {
|
|
865
|
+
const next = input + char;
|
|
866
|
+
setInput(next);
|
|
867
|
+
updateAll(next, history);
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
// Compute windowed suggestions (used in render)
|
|
872
|
+
const WINDOW = 8;
|
|
873
|
+
const sel = selectedSuggestion >= 0 ? selectedSuggestion : 0;
|
|
874
|
+
const windowStart = suggestions.length > 0
|
|
875
|
+
? Math.max(0, Math.min(sel - Math.floor(WINDOW / 2), suggestions.length - WINDOW))
|
|
876
|
+
: 0;
|
|
877
|
+
const windowEnd = Math.min(windowStart + WINDOW, suggestions.length);
|
|
878
|
+
const visibleSuggestions = suggestions.slice(windowStart, windowEnd);
|
|
879
|
+
|
|
880
|
+
const t = getTheme();
|
|
881
|
+
|
|
882
|
+
return (
|
|
883
|
+
<Box flexDirection="column">
|
|
884
|
+
|
|
885
|
+
{/* zsh-style path menu — outside the border, above input */}
|
|
886
|
+
{pathEntries.length > 0 ? (
|
|
887
|
+
<PathMenu
|
|
888
|
+
entries={pathEntries}
|
|
889
|
+
selectedIdx={selectedPath}
|
|
890
|
+
label={pathContext?.partial === "" || pathContext?.partial == null ? "directory" : "matches"}
|
|
891
|
+
/>
|
|
892
|
+
) : null}
|
|
893
|
+
|
|
894
|
+
{/* Main bordered box — contains dropdown (when open) + input line */}
|
|
895
|
+
<Box borderStyle="round" borderColor={t.border} flexDirection="column" paddingLeft={1} paddingRight={1}>
|
|
896
|
+
|
|
897
|
+
{/* Dropdown lives INSIDE the border so no extra border line appears below it */}
|
|
898
|
+
{suggestions.length > 0 ? (
|
|
899
|
+
<Box flexDirection="column">
|
|
900
|
+
{/* ▲ more above indicator */}
|
|
901
|
+
{windowStart > 0 ? (
|
|
902
|
+
<Text dimColor={t.useDim} color={t.textDim}>{` ▲ ${windowStart} more`}</Text>
|
|
903
|
+
) : null}
|
|
904
|
+
|
|
905
|
+
{visibleSuggestions.map((s, vi) => {
|
|
906
|
+
const gi = windowStart + vi;
|
|
907
|
+
const isSelected = gi === sel;
|
|
908
|
+
const display = s.cmd.startsWith("@file ") ? "@" + s.cmd.slice(6) : s.cmd;
|
|
909
|
+
const color = getSuggestionColor(s);
|
|
910
|
+
return isSelected ? (
|
|
911
|
+
<Box key={s.cmd}>
|
|
912
|
+
<Text color={t.selected} bold>{` ${display}`}</Text>
|
|
913
|
+
{s.desc && s.desc !== "file" && s.desc !== "dir" ? (
|
|
914
|
+
<Text color={t.textDim}>{` ${s.desc}`}</Text>
|
|
915
|
+
) : null}
|
|
916
|
+
</Box>
|
|
917
|
+
) : (
|
|
918
|
+
<Box key={s.cmd}>
|
|
919
|
+
<Text color={color} dimColor={t.useDim}>{` ${display}`}</Text>
|
|
920
|
+
</Box>
|
|
921
|
+
);
|
|
922
|
+
})}
|
|
923
|
+
|
|
924
|
+
{/* ▼ more below indicator */}
|
|
925
|
+
{windowEnd < suggestions.length ? (
|
|
926
|
+
<Text dimColor={t.useDim} color={t.textDim}>{` ▼ ${suggestions.length - windowEnd} more`}</Text>
|
|
927
|
+
) : null}
|
|
928
|
+
|
|
929
|
+
{/* hint + position counter */}
|
|
930
|
+
<Text dimColor={t.useDim} color={t.textDim}>{` ↑/↓ navigate Enter select Esc close ${sel + 1}/${suggestions.length}`}</Text>
|
|
931
|
+
</Box>
|
|
932
|
+
) : null}
|
|
933
|
+
|
|
934
|
+
{/* Input line */}
|
|
935
|
+
<Box>
|
|
936
|
+
<Text color={t.accent}>{"● "}</Text>
|
|
937
|
+
{input.length > 0 ? <Text>{input}</Text> : null}
|
|
938
|
+
<Text color={t.cursor}>{"▊"}</Text>
|
|
939
|
+
{input.length === 0 ? (
|
|
940
|
+
<Text color={t.placeholder} dimColor={t.useDim}>{"Ask anything, @ for context, / for commands, ! for shell"}</Text>
|
|
941
|
+
) : ghost && suggestions.length === 0 && pathEntries.length === 0 ? (
|
|
942
|
+
<Text color={t.placeholder} dimColor={t.useDim}>{ghost.suffix}</Text>
|
|
943
|
+
) : null}
|
|
944
|
+
</Box>
|
|
945
|
+
</Box>
|
|
946
|
+
|
|
947
|
+
{/* Keyboard hints below input */}
|
|
948
|
+
<Box paddingLeft={2}>
|
|
949
|
+
<Text color={t.accent} dimColor={t.useDim}>{"Ctrl+V paste · Ctrl+L clear · Shift+Tab cycle mode"}</Text>
|
|
950
|
+
</Box>
|
|
951
|
+
|
|
952
|
+
</Box>
|
|
953
|
+
);
|
|
954
|
+
};
|