thepopebot 1.2.76-beta.2 → 1.2.76-beta.21
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/README.md +3 -3
- package/api/CLAUDE.md +11 -4
- package/api/index.js +56 -18
- package/bin/CLAUDE.md +7 -4
- package/bin/cli.js +25 -45
- package/config/CLAUDE.md +23 -4
- package/drizzle/0021_coding_agent_workspace.sql +1 -0
- package/drizzle/0022_organic_apocalypse.sql +16 -0
- package/drizzle/0023_needy_ender_wiggin.sql +1 -0
- package/drizzle/meta/0021_snapshot.json +639 -0
- package/drizzle/meta/0022_snapshot.json +743 -0
- package/drizzle/meta/0023_snapshot.json +750 -0
- package/drizzle/meta/_journal.json +21 -0
- package/lib/CLAUDE.md +2 -2
- package/lib/actions.js +9 -1
- package/lib/ai/CLAUDE.md +72 -57
- package/lib/ai/helper-llm.js +108 -0
- package/lib/ai/index.js +308 -438
- package/lib/ai/line-mappers.js +42 -24
- package/lib/ai/scope.js +26 -0
- package/lib/ai/sdk-adapters/CLAUDE.md +114 -0
- package/lib/ai/sdk-adapters/claude-code.js +120 -8
- package/lib/ai/system-prompt.js +34 -0
- package/lib/ai/workspace-setup.js +19 -35
- package/lib/channels/CLAUDE.md +14 -4
- package/lib/channels/base.js +6 -2
- package/lib/channels/commands/index.js +42 -0
- package/lib/channels/commands/session.js +53 -0
- package/lib/channels/commands/verify.js +18 -0
- package/lib/channels/telegram.js +79 -28
- package/lib/chat/CLAUDE.md +4 -4
- package/lib/chat/actions.js +270 -49
- package/lib/chat/api.js +185 -31
- package/lib/chat/components/CLAUDE.md +6 -2
- package/lib/chat/components/chat-input.js +77 -47
- package/lib/chat/components/chat-input.jsx +77 -40
- package/lib/chat/components/chat-page.js +2 -0
- package/lib/chat/components/chat-page.jsx +3 -0
- package/lib/chat/components/chat.js +62 -14
- package/lib/chat/components/chat.jsx +68 -10
- package/lib/chat/components/code-mode-toggle.js +141 -22
- package/lib/chat/components/code-mode-toggle.jsx +129 -20
- package/lib/chat/components/containers-page.js +58 -40
- package/lib/chat/components/containers-page.jsx +64 -25
- package/lib/chat/components/crons-page.js +17 -3
- package/lib/chat/components/crons-page.jsx +34 -6
- package/lib/chat/components/index.js +2 -2
- package/lib/chat/components/message.js +18 -3
- package/lib/chat/components/message.jsx +18 -3
- package/lib/chat/components/profile-page.js +182 -4
- package/lib/chat/components/profile-page.jsx +196 -1
- package/lib/chat/components/scope-picker.js +21 -0
- package/lib/chat/components/scope-picker.jsx +27 -0
- package/lib/chat/components/settings-chat-page.js +11 -11
- package/lib/chat/components/settings-chat-page.jsx +14 -18
- package/lib/chat/components/settings-coding-agents-page.js +110 -16
- package/lib/chat/components/settings-coding-agents-page.jsx +87 -3
- package/lib/chat/components/settings-github-page.js +5 -0
- package/lib/chat/components/settings-github-page.jsx +5 -0
- package/lib/chat/components/settings-layout.js +3 -3
- package/lib/chat/components/settings-layout.jsx +3 -3
- package/lib/chat/components/settings-secrets-layout.js +1 -2
- package/lib/chat/components/settings-secrets-layout.jsx +1 -2
- package/lib/chat/components/settings-secrets-page.js +180 -75
- package/lib/chat/components/settings-secrets-page.jsx +212 -66
- package/lib/chat/components/triggers-page.js +17 -3
- package/lib/chat/components/triggers-page.jsx +34 -6
- package/lib/chat/components/ui/combobox.js +18 -2
- package/lib/chat/components/ui/combobox.jsx +17 -1
- package/lib/chat/components/ui/dropdown-menu.js +23 -2
- package/lib/chat/components/ui/dropdown-menu.jsx +27 -2
- package/lib/chat/telegram-profile.js +33 -0
- package/lib/cluster/CLAUDE.md +9 -3
- package/lib/code/CLAUDE.md +11 -3
- package/lib/code/actions.js +47 -8
- package/lib/code/terminal-view.js +31 -21
- package/lib/code/terminal-view.jsx +32 -23
- package/lib/config.js +15 -4
- package/lib/containers/CLAUDE.md +16 -6
- package/lib/db/CLAUDE.md +5 -2
- package/lib/db/chats.js +9 -17
- package/lib/db/code-workspaces.js +8 -3
- package/lib/db/config.js +0 -1
- package/lib/db/index.js +12 -0
- package/lib/db/schema.js +24 -1
- package/lib/db/user-channels.js +129 -0
- package/lib/llm-providers.js +8 -0
- package/lib/maintenance.js +31 -21
- package/lib/tools/CLAUDE.md +12 -3
- package/lib/tools/assemblyai.js +17 -0
- package/lib/tools/create-agent-job.js +12 -8
- package/lib/tools/docker.js +34 -10
- package/lib/tools/github.js +34 -0
- package/lib/tools/telegram.js +106 -0
- package/lib/utils/render-md.js +44 -18
- package/package.json +8 -8
- package/setup/CLAUDE.md +11 -5
- package/setup/lib/providers.mjs +2 -1
- package/setup/lib/targets.mjs +13 -16
- package/setup/lib/telegram.mjs +8 -69
- package/templates/.env.example +0 -7
- package/templates/.github/workflows/rebuild-event-handler.yml +1 -1
- package/templates/.gitignore.template +1 -3
- package/templates/CLAUDE.md +1 -1
- package/templates/CLAUDE.md.template +29 -7
- package/templates/agent-job/CLAUDE.md.template +5 -3
- package/templates/agent-job/CRONS.json +16 -0
- package/templates/agent-job/SYSTEM.md +16 -11
- package/templates/agents/CLAUDE.md.template +17 -17
- package/templates/coding-workspace/CLAUDE.md.template +7 -0
- package/templates/data/CLAUDE.md.template +1 -1
- package/templates/docker-compose.custom.yml +1 -0
- package/templates/docker-compose.yml +1 -0
- package/templates/event-handler/CLAUDE.md.template +79 -0
- package/templates/event-handler/TRIGGERS.json +18 -2
- package/templates/skills/CLAUDE.md.template +20 -22
- package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/SKILL.md +2 -2
- package/lib/ai/agent.js +0 -65
- package/lib/ai/async-channel.js +0 -51
- package/lib/ai/model.js +0 -130
- package/lib/ai/tools.js +0 -164
- package/lib/tools/openai.js +0 -37
- package/setup/lib/telegram-verify.mjs +0 -63
- package/setup/setup-telegram.mjs +0 -260
- package/templates/agent-job/SOUL.md +0 -17
- /package/templates/{skills/active/.gitkeep → coding-workspace/SYSTEM.md} +0 -0
- /package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/agent-job-secrets.js +0 -0
- /package/templates/skills/{library/playwright-cli → playwright-cli}/SKILL.md +0 -0
package/lib/chat/api.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { auth } from '../auth/index.js';
|
|
2
2
|
import { chatStream } from '../ai/index.js';
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
-
import { getConfig } from '../config.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* POST handler for /stream/chat — streaming chat with session auth.
|
|
@@ -14,7 +13,7 @@ export async function POST(request) {
|
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
const body = await request.json();
|
|
17
|
-
const { messages, chatId: rawChatId, trigger, codeMode, repo, branch, workspaceId, codeModeType } = body;
|
|
16
|
+
const { messages, chatId: rawChatId, trigger, codeMode, repo, branch, workspaceId, codeModeType, scope } = body;
|
|
18
17
|
|
|
19
18
|
if (!messages?.length) {
|
|
20
19
|
return Response.json({ error: 'No messages' }, { status: 400 });
|
|
@@ -87,6 +86,7 @@ export async function POST(request) {
|
|
|
87
86
|
codeModeType: codeModeType || 'plan',
|
|
88
87
|
};
|
|
89
88
|
if (workspaceId) streamOptions.workspaceId = workspaceId;
|
|
89
|
+
if (scope) streamOptions.scope = scope;
|
|
90
90
|
const chunks = chatStream(threadId, userText, attachments, streamOptions);
|
|
91
91
|
|
|
92
92
|
// Signal start of assistant message
|
|
@@ -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,47 +127,49 @@ 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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (chunk.toolName === 'coding_agent') {
|
|
121
|
-
const agent = getConfig('CODING_AGENT') || 'claude-code';
|
|
122
|
-
const providerKeys = {
|
|
123
|
-
'claude-code': 'CODING_AGENT_CLAUDE_CODE_BACKEND',
|
|
124
|
-
'pi-coding-agent': 'CODING_AGENT_PI_PROVIDER',
|
|
125
|
-
'gemini-cli': 'CODING_AGENT_GEMINI_CLI_PROVIDER',
|
|
126
|
-
'codex-cli': 'CODING_AGENT_CODEX_CLI_PROVIDER',
|
|
127
|
-
'opencode': 'CODING_AGENT_OPENCODE_PROVIDER',
|
|
128
|
-
'kimi-cli': 'CODING_AGENT_KIMI_CLI_PROVIDER',
|
|
129
|
-
};
|
|
130
|
-
const backendApi = getConfig(providerKeys[agent]) || 'anthropic';
|
|
131
|
-
input = { ...chunk.args, codingAgent: agent, backendApi };
|
|
130
|
+
if (!openedToolCalls.has(chunk.toolCallId)) {
|
|
131
|
+
// First time seeing this ID — open the tool block
|
|
132
|
+
if (textStarted) {
|
|
133
|
+
writer.write({ type: 'text-end', id: textId });
|
|
134
|
+
textStarted = false;
|
|
135
|
+
}
|
|
136
|
+
writer.write({
|
|
137
|
+
type: 'tool-input-start',
|
|
138
|
+
toolCallId: chunk.toolCallId,
|
|
139
|
+
toolName: chunk.toolName,
|
|
140
|
+
});
|
|
141
|
+
openedToolCalls.add(chunk.toolCallId);
|
|
132
142
|
}
|
|
143
|
+
// Always emit tool-input-available: first call shows empty args while
|
|
144
|
+
// streaming, second call (content_block_stop) updates to complete args
|
|
133
145
|
writer.write({
|
|
134
146
|
type: 'tool-input-available',
|
|
135
147
|
toolCallId: chunk.toolCallId,
|
|
136
148
|
toolName: chunk.toolName,
|
|
137
|
-
input,
|
|
149
|
+
input: chunk.args,
|
|
138
150
|
});
|
|
139
151
|
|
|
140
152
|
} else if (chunk.type === 'tool-result') {
|
|
141
|
-
|
|
142
|
-
|
|
153
|
+
if (!openedToolCalls.has(chunk.toolCallId)) {
|
|
154
|
+
// tool-result arrived with no preceding tool-call in this stream
|
|
155
|
+
// (session resume replays tool results from skipped assistant messages).
|
|
156
|
+
// Emit the required open events so the AI SDK does not throw.
|
|
157
|
+
if (textStarted) {
|
|
158
|
+
writer.write({ type: 'text-end', id: textId });
|
|
159
|
+
textStarted = false;
|
|
160
|
+
}
|
|
161
|
+
writer.write({
|
|
162
|
+
type: 'tool-input-start',
|
|
163
|
+
toolCallId: chunk.toolCallId,
|
|
164
|
+
toolName: chunk.toolName || 'unknown',
|
|
165
|
+
});
|
|
143
166
|
writer.write({
|
|
144
167
|
type: 'tool-input-available',
|
|
145
168
|
toolCallId: chunk.toolCallId,
|
|
146
|
-
toolName: chunk.toolName,
|
|
147
|
-
input: chunk.args,
|
|
169
|
+
toolName: chunk.toolName || 'unknown',
|
|
170
|
+
input: chunk.args || {},
|
|
148
171
|
});
|
|
172
|
+
openedToolCalls.add(chunk.toolCallId);
|
|
149
173
|
}
|
|
150
174
|
writer.write({
|
|
151
175
|
type: 'tool-output-available',
|
|
@@ -153,9 +177,60 @@ export async function POST(request) {
|
|
|
153
177
|
output: chunk.result,
|
|
154
178
|
});
|
|
155
179
|
|
|
180
|
+
} else if (chunk.type === 'thinking-start') {
|
|
181
|
+
// Open a new ephemeral thinking block as a pseudo-tool
|
|
182
|
+
if (textStarted) {
|
|
183
|
+
writer.write({ type: 'text-end', id: textId });
|
|
184
|
+
textStarted = false;
|
|
185
|
+
}
|
|
186
|
+
thinkingId = uuidv4();
|
|
187
|
+
thinkingText = '';
|
|
188
|
+
writer.write({
|
|
189
|
+
type: 'tool-input-start',
|
|
190
|
+
toolCallId: thinkingId,
|
|
191
|
+
toolName: '__thinking__',
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
} else if (chunk.type === 'thinking') {
|
|
195
|
+
// Accumulate and stream thinking deltas progressively
|
|
196
|
+
if (thinkingId) {
|
|
197
|
+
thinkingText += chunk.delta;
|
|
198
|
+
writer.write({
|
|
199
|
+
type: 'tool-input-available',
|
|
200
|
+
toolCallId: thinkingId,
|
|
201
|
+
toolName: '__thinking__',
|
|
202
|
+
input: thinkingText,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
} else if (chunk.type === 'thinking-end') {
|
|
207
|
+
// Close the thinking block — empty output marks it done
|
|
208
|
+
if (thinkingId) {
|
|
209
|
+
writer.write({
|
|
210
|
+
type: 'tool-output-available',
|
|
211
|
+
toolCallId: thinkingId,
|
|
212
|
+
output: '',
|
|
213
|
+
});
|
|
214
|
+
thinkingId = null;
|
|
215
|
+
thinkingText = '';
|
|
216
|
+
}
|
|
217
|
+
|
|
156
218
|
} else if (chunk.type === 'meta' || chunk.type === 'result') {
|
|
157
219
|
// Internal events — no SSE output needed
|
|
158
220
|
|
|
221
|
+
} else if (chunk.type === 'error') {
|
|
222
|
+
// Stream a typed data part so the client renders a red error message.
|
|
223
|
+
// Persisted by chatStream() as a JSON row — rehydrated in chat-page.jsx.
|
|
224
|
+
if (textStarted) {
|
|
225
|
+
writer.write({ type: 'text-end', id: textId });
|
|
226
|
+
textStarted = false;
|
|
227
|
+
}
|
|
228
|
+
writer.write({
|
|
229
|
+
type: 'data-error',
|
|
230
|
+
id: `error-${uuidv4().slice(0, 8)}`,
|
|
231
|
+
data: { message: chunk.message },
|
|
232
|
+
});
|
|
233
|
+
|
|
159
234
|
} else if (chunk.type === 'unknown') {
|
|
160
235
|
// Close any open text block before unknown event
|
|
161
236
|
if (textStarted) {
|
|
@@ -449,6 +524,28 @@ export async function getRepositoriesHandler() {
|
|
|
449
524
|
}
|
|
450
525
|
}
|
|
451
526
|
|
|
527
|
+
/**
|
|
528
|
+
* POST handler for /code/repositories/create — create a new GitHub repository.
|
|
529
|
+
*/
|
|
530
|
+
export async function createRepositoryHandler(request) {
|
|
531
|
+
const session = await auth();
|
|
532
|
+
if (!session?.user?.id) {
|
|
533
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const { name } = await request.json();
|
|
537
|
+
if (!name || typeof name !== 'string') {
|
|
538
|
+
return Response.json({ error: 'Repository name is required' }, { status: 400 });
|
|
539
|
+
}
|
|
540
|
+
const { createRepository } = await import('../tools/github.js');
|
|
541
|
+
const repo = await createRepository(name.trim());
|
|
542
|
+
return Response.json(repo);
|
|
543
|
+
} catch (err) {
|
|
544
|
+
const message = err.message || 'Failed to create repository';
|
|
545
|
+
return Response.json({ error: message }, { status: 422 });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
452
549
|
/**
|
|
453
550
|
* GET handler for /code/branches?repo=owner/name — list branches with session auth.
|
|
454
551
|
*/
|
|
@@ -469,6 +566,26 @@ export async function getBranchesHandler(request) {
|
|
|
469
566
|
}
|
|
470
567
|
}
|
|
471
568
|
|
|
569
|
+
/**
|
|
570
|
+
* GET handler for /code/default-branch?repo=owner/name — repo's default branch.
|
|
571
|
+
*/
|
|
572
|
+
export async function getDefaultBranchHandler(request) {
|
|
573
|
+
const session = await auth();
|
|
574
|
+
if (!session?.user?.id) {
|
|
575
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
576
|
+
}
|
|
577
|
+
const url = new URL(request.url);
|
|
578
|
+
const repoFullName = url.searchParams.get('repo');
|
|
579
|
+
if (!repoFullName) return Response.json({ branch: null });
|
|
580
|
+
try {
|
|
581
|
+
const { getDefaultBranch } = await import('../tools/github.js');
|
|
582
|
+
const branch = await getDefaultBranch(repoFullName);
|
|
583
|
+
return Response.json({ branch });
|
|
584
|
+
} catch {
|
|
585
|
+
return Response.json({ branch: null });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
472
589
|
/**
|
|
473
590
|
* GET handler for /chat/voice-token — AssemblyAI temporary token with session auth.
|
|
474
591
|
*/
|
|
@@ -493,6 +610,43 @@ export async function getVoiceTokenHandler(request) {
|
|
|
493
610
|
return Response.json({ token: data.token });
|
|
494
611
|
}
|
|
495
612
|
|
|
613
|
+
/**
|
|
614
|
+
* GET handler for /chat/scopes — list available agent scopes (subdirectories in agents/).
|
|
615
|
+
* Returns an array of { name, path } objects.
|
|
616
|
+
*/
|
|
617
|
+
export async function getScopesHandler() {
|
|
618
|
+
const session = await auth();
|
|
619
|
+
if (!session?.user?.id) {
|
|
620
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
const { readdirSync, statSync } = await import('fs');
|
|
624
|
+
const { join } = await import('path');
|
|
625
|
+
const { PROJECT_ROOT } = await import('../paths.js');
|
|
626
|
+
const agentsDir = join(PROJECT_ROOT, 'agents');
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
const entries = readdirSync(agentsDir, { withFileTypes: true });
|
|
630
|
+
const scopes = entries
|
|
631
|
+
.filter(e => e.isDirectory() || e.isSymbolicLink())
|
|
632
|
+
.filter(e => {
|
|
633
|
+
// Verify symlinks resolve to directories
|
|
634
|
+
if (e.isSymbolicLink()) {
|
|
635
|
+
try { return statSync(join(agentsDir, e.name)).isDirectory(); } catch { return false; }
|
|
636
|
+
}
|
|
637
|
+
return true;
|
|
638
|
+
})
|
|
639
|
+
.map(e => ({ name: e.name, path: `agents/${e.name}` }));
|
|
640
|
+
return Response.json(scopes);
|
|
641
|
+
} catch (err) {
|
|
642
|
+
if (err.code === 'ENOENT') return Response.json([]);
|
|
643
|
+
throw err;
|
|
644
|
+
}
|
|
645
|
+
} catch {
|
|
646
|
+
return Response.json([]);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
496
650
|
export async function finalizeChat(request) {
|
|
497
651
|
const session = await auth();
|
|
498
652
|
if (!session?.user?.id) {
|
|
@@ -8,8 +8,8 @@ Admin pages live under `/admin/` with two top-level sections:
|
|
|
8
8
|
|
|
9
9
|
- **`/admin/event-handler/`** — Event handler config with pill-style sub-tabs via `SubTabLayout` (`settings-secrets-layout.jsx`):
|
|
10
10
|
- `/admin/event-handler/llms` — LLM provider credentials
|
|
11
|
-
- `/admin/event-handler/chat` — Chat LLM settings
|
|
12
11
|
- `/admin/event-handler/coding-agents` — Multi-agent config (5 backends)
|
|
12
|
+
- `/admin/event-handler/helper-llm` — Helper LLM settings (provider/model used for chat titles, agent-job titles + summaries)
|
|
13
13
|
- `/admin/event-handler/jobs` — Agent job custom secrets
|
|
14
14
|
- `/admin/event-handler/telegram` — Telegram integration
|
|
15
15
|
- `/admin/event-handler/voice` — Voice input (AssemblyAI)
|
|
@@ -29,7 +29,11 @@ Admin pages live under `/admin/` with two top-level sections:
|
|
|
29
29
|
|
|
30
30
|
`tool-names.js` auto-generates display names from the tool's snake_case name (split on `_`, capitalize each word). No map to maintain — adding a new tool automatically gets a display name.
|
|
31
31
|
|
|
32
|
-
This file is **UI-only** — it controls display text
|
|
32
|
+
This file is **UI-only** — it controls display text. There is no host-side tool registry; every tool name in the UI comes from the coding agent's own stream (e.g. `Read`, `Bash`, `Edit`) plus the synthetic `workspace` setup tool emitted by `chatStream()`.
|
|
33
|
+
|
|
34
|
+
## Error Messages
|
|
35
|
+
|
|
36
|
+
`chatStream()` emits `{ type: 'error', message }` on failure. `lib/chat/api.js` writes it as an AI SDK `data-error` part; `chat-page.jsx` rehydrates stored errors into the same part shape on refresh; `message.jsx` renders `data-error` parts as a red banner using `text-destructive` / `border-destructive/30 bg-destructive/5`. Errors persist to the `messages` table as JSON (`{"type":"error","message":"..."}`).
|
|
33
37
|
|
|
34
38
|
## Settings UI Standards
|
|
35
39
|
|
|
@@ -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;
|
|
@@ -127,6 +146,7 @@ function ChatInput({ input, setInput, onSubmit, status, stop, files, setFiles, d
|
|
|
127
146
|
if (e) e.preventDefault();
|
|
128
147
|
if (disabled || !input.trim() && !partialText.trim() && files.length === 0 || isStreaming) return;
|
|
129
148
|
if (canSendOverride !== void 0 && !canSendOverride) return;
|
|
149
|
+
if (isRecording) stopRecording();
|
|
130
150
|
if (partialText) {
|
|
131
151
|
const needsSpace = input && !input.endsWith(" ");
|
|
132
152
|
setInput(input + (needsSpace ? " " : "") + partialText);
|
|
@@ -240,10 +260,10 @@ function ChatInput({ input, setInput, onSubmit, status, stop, files, setFiles, d
|
|
|
240
260
|
onClick: () => setModeDropdownOpen((prev) => !prev),
|
|
241
261
|
className: cn(
|
|
242
262
|
"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors",
|
|
243
|
-
codeModeSettings.mode === "code" ? "bg-green-500/15 text-green-500 hover:bg-green-500/25" :
|
|
263
|
+
codeModeSettings.mode === "code" ? "bg-green-500/15 text-green-500 hover:bg-green-500/25" : "bg-destructive/10 text-destructive hover:bg-destructive/20"
|
|
244
264
|
),
|
|
245
265
|
children: [
|
|
246
|
-
codeModeSettings.mode === "code" ? "Code" :
|
|
266
|
+
codeModeSettings.mode === "code" ? "Code" : "Plan",
|
|
247
267
|
" \u25BE"
|
|
248
268
|
]
|
|
249
269
|
}
|
|
@@ -278,58 +298,68 @@ function ChatInput({ input, setInput, onSubmit, status, stop, files, setFiles, d
|
|
|
278
298
|
),
|
|
279
299
|
children: "Code"
|
|
280
300
|
}
|
|
281
|
-
)
|
|
282
|
-
|
|
301
|
+
)
|
|
302
|
+
] })
|
|
303
|
+
] }),
|
|
304
|
+
codeModeSettings && !codeModeSettings.isInteractiveActive && /* @__PURE__ */ jsxs("div", { className: "relative", ref: agentPickerRef, children: [
|
|
305
|
+
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: [
|
|
306
|
+
/* @__PURE__ */ jsx("p", { className: "px-3 pt-0.5 pb-1 text-[10px] font-medium text-muted-foreground uppercase tracking-wide", children: "Launch with" }),
|
|
307
|
+
codeModeSettings.availableAgents.map((agent) => /* @__PURE__ */ jsx(
|
|
283
308
|
"button",
|
|
284
309
|
{
|
|
285
310
|
type: "button",
|
|
286
311
|
onClick: () => {
|
|
287
|
-
|
|
288
|
-
|
|
312
|
+
setAgentPickerOpen(false);
|
|
313
|
+
codeModeSettings.onInteractiveToggle(agent.value);
|
|
289
314
|
},
|
|
290
|
-
className:
|
|
291
|
-
|
|
292
|
-
|
|
315
|
+
className: "w-full text-left px-3 py-1.5 text-xs text-foreground hover:bg-muted transition-colors",
|
|
316
|
+
children: agent.label
|
|
317
|
+
},
|
|
318
|
+
agent.value
|
|
319
|
+
))
|
|
320
|
+
] }),
|
|
321
|
+
/* @__PURE__ */ jsxs(
|
|
322
|
+
"button",
|
|
323
|
+
{
|
|
324
|
+
type: "button",
|
|
325
|
+
onClick: () => codeModeSettings.onInteractiveToggle(),
|
|
326
|
+
onContextMenu: (e) => {
|
|
327
|
+
e.preventDefault();
|
|
328
|
+
if (!codeModeSettings.togglingMode && codeModeSettings.availableAgents?.length > 1 && codeModeSettings.hasMessages) {
|
|
329
|
+
setAgentPickerOpen((prev) => !prev);
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
disabled: codeModeSettings.togglingMode || codeModeSettings.isInteractiveActive,
|
|
333
|
+
title: codeModeSettings.availableAgents?.length > 1 && codeModeSettings.hasMessages ? "Left-click to launch \xB7 Right-click to pick agent" : void 0,
|
|
334
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors",
|
|
335
|
+
children: [
|
|
336
|
+
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: [
|
|
337
|
+
/* @__PURE__ */ jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
|
|
338
|
+
/* @__PURE__ */ jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" })
|
|
339
|
+
] }),
|
|
340
|
+
/* @__PURE__ */ jsx(
|
|
341
|
+
"span",
|
|
342
|
+
{
|
|
343
|
+
className: cn(
|
|
344
|
+
"relative inline-flex h-3.5 w-6 shrink-0 rounded-full transition-colors duration-200",
|
|
345
|
+
codeModeSettings.isInteractiveActive ? "bg-primary" : "bg-muted-foreground/30"
|
|
346
|
+
),
|
|
347
|
+
children: /* @__PURE__ */ jsx(
|
|
348
|
+
"span",
|
|
349
|
+
{
|
|
350
|
+
className: cn(
|
|
351
|
+
"absolute top-0.5 left-0.5 h-2.5 w-2.5 rounded-full bg-white shadow-sm transition-transform duration-200",
|
|
352
|
+
codeModeSettings.isInteractiveActive && "translate-x-2.5"
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
}
|
|
293
357
|
),
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
358
|
+
codeModeSettings.togglingMode ? "Launching..." : "Interactive"
|
|
359
|
+
]
|
|
360
|
+
}
|
|
361
|
+
)
|
|
298
362
|
] }),
|
|
299
|
-
codeModeSettings && !codeModeSettings.isInteractiveActive && /* @__PURE__ */ jsxs(
|
|
300
|
-
"button",
|
|
301
|
-
{
|
|
302
|
-
type: "button",
|
|
303
|
-
onClick: codeModeSettings.onInteractiveToggle,
|
|
304
|
-
disabled: codeModeSettings.togglingMode || codeModeSettings.isInteractiveActive,
|
|
305
|
-
className: "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors",
|
|
306
|
-
children: [
|
|
307
|
-
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: [
|
|
308
|
-
/* @__PURE__ */ jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
|
|
309
|
-
/* @__PURE__ */ jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" })
|
|
310
|
-
] }),
|
|
311
|
-
/* @__PURE__ */ jsx(
|
|
312
|
-
"span",
|
|
313
|
-
{
|
|
314
|
-
className: cn(
|
|
315
|
-
"relative inline-flex h-3.5 w-6 shrink-0 rounded-full transition-colors duration-200",
|
|
316
|
-
codeModeSettings.isInteractiveActive ? "bg-primary" : "bg-muted-foreground/30"
|
|
317
|
-
),
|
|
318
|
-
children: /* @__PURE__ */ jsx(
|
|
319
|
-
"span",
|
|
320
|
-
{
|
|
321
|
-
className: cn(
|
|
322
|
-
"absolute top-0.5 left-0.5 h-2.5 w-2.5 rounded-full bg-white shadow-sm transition-transform duration-200",
|
|
323
|
-
codeModeSettings.isInteractiveActive && "translate-x-2.5"
|
|
324
|
-
)
|
|
325
|
-
}
|
|
326
|
-
)
|
|
327
|
-
}
|
|
328
|
-
),
|
|
329
|
-
codeModeSettings.togglingMode ? "Launching..." : "Interactive"
|
|
330
|
-
]
|
|
331
|
-
}
|
|
332
|
-
),
|
|
333
363
|
/* @__PURE__ */ jsx(
|
|
334
364
|
"input",
|
|
335
365
|
{
|
|
@@ -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;
|
|
@@ -119,6 +140,7 @@ export function ChatInput({ input, setInput, onSubmit, status, stop, files, setF
|
|
|
119
140
|
if (e) e.preventDefault();
|
|
120
141
|
if (disabled || (!input.trim() && !partialText.trim() && files.length === 0) || isStreaming) return;
|
|
121
142
|
if (canSendOverride !== undefined && !canSendOverride) return;
|
|
143
|
+
if (isRecording) stopRecording();
|
|
122
144
|
if (partialText) {
|
|
123
145
|
const needsSpace = input && !input.endsWith(' ');
|
|
124
146
|
setInput(input + (needsSpace ? ' ' : '') + partialText);
|
|
@@ -247,7 +269,7 @@ export function ChatInput({ input, setInput, onSubmit, status, stop, files, setF
|
|
|
247
269
|
<PaperclipIcon size={16} />
|
|
248
270
|
</button>
|
|
249
271
|
|
|
250
|
-
{/* Plan/Code
|
|
272
|
+
{/* Plan/Code dropdown */}
|
|
251
273
|
{codeModeSettings && (
|
|
252
274
|
<div className="relative" ref={dropdownRef}>
|
|
253
275
|
<button
|
|
@@ -257,12 +279,10 @@ export function ChatInput({ input, setInput, onSubmit, status, stop, files, setF
|
|
|
257
279
|
'inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors',
|
|
258
280
|
codeModeSettings.mode === 'code'
|
|
259
281
|
? 'bg-green-500/15 text-green-500 hover:bg-green-500/25'
|
|
260
|
-
:
|
|
261
|
-
? 'bg-blue-500/15 text-blue-500 hover:bg-blue-500/25'
|
|
262
|
-
: 'bg-destructive/10 text-destructive hover:bg-destructive/20'
|
|
282
|
+
: 'bg-destructive/10 text-destructive hover:bg-destructive/20'
|
|
263
283
|
)}
|
|
264
284
|
>
|
|
265
|
-
{codeModeSettings.mode === 'code' ? 'Code' :
|
|
285
|
+
{codeModeSettings.mode === 'code' ? 'Code' : 'Plan'} ▾
|
|
266
286
|
</button>
|
|
267
287
|
{modeDropdownOpen && (
|
|
268
288
|
<div className="absolute bottom-full left-0 mb-1 rounded-lg border border-border bg-background shadow-lg py-1 min-w-[100px] z-50">
|
|
@@ -286,52 +306,69 @@ export function ChatInput({ input, setInput, onSubmit, status, stop, files, setF
|
|
|
286
306
|
>
|
|
287
307
|
Code
|
|
288
308
|
</button>
|
|
289
|
-
{!codeMode && (
|
|
290
|
-
<button
|
|
291
|
-
type="button"
|
|
292
|
-
onClick={() => { codeModeSettings.onModeChange('job'); setModeDropdownOpen(false); }}
|
|
293
|
-
className={cn(
|
|
294
|
-
'w-full text-left px-3 py-1.5 text-xs hover:bg-muted transition-colors',
|
|
295
|
-
codeModeSettings.mode === 'job' ? 'text-blue-500 font-medium' : 'text-foreground'
|
|
296
|
-
)}
|
|
297
|
-
>
|
|
298
|
-
Job
|
|
299
|
-
</button>
|
|
300
|
-
)}
|
|
301
309
|
</div>
|
|
302
310
|
)}
|
|
303
311
|
</div>
|
|
304
312
|
)}
|
|
305
313
|
|
|
306
|
-
{/* Interactive toggle
|
|
314
|
+
{/* Interactive toggle — left-click to launch with default agent,
|
|
315
|
+
right-click to pick a specific agent (when multiple are available) */}
|
|
307
316
|
{codeModeSettings && !codeModeSettings.isInteractiveActive && (
|
|
308
|
-
<
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
317
|
+
<div className="relative" ref={agentPickerRef}>
|
|
318
|
+
{/* Agent picker popup — appears above the toggle on right-click */}
|
|
319
|
+
{agentPickerOpen && codeModeSettings.availableAgents?.length > 1 && (
|
|
320
|
+
<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">
|
|
321
|
+
<p className="px-3 pt-0.5 pb-1 text-[10px] font-medium text-muted-foreground uppercase tracking-wide">Launch with</p>
|
|
322
|
+
{codeModeSettings.availableAgents.map(agent => (
|
|
323
|
+
<button
|
|
324
|
+
key={agent.value}
|
|
325
|
+
type="button"
|
|
326
|
+
onClick={() => {
|
|
327
|
+
setAgentPickerOpen(false);
|
|
328
|
+
codeModeSettings.onInteractiveToggle(agent.value);
|
|
329
|
+
}}
|
|
330
|
+
className="w-full text-left px-3 py-1.5 text-xs text-foreground hover:bg-muted transition-colors"
|
|
331
|
+
>
|
|
332
|
+
{agent.label}
|
|
333
|
+
</button>
|
|
334
|
+
))}
|
|
335
|
+
</div>
|
|
319
336
|
)}
|
|
320
|
-
<
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
onClick={() => codeModeSettings.onInteractiveToggle()}
|
|
340
|
+
onContextMenu={(e) => {
|
|
341
|
+
e.preventDefault();
|
|
342
|
+
if (!codeModeSettings.togglingMode && codeModeSettings.availableAgents?.length > 1 && codeModeSettings.hasMessages) {
|
|
343
|
+
setAgentPickerOpen(prev => !prev);
|
|
344
|
+
}
|
|
345
|
+
}}
|
|
346
|
+
disabled={codeModeSettings.togglingMode || codeModeSettings.isInteractiveActive}
|
|
347
|
+
title={codeModeSettings.availableAgents?.length > 1 && codeModeSettings.hasMessages ? 'Left-click to launch · Right-click to pick agent' : undefined}
|
|
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"
|
|
325
349
|
>
|
|
350
|
+
{codeModeSettings.togglingMode && (
|
|
351
|
+
<svg className="animate-spin h-3 w-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
352
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
353
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
354
|
+
</svg>
|
|
355
|
+
)}
|
|
326
356
|
<span
|
|
327
357
|
className={cn(
|
|
328
|
-
'
|
|
329
|
-
codeModeSettings.isInteractiveActive
|
|
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'
|
|
330
360
|
)}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
361
|
+
>
|
|
362
|
+
<span
|
|
363
|
+
className={cn(
|
|
364
|
+
'absolute top-0.5 left-0.5 h-2.5 w-2.5 rounded-full bg-white shadow-sm transition-transform duration-200',
|
|
365
|
+
codeModeSettings.isInteractiveActive && 'translate-x-2.5'
|
|
366
|
+
)}
|
|
367
|
+
/>
|
|
368
|
+
</span>
|
|
369
|
+
{codeModeSettings.togglingMode ? 'Launching...' : 'Interactive'}
|
|
370
|
+
</button>
|
|
371
|
+
</div>
|
|
335
372
|
)}
|
|
336
373
|
|
|
337
374
|
<input
|