@saadnvd1/agent-os 0.3.649 → 0.3.654

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.
@@ -43,10 +43,11 @@ import { LearningCardsHost } from "@/components/LearningCards/LearningCard";
43
43
  import { composerCompacts } from "@/lib/chat/stale";
44
44
  import { useFreshStartQuery } from "@/data/fresh-start";
45
45
  import { QuoteButton } from "./QuoteButton";
46
+ import { quoteIntoComposer } from "./quote-selection";
46
47
  import { ToolBodies, ToolGroup } from "./Tools";
47
48
  import { RowState, RowStore } from "./rowState";
49
+ import { useKeybindings } from "@/hooks/useKeybindings";
48
50
  import { Background, type BackgroundValue } from "./background";
49
- import { backgroundKey } from "@/lib/chat/background";
50
51
  import { FindBar } from "./FindBar";
51
52
  import { FIND_EVENT, isFindKey, useFind } from "./useFind";
52
53
  import { McpServers } from "./McpServers";
@@ -365,6 +366,15 @@ export function ChatPanel({
365
366
  ?.focus(),
366
367
  []
367
368
  );
369
+ // Quote button or Enter: the text goes in, and the caret is ready to reply.
370
+ const quote = (text: string) =>
371
+ quoteIntoComposer(
372
+ text,
373
+ // After the draft, so the caret ends at the end of the composer.
374
+ (t) =>
375
+ setInsert({ text: quoteMarkdown(t), at: Date.now(), append: true }),
376
+ focusComposer
377
+ );
368
378
  const togglePlan = plan === null ? undefined : () => setPlan(!plan);
369
379
  const sendText = (text: string) => {
370
380
  toEnd();
@@ -377,39 +387,24 @@ export function ChatPanel({
377
387
  const asIs = asIsFor === sessionId;
378
388
  const setAsIs = (on: boolean) => setAsIsFor(on ? sessionId : null);
379
389
 
390
+ const inPanel = (e: KeyboardEvent) =>
391
+ !!rootRef.current?.contains(e.target as Node);
380
392
  // Shift+Tab switches plan mode from the composer, as in the terminal.
381
- const shiftTab = useRef(togglePlan);
382
- useEffect(() => {
383
- shiftTab.current = togglePlan;
384
- });
385
- useEffect(() => {
386
- const onKey = (e: KeyboardEvent) => {
387
- if (e.key !== "Tab" || !e.shiftKey || e.metaKey || e.ctrlKey) return;
388
- const target = e.target as HTMLElement;
389
- if (
390
- !shiftTab.current ||
391
- !rootRef.current?.contains(target) ||
392
- !(target === rootRef.current || target.closest(".chat-composer"))
393
- )
394
- return;
395
- e.preventDefault();
396
- shiftTab.current();
397
- };
398
- window.addEventListener("keydown", onKey, true);
399
- return () => window.removeEventListener("keydown", onKey, true);
400
- }, []);
401
-
402
- useChatCommands({
403
- sessionId,
404
- connected,
405
- running,
406
- context,
407
- plan,
408
- send: sendText,
409
- setPlan,
410
- interrupt,
411
- focusComposer,
412
- });
393
+ useKeybindings(`chat-plan:${sessionId}`, [
394
+ {
395
+ id: "chat.plan",
396
+ keys: "shift+tab",
397
+ when: (e) => {
398
+ const target = e.target as HTMLElement;
399
+ return (
400
+ !!togglePlan &&
401
+ inPanel(e) &&
402
+ (target === rootRef.current || !!target.closest(".chat-composer"))
403
+ );
404
+ },
405
+ run: () => togglePlan?.(),
406
+ },
407
+ ]);
413
408
 
414
409
  const blocks = useMemo(
415
410
  () => groupTimeline(items, { foldEvents: !!orchestratorOf }),
@@ -466,20 +461,27 @@ export function ChatPanel({
466
461
  ),
467
462
  [canBackground, foregroundTasks]
468
463
  );
469
- const moveRef = useRef({ canMove, backgroundTask });
470
- useEffect(() => {
471
- moveRef.current = { canMove, backgroundTask };
464
+ useKeybindings(`chat:${sessionId}`, [
465
+ {
466
+ id: "chat.background",
467
+ keys: "ctrl+b",
468
+ when: (e) => canMove && inPanel(e),
469
+ run: () => backgroundTask(),
470
+ },
471
+ ]);
472
+ useChatCommands({
473
+ sessionId,
474
+ connected,
475
+ running,
476
+ context,
477
+ plan,
478
+ send: sendText,
479
+ setPlan,
480
+ interrupt,
481
+ focusComposer,
482
+ background: canMove ? () => backgroundTask() : undefined,
472
483
  });
473
- useEffect(() => {
474
- const onKey = (e: KeyboardEvent) => {
475
- if (!rootRef.current?.contains(e.target as Node)) return;
476
- if (!backgroundKey(e, moveRef.current.canMove)) return;
477
- e.preventDefault();
478
- moveRef.current.backgroundTask();
479
- };
480
- window.addEventListener("keydown", onKey, true);
481
- return () => window.removeEventListener("keydown", onKey, true);
482
- }, []);
484
+
483
485
  const itemsById = useMemo(
484
486
  () => new Map(items.map((i) => [i.id, i])),
485
487
  [items]
@@ -790,12 +792,7 @@ export function ChatPanel({
790
792
  onStop={stopTask}
791
793
  onLoadOutput={loadTaskOutput}
792
794
  />
793
- <QuoteButton
794
- rootRef={scrollRef}
795
- onQuote={(text) =>
796
- setInsert({ text: quoteMarkdown(text), at: Date.now() })
797
- }
798
- />
795
+ <QuoteButton rootRef={scrollRef} onQuote={quote} />
799
796
  <PausedBanner sessionId={sessionId} />
800
797
  <Queue
801
798
  queue={queue}
@@ -1,10 +1,17 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useState, type RefObject } from "react";
3
+ import { useEffect, useId, useState, type RefObject } from "react";
4
4
  import { Quote } from "lucide-react";
5
5
  import { cn } from "@/lib/utils";
6
6
  import { useCoarsePointer } from "./composer/useCoarsePointer";
7
- import { placeQuote, quotePick, type Placement } from "./quote-selection";
7
+ import { useKeybindings } from "@/hooks/useKeybindings";
8
+ import {
9
+ quoteAction,
10
+ quoteEnter,
11
+ placeQuote,
12
+ quotePick,
13
+ type Placement,
14
+ } from "./quote-selection";
8
15
 
9
16
  interface Picked {
10
17
  text: string;
@@ -57,12 +64,16 @@ export function QuoteButton({
57
64
  };
58
65
  }, [rootRef]);
59
66
 
67
+ const shown = !!picked && (coarse || !!picked.at);
68
+ const quote = quoteAction(picked?.text, {
69
+ onQuote,
70
+ clearSelection: () => window.getSelection()?.removeAllRanges(),
71
+ hide: () => setPicked(null),
72
+ });
73
+ // One source per button: each open chat pane has its own.
74
+ useKeybindings(`chat-quote:${useId()}`, [quoteEnter(shown, quote)]);
75
+
60
76
  if (!picked) return null;
61
- const quote = () => {
62
- onQuote(picked.text);
63
- window.getSelection()?.removeAllRanges();
64
- setPicked(null);
65
- };
66
77
  const button = (
67
78
  <button
68
79
  type="button"
@@ -73,6 +84,12 @@ export function QuoteButton({
73
84
  >
74
85
  <Quote className="h-3.5 w-3.5" />
75
86
  Quote
87
+ <kbd
88
+ aria-hidden
89
+ className="text-muted-foreground bg-muted hidden rounded px-1 font-mono text-[10px] leading-4 md:inline"
90
+ >
91
+ ↵
92
+ </kbd>
76
93
  </button>
77
94
  );
78
95
  if (coarse)
@@ -55,13 +55,25 @@ const keys = (over: Partial<KeyHandlers> = {}): KeyHandlers => ({
55
55
 
56
56
  function press(
57
57
  key: string,
58
- opts: { shift?: boolean; coarse?: boolean } = {},
58
+ opts: {
59
+ shift?: boolean;
60
+ coarse?: boolean;
61
+ alt?: boolean;
62
+ ctrl?: boolean;
63
+ meta?: boolean;
64
+ } = {},
59
65
  h = keys()
60
66
  ) {
61
67
  window.matchMedia = ((q: string) => ({
62
68
  matches: Boolean(opts.coarse) && q.includes("coarse"),
63
69
  })) as never;
64
- const event = new KeyboardEvent("keydown", { key, shiftKey: opts.shift });
70
+ const event = new KeyboardEvent("keydown", {
71
+ key,
72
+ shiftKey: opts.shift,
73
+ altKey: opts.alt,
74
+ ctrlKey: opts.ctrl,
75
+ metaKey: opts.meta,
76
+ });
65
77
  if (!handleKeyDown(editor, event, h)) {
66
78
  editor.view.someProp("handleKeyDown", (f) => f(editor.view, event));
67
79
  }
@@ -289,6 +301,31 @@ describe("Enter", () => {
289
301
  expect(taken).toBe(2);
290
302
  });
291
303
 
304
+ it("→ leaves the caret alone with no suggestion, a modifier, or an open menu", () => {
305
+ make();
306
+ let taken = 0;
307
+ const on = keys({ suggestion: true, onAcceptSuggestion: () => taken++ });
308
+ const off = keys({ suggestion: false, onAcceptSuggestion: () => taken++ });
309
+ press("ArrowRight", {}, off);
310
+ for (const mod of [
311
+ { shift: true },
312
+ { alt: true },
313
+ { ctrl: true },
314
+ { meta: true },
315
+ ])
316
+ press("ArrowRight", mod, on);
317
+ const open = keys({
318
+ suggestion: true,
319
+ menuOpen: true,
320
+ menuHasMatches: true,
321
+ onAcceptSuggestion: () => taken++,
322
+ });
323
+ press("ArrowRight", {}, open);
324
+ expect(taken).toBe(0);
325
+ press("ArrowRight", {}, on);
326
+ expect(taken).toBe(1);
327
+ });
328
+
292
329
  it("Tab picks from an open menu before taking ghost text", () => {
293
330
  make();
294
331
  const got: string[] = [];
@@ -1,6 +1,14 @@
1
1
  import { JSDOM } from "jsdom";
2
- import { beforeEach, describe, expect, it } from "vitest";
3
- import { placeQuote, quotePick, rangeText } from "./quote-selection";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { KeybindingRegistry } from "@/lib/keybindings/registry";
4
+ import {
5
+ placeQuote,
6
+ quoteAction,
7
+ quoteEnter,
8
+ quoteIntoComposer,
9
+ quotePick,
10
+ rangeText,
11
+ } from "./quote-selection";
4
12
 
5
13
  // The suite's setup needs node, so the DOM is built here.
6
14
  const { window } = new JSDOM("<!doctype html><body></body>");
@@ -165,3 +173,177 @@ describe("placeQuote", () => {
165
173
  ).toBeNull();
166
174
  });
167
175
  });
176
+
177
+ describe("Enter quotes", () => {
178
+ let root: HTMLElement;
179
+ beforeEach(() => {
180
+ root = chat();
181
+ });
182
+
183
+ const reply = () => text("#a2 p", 1);
184
+ const key = (target: EventTarget, extra: object = {}) =>
185
+ ({
186
+ key: "Enter",
187
+ code: "Enter",
188
+ target,
189
+ ctrlKey: false,
190
+ metaKey: false,
191
+ altKey: false,
192
+ shiftKey: false,
193
+ isComposing: false,
194
+ repeat: false,
195
+ preventDefault() {},
196
+ ...extra,
197
+ }) as unknown as KeyboardEvent;
198
+
199
+ // Runs a key through a real registry holding the binding.
200
+ function press(shown: boolean, e: KeyboardEvent) {
201
+ const run = vi.fn();
202
+ const reg = new KeybindingRegistry({
203
+ addEventListener() {},
204
+ removeEventListener() {},
205
+ });
206
+ reg.register("t", [quoteEnter(shown, run, () => window.getSelection())]);
207
+ return { handled: reg.handle(e), run };
208
+ }
209
+
210
+ it("fires with a selection in a reply and the button showing", () => {
211
+ select([reply(), 4], [reply(), 8]);
212
+ const { handled, run } = press(true, key(root));
213
+ expect(handled).toBe(true);
214
+ expect(run).toHaveBeenCalledOnce();
215
+ });
216
+
217
+ it("leaves Enter alone when the button isn't showing", () => {
218
+ select([reply(), 4], [reply(), 8]);
219
+ expect(press(false, key(root)).handled).toBe(false);
220
+ });
221
+
222
+ it("leaves Enter alone with no selection", () => {
223
+ window.getSelection()!.removeAllRanges();
224
+ expect(press(true, key(root)).handled).toBe(false);
225
+ });
226
+
227
+ it("leaves Enter alone in the composer, key or selection", () => {
228
+ const composer = document.getElementById("composer") as HTMLTextAreaElement;
229
+ select([reply(), 4], [reply(), 8]);
230
+ expect(press(true, key(composer)).handled).toBe(false);
231
+ composer.select();
232
+ select([composer.parentNode!, 0], [composer.parentNode!, 1]);
233
+ const inside = document.createElement("div");
234
+ inside.setAttribute("contenteditable", "true");
235
+ inside.textContent = "draft";
236
+ document.body.append(inside);
237
+ select([inside.firstChild!, 0], [inside.firstChild!, 3]);
238
+ expect(press(true, key(root)).handled).toBe(false);
239
+ });
240
+
241
+ it("leaves a focused button to its own Enter", () => {
242
+ select([reply(), 4], [reply(), 8]);
243
+ const button = document.querySelector("#a2 button")!;
244
+ expect(press(true, key(button)).handled).toBe(false);
245
+ });
246
+
247
+ it.each(["shiftKey", "metaKey", "ctrlKey", "altKey"])(
248
+ "ignores Enter with %s",
249
+ (mod) => {
250
+ select([reply(), 4], [reply(), 8]);
251
+ expect(press(true, key(root, { [mod]: true })).handled).toBe(false);
252
+ }
253
+ );
254
+
255
+ it("ignores IME composition and repeats", () => {
256
+ select([reply(), 4], [reply(), 8]);
257
+ expect(press(true, key(root, { isComposing: true })).handled).toBe(false);
258
+ expect(press(true, key(root, { repeat: true })).handled).toBe(false);
259
+ });
260
+ });
261
+
262
+ describe("quoteIntoComposer", () => {
263
+ it("puts the text in, then focuses the composer", () => {
264
+ const order: string[] = [];
265
+ quoteIntoComposer(
266
+ "hello",
267
+ (t) => order.push(`put:${t}`),
268
+ () => order.push("focus")
269
+ );
270
+ expect(order).toEqual(["put:hello", "focus"]);
271
+ });
272
+ });
273
+
274
+ describe("Enter quoting wiring", () => {
275
+ const sel = () => window.getSelection()!;
276
+ const enter = (): KeyboardEvent =>
277
+ ({
278
+ key: "Enter",
279
+ code: "Enter",
280
+ target: document.body,
281
+ ctrlKey: false,
282
+ metaKey: false,
283
+ altKey: false,
284
+ shiftKey: false,
285
+ isComposing: false,
286
+ repeat: false,
287
+ preventDefault() {},
288
+ }) as unknown as KeyboardEvent;
289
+ const registry = () =>
290
+ new KeybindingRegistry({
291
+ addEventListener() {},
292
+ removeEventListener() {},
293
+ });
294
+ const pick = () => {
295
+ const root = chat();
296
+ select([text("#a2 p", 1), 4], [text("#a2 p", 1), 8]);
297
+ return root;
298
+ };
299
+
300
+ it("Enter hands the text over, clears the selection, hides, focuses", () => {
301
+ pick();
302
+ const calls: string[] = [];
303
+ const action = quoteAction("very", {
304
+ onQuote: (t) =>
305
+ quoteIntoComposer(
306
+ t,
307
+ (m) => calls.push(`put:${m}`),
308
+ () => calls.push("focus")
309
+ ),
310
+ clearSelection: () => {
311
+ sel().removeAllRanges();
312
+ calls.push("clear");
313
+ },
314
+ hide: () => calls.push("hide"),
315
+ });
316
+ const reg = registry();
317
+ reg.register("a", [quoteEnter(true, action, sel)]);
318
+ expect(reg.handle(enter())).toBe(true);
319
+ expect(calls).toEqual(["put:very", "focus", "clear", "hide"]);
320
+ expect(sel().isCollapsed).toBe(true);
321
+ });
322
+
323
+ it("does nothing without picked text", () => {
324
+ const onQuote = vi.fn();
325
+ quoteAction(undefined, {
326
+ onQuote,
327
+ clearSelection: vi.fn(),
328
+ hide: vi.fn(),
329
+ })();
330
+ expect(onQuote).not.toHaveBeenCalled();
331
+ });
332
+
333
+ it("keeps one binding per source: panes don't replace each other", () => {
334
+ pick();
335
+ const reg = registry();
336
+ const a = vi.fn();
337
+ const b = vi.fn();
338
+ const offA = reg.register("chat-quote:a", [quoteEnter(true, a, sel)]);
339
+ const offB = reg.register("chat-quote:b", [quoteEnter(false, b, sel)]);
340
+ expect(reg.handle(enter())).toBe(true);
341
+ expect(a).toHaveBeenCalledOnce();
342
+ expect(b).not.toHaveBeenCalled();
343
+ offB();
344
+ expect(reg.handle(enter())).toBe(true);
345
+ expect(a).toHaveBeenCalledTimes(2);
346
+ offA();
347
+ expect(reg.handle(enter())).toBe(false);
348
+ });
349
+ });
@@ -1,3 +1,4 @@
1
+ import type { Keybinding } from "@/lib/keybindings/registry";
1
2
  // What a selection in the chat quotes: the parts of it inside replies
2
3
  // (elements marked data-quotable), whatever its ends touch. A triple-click
3
4
  // or a drag past a reply's last line ends the range outside the reply, in
@@ -114,3 +115,72 @@ export function placeQuote(sel: Box, view: Box, size = 48): Placement | null {
114
115
  side: "below",
115
116
  };
116
117
  }
118
+
119
+ const TYPES_OR_ACTS =
120
+ 'input, textarea, select, button, a[href], summary, [contenteditable]:not([contenteditable="false"])';
121
+
122
+ function inTypingOrAction(node: Node | null): boolean {
123
+ const el =
124
+ node && (node.nodeType === 1 ? (node as Element) : node.parentElement);
125
+ return !!el?.closest(TYPES_OR_ACTS);
126
+ }
127
+
128
+ // Whether Enter quotes: the button is showing for a live selection, and
129
+ // neither the key's target nor the selection sits in the composer or any
130
+ // other field, where Enter keeps its own meaning.
131
+ export function enterQuotes(
132
+ visible: boolean,
133
+ target: EventTarget | null,
134
+ sel: Selection | null
135
+ ): boolean {
136
+ if (!visible || !sel || sel.isCollapsed || !sel.rangeCount) return false;
137
+ if (!sel.toString().trim()) return false;
138
+ return (
139
+ !inTypingOrAction(target as Node | null) &&
140
+ !inTypingOrAction(sel.anchorNode) &&
141
+ !inTypingOrAction(sel.focusNode)
142
+ );
143
+ }
144
+
145
+ // What Quote does, from the button or from Enter: the text goes in the
146
+ // composer, then the composer takes the caret so a reply can follow.
147
+ export function quoteIntoComposer(
148
+ text: string,
149
+ put: (markdown: string) => void,
150
+ focus: () => void
151
+ ) {
152
+ put(text);
153
+ focus();
154
+ }
155
+
156
+ // Enter does what the Quote button does, while it's showing.
157
+ export function quoteEnter(
158
+ shown: boolean,
159
+ run: () => void,
160
+ getSel: () => Selection | null = () => window.getSelection()
161
+ ): Keybinding {
162
+ return {
163
+ id: "chat.quote",
164
+ keys: "enter",
165
+ when: (e) => enterQuotes(shown, e.target, getSel()),
166
+ run,
167
+ };
168
+ }
169
+
170
+ // What the button does with the picked text, by click or by Enter: hand it
171
+ // over, clear the selection, hide the button.
172
+ export function quoteAction(
173
+ text: string | undefined,
174
+ io: {
175
+ onQuote: (text: string) => void;
176
+ clearSelection: () => void;
177
+ hide: () => void;
178
+ }
179
+ ) {
180
+ return () => {
181
+ if (text === undefined) return;
182
+ io.onQuote(text);
183
+ io.clearSelection();
184
+ io.hide();
185
+ };
186
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useEffect, useRef } from "react";
4
4
  import { useSnapshot } from "valtio";
5
- import { ListChecks, Minimize2, Square } from "lucide-react";
5
+ import { Layers, ListChecks, Minimize2, Square } from "lucide-react";
6
6
  import type { ChatContext } from "@/lib/chat/events";
7
7
  import { usePaletteCommands } from "@/hooks/usePaletteCommands";
8
8
  import { chatMeta, chatMetaActions, type ChatActions } from "@/stores/chatMeta";
@@ -15,6 +15,7 @@ export function useChatCommands({
15
15
  running,
16
16
  context,
17
17
  plan,
18
+ background,
18
19
  ...handlers
19
20
  }: ChatActions & {
20
21
  sessionId: string;
@@ -22,10 +23,12 @@ export function useChatCommands({
22
23
  running: boolean;
23
24
  context: ChatContext | null;
24
25
  plan: boolean | null;
26
+ // Set while a running call can move to the background.
27
+ background?: () => void;
25
28
  }) {
26
- const latest = useRef(handlers);
29
+ const latest = useRef({ ...handlers, background });
27
30
  useEffect(() => {
28
- latest.current = handlers;
31
+ latest.current = { ...handlers, background };
29
32
  });
30
33
 
31
34
  useEffect(() => {
@@ -69,6 +72,19 @@ export function useChatCommands({
69
72
  icon: Minimize2,
70
73
  run: () => latest.current.send("/compact"),
71
74
  },
75
+ ...(background
76
+ ? [
77
+ {
78
+ id: "chat.background",
79
+ title: "Run in background",
80
+ group: "Chat",
81
+ keywords: ["move", "detach", "task"],
82
+ hint: "Ctrl+B",
83
+ icon: Layers,
84
+ run: () => latest.current.background?.(),
85
+ },
86
+ ]
87
+ : []),
72
88
  ...(running
73
89
  ? [
74
90
  {
@@ -56,6 +56,7 @@ export const OrchestratorPinRow = memo(function OrchestratorPinRow({
56
56
 
57
57
  const content = (
58
58
  <RowFrame
59
+ data-session-row={session.id}
59
60
  data-tip={`${label}\n${summary}`}
60
61
  onActivate={() => !renaming && ctx.onSelect(session.id)}
61
62
  className={
@@ -76,7 +76,7 @@ export const SessionRow = memo(function SessionRow({
76
76
  );
77
77
 
78
78
  return (
79
- <div>
79
+ <div data-session-row={session.id}>
80
80
  <RowShell
81
81
  title={session.name}
82
82
  tipKey={`session:${session.id}`}
@@ -8,6 +8,7 @@ import {
8
8
  doneSummary,
9
9
  type Shelves,
10
10
  } from "@/lib/sidebar/shelves";
11
+ import { shownShelves } from "@/lib/sidebar/visible-order";
11
12
  import type { MachineGroup } from "@/lib/sidebar/machines";
12
13
  import { sidebarUiActions } from "@/stores/sidebarUi";
13
14
  import { cn } from "@/lib/utils";
@@ -124,8 +125,10 @@ export function SessionShelves({
124
125
  snoozedCollapsed: boolean;
125
126
  donePages: number;
126
127
  }) {
128
+ const folds = { doneCollapsed, snoozedCollapsed, donePages };
127
129
  const shown = shelves.done.slice(0, doneLimit(donePages));
128
130
  const hidden = shelves.done.length - shown.length;
131
+ const visible = shownShelves(shelves, folds);
129
132
  const pendingCards = usePendingCardSessions();
130
133
  const doneCards = shelves.done.reduce(
131
134
  (n, r) => n + (pendingCards.get(r.session.id) ?? 0),
@@ -210,10 +213,9 @@ export function SessionShelves({
210
213
  collapsed={snoozedCollapsed}
211
214
  onToggle={sidebarUiActions.toggleSnoozed}
212
215
  />
213
- {!snoozedCollapsed &&
214
- shelves.snoozed.map((r) => (
215
- <SessionRow key={r.session.id} row={r} muted />
216
- ))}
216
+ {visible.snoozed.map((r) => (
217
+ <SessionRow key={r.session.id} row={r} muted />
218
+ ))}
217
219
  </section>
218
220
  )}
219
221
  {shelves.done.length > 0 && (
@@ -223,8 +225,9 @@ export function SessionShelves({
223
225
  collapsed={doneCollapsed}
224
226
  onToggle={sidebarUiActions.toggleDone}
225
227
  />
226
- {!doneCollapsed &&
227
- shown.map((r) => <SessionRow key={r.session.id} row={r} muted />)}
228
+ {visible.done.map((r) => (
229
+ <SessionRow key={r.session.id} row={r} muted />
230
+ ))}
228
231
  {!doneCollapsed && hidden > 0 && (
229
232
  <button
230
233
  type="button"