@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.
Files changed (118) hide show
  1. package/.cdoing/permissions.json +8 -0
  2. package/dist/callbacks.d.ts +17 -0
  3. package/dist/callbacks.d.ts.map +1 -0
  4. package/dist/callbacks.js +265 -0
  5. package/dist/callbacks.js.map +1 -0
  6. package/dist/chat.d.ts +27 -0
  7. package/dist/chat.d.ts.map +1 -0
  8. package/dist/chat.js +57 -0
  9. package/dist/chat.js.map +1 -0
  10. package/dist/commands.d.ts +22 -0
  11. package/dist/commands.d.ts.map +1 -0
  12. package/dist/commands.js +452 -0
  13. package/dist/commands.js.map +1 -0
  14. package/dist/config.d.ts +84 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +427 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/help.d.ts +9 -0
  19. package/dist/help.d.ts.map +1 -0
  20. package/dist/help.js +167 -0
  21. package/dist/help.js.map +1 -0
  22. package/dist/history.d.ts +51 -0
  23. package/dist/history.d.ts.map +1 -0
  24. package/dist/history.js +207 -0
  25. package/dist/history.js.map +1 -0
  26. package/dist/index.d.ts +7 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +220 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/oauth.d.ts +13 -0
  31. package/dist/oauth.d.ts.map +1 -0
  32. package/dist/oauth.js +182 -0
  33. package/dist/oauth.js.map +1 -0
  34. package/dist/review.d.ts +26 -0
  35. package/dist/review.d.ts.map +1 -0
  36. package/dist/review.js +198 -0
  37. package/dist/review.js.map +1 -0
  38. package/dist/serve.d.ts +23 -0
  39. package/dist/serve.d.ts.map +1 -0
  40. package/dist/serve.js +293 -0
  41. package/dist/serve.js.map +1 -0
  42. package/dist/tools.d.ts +14 -0
  43. package/dist/tools.d.ts.map +1 -0
  44. package/dist/tools.js +57 -0
  45. package/dist/tools.js.map +1 -0
  46. package/dist/ui/App.d.ts +24 -0
  47. package/dist/ui/App.d.ts.map +1 -0
  48. package/dist/ui/App.js +321 -0
  49. package/dist/ui/App.js.map +1 -0
  50. package/dist/ui/MessageList.d.ts +14 -0
  51. package/dist/ui/MessageList.d.ts.map +1 -0
  52. package/dist/ui/MessageList.js +147 -0
  53. package/dist/ui/MessageList.js.map +1 -0
  54. package/dist/ui/SessionBrowser.d.ts +18 -0
  55. package/dist/ui/SessionBrowser.d.ts.map +1 -0
  56. package/dist/ui/SessionBrowser.js +149 -0
  57. package/dist/ui/SessionBrowser.js.map +1 -0
  58. package/dist/ui/SetupWizard.d.ts +23 -0
  59. package/dist/ui/SetupWizard.d.ts.map +1 -0
  60. package/dist/ui/SetupWizard.js +402 -0
  61. package/dist/ui/SetupWizard.js.map +1 -0
  62. package/dist/ui/Spinner.d.ts +15 -0
  63. package/dist/ui/Spinner.d.ts.map +1 -0
  64. package/dist/ui/Spinner.js +111 -0
  65. package/dist/ui/Spinner.js.map +1 -0
  66. package/dist/ui/StatusBar.d.ts +16 -0
  67. package/dist/ui/StatusBar.d.ts.map +1 -0
  68. package/dist/ui/StatusBar.js +56 -0
  69. package/dist/ui/StatusBar.js.map +1 -0
  70. package/dist/ui/UserInput.d.ts +13 -0
  71. package/dist/ui/UserInput.d.ts.map +1 -0
  72. package/dist/ui/UserInput.js +872 -0
  73. package/dist/ui/UserInput.js.map +1 -0
  74. package/dist/ui/hooks/helpers.d.ts +55 -0
  75. package/dist/ui/hooks/helpers.d.ts.map +1 -0
  76. package/dist/ui/hooks/helpers.js +304 -0
  77. package/dist/ui/hooks/helpers.js.map +1 -0
  78. package/dist/ui/hooks/useAgent.d.ts +60 -0
  79. package/dist/ui/hooks/useAgent.d.ts.map +1 -0
  80. package/dist/ui/hooks/useAgent.js +213 -0
  81. package/dist/ui/hooks/useAgent.js.map +1 -0
  82. package/dist/ui/hooks/useChat.d.ts +74 -0
  83. package/dist/ui/hooks/useChat.d.ts.map +1 -0
  84. package/dist/ui/hooks/useChat.js +819 -0
  85. package/dist/ui/hooks/useChat.js.map +1 -0
  86. package/dist/ui/theme.d.ts +73 -0
  87. package/dist/ui/theme.d.ts.map +1 -0
  88. package/dist/ui/theme.js +214 -0
  89. package/dist/ui/theme.js.map +1 -0
  90. package/dist/ui/types.d.ts +37 -0
  91. package/dist/ui/types.d.ts.map +1 -0
  92. package/dist/ui/types.js +3 -0
  93. package/dist/ui/types.js.map +1 -0
  94. package/package.json +33 -0
  95. package/src/callbacks.ts +294 -0
  96. package/src/chat.ts +72 -0
  97. package/src/commands.ts +425 -0
  98. package/src/config.ts +462 -0
  99. package/src/help.ts +182 -0
  100. package/src/history.ts +205 -0
  101. package/src/index.ts +248 -0
  102. package/src/oauth.ts +164 -0
  103. package/src/review.ts +233 -0
  104. package/src/serve.ts +290 -0
  105. package/src/tools.ts +104 -0
  106. package/src/ui/App.tsx +426 -0
  107. package/src/ui/MessageList.tsx +222 -0
  108. package/src/ui/SessionBrowser.tsx +161 -0
  109. package/src/ui/SetupWizard.tsx +412 -0
  110. package/src/ui/Spinner.tsx +103 -0
  111. package/src/ui/StatusBar.tsx +106 -0
  112. package/src/ui/UserInput.tsx +954 -0
  113. package/src/ui/hooks/helpers.ts +271 -0
  114. package/src/ui/hooks/useAgent.ts +270 -0
  115. package/src/ui/hooks/useChat.ts +943 -0
  116. package/src/ui/theme.ts +326 -0
  117. package/src/ui/types.ts +41 -0
  118. package/tsconfig.json +18 -0
@@ -0,0 +1,819 @@
1
+ "use strict";
2
+ /**
3
+ * useChat.ts — Main chat state hook.
4
+ *
5
+ * This is the top-level hook consumed by App.tsx. It wires together three
6
+ * concerns that intentionally live in separate modules:
7
+ *
8
+ * 1. Agent lifecycle → ./useAgent.ts
9
+ * Build / rebuild the AgentRunner when settings change.
10
+ *
11
+ * 2. Pure utilities → ./helpers.ts
12
+ * Terminal output, diff printing, help text — no React.
13
+ *
14
+ * 3. This file (useChat.ts)
15
+ * - Message history (what's displayed in the chat window)
16
+ * - Session / conversation persistence
17
+ * - Background jobs (/bg, /jobs)
18
+ * - Slash command dispatch (/model, /dir, /clear, …)
19
+ * - The sendMessage() function that runs the agent and streams tokens
20
+ *
21
+ * Learning note — why split at all?
22
+ * A single 1000-line file works but becomes hard to navigate. Splitting by
23
+ * responsibility means you can read useAgent.ts to understand "how is the
24
+ * AI agent built?" without wading through session management, and vice versa.
25
+ *
26
+ * Data flow:
27
+ * User types → UserInput.tsx → onSubmit → App.tsx → sendMessage()
28
+ * ↓
29
+ * agentRef.current.run()
30
+ * ↓
31
+ * onToken / onToolCall / onComplete
32
+ * ↓
33
+ * setStreamingContent / setMessages
34
+ */
35
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
36
+ if (k2 === undefined) k2 = k;
37
+ var desc = Object.getOwnPropertyDescriptor(m, k);
38
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
39
+ desc = { enumerable: true, get: function() { return m[k]; } };
40
+ }
41
+ Object.defineProperty(o, k2, desc);
42
+ }) : (function(o, m, k, k2) {
43
+ if (k2 === undefined) k2 = k;
44
+ o[k2] = m[k];
45
+ }));
46
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
47
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
48
+ }) : function(o, v) {
49
+ o["default"] = v;
50
+ });
51
+ var __importStar = (this && this.__importStar) || (function () {
52
+ var ownKeys = function(o) {
53
+ ownKeys = Object.getOwnPropertyNames || function (o) {
54
+ var ar = [];
55
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
56
+ return ar;
57
+ };
58
+ return ownKeys(o);
59
+ };
60
+ return function (mod) {
61
+ if (mod && mod.__esModule) return mod;
62
+ var result = {};
63
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
64
+ __setModuleDefault(result, mod);
65
+ return result;
66
+ };
67
+ })();
68
+ var __importDefault = (this && this.__importDefault) || function (mod) {
69
+ return (mod && mod.__esModule) ? mod : { "default": mod };
70
+ };
71
+ Object.defineProperty(exports, "__esModule", { value: true });
72
+ exports.useChat = useChat;
73
+ const react_1 = require("react");
74
+ const path = __importStar(require("path"));
75
+ const fs = __importStar(require("fs"));
76
+ const chalk_1 = __importDefault(require("chalk"));
77
+ const ai_1 = require("@cdoing/ai");
78
+ // History helpers — read / write conversations to ~/.cdoing/history/
79
+ const history_1 = require("../../history");
80
+ const tools_1 = require("../../tools");
81
+ const config_1 = require("../../config");
82
+ const oauth_1 = require("../../oauth");
83
+ const commands_1 = require("../../commands");
84
+ const theme_1 = require("../theme");
85
+ // Split modules
86
+ const useAgent_1 = require("./useAgent");
87
+ const helpers_1 = require("./helpers");
88
+ // ─────────────────────────────────────────────────────────────────────────────
89
+ // Small utilities local to this file
90
+ // ─────────────────────────────────────────────────────────────────────────────
91
+ /** Auto-incrementing message ID — keeps React list keys stable */
92
+ let _msgId = 0;
93
+ function nextId() { return String(++_msgId); }
94
+ /** Short unique ID for background jobs — e.g. "bg-1a2b" */
95
+ function jobId() { return `bg-${Date.now().toString(36).slice(-4)}`; }
96
+ // ─────────────────────────────────────────────────────────────────────────────
97
+ // The hook
98
+ // ─────────────────────────────────────────────────────────────────────────────
99
+ function useChat(opts) {
100
+ // ── 1. Agent infrastructure (from useAgent.ts) ───────────────────────────
101
+ //
102
+ // useAgent owns: agentRef, modelConfigRef, toolRegistryRef, workingDirRef,
103
+ // planManagerRef, rulesManagerRef, effortManagerRef, …
104
+ // rebuildAgent(), resolveContextProviders()
105
+ //
106
+ // We destructure everything we need from it.
107
+ const agent = (0, useAgent_1.useAgent)(opts);
108
+ const { agentRef, modelConfigRef, toolRegistryRef, workingDirRef, planManagerRef, rulesManagerRef, effortManagerRef, mcpManagerRef, contextProvidersRef, rebuildAgent, resolveContextProviders, } = agent;
109
+ // ── 2. UI state — these drive React re-renders ───────────────────────────
110
+ /** All committed messages shown in the chat window (user + assistant + system) */
111
+ const [messages, setMessages] = (0, react_1.useState)([]);
112
+ /** Token stream currently being received — shown in the live streaming area */
113
+ const [streamingContent, setStreamingContent] = (0, react_1.useState)("");
114
+ /** Whether the agent is processing a request (disables input, shows spinner) */
115
+ const [isProcessing, setIsProcessing] = (0, react_1.useState)(false);
116
+ /** Tool currently executing (shows animated spinner with tool name) */
117
+ const [toolActivity, setToolActivity] = (0, react_1.useState)(null);
118
+ /** Last token-usage report from the LLM (shown in the status bar) */
119
+ const [lastUsage, setLastUsage] = (0, react_1.useState)(null);
120
+ /** Current working directory — shown in the status bar, changed by /dir */
121
+ const [workingDir, setWorkingDir] = (0, react_1.useState)(process.cwd());
122
+ /** Context-window fill percentage — used to auto-compact at 80% */
123
+ const [contextUsage, setContextUsage] = (0, react_1.useState)(null);
124
+ /** List of /bg background jobs and their statuses */
125
+ const [backgroundJobs, setBackgroundJobs] = (0, react_1.useState)([]);
126
+ /** Whether the /ls session browser overlay is open */
127
+ const [showSessionBrowser, setShowSessionBrowser] = (0, react_1.useState)(false);
128
+ /** Bumped whenever modelConfigRef is mutated — forces a re-render so StatusBar reflects changes */
129
+ const [_configVersion, _bumpConfigVersion] = (0, react_1.useState)(0);
130
+ /** Snapshot of the live model config — recalculated whenever _configVersion bumps */
131
+ const liveModelConfig = (0, react_1.useMemo)(() => ({ ...modelConfigRef.current }),
132
+ // eslint-disable-next-line react-hooks/exhaustive-deps
133
+ [_configVersion]);
134
+ /** Rebuild agent and trigger a re-render so UI (StatusBar, etc.) shows updated config */
135
+ const rebuildAndRefresh = (0, react_1.useCallback)(() => {
136
+ rebuildAgent();
137
+ _bumpConfigVersion((v) => v + 1);
138
+ }, [rebuildAgent]);
139
+ // ── 3. Mutable refs — data that changes without triggering a re-render ───
140
+ /** The AbortController for the current agent run — set to cancel in-flight requests */
141
+ const abortRef = (0, react_1.useRef)(null);
142
+ /**
143
+ * Message queue — when a message arrives while isProcessing is true it's
144
+ * pushed here. The next message in the queue is dequeued in onComplete.
145
+ */
146
+ const queueRef = (0, react_1.useRef)([]);
147
+ /**
148
+ * Last tool input — saved so printToolResult() can show the diff after the
149
+ * tool finishes. Cleared after each tool result.
150
+ */
151
+ const lastToolInputRef = (0, react_1.useRef)({});
152
+ /** Active conversation — persisted to disk on every message */
153
+ const conversationRef = (0, react_1.useRef)((0, history_1.createConversation)(String(opts.modelConfig.provider || "anthropic"), String(opts.modelConfig.model || "default")));
154
+ /**
155
+ * Last captured terminal output — injected when the user types @terminal.
156
+ * Set by App.tsx after shell commands finish.
157
+ */
158
+ const lastTerminalOutputRef = (0, react_1.useRef)("");
159
+ /** Whether plan-mode is active (agent proposes a plan before acting) */
160
+ const planModeActiveRef = (0, react_1.useRef)(false);
161
+ // ─────────────────────────────────────────────────────────────────────────
162
+ // Helper: add a system notification to the message list
163
+ // ─────────────────────────────────────────────────────────────────────────
164
+ function addSystemMessage(content) {
165
+ setMessages((prev) => [
166
+ ...prev,
167
+ { id: nextId(), role: "system", content },
168
+ ]);
169
+ }
170
+ // ─────────────────────────────────────────────────────────────────────────
171
+ // Helper: auto-generate a session title after the first exchange
172
+ // ─────────────────────────────────────────────────────────────────────────
173
+ /**
174
+ * Fires a lightweight LLM call in the background to produce a short title
175
+ * for the conversation (e.g. "Fix TypeScript build errors").
176
+ * Saved to disk via updateConversationTitle — never blocks the main UI.
177
+ */
178
+ function generateSessionTitle(conv) {
179
+ const firstUser = conv.messages.find((m) => m.role === "user");
180
+ const firstAssistant = conv.messages.find((m) => m.role === "assistant");
181
+ if (!firstUser)
182
+ return;
183
+ const snippet = [
184
+ `User: ${firstUser.content.substring(0, 200)}`,
185
+ firstAssistant ? `Assistant: ${firstAssistant.content.substring(0, 200)}` : "",
186
+ ].filter(Boolean).join("\n");
187
+ // Build a throwaway agent with no tools — we only need a text response
188
+ const titleAgent = new agentRef.current.constructor(modelConfigRef.current, toolRegistryRef.current, opts.permissionManager, opts.hookManager);
189
+ let title = "";
190
+ titleAgent.run(`Generate a concise session title (5–8 words max, no quotes) for this conversation:\n\n${snippet}\n\nTitle:`, {
191
+ onToken: (t) => { title += t; },
192
+ onToolCall: () => { },
193
+ onToolResult: () => { },
194
+ onComplete: () => {
195
+ const clean = title.trim().replace(/^["']|["']$/g, "").replace(/\.$/, "");
196
+ if (clean)
197
+ (0, history_1.updateConversationTitle)(conv.id, clean);
198
+ },
199
+ onError: () => { },
200
+ }).catch(() => { });
201
+ }
202
+ // ─────────────────────────────────────────────────────────────────────────
203
+ // cancelCurrent — abort the running agent
204
+ // ─────────────────────────────────────────────────────────────────────────
205
+ const cancelCurrent = (0, react_1.useCallback)(() => {
206
+ if (abortRef.current) {
207
+ abortRef.current.abort();
208
+ abortRef.current = null;
209
+ }
210
+ setIsProcessing(false);
211
+ setStreamingContent("");
212
+ setToolActivity(null);
213
+ addSystemMessage("⏹ Cancelled.");
214
+ }, []);
215
+ // ─────────────────────────────────────────────────────────────────────────
216
+ // sendMessage — the core function that runs the agent
217
+ // ─────────────────────────────────────────────────────────────────────────
218
+ /**
219
+ * Send a message to the agent and stream the response back into the UI.
220
+ *
221
+ * Flow:
222
+ * 1. If already processing → queue the message and return.
223
+ * 2. Expand any @mention context providers in the text.
224
+ * 3. Add the user message to the visible chat history.
225
+ * 4. Run the agent; wire up streaming callbacks:
226
+ * onToken → update the live streaming area
227
+ * onToolCall → flush text to stdout, show tool spinner
228
+ * onToolResult → clear spinner, print diff
229
+ * onComplete → commit reply, dequeue next message
230
+ * onError → show error message
231
+ * onUsage → update token counter and auto-compact if needed
232
+ */
233
+ const sendMessage = (0, react_1.useCallback)(async (text) => {
234
+ // ── Guard: no agent (no key configured) ─────────────────────────────
235
+ if (!agentRef.current) {
236
+ addSystemMessage("No API key configured. Run /setup to authenticate.");
237
+ return;
238
+ }
239
+ // ── Queue if busy ────────────────────────────────────────────────────
240
+ if (isProcessing) {
241
+ queueRef.current.push(text);
242
+ addSystemMessage(`📬 Queued (${queueRef.current.length} waiting)`);
243
+ return;
244
+ }
245
+ // ── Resolve @mentions ────────────────────────────────────────────────
246
+ const enriched = await resolveContextProviders(text, workingDirRef.current, lastTerminalOutputRef.current);
247
+ // ── Optimistic UI update ─────────────────────────────────────────────
248
+ setIsProcessing(true);
249
+ setMessages((prev) => [
250
+ ...prev,
251
+ { id: nextId(), role: "user", content: text },
252
+ ]);
253
+ (0, history_1.addMessage)(conversationRef.current, "user", text);
254
+ const ctrl = new AbortController();
255
+ abortRef.current = ctrl;
256
+ /**
257
+ * Track how much of fullReply has already been written directly to
258
+ * stdout (to avoid double-printing it when it's committed to messages).
259
+ */
260
+ let fullReply = "";
261
+ let flushedPos = 0;
262
+ /**
263
+ * Flush any un-printed streaming text to stdout BEFORE showing a tool
264
+ * call. Without this, the text would disappear when Ink clears the
265
+ * live area to render the tool spinner.
266
+ */
267
+ function flushStreamingText() {
268
+ const pending = fullReply.slice(flushedPos);
269
+ if (pending.trim())
270
+ process.stdout.write("\n" + pending + "\n");
271
+ flushedPos = fullReply.length;
272
+ setStreamingContent("");
273
+ }
274
+ // ── Agent run ────────────────────────────────────────────────────────
275
+ await agentRef.current.run(enriched, {
276
+ onToken: (token) => {
277
+ fullReply += token;
278
+ // Show only the part not yet flushed to stdout
279
+ setStreamingContent(fullReply.slice(flushedPos));
280
+ },
281
+ onToolCall: (name, input) => {
282
+ flushStreamingText();
283
+ lastToolInputRef.current = input;
284
+ (0, helpers_1.printToolCall)(name, input); // permanent stdout line
285
+ const preview = JSON.stringify(input).substring(0, 60);
286
+ setToolActivity({ name, preview, status: "running" });
287
+ },
288
+ onToolResult: (_name, _result, isError) => {
289
+ (0, helpers_1.printToolResult)(_name, isError, lastToolInputRef.current);
290
+ lastToolInputRef.current = {};
291
+ setToolActivity(null);
292
+ },
293
+ onComplete: () => {
294
+ // Commit only the portion NOT already flushed to stdout
295
+ const remaining = fullReply.slice(flushedPos);
296
+ if (remaining.trim()) {
297
+ setMessages((prev) => [
298
+ ...prev,
299
+ { id: nextId(), role: "assistant", content: remaining },
300
+ ]);
301
+ (0, history_1.addMessage)(conversationRef.current, "assistant", remaining);
302
+ }
303
+ else if (flushedPos > 0) {
304
+ // All text was flushed — print a separator to mark the end
305
+ process.stdout.write(chalk_1.default.gray("─".repeat(40)) + "\n");
306
+ }
307
+ // Save the full reply to the conversation for the non-flushed case
308
+ if (flushedPos === 0 && fullReply.trim()) {
309
+ (0, history_1.addMessage)(conversationRef.current, "assistant", fullReply);
310
+ }
311
+ setStreamingContent("");
312
+ setIsProcessing(false);
313
+ abortRef.current = null;
314
+ // Auto-generate a title after the first exchange
315
+ const conv = conversationRef.current;
316
+ if (conv.title === "New conversation" && conv.messages.length >= 2) {
317
+ generateSessionTitle(conv);
318
+ }
319
+ // Dequeue next message
320
+ const next = queueRef.current.shift();
321
+ if (next)
322
+ sendMessage(next);
323
+ },
324
+ onError: (err) => {
325
+ setMessages((prev) => [
326
+ ...prev,
327
+ { id: nextId(), role: "system", content: `❌ Error: ${err.message}`, isError: true },
328
+ ]);
329
+ setStreamingContent("");
330
+ setIsProcessing(false);
331
+ abortRef.current = null;
332
+ },
333
+ onUsage: (usage) => {
334
+ const u = usage;
335
+ setLastUsage(u);
336
+ // Calculate context-window fill % and auto-compact at 80%
337
+ const provider = String(modelConfigRef.current.provider || "anthropic");
338
+ const model = String(modelConfigRef.current.model || "");
339
+ const maxTokens = (0, helpers_1.getContextWindowMax)(provider, model);
340
+ const percent = Math.min(100, (u.inputTokens / maxTokens) * 100);
341
+ setContextUsage({ inputTokens: u.inputTokens, maxTokens, percent });
342
+ if (percent >= 80) {
343
+ const ag = agentRef.current;
344
+ if (typeof ag.compactHistory === "function") {
345
+ ag.compactHistory();
346
+ addSystemMessage("📦 Context compacted automatically (reached 80%).");
347
+ }
348
+ }
349
+ },
350
+ });
351
+ },
352
+ // eslint-disable-next-line react-hooks/exhaustive-deps
353
+ [isProcessing, resolveContextProviders]);
354
+ // ─────────────────────────────────────────────────────────────────────────
355
+ // handleSlashCommand — dispatch /commands typed in the input
356
+ // ─────────────────────────────────────────────────────────────────────────
357
+ /**
358
+ * Process a slash command string (e.g. "/model gpt-4o").
359
+ *
360
+ * Returns a string to display in the UI, or null if the command takes over
361
+ * (e.g. /ls opens the session browser, /exit terminates the process).
362
+ *
363
+ * Adding a new command? Just add a new case here.
364
+ */
365
+ const handleSlashCommand = (0, react_1.useCallback)(async (command) => {
366
+ const parts = command.split(/\s+/);
367
+ const cmd = parts[0]; // e.g. "/model"
368
+ const arg = parts.slice(1).join(" "); // e.g. "gpt-4o"
369
+ switch (cmd) {
370
+ // ── Conversation management ─────────────────────────────────────────
371
+ case "/help":
372
+ return (0, helpers_1.getHelpText)();
373
+ case "/clear":
374
+ // Reset the agent's internal history AND the visible message list
375
+ agentRef.current?.clearHistory();
376
+ setMessages([]);
377
+ return "Conversation cleared.";
378
+ case "/new":
379
+ agentRef.current?.clearHistory();
380
+ conversationRef.current = (0, history_1.createConversation)(String(modelConfigRef.current.provider || "anthropic"), String(modelConfigRef.current.model || "default"));
381
+ setMessages([]);
382
+ return "New conversation started.";
383
+ case "/history":
384
+ return (0, helpers_1.getConversationListText)();
385
+ case "/ls":
386
+ // Open the interactive TUI session browser (App.tsx renders it)
387
+ setShowSessionBrowser(true);
388
+ return null;
389
+ case "/resume": {
390
+ if (!arg)
391
+ return "Usage: /resume <id>";
392
+ const conv = (0, history_1.loadConversation)(arg);
393
+ if (!conv)
394
+ return `Conversation not found: ${arg}`;
395
+ agentRef.current?.clearHistory();
396
+ for (const m of conv.messages) {
397
+ if (m.role === "user")
398
+ agentRef.current?.addToHistory("user", m.content);
399
+ else if (m.role === "assistant")
400
+ agentRef.current?.addToHistory("assistant", m.content);
401
+ }
402
+ conversationRef.current = conv;
403
+ setMessages(conv.messages.map((m) => ({
404
+ id: nextId(),
405
+ role: m.role,
406
+ content: m.content,
407
+ })));
408
+ return `Resumed conversation: ${arg}`;
409
+ }
410
+ case "/delete": {
411
+ if (!arg)
412
+ return "Usage: /delete <id>";
413
+ const ok = (0, history_1.deleteConversation)(arg);
414
+ return ok ? `Deleted: ${arg}` : `Not found: ${arg}`;
415
+ }
416
+ case "/fork": {
417
+ // Fork creates a copy of a conversation so you can explore a divergent path
418
+ const sourceId = arg || conversationRef.current.id;
419
+ const forked = (0, history_1.forkConversation)(sourceId);
420
+ if (!forked)
421
+ return `Not found: ${sourceId}`;
422
+ return `Forked → new session: ${forked.id}\nTitle: ${forked.title}\nUse /resume ${forked.id} to switch to it.`;
423
+ }
424
+ // ── Background jobs ─────────────────────────────────────────────────
425
+ case "/bg": {
426
+ // Run a prompt in the background without blocking the main chat
427
+ if (!arg)
428
+ return "Usage: /bg <prompt> — run a prompt as a background job";
429
+ const id = jobId();
430
+ const bgJob = { id, prompt: arg, status: "running", startedAt: Date.now() };
431
+ setBackgroundJobs((prev) => [...prev, bgJob]);
432
+ addSystemMessage(`⚡ Background job started: ${id}`);
433
+ // Build a fresh ephemeral agent so it doesn't share history with main chat
434
+ if (!agentRef.current)
435
+ return "No API key configured. Run /setup first.";
436
+ const bgAgent = new agentRef.current.constructor(modelConfigRef.current, toolRegistryRef.current, opts.permissionManager, opts.hookManager);
437
+ let result = "";
438
+ bgAgent.run(arg, {
439
+ onToken: (t) => { result += t; },
440
+ onToolCall: () => { },
441
+ onToolResult: () => { },
442
+ onComplete: () => {
443
+ setBackgroundJobs((prev) => prev.map((j) => j.id === id
444
+ ? { ...j, status: "done", result, completedAt: Date.now() }
445
+ : j));
446
+ addSystemMessage(`✅ Background job done: ${id}`);
447
+ },
448
+ onError: (e) => {
449
+ setBackgroundJobs((prev) => prev.map((j) => j.id === id
450
+ ? { ...j, status: "error", error: e.message, completedAt: Date.now() }
451
+ : j));
452
+ addSystemMessage(`❌ Background job failed: ${id} — ${e.message}`);
453
+ },
454
+ }).catch(() => { });
455
+ return `Job ${id} started in background.`;
456
+ }
457
+ case "/jobs": {
458
+ if (!backgroundJobs.length)
459
+ return "No background jobs.";
460
+ const jobArg = arg.trim();
461
+ if (jobArg) {
462
+ // /jobs <id> — show full result for one job
463
+ const job = backgroundJobs.find((j) => j.id === jobArg);
464
+ if (!job)
465
+ return `Job not found: ${jobArg}`;
466
+ const elapsed = job.completedAt
467
+ ? `${((job.completedAt - job.startedAt) / 1000).toFixed(1)}s`
468
+ : "running";
469
+ return [
470
+ `Job: ${job.id} [${job.status}] ${elapsed}`,
471
+ `Prompt: ${job.prompt.substring(0, 100)}`,
472
+ job.result ? `\nResult:\n${job.result}` : "",
473
+ job.error ? `\nError: ${job.error}` : "",
474
+ ].filter(Boolean).join("\n");
475
+ }
476
+ // /jobs — list all jobs
477
+ return backgroundJobs.map((j) => {
478
+ const elapsed = j.completedAt
479
+ ? `${((j.completedAt - j.startedAt) / 1000).toFixed(1)}s`
480
+ : "running…";
481
+ const icon = j.status === "done" ? "✅" : j.status === "error" ? "❌" : "⚡";
482
+ return `${icon} ${j.id} ${j.status.padEnd(8)} ${elapsed} ${j.prompt.substring(0, 50)}`;
483
+ }).join("\n");
484
+ }
485
+ // ── Model / provider configuration ──────────────────────────────────
486
+ case "/config": {
487
+ if (arg === "show") {
488
+ return ["Stored Config:", ...(0, config_1.getStoredConfigDisplay)()].join("\n ");
489
+ }
490
+ if (arg.startsWith("set ")) {
491
+ const sp = arg.slice(4).trim().split(/\s+/);
492
+ const key = sp[0];
493
+ const val = sp.slice(1).join(" ");
494
+ if (!key || !val)
495
+ return "Usage: /config set <key> <value>\nKeys: provider, model, mode, api-key, base-url, oauth-token";
496
+ // oauth-token is stored in keychain (not config.json) — handle separately
497
+ if (key === "oauth-token") {
498
+ modelConfigRef.current.oauthToken = val;
499
+ modelConfigRef.current.apiKey = undefined;
500
+ rebuildAndRefresh();
501
+ return `OAuth token set (${val.slice(0, 8)}...)`;
502
+ }
503
+ const res = (0, config_1.updateStoredConfig)(key, val);
504
+ if (res.success) {
505
+ // Mutate the ref and rebuild so changes take effect immediately
506
+ if (key === "provider") {
507
+ modelConfigRef.current.provider = val;
508
+ rebuildAndRefresh();
509
+ }
510
+ if (key === "model") {
511
+ modelConfigRef.current.model = val;
512
+ rebuildAndRefresh();
513
+ }
514
+ if (key === "mode")
515
+ opts.permissionManager.setMode((0, config_1.parsePermissionMode)(val));
516
+ if (key === "api-key") {
517
+ modelConfigRef.current.apiKey = val;
518
+ modelConfigRef.current.oauthToken = undefined;
519
+ rebuildAndRefresh();
520
+ }
521
+ if (key === "base-url") {
522
+ modelConfigRef.current.baseURL = val;
523
+ rebuildAndRefresh();
524
+ }
525
+ const masked = key === "api-key" ? val.slice(0, 8) + "..." : val;
526
+ return `Saved: ${key} = ${masked}`;
527
+ }
528
+ return res.error || "Error saving config";
529
+ }
530
+ return [
531
+ `Provider: ${modelConfigRef.current.provider || "anthropic"}`,
532
+ `Model: ${modelConfigRef.current.model || "(default)"}`,
533
+ `Mode: ${opts.permissionManager.getMode()}`,
534
+ `Dir: ${workingDir}`,
535
+ `Chat ID: ${conversationRef.current.id}`,
536
+ ].join("\n");
537
+ }
538
+ case "/model": {
539
+ if (!arg) {
540
+ const provider = String(modelConfigRef.current.provider || "anthropic");
541
+ const def = (0, ai_1.getDefaultModel)(provider) || "(none)";
542
+ const cur = modelConfigRef.current.model || `(default: ${def})`;
543
+ return [
544
+ `Current model: ${cur}`,
545
+ `Usage: /model <name> — switch to a specific model`,
546
+ ` /model default — reset to provider default (${def})`,
547
+ `Provider models:`,
548
+ ` anthropic: claude-sonnet-4-6, claude-opus-4-6, claude-haiku-4-5`,
549
+ ` openai: gpt-4o, gpt-4o-mini, o3-mini`,
550
+ ` google: gemini-2.0-flash, gemini-1.5-pro`,
551
+ ` ollama: llama3.1, mistral, codellama`,
552
+ ].join("\n");
553
+ }
554
+ if (arg === "default") {
555
+ modelConfigRef.current.model = undefined;
556
+ rebuildAndRefresh();
557
+ const def = (0, ai_1.getDefaultModel)(String(modelConfigRef.current.provider || "anthropic")) || "provider default";
558
+ return `Model reset to default: ${def}`;
559
+ }
560
+ modelConfigRef.current.model = arg;
561
+ rebuildAndRefresh();
562
+ return `Model switched to: ${arg}`;
563
+ }
564
+ case "/provider": {
565
+ if (!arg) {
566
+ return [
567
+ `Current provider: ${modelConfigRef.current.provider || "anthropic"}`,
568
+ `Usage: /provider <name> — switch provider`,
569
+ ` /provider default — reset to anthropic + default model`,
570
+ `Options: anthropic, openai, google, ollama`,
571
+ ].join("\n");
572
+ }
573
+ if (arg === "default") {
574
+ modelConfigRef.current.provider = "anthropic";
575
+ modelConfigRef.current.model = undefined;
576
+ modelConfigRef.current.apiKey = undefined;
577
+ rebuildAndRefresh();
578
+ return `Reset to default: anthropic / ${(0, ai_1.getDefaultModel)("anthropic")}`;
579
+ }
580
+ modelConfigRef.current.provider = arg.toLowerCase();
581
+ modelConfigRef.current.model = undefined;
582
+ rebuildAndRefresh();
583
+ return `Provider switched to: ${arg}\nTip: use /model to pick a model`;
584
+ }
585
+ case "/mode": {
586
+ if (!arg)
587
+ return `Current mode: ${opts.permissionManager.getMode()}\nUsage: /mode <ask|auto-edit|auto>`;
588
+ opts.permissionManager.setMode((0, config_1.parsePermissionMode)(arg));
589
+ return `Permission mode: ${arg}`;
590
+ }
591
+ // ── Working directory ────────────────────────────────────────────────
592
+ case "/dir": {
593
+ if (!arg)
594
+ return `Working directory: ${workingDir}`;
595
+ const newDir = path.resolve(workingDir, arg);
596
+ if (!fs.existsSync(newDir) || !fs.statSync(newDir).isDirectory())
597
+ return `Not a valid directory: ${newDir}`;
598
+ // Sync all references to the new directory
599
+ workingDirRef.current = newDir;
600
+ setWorkingDir(newDir);
601
+ toolRegistryRef.current = (0, tools_1.createToolRegistry)(newDir);
602
+ opts.permissionManager.setProjectDir(newDir);
603
+ opts.hookManager.setWorkingDir(newDir);
604
+ rebuildAndRefresh();
605
+ return `Working directory: ${newDir}`;
606
+ }
607
+ // ── Context window & compaction ──────────────────────────────────────
608
+ case "/compact": {
609
+ const ag = agentRef.current;
610
+ ag.compactHistory?.();
611
+ return "Context compacted.";
612
+ }
613
+ // ── Permissions, memory, hooks ───────────────────────────────────────
614
+ case "/permissions": {
615
+ const pm = opts.permissionManager;
616
+ if (arg === "clear") {
617
+ pm.clearStored?.();
618
+ return "Stored permissions cleared.";
619
+ }
620
+ const perms = pm.getAllStored?.() || {};
621
+ const lines = Object.entries(perms);
622
+ return lines.length ? lines.map(([k, v]) => `${k}: ${v}`).join("\n") : "No stored permissions.";
623
+ }
624
+ case "/memory": {
625
+ const ms = opts.memoryStore;
626
+ if (arg === "clear") {
627
+ ms.clear?.();
628
+ return "Memory cleared.";
629
+ }
630
+ return opts.memoryStore.formatForPrompt() || "No memory stored.";
631
+ }
632
+ case "/hooks": {
633
+ const hm = opts.hookManager;
634
+ return JSON.stringify(hm.getConfig?.() || {}, null, 2);
635
+ }
636
+ // ── Usage stats ──────────────────────────────────────────────────────
637
+ case "/usage": {
638
+ if (!lastUsage)
639
+ return "No usage data yet.";
640
+ return [
641
+ `Input tokens: ${lastUsage.inputTokens.toLocaleString()}`,
642
+ `Output tokens: ${lastUsage.outputTokens.toLocaleString()}`,
643
+ `Total tokens: ${lastUsage.totalTokens.toLocaleString()}`,
644
+ lastUsage.cost !== undefined ? `Cost: $${lastUsage.cost.toFixed(4)}` : "",
645
+ ].filter(Boolean).join("\n");
646
+ }
647
+ // ── Tasks & todos ────────────────────────────────────────────────────
648
+ case "/tasks": {
649
+ const todos = opts.todoStore?.getAll?.() || [];
650
+ if (!todos.length)
651
+ return "No tasks.";
652
+ return todos
653
+ .map((t) => `[${t.status}] ${t.id}: ${t.subject}`)
654
+ .join("\n");
655
+ }
656
+ // ── Plan mode ────────────────────────────────────────────────────────
657
+ case "/plan": {
658
+ if (arg === "off" || arg === "cancel") {
659
+ planModeActiveRef.current = false;
660
+ planManagerRef.current.clearPlan();
661
+ return "Plan mode disabled.";
662
+ }
663
+ if (arg === "show") {
664
+ const plan = planManagerRef.current.getCurrentPlan();
665
+ return plan ? planManagerRef.current.formatPlan() : "No active plan.";
666
+ }
667
+ if (arg === "approve" || arg === "yes") {
668
+ if (planManagerRef.current.approvePlan()) {
669
+ planModeActiveRef.current = false;
670
+ const plan = planManagerRef.current.getCurrentPlan();
671
+ if (plan) {
672
+ planManagerRef.current.startExecution();
673
+ sendMessage(`Execute this plan:\n\n${planManagerRef.current.formatPlan()}\n\nOriginal request: ${plan.originalRequest}`);
674
+ }
675
+ return "Plan approved! Executing...";
676
+ }
677
+ return "No plan to approve.";
678
+ }
679
+ if (arg === "reject" || arg === "no") {
680
+ planManagerRef.current.rejectPlan();
681
+ planModeActiveRef.current = false;
682
+ return "Plan rejected.";
683
+ }
684
+ if (!arg) {
685
+ planModeActiveRef.current = !planModeActiveRef.current;
686
+ return planModeActiveRef.current
687
+ ? "Plan mode ON — next message will generate a plan."
688
+ : "Plan mode OFF.";
689
+ }
690
+ // /plan <request> — immediately generate a plan for the given request
691
+ planModeActiveRef.current = true;
692
+ sendMessage(`[PLAN MODE] Analyze this request and create a step-by-step plan. Do NOT modify files.\n\nRequest: ${arg}`);
693
+ return "Generating plan...";
694
+ }
695
+ // ── Effort level ─────────────────────────────────────────────────────
696
+ case "/effort": {
697
+ if (!arg)
698
+ return `Current effort: ${effortManagerRef.current.getLevel()}\nUsage: /effort <low|medium|high|max>`;
699
+ effortManagerRef.current.setLevel(arg);
700
+ rebuildAndRefresh();
701
+ return `Effort level: ${arg}`;
702
+ }
703
+ // ── Ephemeral / one-shot questions ───────────────────────────────────
704
+ case "/btw": {
705
+ // Ask a question that doesn't get added to the conversation history
706
+ if (!arg)
707
+ return "Usage: /btw <question> (ask without adding to history)";
708
+ setIsProcessing(true);
709
+ if (!agentRef.current) {
710
+ setIsProcessing(false);
711
+ return "No API key configured. Run /setup first.";
712
+ }
713
+ const ephemeralAgent = new agentRef.current.constructor(modelConfigRef.current, toolRegistryRef.current, opts.permissionManager, opts.hookManager);
714
+ let reply = "";
715
+ await ephemeralAgent.run(arg, {
716
+ onToken: (t) => { reply += t; setStreamingContent(reply); },
717
+ onToolCall: () => { },
718
+ onToolResult: () => { },
719
+ onComplete: () => {
720
+ setMessages((prev) => [...prev, { id: nextId(), role: "assistant", content: reply }]);
721
+ setStreamingContent("");
722
+ setIsProcessing(false);
723
+ },
724
+ onError: (e) => {
725
+ addSystemMessage(`❌ ${e.message}`);
726
+ setStreamingContent("");
727
+ setIsProcessing(false);
728
+ },
729
+ });
730
+ return null; // streaming has taken over the display
731
+ }
732
+ // ── Misc ─────────────────────────────────────────────────────────────
733
+ case "/rules":
734
+ return rulesManagerRef.current.formatForDisplay();
735
+ case "/mcp": {
736
+ const mcp = mcpManagerRef.current;
737
+ return mcp.getStatus?.() || "No MCP servers configured.";
738
+ }
739
+ case "/context": {
740
+ const ps = contextProvidersRef.current.getAll();
741
+ if (!ps.length)
742
+ return "No context providers registered.";
743
+ return ps.map((p) => `${p.trigger} — ${p.description || ""}`).join("\n");
744
+ }
745
+ case "/queue": {
746
+ if (!queueRef.current.length)
747
+ return "No messages in queue.";
748
+ return queueRef.current.map((m, i) => `${i + 1}. ${m.substring(0, 60)}`).join("\n");
749
+ }
750
+ case "/theme": {
751
+ if (!arg) {
752
+ const current = (0, theme_1.getThemeName)();
753
+ const available = (0, theme_1.getAvailableThemes)().join(", ");
754
+ return `Current theme: ${current}\nUsage: /theme <${available}>`;
755
+ }
756
+ const validThemes = (0, theme_1.getAvailableThemes)();
757
+ if (!validThemes.includes(arg)) {
758
+ return `Unknown theme "${arg}". Available: ${validThemes.join(", ")}`;
759
+ }
760
+ const result = (0, theme_1.setTheme)(arg);
761
+ return result;
762
+ }
763
+ case "/doctor":
764
+ (0, commands_1.handleDoctor)();
765
+ return "Doctor check complete.";
766
+ case "/init":
767
+ (0, commands_1.handleInit)();
768
+ return "Project initialized.";
769
+ case "/logout":
770
+ modelConfigRef.current.oauthToken = undefined;
771
+ modelConfigRef.current.apiKey = undefined;
772
+ return (0, oauth_1.oauthLogout)() + "\nRun /setup to configure a new API key or log in again.";
773
+ case "/login":
774
+ return "Use /setup to configure provider, model, and authentication.";
775
+ case "/auth-status":
776
+ return (0, oauth_1.oauthStatus)();
777
+ case "/exit":
778
+ case "/quit":
779
+ process.exit(0);
780
+ default:
781
+ return `Unknown command: ${cmd}\nType /help for available commands.`;
782
+ }
783
+ },
784
+ // The deps array includes everything the callback closes over that changes
785
+ [workingDir, lastUsage, sendMessage, backgroundJobs, opts]);
786
+ // ─────────────────────────────────────────────────────────────────────────
787
+ // Public API — what App.tsx destructures from useChat()
788
+ // ─────────────────────────────────────────────────────────────────────────
789
+ return {
790
+ // Message list (drives the <Static> / stdout rendering in App.tsx)
791
+ messages,
792
+ setMessages,
793
+ // Live streaming area
794
+ streamingContent,
795
+ // Processing state
796
+ isProcessing,
797
+ toolActivity,
798
+ // Token usage (status bar)
799
+ lastUsage,
800
+ contextUsage,
801
+ // Working directory (status bar + /dir command)
802
+ workingDir,
803
+ // Background jobs (status bar badge)
804
+ backgroundJobs,
805
+ // Session browser overlay (/ls command)
806
+ showSessionBrowser,
807
+ setShowSessionBrowser,
808
+ // Returns all saved conversations (used by SessionBrowser)
809
+ conversations: history_1.listConversations,
810
+ // Current model config (read by StatusBar) — live snapshot, updates on every config change
811
+ modelConfig: liveModelConfig,
812
+ // Actions
813
+ sendMessage,
814
+ handleSlashCommand,
815
+ cancelCurrent,
816
+ addSystemMessage,
817
+ };
818
+ }
819
+ //# sourceMappingURL=useChat.js.map