thepopebot 1.2.76-beta.4 → 1.2.76-beta.5
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/bin/cli.js +32 -0
- package/drizzle/0021_coding_agent_workspace.sql +1 -0
- package/drizzle/meta/_journal.json +7 -0
- package/lib/ai/index.js +2 -2
- package/lib/ai/sdk-adapters/claude-code.js +12 -1
- package/lib/chat/actions.js +60 -10
- package/lib/chat/api.js +114 -15
- package/lib/chat/components/chat-input.js +77 -33
- package/lib/chat/components/chat-input.jsx +73 -23
- package/lib/chat/components/chat.js +25 -4
- package/lib/chat/components/chat.jsx +25 -2
- package/lib/chat/components/code-mode-toggle.js +108 -4
- package/lib/chat/components/code-mode-toggle.jsx +102 -1
- package/lib/chat/components/message.js +32 -3
- package/lib/chat/components/message.jsx +37 -3
- package/lib/chat/components/ui/combobox.js +18 -2
- package/lib/chat/components/ui/combobox.jsx +17 -1
- package/lib/code/actions.js +20 -6
- package/lib/code/terminal-view.js +10 -12
- package/lib/code/terminal-view.jsx +16 -13
- package/lib/db/chats.js +9 -17
- package/lib/db/code-workspaces.js +6 -2
- package/lib/db/schema.js +1 -0
- package/lib/tools/github.js +16 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -305,6 +305,38 @@ async function init() {
|
|
|
305
305
|
console.log(' Created .claude/skills → ../skills/active');
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// Create .codex/skills → ../skills/active symlink
|
|
309
|
+
const codexSkillsLink = path.join(cwd, '.codex', 'skills');
|
|
310
|
+
if (!fs.existsSync(codexSkillsLink)) {
|
|
311
|
+
fs.mkdirSync(path.dirname(codexSkillsLink), { recursive: true });
|
|
312
|
+
createDirLink('../skills/active', codexSkillsLink);
|
|
313
|
+
console.log(' Created .codex/skills → ../skills/active');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Create .gemini/skills → ../skills/active symlink
|
|
317
|
+
const geminiSkillsLink = path.join(cwd, '.gemini', 'skills');
|
|
318
|
+
if (!fs.existsSync(geminiSkillsLink)) {
|
|
319
|
+
fs.mkdirSync(path.dirname(geminiSkillsLink), { recursive: true });
|
|
320
|
+
createDirLink('../skills/active', geminiSkillsLink);
|
|
321
|
+
console.log(' Created .gemini/skills → ../skills/active');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Create .kimi/skills → ../skills/active symlink
|
|
325
|
+
const kimiSkillsLink = path.join(cwd, '.kimi', 'skills');
|
|
326
|
+
if (!fs.existsSync(kimiSkillsLink)) {
|
|
327
|
+
fs.mkdirSync(path.dirname(kimiSkillsLink), { recursive: true });
|
|
328
|
+
createDirLink('../skills/active', kimiSkillsLink);
|
|
329
|
+
console.log(' Created .kimi/skills → ../skills/active');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Create .agents/skills → ../skills/active symlink
|
|
333
|
+
const agentsSkillsLink = path.join(cwd, '.agents', 'skills');
|
|
334
|
+
if (!fs.existsSync(agentsSkillsLink)) {
|
|
335
|
+
fs.mkdirSync(path.dirname(agentsSkillsLink), { recursive: true });
|
|
336
|
+
createDirLink('../skills/active', agentsSkillsLink);
|
|
337
|
+
console.log(' Created .agents/skills → ../skills/active');
|
|
338
|
+
}
|
|
339
|
+
|
|
308
340
|
// Report backed-up files
|
|
309
341
|
if (backedUp.length > 0) {
|
|
310
342
|
console.log(`\n Backed up ${backedUp.length} file(s) to ${path.relative(cwd, backupDir)}/`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `code_workspaces` ADD `coding_agent` text;
|
|
@@ -148,6 +148,13 @@
|
|
|
148
148
|
"when": 1774327178886,
|
|
149
149
|
"tag": "0020_natural_fabian_cortez",
|
|
150
150
|
"breakpoints": true
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"idx": 21,
|
|
154
|
+
"version": "6",
|
|
155
|
+
"when": 1775865600000,
|
|
156
|
+
"tag": "0021_coding_agent_workspace",
|
|
157
|
+
"breakpoints": true
|
|
151
158
|
}
|
|
152
159
|
]
|
|
153
160
|
}
|
package/lib/ai/index.js
CHANGED
|
@@ -185,7 +185,7 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
185
185
|
await ensureWorkspaceRepo({ workspaceDir: repoDir, repo, branch, featureBranch });
|
|
186
186
|
ensureSkills(repoDir, isCodeMode ? 'code' : 'agent');
|
|
187
187
|
if (needsSetup) {
|
|
188
|
-
const result =
|
|
188
|
+
const result = `Workspace ready on ${featureBranch || branch}`;
|
|
189
189
|
yield { type: 'tool-result', toolCallId: setupToolCallId, result };
|
|
190
190
|
persistMessage(threadId, 'assistant', JSON.stringify({
|
|
191
191
|
type: 'tool-invocation',
|
|
@@ -198,7 +198,7 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
|
198
198
|
}
|
|
199
199
|
} catch (err) {
|
|
200
200
|
if (needsSetup) {
|
|
201
|
-
yield { type: 'tool-result', toolCallId: setupToolCallId, result:
|
|
201
|
+
yield { type: 'tool-result', toolCallId: setupToolCallId, result: `Setup failed: ${err.message}` };
|
|
202
202
|
}
|
|
203
203
|
throw err;
|
|
204
204
|
}
|
|
@@ -98,6 +98,7 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
|
|
|
98
98
|
cwd: workspaceDir,
|
|
99
99
|
env,
|
|
100
100
|
includePartialMessages: true,
|
|
101
|
+
model: getConfig('CODING_AGENT_CLAUDE_CODE_MODEL') || undefined,
|
|
101
102
|
};
|
|
102
103
|
|
|
103
104
|
// Permission mode → allowed tools
|
|
@@ -133,6 +134,7 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
|
|
|
133
134
|
|
|
134
135
|
// Track tool call state for mapping stream events
|
|
135
136
|
const activeToolCalls = new Map(); // index → { id, name, argsJson }
|
|
137
|
+
const activeThinkingBlocks = new Set(); // indices of active thinking blocks
|
|
136
138
|
|
|
137
139
|
try {
|
|
138
140
|
for await (const message of query({ prompt: sdkPrompt, options })) {
|
|
@@ -156,8 +158,11 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
|
|
|
156
158
|
if (block.type === 'tool_use') {
|
|
157
159
|
activeToolCalls.set(event.index, { id: block.id, name: block.name, argsJson: '' });
|
|
158
160
|
yield { type: 'tool-call', toolCallId: block.id, toolName: block.name, args: {} };
|
|
161
|
+
} else if (block.type === 'thinking') {
|
|
162
|
+
activeThinkingBlocks.add(event.index);
|
|
163
|
+
yield { type: 'thinking-start' };
|
|
159
164
|
}
|
|
160
|
-
// Skip '
|
|
165
|
+
// Skip 'text' start (deltas handle text)
|
|
161
166
|
continue;
|
|
162
167
|
}
|
|
163
168
|
|
|
@@ -167,11 +172,17 @@ export async function* claudeCodeStream({ prompt, workspaceDir, systemPrompt, se
|
|
|
167
172
|
} else if (event.delta.type === 'input_json_delta') {
|
|
168
173
|
const tc = activeToolCalls.get(event.index);
|
|
169
174
|
if (tc) tc.argsJson += event.delta.partial_json;
|
|
175
|
+
} else if (event.delta.type === 'thinking_delta') {
|
|
176
|
+
yield { type: 'thinking', delta: event.delta.thinking };
|
|
170
177
|
}
|
|
171
178
|
continue;
|
|
172
179
|
}
|
|
173
180
|
|
|
174
181
|
if (event.type === 'content_block_stop') {
|
|
182
|
+
if (activeThinkingBlocks.has(event.index)) {
|
|
183
|
+
activeThinkingBlocks.delete(event.index);
|
|
184
|
+
yield { type: 'thinking-end' };
|
|
185
|
+
}
|
|
175
186
|
const tc = activeToolCalls.get(event.index);
|
|
176
187
|
if (tc && tc.argsJson) {
|
|
177
188
|
try {
|
package/lib/chat/actions.js
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
getChatByWorkspaceId,
|
|
8
8
|
getMessagesByChatId,
|
|
9
9
|
deleteChat as dbDeleteChat,
|
|
10
|
-
deleteAllChatsByUser,
|
|
11
10
|
updateChatTitle,
|
|
12
11
|
toggleChatStarred,
|
|
13
12
|
} from '../db/chats.js';
|
|
@@ -139,15 +138,6 @@ export async function starChat(chatId) {
|
|
|
139
138
|
return { success: true, starred };
|
|
140
139
|
}
|
|
141
140
|
|
|
142
|
-
/**
|
|
143
|
-
* Delete all chats for the authenticated user.
|
|
144
|
-
* @returns {Promise<{success: boolean}>}
|
|
145
|
-
*/
|
|
146
|
-
export async function deleteAllChats() {
|
|
147
|
-
const user = await requireAuth();
|
|
148
|
-
deleteAllChatsByUser(user.id);
|
|
149
|
-
return { success: true };
|
|
150
|
-
}
|
|
151
141
|
|
|
152
142
|
/**
|
|
153
143
|
* Get notifications, newest first, with pagination.
|
|
@@ -722,6 +712,66 @@ export async function getCodingAgentSettings() {
|
|
|
722
712
|
}
|
|
723
713
|
}
|
|
724
714
|
|
|
715
|
+
/**
|
|
716
|
+
* Return the list of coding agents that are enabled and have valid credentials.
|
|
717
|
+
* Used to populate the right-click agent picker on the Interactive toggle.
|
|
718
|
+
* @returns {Promise<Array<{value: string, label: string}>>}
|
|
719
|
+
*/
|
|
720
|
+
export async function getAvailableCodingAgents() {
|
|
721
|
+
await requireAuth();
|
|
722
|
+
try {
|
|
723
|
+
const settings = await getCodingAgentSettings();
|
|
724
|
+
if (settings.error) return [];
|
|
725
|
+
|
|
726
|
+
const statusMap = new Map((settings.credentialStatuses || []).map(s => [s.key, s.isSet]));
|
|
727
|
+
|
|
728
|
+
function isProviderReady(provider) {
|
|
729
|
+
const builtin = settings.builtinProviders?.[provider];
|
|
730
|
+
if (builtin?.credentialKey) return statusMap.get(builtin.credentialKey) || false;
|
|
731
|
+
// Custom providers store their own credentials — if selected they're ready
|
|
732
|
+
const custom = (settings.customProviders || []).find(p => p.id === provider);
|
|
733
|
+
return !!custom;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const available = [];
|
|
737
|
+
|
|
738
|
+
if (settings.claudeCode?.enabled) {
|
|
739
|
+
const { claudeCode } = settings;
|
|
740
|
+
const backend = claudeCode.backend || 'anthropic';
|
|
741
|
+
const ready = backend === 'anthropic'
|
|
742
|
+
? (claudeCode.auth === 'oauth' ? claudeCode.oauthTokenCount > 0 : claudeCode.anthropicKeySet)
|
|
743
|
+
: isProviderReady(backend);
|
|
744
|
+
if (ready) available.push({ value: 'claude-code', label: 'Claude Code' });
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (settings.pi?.enabled && settings.pi?.provider && isProviderReady(settings.pi.provider)) {
|
|
748
|
+
available.push({ value: 'pi-coding-agent', label: 'Pi' });
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (settings.geminiCli?.enabled && settings.geminiCli?.googleKeySet) {
|
|
752
|
+
available.push({ value: 'gemini-cli', label: 'Gemini CLI' });
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (settings.codexCli?.enabled) {
|
|
756
|
+
const { codexCli } = settings;
|
|
757
|
+
const ready = codexCli.auth === 'oauth' ? codexCli.oauthTokenCount > 0 : codexCli.codexKeySet;
|
|
758
|
+
if (ready) available.push({ value: 'codex-cli', label: 'Codex CLI' });
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (settings.openCode?.enabled && settings.openCode?.provider && isProviderReady(settings.openCode.provider)) {
|
|
762
|
+
available.push({ value: 'opencode', label: 'OpenCode' });
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (settings.kimiCli?.enabled && settings.kimiCli?.provider && isProviderReady(settings.kimiCli.provider)) {
|
|
766
|
+
available.push({ value: 'kimi-cli', label: 'Kimi CLI' });
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return available;
|
|
770
|
+
} catch {
|
|
771
|
+
return [];
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
725
775
|
/**
|
|
726
776
|
* Update per-agent coding agent config.
|
|
727
777
|
* @param {string} agent - 'claude-code' or 'pi-coding-agent'
|
package/lib/chat/api.js
CHANGED
|
@@ -94,6 +94,28 @@ export async function POST(request) {
|
|
|
94
94
|
|
|
95
95
|
let textStarted = false;
|
|
96
96
|
let textId = uuidv4();
|
|
97
|
+
// Ephemeral thinking block state — tunneled as __thinking__ tool calls.
|
|
98
|
+
// Content is never persisted to DB (not a real tool-call/result pair in chatStream).
|
|
99
|
+
let thinkingId = null;
|
|
100
|
+
let thinkingText = '';
|
|
101
|
+
// Track which toolCallIds have had tool-input-start emitted.
|
|
102
|
+
//
|
|
103
|
+
// Two problems this solves:
|
|
104
|
+
//
|
|
105
|
+
// 1. The Claude Agent SDK emits tool-call twice per tool use: once at
|
|
106
|
+
// content_block_start (args: {}) and again at content_block_stop
|
|
107
|
+
// (args: complete). Sending tool-input-start twice for the same ID
|
|
108
|
+
// resets the AI SDK's internal part state to input-streaming and
|
|
109
|
+
// clears its stored input, causing a visual flicker. Deduplicate here.
|
|
110
|
+
//
|
|
111
|
+
// 2. When Claude Code resumes a session, the adapter skips assistant
|
|
112
|
+
// messages (to avoid duplicate UI) but still emits tool-result chunks
|
|
113
|
+
// for tool_result blocks in subsequent user messages. Those tool-result
|
|
114
|
+
// chunks have no matching tool-call in this stream, so tool-input-start
|
|
115
|
+
// is never sent for them. The AI SDK then throws
|
|
116
|
+
// "tool-output-error must be preceded by a tool-input-available"
|
|
117
|
+
// when tool-output-available arrives. Emit the open events defensively.
|
|
118
|
+
const openedToolCalls = new Set();
|
|
97
119
|
|
|
98
120
|
for await (const chunk of chunks) {
|
|
99
121
|
if (chunk.type === 'text') {
|
|
@@ -105,17 +127,7 @@ export async function POST(request) {
|
|
|
105
127
|
writer.write({ type: 'text-delta', id: textId, delta: chunk.text });
|
|
106
128
|
|
|
107
129
|
} else if (chunk.type === 'tool-call') {
|
|
108
|
-
//
|
|
109
|
-
if (textStarted) {
|
|
110
|
-
writer.write({ type: 'text-end', id: textId });
|
|
111
|
-
textStarted = false;
|
|
112
|
-
}
|
|
113
|
-
writer.write({
|
|
114
|
-
type: 'tool-input-start',
|
|
115
|
-
toolCallId: chunk.toolCallId,
|
|
116
|
-
toolName: chunk.toolName,
|
|
117
|
-
});
|
|
118
|
-
// Enrich coding_agent input with active agent identity from config
|
|
130
|
+
// Enrich coding_agent input with active agent identity (LangGraph fallback path)
|
|
119
131
|
let input = chunk.args;
|
|
120
132
|
if (chunk.toolName === 'coding_agent') {
|
|
121
133
|
const agent = getConfig('CODING_AGENT') || 'claude-code';
|
|
@@ -130,6 +142,21 @@ export async function POST(request) {
|
|
|
130
142
|
const backendApi = getConfig(providerKeys[agent]) || 'anthropic';
|
|
131
143
|
input = { ...chunk.args, codingAgent: agent, backendApi };
|
|
132
144
|
}
|
|
145
|
+
if (!openedToolCalls.has(chunk.toolCallId)) {
|
|
146
|
+
// First time seeing this ID — open the tool block
|
|
147
|
+
if (textStarted) {
|
|
148
|
+
writer.write({ type: 'text-end', id: textId });
|
|
149
|
+
textStarted = false;
|
|
150
|
+
}
|
|
151
|
+
writer.write({
|
|
152
|
+
type: 'tool-input-start',
|
|
153
|
+
toolCallId: chunk.toolCallId,
|
|
154
|
+
toolName: chunk.toolName,
|
|
155
|
+
});
|
|
156
|
+
openedToolCalls.add(chunk.toolCallId);
|
|
157
|
+
}
|
|
158
|
+
// Always emit tool-input-available: first call shows empty args while
|
|
159
|
+
// streaming, second call (content_block_stop) updates to complete args
|
|
133
160
|
writer.write({
|
|
134
161
|
type: 'tool-input-available',
|
|
135
162
|
toolCallId: chunk.toolCallId,
|
|
@@ -138,14 +165,26 @@ export async function POST(request) {
|
|
|
138
165
|
});
|
|
139
166
|
|
|
140
167
|
} else if (chunk.type === 'tool-result') {
|
|
141
|
-
|
|
142
|
-
|
|
168
|
+
if (!openedToolCalls.has(chunk.toolCallId)) {
|
|
169
|
+
// tool-result arrived with no preceding tool-call in this stream
|
|
170
|
+
// (session resume replays tool results from skipped assistant messages).
|
|
171
|
+
// Emit the required open events so the AI SDK does not throw.
|
|
172
|
+
if (textStarted) {
|
|
173
|
+
writer.write({ type: 'text-end', id: textId });
|
|
174
|
+
textStarted = false;
|
|
175
|
+
}
|
|
176
|
+
writer.write({
|
|
177
|
+
type: 'tool-input-start',
|
|
178
|
+
toolCallId: chunk.toolCallId,
|
|
179
|
+
toolName: chunk.toolName || 'unknown',
|
|
180
|
+
});
|
|
143
181
|
writer.write({
|
|
144
182
|
type: 'tool-input-available',
|
|
145
183
|
toolCallId: chunk.toolCallId,
|
|
146
|
-
toolName: chunk.toolName,
|
|
147
|
-
input: chunk.args,
|
|
184
|
+
toolName: chunk.toolName || 'unknown',
|
|
185
|
+
input: chunk.args || {},
|
|
148
186
|
});
|
|
187
|
+
openedToolCalls.add(chunk.toolCallId);
|
|
149
188
|
}
|
|
150
189
|
writer.write({
|
|
151
190
|
type: 'tool-output-available',
|
|
@@ -153,6 +192,44 @@ export async function POST(request) {
|
|
|
153
192
|
output: chunk.result,
|
|
154
193
|
});
|
|
155
194
|
|
|
195
|
+
} else if (chunk.type === 'thinking-start') {
|
|
196
|
+
// Open a new ephemeral thinking block as a pseudo-tool
|
|
197
|
+
if (textStarted) {
|
|
198
|
+
writer.write({ type: 'text-end', id: textId });
|
|
199
|
+
textStarted = false;
|
|
200
|
+
}
|
|
201
|
+
thinkingId = uuidv4();
|
|
202
|
+
thinkingText = '';
|
|
203
|
+
writer.write({
|
|
204
|
+
type: 'tool-input-start',
|
|
205
|
+
toolCallId: thinkingId,
|
|
206
|
+
toolName: '__thinking__',
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
} else if (chunk.type === 'thinking') {
|
|
210
|
+
// Accumulate and stream thinking deltas progressively
|
|
211
|
+
if (thinkingId) {
|
|
212
|
+
thinkingText += chunk.delta;
|
|
213
|
+
writer.write({
|
|
214
|
+
type: 'tool-input-available',
|
|
215
|
+
toolCallId: thinkingId,
|
|
216
|
+
toolName: '__thinking__',
|
|
217
|
+
input: thinkingText,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
} else if (chunk.type === 'thinking-end') {
|
|
222
|
+
// Close the thinking block — empty output marks it done
|
|
223
|
+
if (thinkingId) {
|
|
224
|
+
writer.write({
|
|
225
|
+
type: 'tool-output-available',
|
|
226
|
+
toolCallId: thinkingId,
|
|
227
|
+
output: '',
|
|
228
|
+
});
|
|
229
|
+
thinkingId = null;
|
|
230
|
+
thinkingText = '';
|
|
231
|
+
}
|
|
232
|
+
|
|
156
233
|
} else if (chunk.type === 'meta' || chunk.type === 'result') {
|
|
157
234
|
// Internal events — no SSE output needed
|
|
158
235
|
|
|
@@ -449,6 +526,28 @@ export async function getRepositoriesHandler() {
|
|
|
449
526
|
}
|
|
450
527
|
}
|
|
451
528
|
|
|
529
|
+
/**
|
|
530
|
+
* POST handler for /code/repositories/create — create a new GitHub repository.
|
|
531
|
+
*/
|
|
532
|
+
export async function createRepositoryHandler(request) {
|
|
533
|
+
const session = await auth();
|
|
534
|
+
if (!session?.user?.id) {
|
|
535
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
const { name } = await request.json();
|
|
539
|
+
if (!name || typeof name !== 'string') {
|
|
540
|
+
return Response.json({ error: 'Repository name is required' }, { status: 400 });
|
|
541
|
+
}
|
|
542
|
+
const { createRepository } = await import('../tools/github.js');
|
|
543
|
+
const repo = await createRepository(name.trim());
|
|
544
|
+
return Response.json(repo);
|
|
545
|
+
} catch (err) {
|
|
546
|
+
const message = err.message || 'Failed to create repository';
|
|
547
|
+
return Response.json({ error: message }, { status: 422 });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
452
551
|
/**
|
|
453
552
|
* GET handler for /code/branches?repo=owner/name — list branches with session auth.
|
|
454
553
|
*/
|
|
@@ -65,8 +65,10 @@ function ChatInput({ input, setInput, onSubmit, status, stop, files, setFiles, d
|
|
|
65
65
|
const fileInputRef = useRef(null);
|
|
66
66
|
const [isDragging, setIsDragging] = useState(false);
|
|
67
67
|
const [modeDropdownOpen, setModeDropdownOpen] = useState(false);
|
|
68
|
+
const [agentPickerOpen, setAgentPickerOpen] = useState(false);
|
|
68
69
|
const [partialText, setPartialText] = useState("");
|
|
69
70
|
const dropdownRef = useRef(null);
|
|
71
|
+
const agentPickerRef = useRef(null);
|
|
70
72
|
const isStreaming = status === "streaming" || status === "submitted";
|
|
71
73
|
const volumeRef = useRef(0);
|
|
72
74
|
const { voiceAvailable, isConnecting, isRecording, startRecording, stopRecording } = useVoiceInput({
|
|
@@ -106,6 +108,23 @@ function ChatInput({ input, setInput, onSubmit, status, stop, files, setFiles, d
|
|
|
106
108
|
document.addEventListener("mousedown", handleClickOutside);
|
|
107
109
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
108
110
|
}, [modeDropdownOpen]);
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!agentPickerOpen) return;
|
|
113
|
+
const handleClickOutside = (e) => {
|
|
114
|
+
if (agentPickerRef.current && !agentPickerRef.current.contains(e.target)) {
|
|
115
|
+
setAgentPickerOpen(false);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const handleKeyDown2 = (e) => {
|
|
119
|
+
if (e.key === "Escape") setAgentPickerOpen(false);
|
|
120
|
+
};
|
|
121
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
122
|
+
document.addEventListener("keydown", handleKeyDown2);
|
|
123
|
+
return () => {
|
|
124
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
125
|
+
document.removeEventListener("keydown", handleKeyDown2);
|
|
126
|
+
};
|
|
127
|
+
}, [agentPickerOpen]);
|
|
109
128
|
const handleFiles = useCallback((fileList) => {
|
|
110
129
|
const newFiles = Array.from(fileList).filter(isAcceptedType);
|
|
111
130
|
if (newFiles.length === 0) return;
|
|
@@ -296,40 +315,65 @@ function ChatInput({ input, setInput, onSubmit, status, stop, files, setFiles, d
|
|
|
296
315
|
)
|
|
297
316
|
] })
|
|
298
317
|
] }),
|
|
299
|
-
codeModeSettings && !codeModeSettings.isInteractiveActive && /* @__PURE__ */ jsxs(
|
|
300
|
-
"
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
)
|
|
318
|
+
codeModeSettings && !codeModeSettings.isInteractiveActive && /* @__PURE__ */ jsxs("div", { className: "relative", ref: agentPickerRef, children: [
|
|
319
|
+
agentPickerOpen && codeModeSettings.availableAgents?.length > 1 && /* @__PURE__ */ jsxs("div", { className: "absolute bottom-full left-0 mb-1.5 z-50 min-w-[140px] rounded-md border border-border bg-background shadow-md py-1 overflow-hidden", children: [
|
|
320
|
+
/* @__PURE__ */ jsx("p", { className: "px-3 pt-0.5 pb-1 text-[10px] font-medium text-muted-foreground uppercase tracking-wide", children: "Launch with" }),
|
|
321
|
+
codeModeSettings.availableAgents.map((agent) => /* @__PURE__ */ jsx(
|
|
322
|
+
"button",
|
|
323
|
+
{
|
|
324
|
+
type: "button",
|
|
325
|
+
onClick: () => {
|
|
326
|
+
setAgentPickerOpen(false);
|
|
327
|
+
codeModeSettings.onInteractiveToggle(agent.value);
|
|
328
|
+
},
|
|
329
|
+
className: "w-full text-left px-3 py-1.5 text-xs text-foreground hover:bg-muted transition-colors",
|
|
330
|
+
children: agent.label
|
|
331
|
+
},
|
|
332
|
+
agent.value
|
|
333
|
+
))
|
|
334
|
+
] }),
|
|
335
|
+
/* @__PURE__ */ jsxs(
|
|
336
|
+
"button",
|
|
337
|
+
{
|
|
338
|
+
type: "button",
|
|
339
|
+
onClick: () => codeModeSettings.onInteractiveToggle(),
|
|
340
|
+
onContextMenu: (e) => {
|
|
341
|
+
e.preventDefault();
|
|
342
|
+
if (!codeModeSettings.togglingMode && codeModeSettings.availableAgents?.length > 1) {
|
|
343
|
+
setAgentPickerOpen((prev) => !prev);
|
|
327
344
|
}
|
|
328
|
-
|
|
329
|
-
codeModeSettings.togglingMode
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
345
|
+
},
|
|
346
|
+
disabled: codeModeSettings.togglingMode || codeModeSettings.isInteractiveActive,
|
|
347
|
+
title: codeModeSettings.availableAgents?.length > 1 ? "Left-click to launch \xB7 Right-click to pick agent" : void 0,
|
|
348
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors",
|
|
349
|
+
children: [
|
|
350
|
+
codeModeSettings.togglingMode && /* @__PURE__ */ jsxs("svg", { className: "animate-spin h-3 w-3", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [
|
|
351
|
+
/* @__PURE__ */ jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
|
|
352
|
+
/* @__PURE__ */ jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" })
|
|
353
|
+
] }),
|
|
354
|
+
/* @__PURE__ */ jsx(
|
|
355
|
+
"span",
|
|
356
|
+
{
|
|
357
|
+
className: cn(
|
|
358
|
+
"relative inline-flex h-3.5 w-6 shrink-0 rounded-full transition-colors duration-200",
|
|
359
|
+
codeModeSettings.isInteractiveActive ? "bg-primary" : "bg-muted-foreground/30"
|
|
360
|
+
),
|
|
361
|
+
children: /* @__PURE__ */ jsx(
|
|
362
|
+
"span",
|
|
363
|
+
{
|
|
364
|
+
className: cn(
|
|
365
|
+
"absolute top-0.5 left-0.5 h-2.5 w-2.5 rounded-full bg-white shadow-sm transition-transform duration-200",
|
|
366
|
+
codeModeSettings.isInteractiveActive && "translate-x-2.5"
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
),
|
|
372
|
+
codeModeSettings.togglingMode ? "Launching..." : "Interactive"
|
|
373
|
+
]
|
|
374
|
+
}
|
|
375
|
+
)
|
|
376
|
+
] }),
|
|
333
377
|
/* @__PURE__ */ jsx(
|
|
334
378
|
"input",
|
|
335
379
|
{
|
|
@@ -46,8 +46,10 @@ export function ChatInput({ input, setInput, onSubmit, status, stop, files, setF
|
|
|
46
46
|
const fileInputRef = useRef(null);
|
|
47
47
|
const [isDragging, setIsDragging] = useState(false);
|
|
48
48
|
const [modeDropdownOpen, setModeDropdownOpen] = useState(false);
|
|
49
|
+
const [agentPickerOpen, setAgentPickerOpen] = useState(false);
|
|
49
50
|
const [partialText, setPartialText] = useState('');
|
|
50
51
|
const dropdownRef = useRef(null);
|
|
52
|
+
const agentPickerRef = useRef(null);
|
|
51
53
|
const isStreaming = status === 'streaming' || status === 'submitted';
|
|
52
54
|
const volumeRef = useRef(0);
|
|
53
55
|
|
|
@@ -94,6 +96,25 @@ export function ChatInput({ input, setInput, onSubmit, status, stop, files, setF
|
|
|
94
96
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
95
97
|
}, [modeDropdownOpen]);
|
|
96
98
|
|
|
99
|
+
// Close agent picker on outside click or Escape
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!agentPickerOpen) return;
|
|
102
|
+
const handleClickOutside = (e) => {
|
|
103
|
+
if (agentPickerRef.current && !agentPickerRef.current.contains(e.target)) {
|
|
104
|
+
setAgentPickerOpen(false);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const handleKeyDown = (e) => {
|
|
108
|
+
if (e.key === 'Escape') setAgentPickerOpen(false);
|
|
109
|
+
};
|
|
110
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
111
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
112
|
+
return () => {
|
|
113
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
114
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
115
|
+
};
|
|
116
|
+
}, [agentPickerOpen]);
|
|
117
|
+
|
|
97
118
|
const handleFiles = useCallback((fileList) => {
|
|
98
119
|
const newFiles = Array.from(fileList).filter(isAcceptedType);
|
|
99
120
|
if (newFiles.length === 0) return;
|
|
@@ -303,35 +324,64 @@ export function ChatInput({ input, setInput, onSubmit, status, stop, files, setF
|
|
|
303
324
|
</div>
|
|
304
325
|
)}
|
|
305
326
|
|
|
306
|
-
{/* Interactive toggle
|
|
327
|
+
{/* Interactive toggle — left-click to launch with default agent,
|
|
328
|
+
right-click to pick a specific agent (when multiple are available) */}
|
|
307
329
|
{codeModeSettings && !codeModeSettings.isInteractiveActive && (
|
|
308
|
-
<
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
330
|
+
<div className="relative" ref={agentPickerRef}>
|
|
331
|
+
{/* Agent picker popup — appears above the toggle on right-click */}
|
|
332
|
+
{agentPickerOpen && codeModeSettings.availableAgents?.length > 1 && (
|
|
333
|
+
<div className="absolute bottom-full left-0 mb-1.5 z-50 min-w-[140px] rounded-md border border-border bg-background shadow-md py-1 overflow-hidden">
|
|
334
|
+
<p className="px-3 pt-0.5 pb-1 text-[10px] font-medium text-muted-foreground uppercase tracking-wide">Launch with</p>
|
|
335
|
+
{codeModeSettings.availableAgents.map(agent => (
|
|
336
|
+
<button
|
|
337
|
+
key={agent.value}
|
|
338
|
+
type="button"
|
|
339
|
+
onClick={() => {
|
|
340
|
+
setAgentPickerOpen(false);
|
|
341
|
+
codeModeSettings.onInteractiveToggle(agent.value);
|
|
342
|
+
}}
|
|
343
|
+
className="w-full text-left px-3 py-1.5 text-xs text-foreground hover:bg-muted transition-colors"
|
|
344
|
+
>
|
|
345
|
+
{agent.label}
|
|
346
|
+
</button>
|
|
347
|
+
))}
|
|
348
|
+
</div>
|
|
319
349
|
)}
|
|
320
|
-
<
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
350
|
+
<button
|
|
351
|
+
type="button"
|
|
352
|
+
onClick={() => codeModeSettings.onInteractiveToggle()}
|
|
353
|
+
onContextMenu={(e) => {
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
if (!codeModeSettings.togglingMode && codeModeSettings.availableAgents?.length > 1) {
|
|
356
|
+
setAgentPickerOpen(prev => !prev);
|
|
357
|
+
}
|
|
358
|
+
}}
|
|
359
|
+
disabled={codeModeSettings.togglingMode || codeModeSettings.isInteractiveActive}
|
|
360
|
+
title={codeModeSettings.availableAgents?.length > 1 ? 'Left-click to launch · Right-click to pick agent' : undefined}
|
|
361
|
+
className="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
325
362
|
>
|
|
363
|
+
{codeModeSettings.togglingMode && (
|
|
364
|
+
<svg className="animate-spin h-3 w-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
365
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
366
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
367
|
+
</svg>
|
|
368
|
+
)}
|
|
326
369
|
<span
|
|
327
370
|
className={cn(
|
|
328
|
-
'
|
|
329
|
-
codeModeSettings.isInteractiveActive
|
|
371
|
+
'relative inline-flex h-3.5 w-6 shrink-0 rounded-full transition-colors duration-200',
|
|
372
|
+
codeModeSettings.isInteractiveActive ? 'bg-primary' : 'bg-muted-foreground/30'
|
|
330
373
|
)}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
374
|
+
>
|
|
375
|
+
<span
|
|
376
|
+
className={cn(
|
|
377
|
+
'absolute top-0.5 left-0.5 h-2.5 w-2.5 rounded-full bg-white shadow-sm transition-transform duration-200',
|
|
378
|
+
codeModeSettings.isInteractiveActive && 'translate-x-2.5'
|
|
379
|
+
)}
|
|
380
|
+
/>
|
|
381
|
+
</span>
|
|
382
|
+
{codeModeSettings.togglingMode ? 'Launching...' : 'Interactive'}
|
|
383
|
+
</button>
|
|
384
|
+
</div>
|
|
335
385
|
)}
|
|
336
386
|
|
|
337
387
|
<input
|