Skip to content

Commit 4b528a7

Browse files
Guilhem-lmclaude
andauthored
feat: clearer worktree status indicators (#265)
* fix: don't mark the open conversation as unread Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: merge agent-status and unread into one row indicator Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: add floating bars summarising off-screen agent statuses Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: drop as-cast and make status-bar cycle resilient to scroll Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf: re-observe rows only on add/remove and prune stale positions Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: derive observer margin from measured bar height Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: don't lay out an empty agent-icon slot for read/idle rows Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: surface unread on the selected branch when the tab is hidden Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 785336e commit 4b528a7

6 files changed

Lines changed: 584 additions & 204 deletions

File tree

frontend/src/App.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,12 @@
238238
239239
function handleNotification(n: AppNotification): void {
240240
notifications = [...notifications, n];
241-
notifiedBranches = new Set([...notifiedBranches, n.branch]);
241+
// Only suppress the unread dot when the user is actually looking at this branch
242+
// (selected and tab visible); otherwise a finished run should still surface.
243+
const viewingThisBranch = n.branch === selectedBranch && !document.hidden;
244+
if (!viewingThisBranch) {
245+
notifiedBranches = new Set([...notifiedBranches, n.branch]);
246+
}
242247
notificationHistory = [n, ...notificationHistory].slice(0, MAX_HISTORY);
243248
unreadCount++;
244249
// Auto-dismiss after timeout

frontend/src/lib/AgentStatusIcon.svelte

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
1+
<script module lang="ts">
2+
// Whether the icon renders a visible mark for this status — kept beside the
3+
// template below so callers can avoid laying out an empty slot. Mirrors the
4+
// {#if} branches in the icon snippet.
5+
export function agentIconVisible(status: string, unread: boolean): boolean {
6+
return (
7+
status === "working" ||
8+
status === "waiting" ||
9+
status === "error" ||
10+
(status === "done" && unread)
11+
);
12+
}
13+
</script>
14+
115
<script lang="ts">
216
let {
317
status,
418
size = 10,
519
pill = false,
6-
}: { status: string; size?: number; pill?: boolean } = $props();
20+
unread = false,
21+
}: { status: string; size?: number; pill?: boolean; unread?: boolean } =
22+
$props();
723
824
function pillClass(s: string): string {
925
if (s === "working") return "bg-success/15 text-success";
@@ -17,14 +33,14 @@
1733
{#snippet icon()}
1834
{#if status === "working"}
1935
<svg
20-
class="text-success"
36+
class="text-success working-dots"
2137
xmlns="http://www.w3.org/2000/svg"
2238
width={size}
2339
height={size}
2440
viewBox="0 0 24 24"
2541
fill="currentColor"
2642
stroke="none"
27-
><circle cx="5" cy="12" r="1.5" /><circle cx="12" cy="12" r="1.5" /><circle cx="19" cy="12" r="1.5" /></svg
43+
><circle cx="3" cy="12" r="2.5" /><circle cx="12" cy="12" r="2.5" /><circle cx="21" cy="12" r="2.5" /></svg
2844
>
2945
{:else if status === "waiting"}
3046
<svg
@@ -43,19 +59,18 @@
4359
/><path d="M12 7v2" /><path d="M12 13h.01" /></svg
4460
>
4561
{:else if status === "done"}
46-
<svg
47-
class="text-success"
48-
xmlns="http://www.w3.org/2000/svg"
49-
width={size}
50-
height={size}
51-
viewBox="0 0 24 24"
52-
fill="none"
53-
stroke="currentColor"
54-
stroke-width="3"
55-
stroke-linecap="round"
56-
stroke-linejoin="round"
57-
><polyline points="20 6 9 17 4 12" /></svg
58-
>
62+
{#if unread}
63+
<svg
64+
class="text-accent"
65+
xmlns="http://www.w3.org/2000/svg"
66+
width={size}
67+
height={size}
68+
viewBox="0 0 24 24"
69+
fill="currentColor"
70+
stroke="none"
71+
><circle cx="12" cy="12" r="6" /></svg
72+
>
73+
{/if}
5974
{:else if status === "error"}
6075
<svg
6176
class="text-danger"
@@ -78,6 +93,42 @@
7893
{/if}
7994
{/snippet}
8095

96+
<style>
97+
.working-dots circle {
98+
transform-box: fill-box;
99+
transform-origin: center;
100+
animation: dot-wave 1.1s ease-in-out infinite;
101+
}
102+
.working-dots circle:nth-child(1) {
103+
animation-delay: 0s;
104+
}
105+
.working-dots circle:nth-child(2) {
106+
animation-delay: 0.18s;
107+
}
108+
.working-dots circle:nth-child(3) {
109+
animation-delay: 0.36s;
110+
}
111+
@keyframes dot-wave {
112+
0%,
113+
70%,
114+
100% {
115+
opacity: 0.25;
116+
transform: scale(0.8);
117+
}
118+
35% {
119+
opacity: 1;
120+
transform: scale(1.15);
121+
}
122+
}
123+
@media (prefers-reduced-motion: reduce) {
124+
.working-dots circle {
125+
animation: none;
126+
opacity: 1;
127+
transform: none;
128+
}
129+
}
130+
</style>
131+
81132
{#if pill}
82133
<span
83134
class="text-xs px-2 py-0.5 rounded-xl flex items-center gap-1 {pillClass(
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, expect, it } from "vitest";
2+
import { agentIconVisible } from "./AgentStatusIcon.svelte";
3+
4+
describe("agentIconVisible", () => {
5+
it("is visible for working, waiting and error regardless of unread", () => {
6+
expect(agentIconVisible("working", false)).toBe(true);
7+
expect(agentIconVisible("waiting", false)).toBe(true);
8+
expect(agentIconVisible("error", false)).toBe(true);
9+
});
10+
11+
it("is visible for done only when unread", () => {
12+
expect(agentIconVisible("done", true)).toBe(true);
13+
expect(agentIconVisible("done", false)).toBe(false);
14+
});
15+
16+
it("is hidden for idle and unknown statuses", () => {
17+
expect(agentIconVisible("idle", true)).toBe(false);
18+
expect(agentIconVisible("", false)).toBe(false);
19+
});
20+
});

0 commit comments

Comments
 (0)