woopcode 0.1.1 → 0.3.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.
@@ -1,20 +1,13 @@
1
1
  import { Command } from "commander";
2
- import {
3
- buildRepositoryContext,
4
- getConfig,
5
- getConversation,
6
- saveConversation,
7
- } from "../config/config";
8
- import { createProviderClient } from "../config/client";
9
- import type { Message } from "../config/types";
2
+ import { getConfig } from "../config/config";
10
3
  import type { AgentCallbacks } from "../config/types";
11
- import { agentLoop } from "../config/runtime";
12
4
  import { App, store } from "../tui/src";
13
5
  import { render } from "ink";
14
6
  import { AgentController } from "./agentController";
15
7
  import { ACTIVE_PROVIDER_MODELS } from "../config/client";
16
8
  import type { HomeScreenData } from "../tui/src/components/HomeScreen";
17
9
  import { ensureProviderConfigured } from "../onboarding";
10
+ import { registerCommands } from "./slash";
18
11
 
19
12
  export const agentCommand = new Command("agent")
20
13
  .description("Runs the agent")
@@ -23,6 +16,9 @@ export const agentCommand = new Command("agent")
23
16
 
24
17
  /** Runs the interactive agent from either `woopcode` or `woopcode agent`. */
25
18
  export async function runAgent() {
19
+ // Register slash commands
20
+ registerCommands();
21
+
26
22
  // Ensure provider is configured (launches onboarding if needed)
27
23
  await ensureProviderConfigured();
28
24
 
@@ -32,48 +28,48 @@ export async function runAgent() {
32
28
  const apiKey = config.providers[provider].apiKey;
33
29
 
34
30
  const callbacks: AgentCallbacks = {
35
- onStatus(status) {
36
- if (cancelStatusTimeout) {
37
- clearTimeout(cancelStatusTimeout);
38
- cancelStatusTimeout = undefined;
39
- }
40
- store.setStatus(status);
41
- },
42
-
43
- onToolStart(tool) {
44
- store.finishAssistantMessage();
45
- store.startTool(tool);
46
- store.setStatus(`Running ${tool.name}...`);
47
- },
48
-
49
- onToolFinish(tool) {
50
- store.finishTool(tool.id);
51
- store.setStatus("Thinking...");
52
- },
53
-
54
- onText(text) {
55
- store.appendAssistantText(text);
56
- },
57
-
58
- onDone() {
59
- //console.log("onDone received");
60
- store.finishAssistantMessage();
31
+ onStatus(status) {
32
+ if (cancelStatusTimeout) {
33
+ clearTimeout(cancelStatusTimeout);
34
+ cancelStatusTimeout = undefined;
35
+ }
36
+ store.setStatus(status);
37
+ },
38
+
39
+ onToolStart(tool) {
40
+ store.finishAssistantMessage();
41
+ store.startTool(tool);
42
+ store.setStatus(`Running ${tool.name}...`);
43
+ },
44
+
45
+ onToolFinish(tool) {
46
+ store.finishTool(tool.id);
47
+ store.setStatus("Thinking...");
48
+ },
49
+
50
+ onText(text) {
51
+ store.appendAssistantText(text);
52
+ },
53
+
54
+ onDone() {
55
+ //console.log("onDone received");
56
+ store.finishAssistantMessage();
57
+ store.setStatus("Ready");
58
+ },
59
+
60
+ onError(error) {
61
+ store.finishAssistantMessage();
62
+ store.setStatus(`Error: ${error.message}`);
63
+ },
64
+
65
+ onCancel() {
66
+ store.finishAssistantMessage();
67
+ store.setStatus("Cancelled");
68
+
69
+ cancelStatusTimeout = setTimeout(() => {
61
70
  store.setStatus("Ready");
62
- },
63
-
64
- onError(error) {
65
- store.finishAssistantMessage();
66
- store.setStatus(`Error: ${error.message}`);
67
- },
68
-
69
- onCancel() {
70
- store.finishAssistantMessage();
71
- store.setStatus("Cancelled");
72
-
73
- cancelStatusTimeout = setTimeout(() => {
74
- store.setStatus("Ready");
75
- }, 1000);
76
- },
71
+ }, 1000);
72
+ },
77
73
  };
78
74
  const controller = new AgentController(provider, apiKey, callbacks);
79
75
  await controller.initialize();
@@ -102,7 +98,8 @@ export async function runAgent() {
102
98
  }
103
99
 
104
100
  async function buildHomeScreen(provider: string): Promise<HomeScreenData> {
105
- const repository = process.cwd().split("/").filter(Boolean).at(-1) ?? "workspace";
101
+ const repository =
102
+ process.cwd().split("/").filter(Boolean).at(-1) ?? "workspace";
106
103
  const branch = await getBranch();
107
104
  const providerLabel = provider === "google" ? "Gemini" : titleCase(provider);
108
105
 
@@ -117,7 +114,15 @@ async function buildHomeScreen(provider: string): Promise<HomeScreenData> {
117
114
  "Optimize performance",
118
115
  "Add authentication",
119
116
  ],
120
- capabilities: ["Build", "Review", "Explain", "Refactor", "Debug", "Test", "Document"],
117
+ capabilities: [
118
+ "Build",
119
+ "Review",
120
+ "Explain",
121
+ "Refactor",
122
+ "Debug",
123
+ "Test",
124
+ "Document",
125
+ ],
121
126
  repository,
122
127
  branch,
123
128
  providerName: providerLabel,
@@ -0,0 +1,114 @@
1
+ # Slash Commands
2
+
3
+ Local commands that execute instantly without invoking the LLM.
4
+
5
+ ## Available Commands
6
+
7
+ ### Session Commands
8
+
9
+ - **`/new`** (aliases: `clear`, `reset`) - Start a new conversation
10
+ - **`/exit`** (aliases: `quit`, `q`) - Exit Woopcode
11
+
12
+ ### Configuration Commands
13
+
14
+ - **`/provider [name]`** (alias: `p`) - Show current provider or switch to another
15
+ - Without arguments: Shows current provider and all available providers
16
+ - With provider name: Switches to the specified provider (if configured)
17
+
18
+ - **`/login <provider> <api-key>`** - Login to a provider from within the app
19
+ - Example: `/login google YOUR_API_KEY`
20
+ - Validates the API key before saving
21
+ - Sets the provider as active
22
+
23
+ - **`/logout [provider]`** - Logout from a provider
24
+ - Without arguments: Logs out from current provider
25
+ - With provider name: Logs out from specified provider
26
+ - Automatically switches to another logged-in provider if available
27
+
28
+ - **`/model`** (alias: `m`) - Show current model and available models
29
+ - Currently read-only; model switching will be added in future versions
30
+
31
+ ### Workspace Commands
32
+
33
+ - **`/workspace`** (alias: `ws`) - Show workspace information
34
+ - Displays: workspace name, path, git branch, file count
35
+
36
+ - **`/status`** (alias: `info`) - Show comprehensive system status
37
+ - Displays: workspace info, provider, model, conversation stats, tools, version
38
+
39
+ ### Other Commands
40
+
41
+ - **`/help`** (aliases: `h`, `?`) - Show all available commands
42
+ - **`/version`** (alias: `v`) - Show Woopcode version
43
+
44
+ ## Discovery Mode
45
+
46
+ Type `/` alone to see a quick list of all available commands.
47
+
48
+ ## Architecture
49
+
50
+ The slash command system consists of:
51
+
52
+ - **Parser** (`parser.ts`) - Parses user input into commands and arguments
53
+ - **Registry** (`registry.ts`) - Manages command registration and lookup
54
+ - **Handler** (`handler.ts`) - Executes commands and handles errors
55
+ - **Commands** (`commands.ts`) - All command implementations
56
+
57
+ ### Adding New Commands
58
+
59
+ 1. Define command in `commands.ts`:
60
+
61
+ ```typescript
62
+ const myCommand: SlashCommand = {
63
+ name: "mycommand",
64
+ aliases: ["mc", "mycmd"],
65
+ description: "Does something useful",
66
+ category: "other", // session | configuration | workspace | other
67
+
68
+ async execute(context, args) {
69
+ // Implementation
70
+ return "Command output";
71
+ }
72
+ };
73
+ ```
74
+
75
+ 2. Register in `registerCommands()`:
76
+
77
+ ```typescript
78
+ export function registerCommands() {
79
+ // ... existing commands
80
+ registry.register(myCommand);
81
+ }
82
+ ```
83
+
84
+ That's it! The command will automatically appear in `/help` and support tab completion.
85
+
86
+ ## Integration
87
+
88
+ Slash commands are intercepted in `tui/src/prompt.tsx` before being sent to the agent:
89
+
90
+ ```typescript
91
+ const result = await handleSlashCommand(prompt, context);
92
+
93
+ if (result.handled) {
94
+ // Command was executed, don't send to LLM
95
+ return;
96
+ }
97
+
98
+ // Otherwise continue to agent
99
+ await controller.run(prompt);
100
+ ```
101
+
102
+ ## Testing
103
+
104
+ Tests are located in `packages/tests/slash/`:
105
+
106
+ - `parser.test.ts` - Parser logic
107
+ - `registry.test.ts` - Command registry
108
+ - `handler.test.ts` - Command execution
109
+
110
+ Run tests:
111
+
112
+ ```bash
113
+ bun test packages/tests/slash/
114
+ ```
@@ -0,0 +1,328 @@
1
+ import type { SlashCommand, SlashCommandContext } from "./types";
2
+ import { registry } from "./registry";
3
+ import {
4
+ getConfig,
5
+ saveConfig,
6
+ getConversation,
7
+ saveConversation,
8
+ } from "../../config/config";
9
+
10
+ // Read version from package.json
11
+ const packageJsonPath = `${import.meta.dir}/../../package.json`;
12
+ const packageJson = await Bun.file(packageJsonPath).json();
13
+ const version = packageJson.version as string;
14
+
15
+ // Read models from models.json
16
+ const modelsJsonPath = `${import.meta.dir}/../../config/models.json`;
17
+ const modelsData = await Bun.file(modelsJsonPath).json();
18
+ const models = modelsData as Array<{
19
+ id: string;
20
+ provider: string;
21
+ name: string;
22
+ contextWindow: number | string;
23
+ }>;
24
+
25
+ // ==================== SESSION COMMANDS ====================
26
+
27
+ const helpCommand: SlashCommand = {
28
+ name: "help",
29
+ aliases: ["h", "?"],
30
+ description: "Show available commands",
31
+ category: "other",
32
+
33
+ async execute(context, args) {
34
+ return registry.generateHelp();
35
+ },
36
+ };
37
+
38
+ const newCommand: SlashCommand = {
39
+ name: "new",
40
+ aliases: ["clear", "reset"],
41
+ description: "Start a new conversation",
42
+ category: "session",
43
+
44
+ async execute(context, args) {
45
+ await saveConversation([]);
46
+ return "Started new conversation";
47
+ },
48
+ };
49
+
50
+ const exitCommand: SlashCommand = {
51
+ name: "exit",
52
+ aliases: ["quit", "q"],
53
+ description: "Exit Woopcode",
54
+ category: "session",
55
+
56
+ async execute(context, args) {
57
+ await context.onExit();
58
+ return "Exiting...";
59
+ },
60
+ };
61
+
62
+ // ==================== CONFIGURATION COMMANDS ====================
63
+
64
+ const providerCommand: SlashCommand = {
65
+ name: "provider",
66
+ aliases: ["p"],
67
+ description: "Show or switch provider",
68
+ category: "configuration",
69
+ usage: "/provider [provider-name]",
70
+
71
+ async execute(context, args) {
72
+ const config = await getConfig();
73
+
74
+ if (args.length === 0) {
75
+ const current = config.defaultProvider;
76
+ const providers = Object.entries(config.providers)
77
+ .map(([name, details]: [string, any]) => {
78
+ const status = details.apiKey ? "✓" : "✗";
79
+ const active = name === current ? "(active)" : "";
80
+ return ` ${status} ${name} ${active}`;
81
+ })
82
+ .join("\n");
83
+
84
+ return `Current Provider: ${current}\n\nAvailable:\n${providers}\n\nTip: Use /login or /logout to manage authentication`;
85
+ }
86
+
87
+ const newProvider = args[0];
88
+ if (!newProvider) {
89
+ return `Provider name required.\nUsage: /provider <provider-name>`;
90
+ }
91
+
92
+ const available = Object.keys(config.providers);
93
+
94
+ if (!available.includes(newProvider)) {
95
+ return `Provider "${newProvider}" not found.\nAvailable: ${available.join(", ")}`;
96
+ }
97
+
98
+ const providerConfig = config.providers[newProvider];
99
+ if (!providerConfig || !providerConfig.apiKey) {
100
+ return `Provider "${newProvider}" not configured.\nUse: /login ${newProvider} <api-key>`;
101
+ }
102
+
103
+ config.defaultProvider = newProvider;
104
+ await saveConfig(config);
105
+
106
+ return `Switched to: ${newProvider}`;
107
+ },
108
+ };
109
+
110
+ const modelCommand: SlashCommand = {
111
+ name: "model",
112
+ aliases: ["m"],
113
+ description: "Show current model and available models",
114
+ category: "configuration",
115
+
116
+ async execute(context, args) {
117
+ const config = await getConfig();
118
+ const provider = config.defaultProvider;
119
+
120
+ // Show current model (read-only for now)
121
+ let output = `Current Model: gemini-3.5-flash-lite\n\n`;
122
+
123
+ // List available models for current provider
124
+ const providerModels = models.filter((m) => m.provider === provider);
125
+
126
+ if (providerModels.length > 0) {
127
+ output += `Available models for ${provider}:\n`;
128
+ providerModels.forEach((m) => {
129
+ output += ` ${m.id} - ${m.name}\n`;
130
+ });
131
+ }
132
+
133
+ return output.trim();
134
+ },
135
+ };
136
+
137
+ const loginCommand: SlashCommand = {
138
+ name: "login",
139
+ description: "Login to a provider",
140
+ category: "configuration",
141
+ usage: "/login <provider> <api-key>",
142
+
143
+ async execute(context, args) {
144
+ if (args.length < 2) {
145
+ return `Usage: /login <provider> <api-key>\nExample: /login google YOUR_API_KEY`;
146
+ }
147
+
148
+ const provider = args[0];
149
+ const apiKey = args.slice(1).join(" "); // Allow API keys with spaces
150
+
151
+ const config = await getConfig();
152
+
153
+ if (!config.providers[provider]) {
154
+ const available = Object.keys(config.providers);
155
+ return `Unknown provider "${provider}".\nAvailable: ${available.join(", ")}`;
156
+ }
157
+
158
+ // Validate API key
159
+ const { loginProvider } = await import("../../config/authProvider");
160
+ const isValid = await loginProvider(provider, apiKey);
161
+
162
+ if (!isValid) {
163
+ return `Invalid API key for ${provider}.\nPlease check your API key and try again.`;
164
+ }
165
+
166
+ // Save the API key
167
+ config.providers[provider].apiKey = apiKey;
168
+ config.defaultProvider = provider;
169
+ await saveConfig(config);
170
+
171
+ return `Successfully logged in to ${provider}!\nThis is now your active provider.`;
172
+ },
173
+ };
174
+
175
+ const logoutCommand: SlashCommand = {
176
+ name: "logout",
177
+ description: "Logout from a provider",
178
+ category: "configuration",
179
+ usage: "/logout [provider]",
180
+
181
+ async execute(context, args) {
182
+ const config = await getConfig();
183
+
184
+ // If no provider specified, logout from current
185
+ const provider = args[0] || config.defaultProvider;
186
+
187
+ if (!config.providers[provider]) {
188
+ const available = Object.keys(config.providers);
189
+ return `Unknown provider "${provider}".\nAvailable: ${available.join(", ")}`;
190
+ }
191
+
192
+ const providerConfig = config.providers[provider];
193
+ if (!providerConfig?.apiKey) {
194
+ return `Already logged out from ${provider}.`;
195
+ }
196
+
197
+ // Remove API key
198
+ delete config.providers[provider].apiKey;
199
+
200
+ // If logging out from default provider, clear default
201
+ if (config.defaultProvider === provider) {
202
+ // Find another logged-in provider
203
+ const otherProvider = Object.entries(config.providers).find(
204
+ ([name, details]: [string, any]) => name !== provider && details.apiKey
205
+ );
206
+
207
+ if (otherProvider) {
208
+ config.defaultProvider = otherProvider[0];
209
+ } else {
210
+ config.defaultProvider = "";
211
+ }
212
+ }
213
+
214
+ await saveConfig(config);
215
+
216
+ const nextProvider = config.defaultProvider
217
+ ? `\nActive provider: ${config.defaultProvider}`
218
+ : "\nNo providers logged in. Use /login to authenticate.";
219
+
220
+ return `Logged out from ${provider}.${nextProvider}`;
221
+ },
222
+ };
223
+
224
+ // ==================== WORKSPACE COMMANDS ====================
225
+
226
+ const workspaceCommand: SlashCommand = {
227
+ name: "workspace",
228
+ aliases: ["ws"],
229
+ description: "Show workspace information",
230
+ category: "workspace",
231
+
232
+ async execute(context, args) {
233
+ const cwd = process.cwd();
234
+ const parts = cwd.split("/").filter(Boolean);
235
+ const repoName = parts[parts.length - 1] ?? "unknown";
236
+
237
+ let branch = "not a git repository";
238
+ try {
239
+ branch =
240
+ (await Bun.$`git branch --show-current`.text()).trim() || "detached";
241
+ } catch {}
242
+
243
+ let fileCount = 0;
244
+ try {
245
+ for await (const entry of new Bun.Glob("**/*").scan(cwd)) {
246
+ if (
247
+ !entry.startsWith("node_modules") &&
248
+ !entry.startsWith(".git") &&
249
+ !entry.startsWith("dist")
250
+ ) {
251
+ fileCount++;
252
+ }
253
+ }
254
+ } catch {}
255
+
256
+ return [
257
+ `Workspace: ${repoName}`,
258
+ `Path: ${cwd}`,
259
+ `Branch: ${branch}`,
260
+ `Files: ${fileCount > 0 ? fileCount : "counting..."}`,
261
+ ].join("\n");
262
+ },
263
+ };
264
+
265
+ const statusCommand: SlashCommand = {
266
+ name: "status",
267
+ aliases: ["info"],
268
+ description: "Show comprehensive system status",
269
+ category: "other",
270
+
271
+ async execute(context, args) {
272
+ const config = await getConfig();
273
+ const conversation = await getConversation();
274
+ const cwd = process.cwd();
275
+ const parts = cwd.split("/").filter(Boolean);
276
+ const repoName = parts[parts.length - 1] ?? "workspace";
277
+
278
+ let branch = "not a git repository";
279
+ try {
280
+ branch =
281
+ (await Bun.$`git branch --show-current`.text()).trim() || "detached";
282
+ } catch {}
283
+
284
+ const provider = config.defaultProvider;
285
+ const providerLabel = provider === "google" ? "Google Gemini" : provider;
286
+
287
+ return [
288
+ `Workspace: ${repoName}`,
289
+ `Path: ${cwd}`,
290
+ `Branch: ${branch}`,
291
+ ``,
292
+ `Provider: ${providerLabel}`,
293
+ `Model: gemini-3.5-flash-lite`,
294
+ ``,
295
+ `Conversation: ${conversation.length} messages`,
296
+ `Tools: 9 registered`,
297
+ `Version: ${version}`,
298
+ ].join("\n");
299
+ },
300
+ };
301
+
302
+ // ==================== OTHER COMMANDS ====================
303
+
304
+ const versionCommand: SlashCommand = {
305
+ name: "version",
306
+ aliases: ["v"],
307
+ description: "Show Woopcode version",
308
+ category: "other",
309
+
310
+ async execute(context, args) {
311
+ return `Woopcode v${version}`;
312
+ },
313
+ };
314
+
315
+ // ==================== REGISTRATION ====================
316
+
317
+ export function registerCommands() {
318
+ registry.register(helpCommand);
319
+ registry.register(newCommand);
320
+ registry.register(exitCommand);
321
+ registry.register(providerCommand);
322
+ registry.register(loginCommand);
323
+ registry.register(logoutCommand);
324
+ registry.register(modelCommand);
325
+ registry.register(workspaceCommand);
326
+ registry.register(statusCommand);
327
+ registry.register(versionCommand);
328
+ }
@@ -0,0 +1,40 @@
1
+ import { parseInput } from "./parser";
2
+ import { registry } from "./registry";
3
+ import type { SlashCommandContext } from "./types";
4
+
5
+ export async function handleSlashCommand(
6
+ input: string,
7
+ context: SlashCommandContext,
8
+ ): Promise<{ handled: boolean; output?: string }> {
9
+ const parsed = parseInput(input);
10
+
11
+ // Discovery mode: show available commands
12
+ if (parsed.type === "discovery") {
13
+ const output = registry.generateDiscoveryList();
14
+ context.onOutput(output);
15
+ return { handled: true, output };
16
+ }
17
+
18
+ if (parsed.type !== "command") {
19
+ return { handled: false };
20
+ }
21
+
22
+ const command = registry.get(parsed.command!);
23
+
24
+ if (!command) {
25
+ const output = `Unknown command "/${parsed.command}"\nRun /help to see available commands.`;
26
+ context.onOutput(output);
27
+ return { handled: true, output };
28
+ }
29
+
30
+ try {
31
+ const output = await command.execute(context, parsed.args!);
32
+ context.onOutput(output);
33
+ return { handled: true, output };
34
+ } catch (error) {
35
+ const message = error instanceof Error ? error.message : String(error);
36
+ const output = `Error: ${message}`;
37
+ context.onOutput(output);
38
+ return { handled: true, output };
39
+ }
40
+ }
@@ -0,0 +1,9 @@
1
+ export { handleSlashCommand } from "./handler";
2
+ export { registry } from "./registry";
3
+ export { registerCommands } from "./commands";
4
+ export { parseInput } from "./parser";
5
+ export type {
6
+ SlashCommand,
7
+ SlashCommandContext,
8
+ ParsedCommand,
9
+ } from "./types";
@@ -0,0 +1,25 @@
1
+ import type { ParsedCommand } from "./types";
2
+
3
+ export function parseInput(input: string): ParsedCommand {
4
+ const trimmed = input.trim();
5
+
6
+ //discovery mode
7
+ if (trimmed === "/") {
8
+ return { type: "discovery", originalInput: input };
9
+ }
10
+
11
+ if (!trimmed.startsWith("/")) {
12
+ return { type: "text", originalInput: input };
13
+ }
14
+
15
+ const parts = trimmed.slice(1).split(/\s+/);
16
+ const command = parts[0]?.toLowerCase() || "";
17
+ const args = parts.slice(1);
18
+
19
+ return {
20
+ type: "command",
21
+ command,
22
+ args,
23
+ originalInput: input,
24
+ };
25
+ }
@@ -0,0 +1,66 @@
1
+ import type { SlashCommand } from "./types";
2
+
3
+ export class SlashCommandRegistry {
4
+ private commands = new Map<string, SlashCommand>();
5
+ private aliases = new Map<string, string>();
6
+
7
+ register(command: SlashCommand): void {
8
+ this.commands.set(command.name, command);
9
+
10
+ if (command.aliases) {
11
+ command.aliases.forEach((alias) => {
12
+ this.aliases.set(alias, command.name);
13
+ });
14
+ }
15
+ }
16
+
17
+ get(nameOrAlias: string): SlashCommand | undefined {
18
+ const name = this.aliases.get(nameOrAlias) ?? nameOrAlias;
19
+ return this.commands.get(name);
20
+ }
21
+
22
+ getAll(): SlashCommand[] {
23
+ return Array.from(this.commands.values());
24
+ }
25
+
26
+ getByCategory(category: string): SlashCommand[] {
27
+ return this.getAll().filter((cmd) => cmd.category === category);
28
+ }
29
+
30
+ // Auto-generated help
31
+ generateHelp(): string {
32
+ const categories = {
33
+ session: "Session",
34
+ configuration: "Configuration",
35
+ workspace: "Workspace",
36
+ other: "Other",
37
+ };
38
+
39
+ let output = "Available Commands:\n\n";
40
+
41
+ for (const [key, label] of Object.entries(categories)) {
42
+ const commands = this.getByCategory(key);
43
+ if (commands.length === 0) continue;
44
+
45
+ output += `${label}:\n`;
46
+ commands.forEach((cmd) => {
47
+ const aliases = cmd.aliases?.length
48
+ ? ` (${cmd.aliases.join(", ")})`
49
+ : "";
50
+ output += ` /${cmd.name}${aliases} - ${cmd.description}\n`;
51
+ });
52
+ output += "\n";
53
+ }
54
+
55
+ return output.trim();
56
+ }
57
+
58
+ // 🔥 Discovery list
59
+ generateDiscoveryList(): string {
60
+ return this.getAll()
61
+ .map((cmd) => `/${cmd.name}`)
62
+ .join(" ");
63
+ }
64
+ }
65
+
66
+ export const registry = new SlashCommandRegistry();
@@ -0,0 +1,23 @@
1
+ import type { AgentController } from "../agentController";
2
+
3
+ export interface SlashCommandContext {
4
+ controller: AgentController;
5
+ onExit: () => Promise<void>;
6
+ onOutput: (message: string) => void;
7
+ }
8
+
9
+ export interface SlashCommand {
10
+ name: string;
11
+ aliases?: string[];
12
+ description: string;
13
+ category: "session" | "configuration" | "workspace" | "other";
14
+ usage?: string;
15
+ execute(context: SlashCommandContext, args: string[]): Promise<string>;
16
+ }
17
+
18
+ export interface ParsedCommand {
19
+ type: "command" | "text" | "discovery";
20
+ command?: string;
21
+ args?: string[];
22
+ originalInput?: string;
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "woopcode",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "An autonomous AI coding assistant built for the terminal",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -2,6 +2,8 @@ import { Box, Text, useInput } from "ink";
2
2
  import TextInput from "ink-text-input";
3
3
  import { useRef } from "react";
4
4
  import type { AgentController } from "../../commands/agentController";
5
+ import { handleSlashCommand } from "../../commands/slash";
6
+ import { store } from "./store/ui-store";
5
7
 
6
8
  interface PromptProps {
7
9
  controller: AgentController;
@@ -42,6 +44,24 @@ export function Prompt({
42
44
  if (!prompt || controller.isBusy()) {
43
45
  return;
44
46
  }
47
+
48
+ // 🔥 Slash command interception
49
+ const context = {
50
+ controller,
51
+ onExit,
52
+ onOutput: (message: string) => {
53
+ store.addSystemMessage(message);
54
+ },
55
+ };
56
+
57
+ const result = await handleSlashCommand(prompt, context);
58
+
59
+ if (result.handled) {
60
+ onValueChange("");
61
+ return;
62
+ }
63
+
64
+ // Original flow
45
65
  if (prompt === "/exit") {
46
66
  await onExit();
47
67
  return;
@@ -59,6 +59,22 @@ export class UIStore {
59
59
  this.emit();
60
60
  }
61
61
 
62
+ addSystemMessage(content: string) {
63
+ this.state = {
64
+ ...this.state,
65
+ timeline: [
66
+ ...this.state.timeline,
67
+ {
68
+ id: crypto.randomUUID(),
69
+ type: "system",
70
+ content,
71
+ },
72
+ ],
73
+ };
74
+
75
+ this.emit();
76
+ }
77
+
62
78
  startTool(tool: ToolCall) {
63
79
  this.state = {
64
80
  ...this.state,
@@ -47,6 +47,18 @@ function TimelineItem({ item }: { item: TimeLineItem }) {
47
47
  </Box>
48
48
  );
49
49
 
50
+ case "system":
51
+ return (
52
+ <Box flexDirection="column" marginBottom={1}>
53
+ <Text bold color="yellow">
54
+ System
55
+ </Text>
56
+ <Box paddingLeft={1}>
57
+ <Text>{item.content}</Text>
58
+ </Box>
59
+ </Box>
60
+ );
61
+
50
62
  case "tool": {
51
63
  const label = toolLabel(item.name);
52
64
  const target = formatToolArgument(item.arguments);
package/tui/src/types.ts CHANGED
@@ -10,6 +10,11 @@ export type TimeLineItem =
10
10
  content: string;
11
11
  streaming: boolean;
12
12
  }
13
+ | {
14
+ id: string;
15
+ type: "system";
16
+ content: string;
17
+ }
13
18
  | {
14
19
  id: string;
15
20
  type: "tool";