@kabmo-labs/kabmo 0.3.0 → 0.4.0

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 (136) hide show
  1. package/dist/client/index.html +1 -1
  2. package/dist/client/index.js +412 -383
  3. package/dist/client/static/js/index-3qamd33d.css +1 -0
  4. package/dist/client/static/js/{index-8x5j8zrr.js → index-twsk4wfa.js} +791 -791
  5. package/package.json +1 -1
  6. package/src/client/components/common/batch-selection/BatchActionBar.tsx +8 -1
  7. package/src/client/components/common/session-actions-menu/index.tsx +23 -2
  8. package/src/client/components/layout/{RightActivityBar.test.ts → ActivityBar.test.ts} +5 -5
  9. package/src/client/components/layout/{RightActivityBar.tsx → ActivityBar.tsx} +87 -52
  10. package/src/client/components/layout/LeftActivityBar.test.ts +201 -0
  11. package/src/client/components/layout/LeftDock.tsx +119 -0
  12. package/src/client/components/layout/desktop-layout/LeftColumn.tsx +172 -0
  13. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +22 -5
  14. package/src/client/components/layout/desktop-layout/index.tsx +88 -61
  15. package/src/client/components/layout/desktop-layout/left-column.test.tsx +192 -0
  16. package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +6 -0
  17. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +9 -0
  18. package/src/client/components/layout/session-sidebar/SessionItem.tsx +30 -5
  19. package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
  20. package/src/client/components/layout/session-sidebar/{index.tsx → SessionsView.tsx} +48 -72
  21. package/src/client/components/layout/session-sidebar/new-session-context.test.ts +55 -0
  22. package/src/client/components/mobile/RightSidebar.tsx +8 -5
  23. package/src/client/components/mobile/SessionSidebar.tsx +6 -0
  24. package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -3
  25. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +34 -44
  26. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +9 -1
  27. package/src/client/components/mobile/mobile-session-sidebar/RevealRow.tsx +60 -0
  28. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +41 -13
  29. package/src/client/components/mobile/mobile-session-sidebar/Skeleton.tsx +2 -2
  30. package/src/client/components/settings/categories/panel-plugins/InstalledList.tsx +2 -2
  31. package/src/client/components/settings/categories/panel-plugins/LeftDockSettings.tsx +268 -0
  32. package/src/client/components/settings/categories/panel-plugins/Section.tsx +8 -0
  33. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +69 -1
  34. package/src/client/components/workspace/chat-timeline/MessageItem.gutter.test.ts +118 -0
  35. package/src/client/components/workspace/chat-timeline/MessageItem.tsx +13 -3
  36. package/src/client/components/workspace/chat-timeline/MessageList.tsx +1 -0
  37. package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +106 -0
  38. package/src/client/components/workspace/chat-timeline/SessionSkeleton.tsx +1 -1
  39. package/src/client/components/workspace/chat-timeline/SubagentView.tsx +4 -1
  40. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +5 -2
  41. package/src/client/components/workspace/chat-timeline/ThinkingSection.tsx +6 -2
  42. package/src/client/components/workspace/chat-timeline/TimelineBody.tsx +1 -1
  43. package/src/client/components/workspace/chat-timeline/ToolCallingSection.test.ts +5 -5
  44. package/src/client/components/workspace/chat-timeline/ToolCallingSection.tsx +5 -2
  45. package/src/client/components/workspace/chat-timeline/chat-input/AttachmentToolbar.tsx +25 -1
  46. package/src/client/components/workspace/chat-timeline/chat-input/Toolbar.tsx +14 -9
  47. package/src/client/components/workspace/chat-timeline/chat-input/WaitingDependencyPicker.test.ts +84 -0
  48. package/src/client/components/workspace/chat-timeline/chat-input/WaitingDependencyPicker.tsx +185 -0
  49. package/src/client/components/workspace/chat-timeline/chat-input/compact.test.tsx +154 -0
  50. package/src/client/components/workspace/chat-timeline/chat-input/expansion.test.ts +22 -21
  51. package/src/client/components/workspace/chat-timeline/chat-input/expansion.ts +12 -20
  52. package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +54 -27
  53. package/src/client/components/workspace/chat-timeline/chat-input/props.ts +16 -0
  54. package/src/client/components/workspace/chat-timeline/index.tsx +15 -16
  55. package/src/client/components/workspace/editor/PluginRail.tsx +79 -0
  56. package/src/client/components/workspace/editor/index.tsx +1 -0
  57. package/src/client/components/workspace/editor/plugin-rail.test.tsx +126 -0
  58. package/src/client/components/workspace/model-dropdown/groups.test.ts +60 -0
  59. package/src/client/components/workspace/model-dropdown/groups.ts +7 -1
  60. package/src/client/components/workspace/model-dropdown/use-catalog.ts +8 -2
  61. package/src/client/components/workspace/panels/builtin.tsx +8 -0
  62. package/src/client/components/workspace/plugin-panel/catalog.tsx +237 -0
  63. package/src/client/components/workspace/plugin-panel/resolve.tsx +19 -122
  64. package/src/client/hooks/chat/composer/compact.ts +119 -0
  65. package/src/client/hooks/chat/omp/pending-rows.ts +4 -0
  66. package/src/client/hooks/chat/omp/pending-sessions.ts +47 -0
  67. package/src/client/hooks/chat/omp/pending-sessions.waiting.test.ts +102 -0
  68. package/src/client/hooks/chat/omp/pending-waiting.ts +48 -0
  69. package/src/client/hooks/chat/omp/session-sidebar-controller.ts +29 -0
  70. package/src/client/hooks/chat/timeline/actions.ts +34 -35
  71. package/src/client/hooks/chat/timeline/composer-refs.ts +47 -0
  72. package/src/client/hooks/chat/timeline/enqueue-follow-up.ts +37 -0
  73. package/src/client/hooks/chat/timeline/index.ts +15 -19
  74. package/src/client/hooks/chat/timeline/local-commands.ts +29 -0
  75. package/src/client/hooks/chat/timeline/run-identity.ts +45 -0
  76. package/src/client/hooks/chat/waiting/cancel.ts +34 -0
  77. package/src/client/hooks/chat/waiting/choice.ts +53 -0
  78. package/src/client/hooks/chat/waiting/dispatch.ts +91 -0
  79. package/src/client/hooks/chat/waiting/held.ts +42 -0
  80. package/src/client/hooks/chat/waiting/hold.test.ts +139 -0
  81. package/src/client/hooks/chat/waiting/hold.ts +156 -0
  82. package/src/client/hooks/chat/waiting/store.ts +100 -0
  83. package/src/client/hooks/workspace/left-dock.ts +132 -0
  84. package/src/client/hooks/workspace/panel-registry.ts +13 -0
  85. package/src/client/tailwind.css +22 -0
  86. package/src/server/db.server.test.ts +52 -1
  87. package/src/server/db.server.ts +49 -1
  88. package/src/server/index.ts +9 -0
  89. package/src/server/lib/assets/compressed-cache.server.test.ts +78 -0
  90. package/src/server/lib/assets/compressed-cache.server.ts +88 -0
  91. package/src/server/lib/db/schema.test.ts +1 -0
  92. package/src/server/lib/db/schema.ts +3 -0
  93. package/src/server/lib/fs/fs-path.test.ts +1 -1
  94. package/src/server/lib/omp/rpc/terminal-settle.ts +7 -0
  95. package/src/server/lib/omp/session/delete.server.ts +8 -0
  96. package/src/server/lib/omp/session/ownership.server.test.ts +45 -4
  97. package/src/server/lib/panels/git-update.server.test.ts +18 -1
  98. package/src/server/lib/panels/registry.server.ts +2 -2
  99. package/src/server/lib/queue/delivery.server.ts +31 -39
  100. package/src/server/lib/queue/payload.ts +73 -0
  101. package/src/server/lib/realtime/signals.server.ts +10 -1
  102. package/src/server/lib/realtime/topics/catalog.server.ts +2 -0
  103. package/src/server/lib/realtime/topics/global.server.ts +5 -0
  104. package/src/server/lib/realtime/topics.server.ts +2 -0
  105. package/src/server/lib/util/with-timeout.ts +29 -0
  106. package/src/server/lib/waiting/row-clock.server.ts +37 -0
  107. package/src/server/lib/waiting/runtime.server.test.ts +278 -0
  108. package/src/server/lib/waiting/runtime.server.ts +313 -0
  109. package/src/server/lib/waiting/schema.server.ts +46 -0
  110. package/src/server/lib/waiting/settle.test.ts +84 -0
  111. package/src/server/lib/waiting/settle.ts +77 -0
  112. package/src/server/lib/waiting/store.server.test.ts +167 -0
  113. package/src/server/lib/waiting/store.server.ts +268 -0
  114. package/src/server/lib/waiting/sweep-trigger.ts +35 -0
  115. package/src/server/plugins/compress.ts +18 -6
  116. package/src/server/plugins/ssr.ts +16 -8
  117. package/src/server/plugins/static.test.ts +59 -0
  118. package/src/server/plugins/static.ts +56 -14
  119. package/src/server/routes/index.ts +2 -0
  120. package/src/server/routes/waiting/bindings.ts +14 -0
  121. package/src/server/routes/waiting/index.ts +123 -0
  122. package/src/server/routes/waiting/routes.test.ts +121 -0
  123. package/src/shared/lib/chat/timeline/gutter.test.ts +37 -0
  124. package/src/shared/lib/chat/timeline/gutter.ts +37 -0
  125. package/src/shared/lib/realtime/protocol.ts +10 -0
  126. package/src/shared/lib/workspace/left-dock-width.ts +78 -0
  127. package/src/shared/lib/workspace/left-dock.test.ts +203 -0
  128. package/src/shared/lib/workspace/left-dock.ts +197 -0
  129. package/src/shared/lib/workspace/panel-ids.test.ts +59 -10
  130. package/src/shared/lib/workspace/panel-ids.ts +114 -26
  131. package/src/shared/lib/workspace/sidebar-width.ts +4 -0
  132. package/src/shared/types/index.ts +1 -0
  133. package/src/shared/types/waiting.ts +54 -0
  134. package/src/shared/types/workspace.ts +8 -0
  135. package/src/test-support/isolated-db.ts +3 -4
  136. package/dist/client/static/js/index-pdp5dwc8.css +0 -1
@@ -4,11 +4,14 @@ import { BrandWordmark } from '@/client/components/common/brand-wordmark';
4
4
  export function MobileSessionHeader({ onClose }: { onClose: () => void }) {
5
5
  return (
6
6
  <div
7
- className="border-b border-ink/10 flex items-center justify-between px-3.5 flex-shrink-0 bg-canvas titlebar-drag-region select-none"
7
+ className="border-b border-ink/10 flex items-center justify-between px-3 flex-shrink-0 bg-canvas titlebar-drag-region select-none"
8
8
  style={{
9
9
  height: 'calc(3.5rem + env(safe-area-inset-top, 0px))',
10
- paddingLeft: 'max(0.875rem, env(safe-area-inset-left, 0px), env(titlebar-area-x, 0px))',
11
- paddingRight: 'max(0.875rem, env(safe-area-inset-right, 0px))',
10
+ // `px-3`, the toolbar's own inset: the wordmark above and the New
11
+ // Session button below share one left edge, which the header's old
12
+ // 14px did not.
13
+ paddingLeft: 'max(0.75rem, env(safe-area-inset-left, 0px), env(titlebar-area-x, 0px))',
14
+ paddingRight: 'max(0.75rem, env(safe-area-inset-right, 0px))',
12
15
  paddingTop: 'env(safe-area-inset-top, 0px)',
13
16
  }}
14
17
  >
@@ -1,5 +1,5 @@
1
1
  import { useRef, useState } from 'preact/hooks';
2
- import { ChevronDown, ChevronRight, ChevronUp, GitBranch, MessageSquare, MoreHorizontal, Plus } from 'lucide-preact';
2
+ import { ChevronDown, ChevronRight, GitBranch, MessageSquare, MoreHorizontal, Plus } from 'lucide-preact';
3
3
  import { useSearchParams } from '@/client/lib/router/search-params';
4
4
  import { MobileSessionRow } from '@/client/components/mobile/mobile-session-sidebar/SessionRow';
5
5
  import { SubagentList } from '@/client/components/common/subagent-list';
@@ -17,6 +17,7 @@ import type { WorkspaceFolderData } from '@/shared/types';
17
17
  import type { BatchBinding } from '@/client/components/common/batch-selection/types';
18
18
  import { folderBatchState } from '@/client/components/common/batch-selection/folder-state';
19
19
  import { SelectionCheckbox } from '@/client/components/common/batch-selection/SelectionCheckbox';
20
+ import { SessionRevealRow } from '@/client/components/mobile/mobile-session-sidebar/RevealRow';
20
21
 
21
22
  interface MobileSessionCategoryProps {
22
23
  folder: WorkspaceFolderData;
@@ -32,6 +33,8 @@ interface MobileSessionCategoryProps {
32
33
  batch?: BatchBinding;
33
34
  /** Start a session selection scoped to this workspace, from its `⋯` menu. */
34
35
  onStartSessionSelection?: (folderId: number) => void;
36
+ /** Drop a held prompt from its row's menu. */
37
+ onCancelWait?: (waitId: string) => void;
35
38
  }
36
39
 
37
40
  export function MobileSessionCategory({
@@ -46,6 +49,7 @@ export function MobileSessionCategory({
46
49
  onToast,
47
50
  batch,
48
51
  onStartSessionSelection,
52
+ onCancelWait,
49
53
  }: MobileSessionCategoryProps) {
50
54
  const { visibleCount, showMore, showLess, canShowLess } = useShowMore();
51
55
  const { refresh } = useSidebarData();
@@ -125,7 +129,7 @@ export function MobileSessionCategory({
125
129
  <div className="mb-4">
126
130
  <div
127
131
  onClick={batch ? undefined : onToggleExpand}
128
- className="flex items-center justify-between pl-4 pr-3 py-2 cursor-pointer hover:bg-ink/5 rounded-lg select-none transition-colors"
132
+ className="flex items-center justify-between gap-2 py-2 cursor-pointer hover:bg-ink/5 rounded-lg select-none transition-colors"
129
133
  >
130
134
  <div className="flex items-center space-x-2 min-w-0">
131
135
  {folderCheckbox && folderSummary ? (
@@ -138,11 +142,11 @@ export function MobileSessionCategory({
138
142
  />
139
143
  ) : (
140
144
  <>
141
- {/* The folder glyph occupies the same 16px left slot the session rows
142
- give their roster toggle, so the two columns line up down the
143
- drawer — the icon, the chevron and every row's status glyph share
144
- one x. Desktop gets this from one shared slot; the drawer's
145
- padding is what differs, hence the matching `pl-4`. */}
145
+ {/* The folder glyph occupies the same 16px slot the session rows give
146
+ their status/roster glyph, and the row carries NO leading inset of
147
+ its own: the drawer list's `p-3` is the gutter, so this glyph starts
148
+ at the same x as the New Session button above it (12px) and as the
149
+ search field's frame. One left edge down the whole drawer. */}
146
150
  <span className="w-4 h-4 flex items-center justify-center flex-shrink-0">
147
151
  {folder.customIconUrl ? (
148
152
  <img
@@ -227,14 +231,23 @@ export function MobileSessionCategory({
227
231
  )}
228
232
  </div>
229
233
 
230
- {isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
234
+ {/* The disclosure glyph's BOX ends on the list's content edge — the
235
+ same x the toolbar row's last control ends on above it — and its
236
+ ink sits 4px inside it (`pr-1`), which is the inset a session
237
+ row's `⋯` / roster chevron uses. So the drawer's right column is
238
+ one line of INK, not just one line of boxes: measured before, the
239
+ folder chevron's ink ran 6px further right than every row glyph
240
+ beneath it. */}
241
+ <span className="flex items-center justify-center pr-1">
242
+ {isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
243
+ </span>
231
244
  </>
232
245
  )}
233
246
  </div>
234
247
  </div>
235
248
 
236
249
  {listOpen && (
237
- <div className="mt-0.5 space-y-0.5 pl-2">
250
+ <div className="mt-0.5 space-y-0.5">
238
251
  {visibleSessions.map((session) => {
239
252
  const sessionKey = String(session.id);
240
253
  const isSessionActive = String(activeSessionId) === sessionKey;
@@ -272,6 +285,12 @@ export function MobileSessionCategory({
272
285
  onRenameWithAi={
273
286
  sessionKey.startsWith('new-') ? undefined : () => void handleRenameWithAi(session)
274
287
  }
288
+ waitingFor={session.waitingFor ? { title: session.waitingFor.title } : undefined}
289
+ onCancelWait={
290
+ session.waitingFor
291
+ ? () => onCancelWait?.(session.waitingFor!.waitId)
292
+ : undefined
293
+ }
275
294
  selectable={sessionCheckboxes}
276
295
  selected={Boolean(batch?.selected.includes(sessionKey))}
277
296
  onToggleSelect={batch ? () => batch.toggle(sessionKey) : undefined}
@@ -283,42 +302,13 @@ export function MobileSessionCategory({
283
302
  );
284
303
  })}
285
304
 
286
- {/* Reveal / collapse controls, one row — parity with the desktop
287
- category. Same window, so the phone cannot offer a different
288
- boundary than the sidebar. */}
289
- {(hiddenCount > 0 || canShowLess) && !sessionCheckboxes && (
290
- <div className="flex items-center gap-1">
291
- {hiddenCount > 0 && (
292
- <button
293
- type="button"
294
- onClick={showMore}
295
- className="flex items-center min-w-0 text-left pl-2 pr-3 py-2 text-xs text-ink/60 hover:text-ink"
296
- >
297
- {/* `pl-2` inside the `pl-2` list is what lands this chevron's
298
- centre on the folder header's icon column (16px + 8px), and
299
- the label then starts at 40px — the x a row's title occupies
300
- (16px slot + 8px gap). The `space-x-1.5` this replaced put
301
- both 4px left of the list they belong to. */}
302
- <span className="w-4 h-4 flex-shrink-0 flex items-center justify-center">
303
- <ChevronDown size={12} />
304
- </span>
305
- <span className="w-2 shrink-0" />
306
- <span className="truncate">Show more sessions ({hiddenCount})</span>
307
- </button>
308
- )}
305
+ <SessionRevealRow
306
+ hiddenCount={sessionCheckboxes ? 0 : hiddenCount}
307
+ canShowLess={!sessionCheckboxes && canShowLess}
308
+ onShowMore={showMore}
309
+ onShowLess={showLess}
310
+ />
309
311
 
310
- {canShowLess && (
311
- <button
312
- type="button"
313
- onClick={showLess}
314
- className="ml-auto shrink-0 flex items-center gap-1 text-xs text-ink/60 hover:text-ink px-3 py-2"
315
- >
316
- <ChevronUp size={12} />
317
- <span className="truncate">Show less</span>
318
- </button>
319
- )}
320
- </div>
321
- )}
322
312
  </div>
323
313
  )}
324
314
 
@@ -16,6 +16,8 @@ interface MobileSessionListProps {
16
16
  /** Present only while a batch selection is active. */
17
17
  batch?: BatchBinding;
18
18
  onStartSessionSelection?: (folderId: number) => void;
19
+ /** Drop a held prompt from its row's menu. */
20
+ onCancelWait?: (waitId: string) => void;
19
21
  }
20
22
 
21
23
  export function MobileSessionList({
@@ -30,12 +32,17 @@ export function MobileSessionList({
30
32
  onToast,
31
33
  batch,
32
34
  onStartSessionSelection,
35
+ onCancelWait,
33
36
  }: MobileSessionListProps) {
34
37
  // No fade wiring here: the app-wide scrollbar rule reveals this list's bar
35
38
  // from the document's own capture-phase listener (`lib/scrollbar-activity`),
36
39
  // so the list does not need a ref, a handler, or a re-render per scroll.
40
+ // `pl-3 pr-2`, not an even `p-3`: this container scrolls, so its scrollbar is
41
+ // taken out of the box and the same 12px on both sides LOOKS wider on the
42
+ // right. One step less on the right evens the optical margin back up, and it
43
+ // is why the rows below are aligned to 12 and not to 12 + the bar.
37
44
  return (
38
- <div className="flex-1 scrollbar-overlay-container p-3">
45
+ <div className="flex-1 scrollbar-overlay-container pl-3 pr-2 py-3">
39
46
  {folders.length === 0 ? (
40
47
  <div className="text-center py-12 text-xs text-ink/50 italic">
41
48
  No matching sessions found
@@ -55,6 +62,7 @@ export function MobileSessionList({
55
62
  onToast={onToast}
56
63
  batch={batch}
57
64
  onStartSessionSelection={onStartSessionSelection}
65
+ onCancelWait={onCancelWait}
58
66
  />
59
67
  ))
60
68
  )}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The drawer's reveal / collapse row, below a workspace's sessions.
8
+ *
9
+ * Split out of `Item.tsx` (which is the folder row AND its list) so that file
10
+ * stays under the repo's per-file ceiling. Parity with the desktop category is
11
+ * the whole point: the same window, so the phone cannot offer a different
12
+ * boundary than the sidebar.
13
+ *
14
+ * The chevron starts at the list's own gutter — the same x as a session row's
15
+ * status slot and the folder header's icon — because the list carries no
16
+ * `pl-2` of its own any more. The label then lands on the title column
17
+ * (16px slot + 8px gap).
18
+ */
19
+
20
+ import { ChevronDown, ChevronUp } from 'lucide-preact';
21
+
22
+ interface Props {
23
+ hiddenCount: number;
24
+ canShowLess: boolean;
25
+ onShowMore: () => void;
26
+ onShowLess: () => void;
27
+ }
28
+
29
+ export function SessionRevealRow({ hiddenCount, canShowLess, onShowMore, onShowLess }: Props) {
30
+ if (hiddenCount <= 0 && !canShowLess) return null;
31
+
32
+ return (
33
+ <div className="flex items-center gap-1">
34
+ {hiddenCount > 0 && (
35
+ <button
36
+ type="button"
37
+ onClick={onShowMore}
38
+ className="flex items-center min-w-0 text-left py-2 pr-3 text-xs text-ink/60 hover:text-ink"
39
+ >
40
+ <span className="w-4 h-4 flex-shrink-0 flex items-center justify-center">
41
+ <ChevronDown size={12} />
42
+ </span>
43
+ <span className="w-2 shrink-0" />
44
+ <span className="truncate">Show more sessions ({hiddenCount})</span>
45
+ </button>
46
+ )}
47
+
48
+ {canShowLess && (
49
+ <button
50
+ type="button"
51
+ onClick={onShowLess}
52
+ className="ml-auto shrink-0 flex items-center gap-1 text-xs text-ink/60 hover:text-ink pl-3 py-2"
53
+ >
54
+ <ChevronUp size={12} />
55
+ <span className="truncate">Show less</span>
56
+ </button>
57
+ )}
58
+ </div>
59
+ );
60
+ }
@@ -1,4 +1,4 @@
1
- import { Check, ChevronDown, ChevronRight, CircleQuestionMark, Loader2, MoreHorizontal } from 'lucide-preact';
1
+ import { Check, ChevronDown, ChevronRight, CircleQuestionMark, Hourglass, Loader2, MoreHorizontal } from 'lucide-preact';
2
2
  import type { SessionItemData } from '@/shared/types';
3
3
  import { useInlineRename } from '@/client/hooks/ui/inline-rename';
4
4
  import { useSessionRenaming } from '@/client/hooks/workspace/renaming-sessions';
@@ -28,6 +28,11 @@ export interface MobileSessionRowProps {
28
28
  onRenameWithAi?: () => void;
29
29
  /** Draw a checkbox and let the row's tap toggle the selection instead of
30
30
  * opening the session (batch mode). */
31
+ /** The row's prompt is held until another session finishes (see the desktop
32
+ * row's `waitingFor`: a pending chat has nothing else to show). */
33
+ waitingFor?: { title: string | null };
34
+ /** Drop this row's held prompt, from the row's own menu. */
35
+ onCancelWait?: () => void;
31
36
  selectable?: boolean;
32
37
  selected?: boolean;
33
38
  onToggleSelect?: () => void;
@@ -47,6 +52,8 @@ export function MobileSessionRow({
47
52
  onDelete,
48
53
  onRename,
49
54
  onRenameWithAi,
55
+ waitingFor,
56
+ onCancelWait,
50
57
  selectable = false,
51
58
  selected = false,
52
59
  onToggleSelect,
@@ -65,7 +72,8 @@ export function MobileSessionRow({
65
72
  const renaming = useSessionRenaming(session.id);
66
73
  // One trigger for every row action; also reachable by long-press contextmenu.
67
74
  const menu = useRowMenu();
68
- const hasActions = Boolean(onRename || onArchive || onDelete);
75
+ const hasActions = Boolean(onRename || onArchive || onDelete || onCancelWait);
76
+ const waiting = Boolean(waitingFor);
69
77
 
70
78
  // The roster toggle is the row's LAST element, so its glyph lands in the same
71
79
  // column as the folder header's expand chevron — one vertical line of
@@ -101,6 +109,15 @@ export function MobileSessionRow({
101
109
  data-testid="session-item"
102
110
  data-session-id={String(session.id)}
103
111
  onContextMenu={hasActions && !selectable ? menu.openAtCursor : undefined}
112
+ // The tint covers the WHOLE row — the `⋯` included. It is the row that
113
+ // is active, not the part of it left over after the triggers, and a
114
+ // block that stopped short of the trigger read as a half-painted
115
+ // selection.
116
+ //
117
+ // The gap between the trigger's ink and the block's rounded corner is
118
+ // `pr-1.5` INSIDE the trigger (see below), never padding on this row:
119
+ // a row inset would shrink the tint and pull every trailing glyph 6px
120
+ // left of the folder header's chevron.
104
121
  className={`w-full rounded-lg flex items-center transition-colors ${
105
122
  selectable && selected
106
123
  ? 'bg-ink/10 ring-1 ring-inset ring-ink/20 text-ink'
@@ -110,7 +127,7 @@ export function MobileSessionRow({
110
127
  <button
111
128
  type="button"
112
129
  onClick={selectable ? onToggleSelect : onSelect}
113
- className="flex-1 text-left pl-2 pr-3 py-2 flex items-center justify-between min-w-0"
130
+ className="flex-1 text-left py-2 flex items-center justify-between min-w-0"
114
131
  >
115
132
  <div className="flex items-center min-w-0 pr-2">
116
133
  {/* Status slot, 16px at the same x as the folder header's icon slot,
@@ -135,6 +152,10 @@ export function MobileSessionRow({
135
152
  </span>
136
153
  ) : awaitingInput ? (
137
154
  <CircleQuestionMark size={13} className="text-ink/70 animate-pulse" />
155
+ ) : waiting ? (
156
+ <span role="status" aria-label={`Waiting for ${waitingFor?.title ?? 'another session'}`}>
157
+ <Hourglass size={13} className="text-ink/60 animate-pulse" />
158
+ </span>
138
159
  ) : status === 'stream' ? (
139
160
  <Loader2 size={13} className="text-ink/50 animate-spin" />
140
161
  ) : status ? (
@@ -142,7 +163,14 @@ export function MobileSessionRow({
142
163
  ) : null}
143
164
  </span>
144
165
  )}
145
- <span className="text-xs truncate leading-snug ml-2">{title}</span>
166
+ <span className="flex flex-col min-w-0 ml-2 leading-snug">
167
+ <span className="text-xs truncate">{title}</span>
168
+ {waiting && (
169
+ <span className="text-[10px] text-ink/40 truncate">
170
+ waiting for {waitingFor?.title ?? 'another session'}
171
+ </span>
172
+ )}
173
+ </span>
146
174
  </div>
147
175
 
148
176
  <span className="text-[11px] text-ink/45 font-mono flex-shrink-0 ml-2">{timeAgo ?? ''}</span>
@@ -151,10 +179,10 @@ export function MobileSessionRow({
151
179
  {/* One trigger for rename / archive / delete, always visible: a touch
152
180
  surface has no hover, and the always-on action buttons this replaced
153
181
  consumed a third of a phone's row width.
154
- `mr-1` only when the row has NO roster toggle — it reserves that
155
- column so the trailing glyph's centre stays 19px from the row's right
156
- edge whether the last element is this trigger or the chevron. Without
157
- it the glyphs step in and out from row to row. */}
182
+ `pl-1.5 pr-1`: the BOX still ends on the row's trailing edge (so its
183
+ right edge matches the folder header's chevron and the toolbar's
184
+ last control), while the glyph's ink sits 4px inside it — clear of
185
+ the tinted block's rounded corner without shrinking that block. */}
158
186
  {hasActions && !selectable && (
159
187
  <button
160
188
  type="button"
@@ -163,15 +191,14 @@ export function MobileSessionRow({
163
191
  aria-label="Session actions"
164
192
  aria-haspopup="menu"
165
193
  aria-expanded={menu.anchor !== null}
166
- className={`flex-shrink-0 p-2 text-ink/35 hover:text-ink rounded-lg cursor-pointer ${showChevron ? '' : 'mr-1'}`}
194
+ className="flex-shrink-0 py-1.5 pl-1.5 pr-1 text-ink/35 hover:text-ink rounded-lg cursor-pointer"
167
195
  >
168
196
  <MoreHorizontal size={16} />
169
197
  </button>
170
198
  )}
171
199
 
172
- {/* Last element, so its glyph sits in the folder header's chevron column.
173
- `p-1.5 mr-1.5` centres a 14px glyph on that column while keeping a
174
- touch-sized tap target. */}
200
+ {/* Last element, so its glyph shares the trigger's column — the same
201
+ inset pair and the same 4px ink margin. */}
175
202
  {showChevron && !selectable && (
176
203
  <button
177
204
  type="button"
@@ -179,7 +206,7 @@ export function MobileSessionRow({
179
206
  title={isExpanded ? 'Collapse subagents' : 'Expand subagents'}
180
207
  aria-expanded={isExpanded}
181
208
  aria-label={isExpanded ? 'Collapse subagents' : 'Expand subagents'}
182
- className="flex-shrink-0 p-1.5 mr-1.5 flex items-center justify-center text-ink/50 hover:text-ink rounded-lg cursor-pointer"
209
+ className="flex-shrink-0 py-1.5 pl-1.5 pr-1 flex items-center justify-center text-ink/50 hover:text-ink rounded-lg cursor-pointer"
183
210
  >
184
211
  {isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
185
212
  </button>
@@ -194,6 +221,7 @@ export function MobileSessionRow({
194
221
  onRenameWithAi={onRenameWithAi}
195
222
  onArchive={onArchive}
196
223
  onDelete={onDelete}
224
+ onCancelWait={onCancelWait}
197
225
  onClose={menu.close}
198
226
  />
199
227
  )}
@@ -16,12 +16,12 @@ export function MobileSessionListSkeleton() {
16
16
  <div className="flex-1 p-3 space-y-4" aria-hidden="true">
17
17
  {PLACEHOLDER_FOLDERS.map((rows, folderIndex) => (
18
18
  <div key={folderIndex} className="space-y-2">
19
- <div className="flex items-center gap-2 px-1 py-2">
19
+ <div className="flex items-center gap-2 py-2">
20
20
  <span className="w-4 h-4 rounded-sm animate-pulse bg-ink/10" />
21
21
  <span className="h-3 w-24 rounded animate-pulse bg-ink/10" />
22
22
  </div>
23
23
  {Array.from({ length: rows }, (_, rowIndex) => (
24
- <div key={rowIndex} className="flex items-center gap-2 rounded px-2 py-2">
24
+ <div key={rowIndex} className="flex items-center gap-2 rounded py-2">
25
25
  <span className="w-4 h-4 rounded-full animate-pulse bg-ink/10" />
26
26
  <span
27
27
  className="h-3 rounded animate-pulse bg-ink/10"
@@ -6,7 +6,7 @@ import { PluginMark } from '@/client/components/settings/categories/panel-plugin
6
6
  import { PluginReadmeModal } from '@/client/components/settings/categories/panel-plugins/ReadmeModal';
7
7
  import { GitFreshness } from '@/client/components/settings/categories/panel-plugins/GitFreshness';
8
8
  import type { PluginSlots, SlotName } from '@/client/lib/plugins/slots';
9
- import { pluginPanelKey } from '@/shared/lib/workspace/panel-ids';
9
+ import { pluginEnablementKey } from '@/shared/lib/workspace/panel-ids';
10
10
 
11
11
  /** The four slots a plugin may fill, in the order a card lists them. */
12
12
  const SLOT_ORDER: readonly SlotName[] = ['rightPanel', 'panel', 'headerPanel', 'settingsSection'];
@@ -74,7 +74,7 @@ export function InstalledList({
74
74
  // The ENABLEMENT key is the panel key, not the plugin id: it is the
75
75
  // id the client filters its panel catalog by, so a switch that wrote
76
76
  // the bare id would report success while the panel stayed hidden.
77
- const panelKey = pluginPanelKey(plugin.pluginId);
77
+ const panelKey = pluginEnablementKey(plugin.pluginId);
78
78
  const toggling = busy === `enable:${panelKey}` || busy === `disable:${panelKey}`;
79
79
  const contributed = entry ? SLOT_ORDER.filter((slot) => entry.components[slot]) : [];
80
80