@gperezmz/bb-plugin-thread-glance 0.1.3 → 0.2.1

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.
Files changed (38) hide show
  1. package/README.md +2 -2
  2. package/dist/app.css +1 -1
  3. package/dist/app.js +32 -32
  4. package/dist/app.meta.json +1 -1
  5. package/dist/server.js +8 -8
  6. package/dist/server.js.map +3 -3
  7. package/dist/server.meta.json +1 -1
  8. package/features/thread-list/components/FoldRows.tsx +14 -13
  9. package/features/thread-list/components/GroupSection.tsx +118 -69
  10. package/features/thread-list/components/Rails.tsx +2 -3
  11. package/features/thread-list/components/ThreadList.tsx +21 -16
  12. package/features/thread-list/components/ThreadRowView.tsx +12 -9
  13. package/features/thread-list/components/Toolbar.tsx +129 -187
  14. package/features/thread-list/components/controller.ts +1 -2
  15. package/features/thread-list/components/glyphs.tsx +1 -1
  16. package/features/thread-list/data/useClientPreferences.ts +1 -1
  17. package/features/thread-list/data/useNeedsYouHold.ts +19 -0
  18. package/features/thread-list/model/counters.ts +13 -19
  19. package/features/thread-list/model/details.ts +2 -2
  20. package/features/thread-list/model/expansion.ts +10 -10
  21. package/features/thread-list/model/families.ts +50 -48
  22. package/features/thread-list/model/labels.ts +2 -1
  23. package/features/thread-list/model/layout.ts +6 -11
  24. package/features/thread-list/model/needs-you.ts +133 -0
  25. package/features/thread-list/model/notes.ts +1 -1
  26. package/features/thread-list/model/settings.ts +54 -0
  27. package/features/thread-list/model/share.ts +7 -1
  28. package/features/thread-list/model/sort.ts +14 -6
  29. package/features/thread-list/model/state.ts +11 -11
  30. package/features/thread-list/model/time.ts +1 -1
  31. package/features/thread-list/model/toggles.ts +2 -7
  32. package/features/thread-list/model/view.ts +139 -143
  33. package/features/thread-list/testing/fixtures.ts +19 -10
  34. package/package.json +1 -1
  35. package/server/cli.ts +2 -2
  36. package/shared/preferences.ts +10 -23
  37. package/skills/thread-glance/SKILL.md +9 -10
  38. package/features/thread-list/model/attention.ts +0 -89
@@ -3,7 +3,7 @@
3
3
  "sdkVersion": "0.5.9",
4
4
  "artifactFormatVersion": 1,
5
5
  "pluginId": "thread-glance",
6
- "pluginVersion": "0.1.3",
6
+ "pluginVersion": "0.2.1",
7
7
  "builtWith": {
8
8
  "bbVersion": "0.43.4",
9
9
  "pluginSdkVersion": "0.5.9"
@@ -1,5 +1,5 @@
1
- // The fold rows (`N older` on a group, `N more child threads` on a family)
2
- // and the environment folder row.
1
+ // The fold rows (`N older` on a group, `N more child threads` on a family),
2
+ // the "+N more" line in Needs you, and the environment folder row.
3
3
  import { memo, useState } from "react";
4
4
  import { experimental_Icon as Icon, experimental_ProviderIcon as ProviderIcon } from "@get-bb/plugin-sdk/app";
5
5
  import type { PluginEnvironmentProvider } from "@get-bb/plugin-sdk/app";
@@ -14,7 +14,7 @@ import { ICONS } from "../icons";
14
14
  import { RowRails } from "./Rails";
15
15
  import { olderRowText } from "../model/labels";
16
16
  import { rowIndent } from "../model/layout";
17
- import { EMPTY_LEVEL_TEXT, type EmptyRow, type EnvironmentRow, type OlderRow } from "../model/view";
17
+ import type { EnvironmentRow, LeftOutRow, OlderRow } from "../model/view";
18
18
  import type { RowController } from "./controller";
19
19
  import { FlagGlyph } from "./glyphs";
20
20
  import { RenameEditor } from "./RenameEditor";
@@ -35,9 +35,9 @@ export const OlderRowView = memo(function OlderRowView({ row, controller }: { ro
35
35
  "relative flex w-full items-center rounded-md pr-2 text-left text-xs text-muted-foreground outline-none hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring",
36
36
  inFamily ? "h-7 gap-1.5 max-md:pointer-coarse:h-9" : "h-6 gap-1",
37
37
  )}
38
- style={{ paddingLeft: rowIndent(row.depth, controller.nesting) + (inFamily ? 0 : 2) }}
38
+ style={{ paddingLeft: rowIndent(row.depth) + (inFamily ? 0 : 2) }}
39
39
  >
40
- <RowRails rails={row.rails} nesting={controller.nesting} />
40
+ <RowRails rails={row.rails} />
41
41
  {inFamily ? (
42
42
  <span aria-hidden className="inline-flex size-4 shrink-0 items-center justify-center">
43
43
  <Icon name={ICONS.more} className="size-3.5" />
@@ -50,16 +50,17 @@ export const OlderRowView = memo(function OlderRowView({ row, controller }: { ro
50
50
  );
51
51
  });
52
52
 
53
- /** A plain line under an opened chip that Needs attention leaves empty; not a control. */
54
- export const EmptyRowView = memo(function EmptyRowView({ row, controller }: { row: EmptyRow; controller: RowController }) {
53
+ /** The "+N more" line under a family in Needs you: a count, not a control. */
54
+ export const LeftOutRowView = memo(function LeftOutRowView({ row }: { row: LeftOutRow }) {
55
+ const noun = row.count === 1 ? "child thread" : "child threads";
55
56
  return (
56
57
  <div
57
- className="relative flex h-7 w-full items-center gap-1.5 pr-2 text-xs text-muted-foreground max-md:pointer-coarse:h-9"
58
- style={{ paddingLeft: rowIndent(row.depth, controller.nesting) }}
58
+ className="relative flex h-6 w-full items-center gap-1.5 pr-2 text-xs text-muted-foreground"
59
+ style={{ paddingLeft: rowIndent(row.depth) }}
59
60
  >
60
- <RowRails rails={row.rails} nesting={controller.nesting} />
61
+ <RowRails rails={row.rails} />
61
62
  <span aria-hidden className="size-4 shrink-0" />
62
- <span>{EMPTY_LEVEL_TEXT}</span>
63
+ <span title={`${row.count} more ${noun}`}>+{row.count} more</span>
63
64
  </div>
64
65
  );
65
66
  });
@@ -81,9 +82,9 @@ export const EnvironmentRowView = memo(function EnvironmentRowView({
81
82
  return (
82
83
  <div
83
84
  className="group/row relative flex h-7 w-full items-center gap-1.5 rounded-md pr-1 text-sm text-muted-foreground hover:bg-sidebar-accent"
84
- style={{ paddingLeft: rowIndent(row.depth, controller.nesting) }}
85
+ style={{ paddingLeft: rowIndent(row.depth) }}
85
86
  >
86
- <RowRails rails={row.rails} nesting={controller.nesting} />
87
+ <RowRails rails={row.rails} />
87
88
  <button
88
89
  type="button"
89
90
  aria-expanded={!row.collapsed}
@@ -1,5 +1,5 @@
1
1
  // A top-level group: its header with counters, and its rows, windowed
2
- // in chunks.
2
+ // in chunks; and the Needs you section above every group.
3
3
  import { memo, useEffect, useRef, useState } from "react";
4
4
  import { useDraggable, useDroppable } from "@dnd-kit/core";
5
5
  import { experimental_Icon as Icon } from "@get-bb/plugin-sdk/app";
@@ -14,10 +14,10 @@ import {
14
14
  } from "@/components/ui/dropdown-menu";
15
15
  import { cn } from "@/lib/utils";
16
16
  import { ICONS } from "../icons";
17
- import type { GroupView, Row } from "../model/view";
17
+ import { NEEDS_YOU_GROUP_ID, type GroupView, type NeedsYouView, type Row } from "../model/view";
18
18
  import { chunk, windowedNavValue } from "../model/windowing";
19
19
  import type { RowController } from "./controller";
20
- import { EmptyRowView, EnvironmentRowView, OlderRowView } from "./FoldRows";
20
+ import { EnvironmentRowView, LeftOutRowView, OlderRowView } from "./FoldRows";
21
21
  import { CounterStrip } from "./glyphs";
22
22
  import { visibleCounters } from "../model/counters";
23
23
  import { RenameEditor } from "./RenameEditor";
@@ -44,8 +44,7 @@ export interface GroupController {
44
44
  export type DropStates = ReadonlyMap<string, "valid" | "blocked" | "unchanged" | "before" | "after">;
45
45
 
46
46
  /** The sticky toolbar's height; headers stick below it. */
47
- export const TOOLBAR_HEIGHT = 32;
48
- const HEADER_HEIGHT = 28;
47
+ export const TOOLBAR_HEIGHT = 28;
49
48
  const ROW_HEIGHT = { compact: 28, comfortable: 44 };
50
49
 
51
50
  function canRename(group: GroupView): boolean {
@@ -267,38 +266,107 @@ function WindowedChunk({
267
266
  );
268
267
  }
269
268
 
270
- export const GroupSection = memo(function GroupSection({
271
- group,
269
+ interface RowsProps {
270
+ rows: readonly Row[];
271
+ /** The group the rows are drawn in, for drag ids. */
272
+ groupId: string;
273
+ inPinned: boolean;
274
+ /** Mount every chunk, whatever is on screen (the More popover). */
275
+ forceMount: boolean;
276
+ rowController: RowController;
277
+ environmentProviders: readonly PluginEnvironmentProvider[];
278
+ dropStates: DropStates;
279
+ activeThreadId: string | null;
280
+ editingId: string | null;
281
+ now: number;
282
+ /** Rows get their own stamps as props, so a stamp renders one row. */
283
+ stamps: Stamps;
284
+ }
285
+
286
+ /** A group's rows, windowed in chunks. */
287
+ function Rows({
288
+ rows: all,
289
+ groupId,
290
+ inPinned,
291
+ forceMount,
272
292
  rowController,
273
- groupController,
274
293
  environmentProviders,
275
294
  dropStates,
276
- dropTargetGroupId,
277
295
  activeThreadId,
278
296
  editingId,
279
297
  now,
280
298
  stamps,
299
+ }: RowsProps) {
300
+ const rowHeight = rowController.comfortable ? ROW_HEIGHT.comfortable : ROW_HEIGHT.compact;
301
+ return (
302
+ <div data-sidebar="group-content" className="flex w-full flex-col text-sm">
303
+ {chunk(all).map((rows) => (
304
+ <WindowedChunk
305
+ key={rowKey(rows[0]!)}
306
+ rows={rows}
307
+ rowHeight={rowHeight}
308
+ forceMount={
309
+ forceMount ||
310
+ rows.some(
311
+ (row) =>
312
+ row.type === "thread" &&
313
+ (row.info.thread.id === activeThreadId || row.info.thread.id === editingId),
314
+ )
315
+ }
316
+ >
317
+ {rows.map((row) =>
318
+ row.type === "thread" ? (
319
+ <ThreadRowView
320
+ key={row.key}
321
+ row={row}
322
+ controller={rowController}
323
+ groupId={groupId}
324
+ inPinned={inPinned}
325
+ dropState={dropStates.get(row.info.thread.id) ?? null}
326
+ active={row.info.thread.id === activeThreadId}
327
+ editing={row.info.thread.id === editingId}
328
+ now={now}
329
+ startedAt={stamps.startedAt[row.info.thread.id]}
330
+ finishedAt={stamps.finishedAt[row.info.thread.id]}
331
+ pendingAt={stamps.pendingAt[row.info.thread.id]}
332
+ />
333
+ ) : row.type === "older" ? (
334
+ <OlderRowView key={row.key} row={row} controller={rowController} />
335
+ ) : row.type === "left-out" ? (
336
+ <LeftOutRowView key={row.key} row={row} />
337
+ ) : (
338
+ <EnvironmentRowView
339
+ key={row.key}
340
+ row={row}
341
+ controller={rowController}
342
+ environmentProviders={environmentProviders}
343
+ />
344
+ ),
345
+ )}
346
+ </WindowedChunk>
347
+ ))}
348
+ </div>
349
+ );
350
+ }
351
+
352
+ type SectionProps = Omit<RowsProps, "rows" | "groupId" | "inPinned" | "forceMount">;
353
+
354
+ export const GroupSection = memo(function GroupSection({
355
+ group,
356
+ groupController,
357
+ dropTargetGroupId,
281
358
  inOverflow = false,
282
- }: {
359
+ ...rest
360
+ }: SectionProps & {
283
361
  group: GroupView;
284
- rowController: RowController;
285
362
  groupController: GroupController;
286
- environmentProviders: readonly PluginEnvironmentProvider[];
287
- dropStates: DropStates;
288
363
  dropTargetGroupId: string | null;
289
- activeThreadId: string | null;
290
- editingId: string | null;
291
- now: number;
292
- /** Rows get their own stamps as props, so a stamp renders one row. */
293
- stamps: Stamps;
294
364
  inOverflow?: boolean;
295
365
  }) {
296
366
  const droppable = useDroppable({
297
367
  id: `drop-group:${group.descriptor.id}`,
298
368
  data: { kind: "group", groupId: group.descriptor.id },
299
369
  });
300
- const rowHeight = rowController.comfortable ? ROW_HEIGHT.comfortable : ROW_HEIGHT.compact;
301
- const inPinned = group.descriptor.id === "pinned";
302
370
  return (
303
371
  <section
304
372
  ref={droppable.setNodeRef}
@@ -314,57 +382,38 @@ export const GroupSection = memo(function GroupSection({
314
382
  dropActive={dropTargetGroupId === group.descriptor.id}
315
383
  />
316
384
  {group.collapsed ? null : group.rows.length === 0 ? (
317
- <p className="py-1 pl-8 text-xs text-muted-foreground">No threads</p>
385
+ // A group whose every family sits in Needs you draws only its header.
386
+ group.rootIds.length === 0 ? <p className="py-1 pl-8 text-xs text-muted-foreground">No threads</p> : null
318
387
  ) : (
319
- <div data-sidebar="group-content" className="flex w-full flex-col text-sm">
320
- {chunk(group.rows).map((rows) => (
321
- <WindowedChunk
322
- key={rowKey(rows[0]!)}
323
- rows={rows}
324
- rowHeight={rowHeight}
325
- forceMount={
326
- inOverflow ||
327
- rows.some(
328
- (row) =>
329
- row.type === "thread" &&
330
- (row.info.thread.id === activeThreadId || row.info.thread.id === editingId),
331
- )
332
- }
333
- >
334
- {rows.map((row) =>
335
- row.type === "thread" ? (
336
- <ThreadRowView
337
- key={row.key}
338
- row={row}
339
- controller={rowController}
340
- groupId={group.descriptor.id}
341
- inPinned={inPinned}
342
- dropState={dropStates.get(row.info.thread.id) ?? null}
343
- stickyTop={row.stickyLevel !== null ? TOOLBAR_HEIGHT + HEADER_HEIGHT + row.stickyLevel * rowHeight : null}
344
- active={row.info.thread.id === activeThreadId}
345
- editing={row.info.thread.id === editingId}
346
- now={now}
347
- startedAt={stamps.startedAt[row.info.thread.id]}
348
- finishedAt={stamps.finishedAt[row.info.thread.id]}
349
- pendingAt={stamps.pendingAt[row.info.thread.id]}
350
- />
351
- ) : row.type === "older" ? (
352
- <OlderRowView key={row.key} row={row} controller={rowController} />
353
- ) : row.type === "empty" ? (
354
- <EmptyRowView key={row.key} row={row} controller={rowController} />
355
- ) : (
356
- <EnvironmentRowView
357
- key={row.key}
358
- row={row}
359
- controller={rowController}
360
- environmentProviders={environmentProviders}
361
- />
362
- ),
363
- )}
364
- </WindowedChunk>
365
- ))}
366
- </div>
388
+ <Rows
389
+ {...rest}
390
+ rows={group.rows}
391
+ groupId={group.descriptor.id}
392
+ inPinned={group.descriptor.id === "pinned"}
393
+ forceMount={inOverflow}
394
+ />
367
395
  )}
368
396
  </section>
369
397
  );
370
398
  });
399
+
400
+ /**
401
+ * The Needs you section: a header with its family count, then its rows. It
402
+ * has no collapse, menu or drag, and no drop target of its own.
403
+ */
404
+ export const NeedsYouSection = memo(function NeedsYouSection({ view, ...rest }: SectionProps & { view: NeedsYouView }) {
405
+ return (
406
+ <section aria-label="Needs you" className="relative flex w-full min-w-0 flex-col">
407
+ <h2
408
+ style={{ top: TOOLBAR_HEIGHT }}
409
+ className="sticky z-20 flex h-7 items-center gap-1 bg-sidebar pl-2 pr-2 text-xs font-medium text-muted-foreground max-md:pointer-coarse:h-9"
410
+ >
411
+ <span className="min-w-0 flex-1 truncate">Needs you</span>
412
+ <span className="tabular-nums" aria-label={`${view.familyCount} ${view.familyCount === 1 ? "family" : "families"}`}>
413
+ {view.familyCount}
414
+ </span>
415
+ </h2>
416
+ <Rows {...rest} rows={view.rows} groupId={NEEDS_YOU_GROUP_ID} inPinned={false} forceMount={false} />
417
+ </section>
418
+ );
419
+ });
@@ -1,6 +1,5 @@
1
1
  // The guide rails of a nested row: one hairline per nesting level,
2
2
  // drawn inside the row so they join up from row to row.
3
- import type { Nesting } from "@/shared/preferences";
4
3
  import { railLeft, type Rail } from "../model/layout";
5
4
 
6
5
  // `start` begins just under the 16px status slot, which is centred in the row.
@@ -10,7 +9,7 @@ const SPAN: Record<Rail, { top: string; bottom: string }> = {
10
9
  end: { top: "0px", bottom: "50%" },
11
10
  };
12
11
 
13
- export function RowRails({ rails, nesting }: { rails: readonly (Rail | null)[]; nesting: Nesting }) {
12
+ export function RowRails({ rails }: { rails: readonly (Rail | null)[] }) {
14
13
  if (rails.every((rail) => rail === null)) return null;
15
14
  return (
16
15
  <span aria-hidden data-rails="" className="pointer-events-none absolute inset-0">
@@ -19,7 +18,7 @@ export function RowRails({ rails, nesting }: { rails: readonly (Rail | null)[];
19
18
  <span
20
19
  key={level}
21
20
  className="absolute w-px bg-border"
22
- style={{ left: railLeft(level, nesting), ...SPAN[rail] }}
21
+ style={{ left: railLeft(level), ...SPAN[rail] }}
23
22
  />
24
23
  ),
25
24
  )}
@@ -29,6 +29,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
29
29
  import { cn } from "@/lib/utils";
30
30
  import { useAutoExpand } from "../data/useAutoExpand";
31
31
  import { useClientPreferences } from "../data/useClientPreferences";
32
+ import { useNeedsYouHold } from "../data/useNeedsYouHold";
32
33
  import { useNow } from "../data/useNow";
33
34
  import { usePreferences } from "../data/usePreferences";
34
35
  import { useScheduled } from "../data/useScheduled";
@@ -50,7 +51,7 @@ import { modelDisplayName } from "../model/details";
50
51
  import { groupIdForRoot } from "../model/groups";
51
52
  import { CounterStrip } from "./glyphs";
52
53
  import { cancelPendingCards } from "./row-card";
53
- import { GroupSection, type DropStates, type GroupController } from "./GroupSection";
54
+ import { GroupSection, NeedsYouSection, type DropStates, type GroupController } from "./GroupSection";
54
55
  import type { ProviderDisplay } from "./ProviderBadge";
55
56
  import { ThreadDetails } from "./ThreadDetails";
56
57
  import { Toolbar } from "./Toolbar";
@@ -159,6 +160,7 @@ function ThreadListBody({
159
160
  [ready, threads, activeThreadId, stamps.finishedAt, stamps.seenAt, draftIds, scheduled, now, notes, prefs.childAttention],
160
161
  );
161
162
  const { targets, prune } = useAutoExpand(hydrated ? forest : null, activeThreadId);
163
+ const heldRootId = useNeedsYouHold(forest, activeThreadId);
162
164
  // Rows and groups that did not change keep their objects, so their
163
165
  // memoized components skip the render.
164
166
  const previousView = useRef<ListView | null>(null);
@@ -172,11 +174,11 @@ function ThreadListBody({
172
174
  projects: sidebar.projects,
173
175
  sections: sidebar.sections,
174
176
  prefs,
175
- filter: client.filter,
176
177
  activeThreadId,
178
+ heldRootId,
177
179
  targets,
178
180
  })),
179
- [forest, threads, sidebar.projects, sidebar.sections, prefs, client.filter, activeThreadId, targets],
181
+ [forest, threads, sidebar.projects, sidebar.sections, prefs, activeThreadId, heldRootId, targets],
180
182
  );
181
183
  useLayoutEffect(() => {
182
184
  previousView.current = view;
@@ -356,7 +358,6 @@ function ThreadListBody({
356
358
  provider: providerDisplay,
357
359
  sections: sidebar.sections,
358
360
  mode: prefs.organizationMode,
359
- nesting: prefs.nesting,
360
361
  onNavigate,
361
362
  onToggleChip: (row) => {
362
363
  const { prefs, forest } = latest.current;
@@ -399,7 +400,6 @@ function ThreadListBody({
399
400
  prefs.showPullRequests,
400
401
  prefs.harnessIcon,
401
402
  prefs.organizationMode,
402
- prefs.nesting,
403
403
  defaultBranches,
404
404
  multiHost,
405
405
  providerDisplay,
@@ -523,7 +523,8 @@ function ThreadListBody({
523
523
  }, []);
524
524
 
525
525
  const dropContext = useMemo(() => {
526
- const groupOfThread = new Map<string, string>();
526
+ // A row in Needs you drops as it would in its home group.
527
+ const groupOfThread = new Map<string, string>(Object.entries(view?.needsYou?.homeGroupIds ?? {}));
527
528
  for (const group of [...(view?.groups ?? []), ...(view?.more ?? [])]) {
528
529
  for (const row of group.rows) if (row.type === "thread") groupOfThread.set(row.info.thread.id, group.descriptor.id);
529
530
  }
@@ -677,19 +678,23 @@ function ThreadListBody({
677
678
  return (
678
679
  <ListLiveContext.Provider value={live}>
679
680
  <div className="flex w-full min-w-0 flex-col px-1.5 pb-2">
680
- <Toolbar prefs={prefs} client={client} attentionCount={view.attentionCount} compact={isCompactViewport} onPrefs={update} onClient={updateClient} />
681
+ <Toolbar prefs={prefs} client={client} onPrefs={update} onClient={updateClient} />
681
682
  {threads.length === 0 ? (
682
- <div className="flex flex-col items-start gap-2 px-3 py-4 text-sm text-muted-foreground">
683
- <p>No threads yet.</p>
684
- <Button size="sm" variant="outline" onClick={() => actions.openNewThread({ focusPrompt: true })}>
685
- <Icon name="Plus" aria-hidden />
686
- New thread
687
- </Button>
688
- </div>
683
+ // bb's own pinned New thread button covers the empty list.
684
+ <p className="px-3 py-4 text-sm text-muted-foreground">No threads yet.</p>
689
685
  ) : (
690
686
  <DndContext sensors={sensors} collisionDetection={collision} onDragMove={onDragMove} onDragEnd={onDragEnd} onDragCancel={onDragCancel}>
691
- {view.groups.length === 0 && client.filter === "attention" ? (
692
- <p className="px-3 py-3 text-sm text-muted-foreground">Nothing needs your attention.</p>
687
+ {view.needsYou !== null ? (
688
+ <NeedsYouSection
689
+ view={view.needsYou}
690
+ rowController={rowController}
691
+ environmentProviders={environmentProviders}
692
+ dropStates={dropStates}
693
+ activeThreadId={activeThreadId}
694
+ editingId={editingId}
695
+ now={now}
696
+ stamps={stamps}
697
+ />
693
698
  ) : null}
694
699
  {view.groups.map((group) => (
695
700
  <GroupSection
@@ -81,7 +81,6 @@ export interface ThreadRowViewProps {
81
81
  inPinned: boolean;
82
82
  /** Drop feedback from the list's drag state. */
83
83
  dropState: "valid" | "blocked" | "unchanged" | "before" | "after" | null;
84
- stickyTop: number | null;
85
84
  /** The open thread is this one. */
86
85
  active: boolean;
87
86
  /** Its title is being renamed. */
@@ -100,7 +99,6 @@ export const ThreadRowView = memo(function ThreadRowView({
100
99
  groupId,
101
100
  inPinned,
102
101
  dropState,
103
- stickyTop,
104
102
  active: isActive,
105
103
  editing,
106
104
  now,
@@ -165,7 +163,7 @@ export const ThreadRowView = memo(function ThreadRowView({
165
163
  finishedAt: finishedAt === undefined ? {} : { [thread.id]: finishedAt },
166
164
  },
167
165
  now,
168
- info.state.kind === "needs-you" ? (pendingAt ?? null) : undefined,
166
+ info.state.kind === "waits-on-you" ? (pendingAt ?? null) : undefined,
169
167
  );
170
168
  const menuItems = rowMenuItems({
171
169
  thread,
@@ -292,7 +290,7 @@ export const ThreadRowView = memo(function ThreadRowView({
292
290
  : {};
293
291
 
294
292
  const chip = row.chip;
295
- const indent = rowIndent(row.depth, controller.nesting);
293
+ const indent = rowIndent(row.depth);
296
294
  const title = titleTreatment(row.depth, { unread: info.unread, active: isActive });
297
295
  const twoLines = controller.comfortable || info.note !== null;
298
296
  // A parent keeps full colour so its children read as the quieter ones.
@@ -342,10 +340,8 @@ export const ThreadRowView = memo(function ThreadRowView({
342
340
  dropState === "blocked" && "ring-1 ring-inset ring-destructive/60",
343
341
  dropState === "before" && "shadow-[inset_0_2px_0_0_var(--sidebar-ring)]",
344
342
  dropState === "after" && "shadow-[inset_0_-2px_0_0_var(--sidebar-ring)]",
345
- stickyTop !== null && "sticky z-10 bg-sidebar",
346
- stickyTop !== null && isActive && "bg-state-active",
347
343
  )}
348
- style={{ paddingLeft: indent, top: stickyTop ?? undefined }}
344
+ style={{ paddingLeft: indent }}
349
345
  >
350
346
  <a
351
347
  ref={anchor}
@@ -363,7 +359,7 @@ export const ThreadRowView = memo(function ThreadRowView({
363
359
  }}
364
360
  className="absolute inset-0 rounded-md outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring"
365
361
  />
366
- <RowRails rails={row.rails} nesting={controller.nesting} />
362
+ <RowRails rails={row.rails} />
367
363
  <span className="pointer-events-none relative flex size-4 shrink-0 items-center justify-center">{stateSlot}</span>
368
364
  {row.nested ? (
369
365
  // Tight against the title, and over the row's gap, so it adds 8px.
@@ -490,7 +486,14 @@ export const ThreadRowView = memo(function ThreadRowView({
490
486
  </kbd>
491
487
  ) : (
492
488
  <>
493
- {time !== null ? (
489
+ {row.homeGroupLabel !== null ? (
490
+ <span
491
+ title={`In ${row.homeGroupLabel}`}
492
+ className={cn("pointer-events-none max-w-24 truncate text-[11px] text-muted-foreground transition-opacity", fadeClass)}
493
+ >
494
+ {row.homeGroupLabel}
495
+ </span>
496
+ ) : time !== null ? (
494
497
  <span
495
498
  title={time.label}
496
499
  aria-label={time.label}