|
30 | 30 | import vibecodingSvg from '$lib/assets/illustrations/vibecoding.svg?raw'; |
31 | 31 | import { useAvailabilityChecking } from '$lib/codegen/availabilityChecking.svelte'; |
32 | 32 | import { CLAUDE_CODE_SERVICE } from '$lib/codegen/claude'; |
| 33 | + import { useSendMessage } from '$lib/codegen/messageQueue.svelte'; |
| 34 | + import { messageQueueSelectors, messageQueueSlice } from '$lib/codegen/messageQueueSlice'; |
33 | 35 | import { |
34 | 36 | currentStatus, |
35 | 37 | formatMessages, |
|
49 | 51 | import { RULES_SERVICE } from '$lib/rules/rulesService.svelte'; |
50 | 52 | import { createWorktreeSelection } from '$lib/selection/key'; |
51 | 53 | import { SETTINGS } from '$lib/settings/userSettings'; |
52 | | - import { CODEGEN_ANALYTICS } from '$lib/soup/codegenAnalytics'; |
53 | 54 | import { pushStatusToColor, pushStatusToIcon } from '$lib/stacks/stack'; |
54 | 55 | import { STACK_SERVICE } from '$lib/stacks/stackService.svelte'; |
| 56 | + import { CLIENT_STATE } from '$lib/state/clientState.svelte'; |
55 | 57 | import { combineResults } from '$lib/state/helpers'; |
56 | 58 | import { UI_STATE } from '$lib/state/uiState.svelte'; |
57 | 59 | import { USER } from '$lib/user/user'; |
58 | 60 | import { createBranchRef } from '$lib/utils/branch'; |
59 | 61 | import { getEditorUri, URL_SERVICE } from '$lib/utils/url'; |
60 | 62 | import { inject } from '@gitbutler/core/context'; |
| 63 | + import { reactive } from '@gitbutler/shared/reactiveUtils.svelte'; |
61 | 64 | import { |
62 | 65 | Badge, |
63 | 66 | Button, |
|
89 | 92 | const stackService = inject(STACK_SERVICE); |
90 | 93 | const projectsService = inject(PROJECTS_SERVICE); |
91 | 94 | const rulesService = inject(RULES_SERVICE); |
92 | | - const codegenAnalytics = inject(CODEGEN_ANALYTICS); |
93 | 95 | const uiState = inject(UI_STATE); |
94 | 96 | const user = inject(USER); |
95 | 97 | const urlService = inject(URL_SERVICE); |
96 | 98 | const userSettings = inject(SETTINGS); |
97 | 99 | const settingsService = inject(SETTINGS_SERVICE); |
| 100 | + const clientState = inject(CLIENT_STATE); |
98 | 101 | const claudeSettings = $derived($settingsService?.claude); |
99 | 102 |
|
100 | 103 | const stacks = $derived(stackService.stacks(projectId)); |
|
108 | 111 | aiRules.some((rule) => rule.action.subject.subject.target.subject === stack.id) |
109 | 112 | ); |
110 | 113 | }); |
111 | | - const [sendClaudeMessage] = claudeCodeService.sendMessage; |
112 | 114 | const mcpConfig = $derived(claudeCodeService.mcpConfig({ projectId })); |
113 | 115 |
|
114 | 116 | let settingsModal: ClaudeCodeSettingsModal | undefined; |
|
164 | 166 | selectedBranch?.stackId ? uiState.lane(selectedBranch.stackId) : undefined |
165 | 167 | ); |
166 | 168 |
|
167 | | - const prompt = $derived( |
168 | | - selectedBranch ? uiState.lane(selectedBranch.stackId).prompt.current : '' |
169 | | - ); |
170 | | - function setPrompt(prompt: string) { |
171 | | - if (!selectedBranch) return; |
172 | | - uiState.lane(selectedBranch.stackId).prompt.set(prompt); |
173 | | - } |
174 | | -
|
175 | 169 | // File list data |
176 | 170 | const branchChanges = $derived( |
177 | 171 | selectedBranch |
|
216 | 210 | } |
217 | 211 | } |
218 | 212 |
|
219 | | - async function sendMessage() { |
220 | | - if (!selectedBranch) return; |
221 | | - if (!prompt) return; |
222 | | -
|
223 | | - if (prompt.startsWith('/compact')) { |
224 | | - compactContext(); |
225 | | - return; |
226 | | - } |
227 | | -
|
228 | | - // Handle /add-dir command |
229 | | - if (prompt.startsWith('/add-dir ')) { |
230 | | - const path = prompt.slice('/add-dir '.length).trim(); |
231 | | - if (path) { |
232 | | - const isValid = await claudeCodeService.verifyPath({ projectId, path }); |
233 | | - if (isValid) { |
234 | | - laneState?.addedDirs.add(path); |
235 | | - chipToasts.success(`Added directory: ${path}`); |
236 | | - } else { |
237 | | - chipToasts.error(`Invalid directory path: ${path}`); |
238 | | - } |
239 | | - } |
240 | | - setPrompt(''); |
241 | | - return; |
242 | | - } |
243 | | -
|
244 | | - if (prompt.startsWith('/')) { |
245 | | - chipToasts.warning('Slash commands are not yet supported'); |
246 | | - setPrompt(''); |
247 | | - return; |
248 | | - } |
249 | | -
|
250 | | - // Await analytics data before sending message |
251 | | - const analyticsProperties = await codegenAnalytics.getCodegenProperties({ |
252 | | - projectId, |
253 | | - stackId: selectedBranch.stackId, |
254 | | - message: prompt, |
255 | | - thinkingLevel: selectedThinkingLevel, |
256 | | - model: selectedModel |
257 | | - }); |
258 | | -
|
259 | | - const promise = sendClaudeMessage( |
260 | | - { |
261 | | - projectId, |
262 | | - stackId: selectedBranch.stackId, |
263 | | - message: prompt, |
264 | | - thinkingLevel: selectedThinkingLevel, |
265 | | - model: selectedModel, |
266 | | - permissionMode: selectedPermissionMode, |
267 | | - disabledMcpServers: uiState.lane(selectedBranch.stackId).disabledMcpServers.current, |
268 | | - addDirs: laneState?.addedDirs.current || [] |
269 | | - }, |
270 | | - { properties: analyticsProperties } |
271 | | - ); |
272 | | -
|
273 | | - setPrompt(''); |
274 | | - await promise; |
275 | | - } |
276 | | -
|
277 | 213 | async function onApproval(id: string) { |
278 | 214 | await claudeCodeService.updatePermissionRequest({ projectId, requestId: id, approval: true }); |
279 | 215 | } |
|
343 | 279 | } |
344 | 280 | } |
345 | 281 |
|
| 282 | + const { prompt, setPrompt, sendMessage } = useSendMessage({ |
| 283 | + projectId: reactive(() => projectId), |
| 284 | + selectedBranch: reactive(() => selectedBranch), |
| 285 | + thinkingLevel: reactive(() => selectedThinkingLevel), |
| 286 | + model: reactive(() => selectedModel), |
| 287 | + permissionMode: reactive(() => selectedPermissionMode) |
| 288 | + }); |
| 289 | +
|
346 | 290 | function insertTemplate(template: string) { |
347 | 291 | setPrompt(prompt + (prompt ? '\n\n' : '') + template); |
348 | 292 | templateContextMenu?.close(); |
|
454 | 398 | cyclePermissionMode(); |
455 | 399 | } |
456 | 400 | } |
| 401 | +
|
| 402 | + const queue = $derived( |
| 403 | + messageQueueSelectors |
| 404 | + .selectAll(clientState.messageQueue) |
| 405 | + .find( |
| 406 | + (q) => |
| 407 | + q.head === selectedBranch?.head && |
| 408 | + q.stackId === selectedBranch?.stackId && |
| 409 | + q.projectId === projectId |
| 410 | + ) |
| 411 | + ); |
457 | 412 | </script> |
458 | 413 |
|
459 | 414 | <svelte:window onkeydown={handleKeydown} /> |
|
668 | 623 | {#if claudeAvailable.response?.status === 'available'} |
669 | 624 | {@const status = currentStatus(events, isStackActive)} |
670 | 625 | <CodegenInput |
671 | | - value={prompt} |
| 626 | + value={prompt.current} |
672 | 627 | onChange={(prompt) => setPrompt(prompt)} |
673 | 628 | loading={['running', 'compacting'].includes(status)} |
674 | 629 | compacting={status === 'compacting'} |
|
753 | 708 |
|
754 | 709 | {#snippet rightSidebar(events: ClaudeMessage[])} |
755 | 710 | {@const addedDirs = laneState?.addedDirs.current || []} |
| 711 | + {@const queueLength = queue?.messages.length || 0} |
756 | 712 | <div class="right-sidebar" bind:this={rightSidebarRef}> |
757 | | - {#if !branchChanges || !selectedBranch || (branchChanges.response && branchChanges.response.changes.length === 0 && getTodos(events).length === 0 && addedDirs.length === 0)} |
| 713 | + {#if !branchChanges || !selectedBranch || (branchChanges.response && branchChanges.response.changes.length === 0 && getTodos(events).length === 0 && addedDirs.length === 0 && queueLength === 0)} |
758 | 714 | <div class="right-sidebar__placeholder"> |
759 | 715 | <EmptyStatePlaceholder |
760 | 716 | image={filesAndChecksSvg} |
|
773 | 729 | <ReduxResult result={branchChanges.result} {projectId}> |
774 | 730 | {#snippet children({ changes }, { projectId })} |
775 | 731 | <Drawer |
776 | | - bottomBorder={todos.length > 0 || addedDirs.length > 0} |
| 732 | + bottomBorder={todos.length > 0 || addedDirs.length > 0 || queueLength > 0} |
777 | 733 | grow |
778 | 734 | defaultCollapsed={todos.length > 0} |
779 | 735 | notFoldable |
|
811 | 767 | {/if} |
812 | 768 |
|
813 | 769 | {#if todos.length > 0} |
814 | | - <Drawer defaultCollapsed={false} noshrink> |
| 770 | + <Drawer |
| 771 | + defaultCollapsed={false} |
| 772 | + noshrink |
| 773 | + bottomBorder={addedDirs.length > 0 || queueLength > 0} |
| 774 | + > |
815 | 775 | {#snippet header()} |
816 | 776 | <h4 class="text-14 text-semibold truncate">Todos</h4> |
817 | 777 | <Badge>{todos.length}</Badge> |
|
826 | 786 | {/if} |
827 | 787 |
|
828 | 788 | {#if addedDirs.length > 0} |
829 | | - <Drawer defaultCollapsed={false} noshrink> |
| 789 | + <Drawer defaultCollapsed={false} noshrink bottomBorder={queueLength > 0}> |
830 | 790 | {#snippet header()} |
831 | 791 | <h4 class="text-14 text-semibold truncate">Added Directories</h4> |
832 | 792 | <Badge>{addedDirs.length}</Badge> |
|
853 | 813 | </div> |
854 | 814 | </Drawer> |
855 | 815 | {/if} |
| 816 | + |
| 817 | + {#if queue && queue.messages.length > 0} |
| 818 | + <Drawer defaultCollapsed={false} noshrink> |
| 819 | + {#snippet header()} |
| 820 | + <h4 class="text-14 text-semibold truncate">Queued Message</h4> |
| 821 | + {/snippet} |
| 822 | + |
| 823 | + <div class="right-sidebar-list right-sidebar-list--small-gap"> |
| 824 | + {#each queue.messages as message} |
| 825 | + <div class="message-queue-item"> |
| 826 | + <span class="text-13 grow-1 message-queue-item-text">{message.prompt}</span> |
| 827 | + <Button |
| 828 | + kind="ghost" |
| 829 | + icon="bin" |
| 830 | + shrinkable |
| 831 | + onclick={() => { |
| 832 | + if (selectedBranch) { |
| 833 | + clientState.dispatch( |
| 834 | + messageQueueSlice.actions.upsert({ |
| 835 | + ...queue, |
| 836 | + messages: queue.messages.filter((m) => m !== message) |
| 837 | + }) |
| 838 | + ); |
| 839 | + } |
| 840 | + }} |
| 841 | + tooltip="Remove prompt from queue" |
| 842 | + /> |
| 843 | + </div> |
| 844 | + {/each} |
| 845 | + </div> |
| 846 | + </Drawer> |
| 847 | + {/if} |
856 | 848 | {/if} |
857 | 849 |
|
858 | 850 | <Resizer |
|
926 | 918 | projectId, |
927 | 919 | stackId |
928 | 920 | })} |
929 | | - {@const sidebarIsStackActive = claudeCodeService.isStackActive(projectId, stackId)} |
| 921 | + {@const isActive = claudeCodeService.isStackActive(projectId, stackId)} |
930 | 922 | {@const rule = rulesService.aiRuleForStack({ projectId, stackId })} |
931 | 923 |
|
932 | 924 | <ReduxResult |
|
935 | 927 | commits.result, |
936 | 928 | branchDetails.result, |
937 | 929 | events.result, |
938 | | - sidebarIsStackActive.result, |
| 930 | + isActive.result, |
939 | 931 | rule.result |
940 | 932 | )} |
941 | 933 | {projectId} |
|
1054 | 1046 | <div class="not-available"> |
1055 | 1047 | <DecorativeSplitView hideDetails img={vibecodingSvg}> |
1056 | 1048 | <div class="not-available__content"> |
1057 | | - <h1 class="text-serif-42">Set up <i>Claude Code</i></h1> |
| 1049 | + <h1 class="text-serif-40">Set up <i>Claude Code</i></h1> |
1058 | 1050 | <ClaudeCheck |
1059 | 1051 | claudeExecutable={claudeExecutable.current} |
1060 | 1052 | recheckedAvailability={recheckedAvailability.current} |
|
1293 | 1285 | justify-content: space-between; |
1294 | 1286 | } |
1295 | 1287 |
|
| 1288 | + .message-queue-item { |
| 1289 | + display: flex; |
| 1290 | + align-items: center; |
| 1291 | + justify-content: space-between; |
| 1292 | + } |
| 1293 | +
|
| 1294 | + .message-queue-item-text { |
| 1295 | + overflow: hidden; |
| 1296 | + text-overflow: ellipsis; |
| 1297 | + white-space: nowrap; |
| 1298 | + } |
| 1299 | +
|
1296 | 1300 | .right-sidebar-list--small-gap { |
1297 | 1301 | gap: 4px; |
1298 | 1302 | } |
|
0 commit comments