Skip to content

Commit 6b1b603

Browse files
Merge pull request #10580 from gitbutlerapp/message-queue
Boiler plate & useSendMessage
2 parents 9b60f81 + 7735568 commit 6b1b603

File tree

11 files changed

+470
-99
lines changed

11 files changed

+470
-99
lines changed

apps/desktop/src/components/codegen/CodegenInput.svelte

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
if (e.key === 'Enter' && !e.shiftKey) {
6767
e.preventDefault();
6868
69-
if (loading || value.trim().length === 0) return;
69+
if (value.trim().length === 0) return;
7070
7171
await handleSubmit();
7272
}
@@ -125,15 +125,14 @@
125125
<button
126126
class="send-button"
127127
type="button"
128-
disabled={loading || value.trim().length === 0}
128+
disabled={value.trim().length === 0}
129129
class:loading
130130
style="pop"
131131
onclick={handleSubmit}
132132
aria-label="Send"
133133
>
134134
<svg
135135
class="circle-icon"
136-
class:spinner={loading}
137136
viewBox="0 0 18 18"
138137
fill="none"
139138
xmlns="http://www.w3.org/2000/svg"
@@ -149,7 +148,6 @@
149148

150149
<svg
151150
class="arrow-icon"
152-
class:spinner={loading}
153151
viewBox="0 0 16 16"
154152
fill="none"
155153
xmlns="http://www.w3.org/2000/svg"

apps/desktop/src/components/codegen/CodegenPage.svelte

Lines changed: 81 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import vibecodingSvg from '$lib/assets/illustrations/vibecoding.svg?raw';
3131
import { useAvailabilityChecking } from '$lib/codegen/availabilityChecking.svelte';
3232
import { CLAUDE_CODE_SERVICE } from '$lib/codegen/claude';
33+
import { useSendMessage } from '$lib/codegen/messageQueue.svelte';
34+
import { messageQueueSelectors, messageQueueSlice } from '$lib/codegen/messageQueueSlice';
3335
import {
3436
currentStatus,
3537
formatMessages,
@@ -49,15 +51,16 @@
4951
import { RULES_SERVICE } from '$lib/rules/rulesService.svelte';
5052
import { createWorktreeSelection } from '$lib/selection/key';
5153
import { SETTINGS } from '$lib/settings/userSettings';
52-
import { CODEGEN_ANALYTICS } from '$lib/soup/codegenAnalytics';
5354
import { pushStatusToColor, pushStatusToIcon } from '$lib/stacks/stack';
5455
import { STACK_SERVICE } from '$lib/stacks/stackService.svelte';
56+
import { CLIENT_STATE } from '$lib/state/clientState.svelte';
5557
import { combineResults } from '$lib/state/helpers';
5658
import { UI_STATE } from '$lib/state/uiState.svelte';
5759
import { USER } from '$lib/user/user';
5860
import { createBranchRef } from '$lib/utils/branch';
5961
import { getEditorUri, URL_SERVICE } from '$lib/utils/url';
6062
import { inject } from '@gitbutler/core/context';
63+
import { reactive } from '@gitbutler/shared/reactiveUtils.svelte';
6164
import {
6265
Badge,
6366
Button,
@@ -89,12 +92,12 @@
8992
const stackService = inject(STACK_SERVICE);
9093
const projectsService = inject(PROJECTS_SERVICE);
9194
const rulesService = inject(RULES_SERVICE);
92-
const codegenAnalytics = inject(CODEGEN_ANALYTICS);
9395
const uiState = inject(UI_STATE);
9496
const user = inject(USER);
9597
const urlService = inject(URL_SERVICE);
9698
const userSettings = inject(SETTINGS);
9799
const settingsService = inject(SETTINGS_SERVICE);
100+
const clientState = inject(CLIENT_STATE);
98101
const claudeSettings = $derived($settingsService?.claude);
99102
100103
const stacks = $derived(stackService.stacks(projectId));
@@ -108,7 +111,6 @@
108111
aiRules.some((rule) => rule.action.subject.subject.target.subject === stack.id)
109112
);
110113
});
111-
const [sendClaudeMessage] = claudeCodeService.sendMessage;
112114
const mcpConfig = $derived(claudeCodeService.mcpConfig({ projectId }));
113115
114116
let settingsModal: ClaudeCodeSettingsModal | undefined;
@@ -164,14 +166,6 @@
164166
selectedBranch?.stackId ? uiState.lane(selectedBranch.stackId) : undefined
165167
);
166168
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-
175169
// File list data
176170
const branchChanges = $derived(
177171
selectedBranch
@@ -216,64 +210,6 @@
216210
}
217211
}
218212
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-
277213
async function onApproval(id: string) {
278214
await claudeCodeService.updatePermissionRequest({ projectId, requestId: id, approval: true });
279215
}
@@ -343,6 +279,14 @@
343279
}
344280
}
345281
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+
346290
function insertTemplate(template: string) {
347291
setPrompt(prompt + (prompt ? '\n\n' : '') + template);
348292
templateContextMenu?.close();
@@ -454,6 +398,17 @@
454398
cyclePermissionMode();
455399
}
456400
}
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+
);
457412
</script>
458413

459414
<svelte:window onkeydown={handleKeydown} />
@@ -668,7 +623,7 @@
668623
{#if claudeAvailable.response?.status === 'available'}
669624
{@const status = currentStatus(events, isStackActive)}
670625
<CodegenInput
671-
value={prompt}
626+
value={prompt.current}
672627
onChange={(prompt) => setPrompt(prompt)}
673628
loading={['running', 'compacting'].includes(status)}
674629
compacting={status === 'compacting'}
@@ -753,8 +708,9 @@
753708

754709
{#snippet rightSidebar(events: ClaudeMessage[])}
755710
{@const addedDirs = laneState?.addedDirs.current || []}
711+
{@const queueLength = queue?.messages.length || 0}
756712
<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)}
758714
<div class="right-sidebar__placeholder">
759715
<EmptyStatePlaceholder
760716
image={filesAndChecksSvg}
@@ -773,7 +729,7 @@
773729
<ReduxResult result={branchChanges.result} {projectId}>
774730
{#snippet children({ changes }, { projectId })}
775731
<Drawer
776-
bottomBorder={todos.length > 0 || addedDirs.length > 0}
732+
bottomBorder={todos.length > 0 || addedDirs.length > 0 || queueLength > 0}
777733
grow
778734
defaultCollapsed={todos.length > 0}
779735
notFoldable
@@ -811,7 +767,11 @@
811767
{/if}
812768

813769
{#if todos.length > 0}
814-
<Drawer defaultCollapsed={false} noshrink>
770+
<Drawer
771+
defaultCollapsed={false}
772+
noshrink
773+
bottomBorder={addedDirs.length > 0 || queueLength > 0}
774+
>
815775
{#snippet header()}
816776
<h4 class="text-14 text-semibold truncate">Todos</h4>
817777
<Badge>{todos.length}</Badge>
@@ -826,7 +786,7 @@
826786
{/if}
827787

828788
{#if addedDirs.length > 0}
829-
<Drawer defaultCollapsed={false} noshrink>
789+
<Drawer defaultCollapsed={false} noshrink bottomBorder={queueLength > 0}>
830790
{#snippet header()}
831791
<h4 class="text-14 text-semibold truncate">Added Directories</h4>
832792
<Badge>{addedDirs.length}</Badge>
@@ -853,6 +813,38 @@
853813
</div>
854814
</Drawer>
855815
{/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}
856848
{/if}
857849

858850
<Resizer
@@ -926,7 +918,7 @@
926918
projectId,
927919
stackId
928920
})}
929-
{@const sidebarIsStackActive = claudeCodeService.isStackActive(projectId, stackId)}
921+
{@const isActive = claudeCodeService.isStackActive(projectId, stackId)}
930922
{@const rule = rulesService.aiRuleForStack({ projectId, stackId })}
931923

932924
<ReduxResult
@@ -935,7 +927,7 @@
935927
commits.result,
936928
branchDetails.result,
937929
events.result,
938-
sidebarIsStackActive.result,
930+
isActive.result,
939931
rule.result
940932
)}
941933
{projectId}
@@ -1054,7 +1046,7 @@
10541046
<div class="not-available">
10551047
<DecorativeSplitView hideDetails img={vibecodingSvg}>
10561048
<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>
10581050
<ClaudeCheck
10591051
claudeExecutable={claudeExecutable.current}
10601052
recheckedAvailability={recheckedAvailability.current}
@@ -1293,6 +1285,18 @@
12931285
justify-content: space-between;
12941286
}
12951287
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+
12961300
.right-sidebar-list--small-gap {
12971301
gap: 4px;
12981302
}

apps/desktop/src/components/codegen/CodegenSidebarEntry.svelte

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,6 @@
153153
<div class="vibe-icon {status}">
154154
{#if status === 'running'}
155155
<Icon name="spinner" />
156-
{:else if status === 'completed'}
157-
<Icon name="success" />
158156
{/if}
159157
</div>
160158
{/snippet}
@@ -293,10 +291,6 @@
293291
294292
.vibe-icon {
295293
display: flex;
296-
297-
&.completed {
298-
color: var(--clr-theme-succ-element);
299-
}
300294
}
301295
302296
.active-indicator {

0 commit comments

Comments
 (0)