@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,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
|