Skip to content

Commit 1be5c3b

Browse files
Merge pull request #10162 from gitbutlerapp/timer-loading-state-improvements
Timer loading state improvements
2 parents a09c126 + bae0670 commit 1be5c3b

File tree

7 files changed

+256
-93
lines changed

7 files changed

+256
-93
lines changed

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
import CodegenChatLayout from '$components/codegen/CodegenChatLayout.svelte';
1313
import CodegenClaudeMessage from '$components/codegen/CodegenClaudeMessage.svelte';
1414
import CodegenInput from '$components/codegen/CodegenInput.svelte';
15-
import CodegenServiceMessage from '$components/codegen/CodegenServiceMessage.svelte';
15+
import CodegenServiceMessageThinking from '$components/codegen/CodegenServiceMessageThinking.svelte';
16+
import CodegenServiceMessageUseTool from '$components/codegen/CodegenServiceMessageUseTool.svelte';
1617
import CodegenSidebar from '$components/codegen/CodegenSidebar.svelte';
1718
import CodegenSidebarEntry from '$components/codegen/CodegenSidebarEntry.svelte';
1819
import CodegenTodo from '$components/codegen/CodegenTodo.svelte';
@@ -29,6 +30,7 @@
2930
getTodos,
3031
lastInteractionTime,
3132
lastUserMessageSentAt,
33+
userFeedbackStatus,
3234
usageStats
3335
} from '$lib/codegen/messages';
3436
import { commitStatusLabel } from '$lib/commits/commit';
@@ -486,7 +488,15 @@
486488
{/if}
487489

488490
{#if currentStatus(events, isStackActive) === 'running' && lastUserMessageSent}
489-
<CodegenServiceMessage {lastUserMessageSent} />
491+
{@const status = userFeedbackStatus(formattedMessages)}
492+
{#if status.waitingForFeedback}
493+
<CodegenServiceMessageUseTool toolCall={status.toolCall} />
494+
{:else}
495+
<CodegenServiceMessageThinking
496+
{lastUserMessageSent}
497+
msSpentWaiting={status.msSpentWaiting}
498+
/>
499+
{/if}
490500
{/if}
491501
{/snippet}
492502

Lines changed: 18 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,22 @@
11
<script lang="ts">
22
import { ButPcAvatar } from '@gitbutler/ui';
33
import { popIn } from '@gitbutler/ui/utils/transitions';
4+
import type { Snippet } from 'svelte';
45
56
type Props = {
6-
lastUserMessageSent: Date;
7+
children: Snippet;
8+
style: 'neutral' | 'pop' | 'error';
9+
face: 'idle' | 'thinking';
710
};
811
9-
const { lastUserMessageSent }: Props = $props();
10-
11-
const words = [
12-
'contemplating',
13-
'reflecting',
14-
'reasoning',
15-
'analyzing',
16-
'pondering',
17-
'deliberating',
18-
'mulling',
19-
'meditating',
20-
'speculating',
21-
'conceptualizing',
22-
'building',
23-
'assembling',
24-
'creating',
25-
'developing',
26-
'forming',
27-
'fabricating',
28-
'composing',
29-
'establishing',
30-
'designing',
31-
'operating',
32-
'functioning',
33-
'executing',
34-
'acting',
35-
'butlering',
36-
'producing',
37-
'laboring',
38-
'performing',
39-
'engaging',
40-
'applying',
41-
'striving'
42-
];
43-
44-
/**
45-
* Securely generates a number between 0 and limit (inclusive)
46-
*/
47-
function randomInt(limit: number) {
48-
const array = new Uint32Array(1);
49-
crypto.getRandomValues(array);
50-
const randFloat = array[0]! / (0xffffffff + 1);
51-
return Math.round(randFloat * limit);
52-
}
53-
54-
function getWord() {
55-
const i = randomInt(words.length - 1);
56-
return words[i];
57-
}
58-
59-
function milisToEnglish(milis: number) {
60-
if (milis === 0) return 'now';
61-
62-
const seconds = milis / 1000;
63-
const minutes = Math.floor(seconds / 60);
64-
const hours = Math.floor(minutes / 60);
65-
66-
if (hours > 0) return `${hours}h ${minutes % 60}m`;
67-
if (minutes > 0) return `${minutes}m ${(seconds % 60).toFixed(1)}s`;
68-
return `${seconds.toFixed(1)}s`;
69-
}
70-
71-
let currentWord = $state(getWord());
72-
let currentDuration = $state(milisToEnglish(Date.now() - lastUserMessageSent.getTime()));
73-
74-
$effect(() => {
75-
const updateWordInterval = setInterval(() => {
76-
currentWord = getWord();
77-
}, 1000 * 15);
78-
79-
const updateTimeInterval = setInterval(() => {
80-
currentDuration = milisToEnglish(Date.now() - lastUserMessageSent.getTime());
81-
}, 100);
82-
83-
return () => {
84-
clearInterval(updateWordInterval);
85-
clearInterval(updateTimeInterval);
86-
};
87-
});
12+
const { children, style, face }: Props = $props();
8813
</script>
8914

9015
<div class="service-message__wrapper">
9116
<div class="service-message">
92-
<ButPcAvatar mode="thinking" />
93-
<div class="service-message__bubble" in:popIn>
94-
<span class="text-13 text-italic">
95-
Claude is
96-
{#key currentWord}
97-
<span class="animated-word">{currentWord}</span>
98-
{/key}
99-
... {currentDuration}
100-
</span>
17+
<ButPcAvatar mode={face} />
18+
<div class="service-message__bubble service-message__bubble--{style}" in:popIn>
19+
{@render children()}
10120
</div>
10221
</div>
10322
</div>
@@ -121,4 +40,14 @@
12140
background-color: var(--clr-bg-2);
12241
color: var(--clr-text-2);
12342
}
43+
44+
.service-message__bubble--error {
45+
background-color: var(--clr-theme-err-soft);
46+
color: var(--clr-theme-err-on-soft);
47+
}
48+
49+
.service-message__bubble--pop {
50+
background-color: var(--clr-theme-pop-soft);
51+
color: var(--clr-text-1);
52+
}
12453
</style>
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<script lang="ts">
2+
import CodegenServiceMessage from '$components/codegen/CodegenServiceMessage.svelte';
3+
4+
type Props = {
5+
lastUserMessageSent: Date;
6+
msSpentWaiting: number;
7+
};
8+
9+
const { lastUserMessageSent, msSpentWaiting }: Props = $props();
10+
11+
const words = [
12+
'contemplating',
13+
'reflecting',
14+
'reasoning',
15+
'analyzing',
16+
'pondering',
17+
'deliberating',
18+
'mulling',
19+
'meditating',
20+
'speculating',
21+
'conceptualizing',
22+
'building',
23+
'assembling',
24+
'creating',
25+
'developing',
26+
'forming',
27+
'fabricating',
28+
'composing',
29+
'establishing',
30+
'designing',
31+
'operating',
32+
'functioning',
33+
'executing',
34+
'acting',
35+
'butlering',
36+
'producing',
37+
'laboring',
38+
'performing',
39+
'engaging',
40+
'applying',
41+
'striving'
42+
];
43+
44+
/**
45+
* Securely generates a number between 0 and limit (inclusive)
46+
*/
47+
function randomInt(limit: number) {
48+
const array = new Uint32Array(1);
49+
crypto.getRandomValues(array);
50+
const randFloat = array[0]! / (0xffffffff + 1);
51+
return Math.round(randFloat * limit);
52+
}
53+
54+
function getWord() {
55+
const i = randomInt(words.length - 1);
56+
return words[i];
57+
}
58+
59+
function milisToEnglish(milis: number) {
60+
if (milis === 0) return 'now';
61+
62+
const seconds = milis / 1000;
63+
const minutes = Math.floor(seconds / 60);
64+
const hours = Math.floor(minutes / 60);
65+
66+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
67+
if (minutes > 0) return `${minutes}m ${(seconds % 60).toFixed(1)}s`;
68+
return `${seconds.toFixed(1)}s`;
69+
}
70+
71+
let currentWord = $state(getWord());
72+
let currentDuration = $state(
73+
milisToEnglish(Date.now() - lastUserMessageSent.getTime() - msSpentWaiting)
74+
);
75+
76+
$effect(() => {
77+
const updateWordInterval = setInterval(() => {
78+
currentWord = getWord();
79+
}, 1000 * 15);
80+
81+
const updateTimeInterval = setInterval(() => {
82+
currentDuration = milisToEnglish(Date.now() - lastUserMessageSent.getTime() - msSpentWaiting);
83+
}, 100);
84+
85+
return () => {
86+
clearInterval(updateWordInterval);
87+
clearInterval(updateTimeInterval);
88+
};
89+
});
90+
</script>
91+
92+
<CodegenServiceMessage style="neutral" face="thinking">
93+
<span class="text-13 text-italic">
94+
Claude is
95+
{#key currentWord}
96+
<span class="animated-word">{currentWord}</span>
97+
{/key}
98+
... {currentDuration}
99+
</span>
100+
</CodegenServiceMessage>
101+
102+
<style lang="postcss">
103+
.service-message__wrapper {
104+
display: flex;
105+
width: 100%;
106+
padding: 8px 0 16px;
107+
}
108+
.service-message {
109+
display: flex;
110+
align-items: flex-end;
111+
gap: 16px;
112+
}
113+
.service-message__bubble {
114+
display: flex;
115+
max-width: var(--message-max-width);
116+
padding: 8px 12px;
117+
border-radius: var(--radius-ml);
118+
background-color: var(--clr-bg-2);
119+
color: var(--clr-text-2);
120+
}
121+
</style>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<script lang="ts">
2+
import CodegenServiceMessage from '$components/codegen/CodegenServiceMessage.svelte';
3+
import type { ToolCall } from '$lib/codegen/messages';
4+
5+
type Props = { toolCall: ToolCall };
6+
7+
const { toolCall }: Props = $props();
8+
9+
const wantsToDo = $derived.by(() => {
10+
switch (toolCall.name) {
11+
case 'Task':
12+
return 'start a subagent';
13+
case 'Bash':
14+
return 'run a command';
15+
case 'Glob':
16+
return 'pattern match some files';
17+
case 'Grep':
18+
return 'perform a content search';
19+
case 'Read':
20+
return 'read a file';
21+
case 'Edit':
22+
return 'edit a file';
23+
case 'MultiEdit':
24+
return 'edit a file';
25+
case 'Write':
26+
return 'create a file';
27+
case 'WebFetch':
28+
return 'search the internet';
29+
case 'WebSearch':
30+
return 'search the internet';
31+
default:
32+
return 'do something requiring your approval';
33+
}
34+
});
35+
36+
const actionName = $derived.by(() => {
37+
switch (toolCall.name) {
38+
case 'Task':
39+
return 'subagent request';
40+
case 'Bash':
41+
return 'command';
42+
case 'Glob':
43+
return 'pattern match';
44+
case 'Grep':
45+
return 'content search';
46+
case 'Read':
47+
return 'read request';
48+
case 'Edit':
49+
return 'edit request';
50+
case 'MultiEdit':
51+
return 'edit request';
52+
case 'Write':
53+
return 'create request';
54+
case 'WebFetch':
55+
return 'internet search';
56+
case 'WebSearch':
57+
return 'internet search';
58+
default:
59+
return 'action';
60+
}
61+
});
62+
</script>
63+
64+
<CodegenServiceMessage style="pop" face="idle">
65+
<div class="flex flex-col gap-6">
66+
<p class="text-13 text-semibold">Claude Code wants to {wantsToDo} 👆</p>
67+
<p class="text-13 text-italic">Review the {actionName} above, then approve or reject.</p>
68+
</div>
69+
</CodegenServiceMessage>

0 commit comments

Comments
 (0)