@kabmo-labs/kabmo 0.3.1 → 0.5.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 (155) 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-28z316qk.js → index-cs3b557t.js} +792 -792
  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/TopNavbar.tsx +5 -3
  14. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +22 -5
  15. package/src/client/components/layout/desktop-layout/index.tsx +88 -61
  16. package/src/client/components/layout/desktop-layout/left-column.test.tsx +192 -0
  17. package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +6 -0
  18. package/src/client/components/layout/session-sidebar/CategoryItem.tsx +9 -0
  19. package/src/client/components/layout/session-sidebar/Header.tsx +2 -1
  20. package/src/client/components/layout/session-sidebar/SessionItem.tsx +42 -6
  21. package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
  22. package/src/client/components/layout/session-sidebar/{index.tsx → SessionsView.tsx} +48 -72
  23. package/src/client/components/layout/session-sidebar/new-session-context.test.ts +55 -0
  24. package/src/client/components/mobile/LayoutWrapper.tsx +4 -0
  25. package/src/client/components/mobile/RightSidebar.tsx +30 -36
  26. package/src/client/components/mobile/SessionSidebar.tsx +124 -101
  27. package/src/client/components/mobile/mobile-main-view/Header.tsx +3 -2
  28. package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -2
  29. package/src/client/components/mobile/mobile-right-sidebar/Header.tsx +3 -2
  30. package/src/client/components/mobile/mobile-session-sidebar/Dialogs.tsx +106 -0
  31. package/src/client/components/mobile/mobile-session-sidebar/Dock.tsx +80 -0
  32. package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +11 -5
  33. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +34 -44
  34. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +9 -1
  35. package/src/client/components/mobile/mobile-session-sidebar/RevealRow.tsx +60 -0
  36. package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +47 -14
  37. package/src/client/components/mobile/mobile-session-sidebar/Skeleton.tsx +2 -2
  38. package/src/client/components/mobile/mobile-session-sidebar/use-dock.ts +89 -0
  39. package/src/client/components/mobile/panel-chips.test.tsx +155 -0
  40. package/src/client/components/mobile/panel-chips.tsx +74 -0
  41. package/src/client/components/settings/categories/panel-plugins/InstalledList.tsx +2 -2
  42. package/src/client/components/settings/categories/panel-plugins/LeftDockSettings.tsx +268 -0
  43. package/src/client/components/settings/categories/panel-plugins/Section.tsx +8 -0
  44. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +69 -1
  45. package/src/client/components/workspace/chat-timeline/MessageItem.gutter.test.ts +118 -0
  46. package/src/client/components/workspace/chat-timeline/MessageItem.tsx +13 -3
  47. package/src/client/components/workspace/chat-timeline/MessageList.tsx +1 -0
  48. package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +106 -0
  49. package/src/client/components/workspace/chat-timeline/SessionSkeleton.tsx +1 -1
  50. package/src/client/components/workspace/chat-timeline/SubagentView.tsx +4 -1
  51. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +5 -2
  52. package/src/client/components/workspace/chat-timeline/ThinkingSection.tsx +6 -2
  53. package/src/client/components/workspace/chat-timeline/TimelineBody.tsx +1 -1
  54. package/src/client/components/workspace/chat-timeline/ToolCallingSection.test.ts +5 -5
  55. package/src/client/components/workspace/chat-timeline/ToolCallingSection.tsx +5 -2
  56. package/src/client/components/workspace/chat-timeline/chat-input/AttachmentToolbar.tsx +25 -1
  57. package/src/client/components/workspace/chat-timeline/chat-input/Toolbar.tsx +15 -10
  58. package/src/client/components/workspace/chat-timeline/chat-input/WaitingDependencyPicker.test.ts +84 -0
  59. package/src/client/components/workspace/chat-timeline/chat-input/WaitingDependencyPicker.tsx +185 -0
  60. package/src/client/components/workspace/chat-timeline/chat-input/compact.test.tsx +162 -0
  61. package/src/client/components/workspace/chat-timeline/chat-input/expansion.test.ts +22 -21
  62. package/src/client/components/workspace/chat-timeline/chat-input/expansion.ts +12 -20
  63. package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +54 -27
  64. package/src/client/components/workspace/chat-timeline/chat-input/props.ts +16 -0
  65. package/src/client/components/workspace/chat-timeline/index.tsx +15 -16
  66. package/src/client/components/workspace/editor/PluginRail.tsx +79 -0
  67. package/src/client/components/workspace/editor/index.tsx +1 -0
  68. package/src/client/components/workspace/editor/plugin-rail.test.tsx +126 -0
  69. package/src/client/components/workspace/model-dropdown/groups.test.ts +60 -0
  70. package/src/client/components/workspace/model-dropdown/groups.ts +7 -1
  71. package/src/client/components/workspace/model-dropdown/index.tsx +3 -3
  72. package/src/client/components/workspace/model-dropdown/trigger.test.ts +123 -0
  73. package/src/client/components/workspace/model-dropdown/use-catalog.ts +8 -2
  74. package/src/client/components/workspace/panels/builtin.tsx +8 -0
  75. package/src/client/components/workspace/plugin-panel/catalog.tsx +237 -0
  76. package/src/client/components/workspace/plugin-panel/resolve.tsx +19 -122
  77. package/src/client/hooks/chat/composer/compact.ts +119 -0
  78. package/src/client/hooks/chat/omp/pending-rows.ts +4 -0
  79. package/src/client/hooks/chat/omp/pending-sessions.ts +47 -0
  80. package/src/client/hooks/chat/omp/pending-sessions.waiting.test.ts +102 -0
  81. package/src/client/hooks/chat/omp/pending-waiting.ts +48 -0
  82. package/src/client/hooks/chat/omp/session-sidebar-controller.ts +29 -0
  83. package/src/client/hooks/chat/timeline/actions.ts +34 -35
  84. package/src/client/hooks/chat/timeline/composer-refs.ts +47 -0
  85. package/src/client/hooks/chat/timeline/enqueue-follow-up.ts +37 -0
  86. package/src/client/hooks/chat/timeline/index.ts +15 -19
  87. package/src/client/hooks/chat/timeline/local-commands.ts +29 -0
  88. package/src/client/hooks/chat/timeline/queue-rewind.test.ts +6 -0
  89. package/src/client/hooks/chat/timeline/run-identity.ts +45 -0
  90. package/src/client/hooks/chat/waiting/cancel.ts +34 -0
  91. package/src/client/hooks/chat/waiting/choice.ts +53 -0
  92. package/src/client/hooks/chat/waiting/dispatch.ts +91 -0
  93. package/src/client/hooks/chat/waiting/held.ts +42 -0
  94. package/src/client/hooks/chat/waiting/hold.test.ts +139 -0
  95. package/src/client/hooks/chat/waiting/hold.ts +156 -0
  96. package/src/client/hooks/chat/waiting/store.ts +100 -0
  97. package/src/client/hooks/ui/toasts.ts +12 -1
  98. package/src/client/hooks/workspace/left-dock.ts +132 -0
  99. package/src/client/hooks/workspace/panel-registry.ts +13 -0
  100. package/src/client/tailwind.css +22 -0
  101. package/src/server/db.server.test.ts +52 -1
  102. package/src/server/db.server.ts +49 -1
  103. package/src/server/index.ts +9 -0
  104. package/src/server/lib/assets/compressed-cache.server.test.ts +78 -0
  105. package/src/server/lib/assets/compressed-cache.server.ts +88 -0
  106. package/src/server/lib/db/schema.test.ts +1 -0
  107. package/src/server/lib/db/schema.ts +3 -0
  108. package/src/server/lib/fs/fs-path.test.ts +1 -1
  109. package/src/server/lib/omp/rpc/terminal-settle.ts +7 -0
  110. package/src/server/lib/omp/session/delete.server.ts +8 -0
  111. package/src/server/lib/omp/session/ownership.server.test.ts +45 -4
  112. package/src/server/lib/panels/registry.server.ts +2 -2
  113. package/src/server/lib/queue/delivery.server.ts +31 -39
  114. package/src/server/lib/queue/payload.ts +73 -0
  115. package/src/server/lib/realtime/signals.server.ts +10 -1
  116. package/src/server/lib/realtime/topics/catalog.server.ts +2 -0
  117. package/src/server/lib/realtime/topics/global.server.ts +5 -0
  118. package/src/server/lib/realtime/topics.server.ts +2 -0
  119. package/src/server/lib/util/with-timeout.ts +29 -0
  120. package/src/server/lib/waiting/row-clock.server.ts +37 -0
  121. package/src/server/lib/waiting/runtime.server.test.ts +278 -0
  122. package/src/server/lib/waiting/runtime.server.ts +313 -0
  123. package/src/server/lib/waiting/schema.server.ts +46 -0
  124. package/src/server/lib/waiting/settle.test.ts +84 -0
  125. package/src/server/lib/waiting/settle.ts +77 -0
  126. package/src/server/lib/waiting/store.server.test.ts +167 -0
  127. package/src/server/lib/waiting/store.server.ts +268 -0
  128. package/src/server/lib/waiting/sweep-trigger.ts +35 -0
  129. package/src/server/plugins/compress.ts +18 -6
  130. package/src/server/plugins/ssr.ts +16 -8
  131. package/src/server/plugins/static.test.ts +59 -0
  132. package/src/server/plugins/static.ts +56 -14
  133. package/src/server/routes/index.ts +2 -0
  134. package/src/server/routes/waiting/bindings.ts +14 -0
  135. package/src/server/routes/waiting/index.ts +123 -0
  136. package/src/server/routes/waiting/routes.test.ts +121 -0
  137. package/src/shared/lib/chat/timeline/gutter.test.ts +37 -0
  138. package/src/shared/lib/chat/timeline/gutter.ts +37 -0
  139. package/src/shared/lib/realtime/protocol.ts +10 -0
  140. package/src/shared/lib/ui/titlebar.test.ts +124 -0
  141. package/src/shared/lib/ui/titlebar.ts +54 -0
  142. package/src/shared/lib/workspace/left-dock-width.ts +78 -0
  143. package/src/shared/lib/workspace/left-dock.test.ts +203 -0
  144. package/src/shared/lib/workspace/left-dock.ts +197 -0
  145. package/src/shared/lib/workspace/mobile-dock.test.ts +65 -0
  146. package/src/shared/lib/workspace/mobile-dock.ts +55 -0
  147. package/src/shared/lib/workspace/panel-ids.test.ts +59 -10
  148. package/src/shared/lib/workspace/panel-ids.ts +114 -26
  149. package/src/shared/lib/workspace/sidebar-width.ts +4 -0
  150. package/src/shared/types/index.ts +1 -0
  151. package/src/shared/types/waiting.ts +54 -0
  152. package/src/shared/types/workspace.ts +8 -0
  153. package/src/test-support/isolated-db.ts +3 -4
  154. package/src/test-support/silent-socket.ts +69 -0
  155. package/dist/client/static/js/index-pdp5dwc8.css +0 -1
@@ -5,8 +5,11 @@ import { PanelHostProvider } from '@kabmo-labs/kabmo-ui';
5
5
  import { useGitStatus } from '@/client/hooks/workspace/git-status';
6
6
  import { useResolvedRepo } from '@/client/hooks/workspace/repo-scope';
7
7
  import { useSessionState } from '@/client/hooks/workspace/session-state';
8
- import { PluginMark } from '@/client/components/settings/categories/panel-plugins/PluginMark';
9
- import { PANEL_MARK_SIZE } from '@/shared/lib/workspace/right-panels';
8
+ import { useLeftDock } from '@/client/hooks/workspace/left-dock';
9
+ import { SESSIONS_VIEW_ID } from '@/shared/lib/workspace/left-dock';
10
+ import { phoneRightPanels } from '@/shared/lib/workspace/mobile-dock';
11
+ import { PanelChips } from '@/client/components/mobile/panel-chips';
12
+ import { titlebarPaddingLeft, titlebarPaddingRight } from '@/shared/lib/ui/titlebar';
10
13
 
11
14
  interface MobileRightSidebarProps {
12
15
  enabled?: boolean;
@@ -43,12 +46,23 @@ export function MobileRightSidebar({
43
46
  // the user never asked for a search. Restoring the tab they left also keeps
44
47
  // the phone and the desktop on the same view.
45
48
  const [activeTab, setActiveTab] = useSessionState<string>('layout.mobileRightPanel', 'files');
46
- // ONE catalog, the same one the desktop bar reads: the built-in views and the
47
- // installed plugins are one list, in one order, so the phone cannot become a
48
- // second list that drifts from the desktop's. A view switched off is absent
49
- // here for the same reason it is absent there.
50
- const catalog = usePanelCatalog();
51
- const activePanel = catalog.find((panel) => panel.id === activeTab);
49
+ // ONE catalog, the same one the desktop bars read, and deliberately `all`:
50
+ // a phone has ONE drawer, so a plugin panel the user placed on the left must
51
+ // stay reachable here — reading only the right column would make placing a
52
+ // panel on the left hide it from every phone. The dock's own session list is
53
+ // filtered out below: the phone already has that as its own drawer screen,
54
+ // and a second copy inside this one would be two lists of the same sessions.
55
+ //
56
+ // The exception is the one case where the LEFT drawer can now reach them
57
+ // itself: with the rail on, a placed panel gets a chip there, and listing it
58
+ // here as well would put one panel on two strips of one phone.
59
+ const dock = useLeftDock();
60
+ const catalog = phoneRightPanels(usePanelCatalog('all'), SESSIONS_VIEW_ID, dock.enabled);
61
+ // A stored tab can name a panel the user has since placed on the LEFT rail.
62
+ // It is gone from this catalog, and saying "No session selected" over a
63
+ // drawer that has views would be a wrong answer — the first view is drawn
64
+ // instead, and the stored id is replaced on the next click.
65
+ const activePanel = catalog.find((panel) => panel.id === activeTab) ?? catalog[0];
52
66
  // Same source the desktop activity bar uses — the Source Control view's own
53
67
  // repo pick, read shared so switching repos moves this dot too. Polls only
54
68
  // while this drawer is the mounted screen (the poll is visibility-gated).
@@ -81,37 +95,17 @@ export function MobileRightSidebar({
81
95
  style={{
82
96
  height: 'calc(3.5rem + env(safe-area-inset-top, 0px))',
83
97
  paddingTop: 'env(safe-area-inset-top, 0px)',
84
- paddingLeft: 'max(0.75rem, env(safe-area-inset-left, 0px))',
85
- paddingRight: 'max(0.75rem, env(safe-area-inset-right, 0px))',
98
+ paddingLeft: titlebarPaddingLeft('0.75rem', true),
99
+ paddingRight: titlebarPaddingRight('0.75rem', true),
86
100
  }}
87
101
  >
88
102
 
89
- <div className="flex items-center space-x-1.5 overflow-x-auto no-scrollbar py-1">
90
- {catalog.map((panel) => (
91
- <button
92
- key={panel.id}
93
- type="button"
94
- onClick={() => setActiveTab(panel.id)}
95
- className={`relative flex items-center transition-all cursor-pointer ${
96
- activeTab === panel.id
97
- ? 'space-x-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold bg-ink text-canvas shadow-sm'
98
- : 'p-2 rounded-lg text-ink/70 hover:bg-ink/5'
99
- }`}
100
- title={panel.title}
101
- >
102
- {panel.builtin ? panel.icon : <PluginMark name={panel.name} iconUrl={panel.iconUrl} size={PANEL_MARK_SIZE} />}
103
- {activeTab === panel.id && (
104
- <span className={panel.id === 'git' ? 'tracking-wide' : undefined}>{panel.label}</span>
105
- )}
106
- {panel.id === 'git' && hasGitChanges && (
107
- <span
108
- className="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-info"
109
- title="Uncommitted changes"
110
- />
111
- )}
112
- </button>
113
- ))}
114
- </div>
103
+ <PanelChips
104
+ panels={catalog}
105
+ activeId={activePanel?.id ?? activeTab}
106
+ onSelect={setActiveTab}
107
+ hasGitChanges={hasGitChanges}
108
+ />
115
109
 
116
110
  <button
117
111
  type="button"
@@ -3,24 +3,24 @@ import { MobileSessionHeader } from '@/client/components/mobile/mobile-session-s
3
3
  import { MobileSessionToolbar } from '@/client/components/mobile/mobile-session-sidebar/Toolbar';
4
4
  import { MobileSessionList } from '@/client/components/mobile/mobile-session-sidebar/List';
5
5
  import { MobileSessionFooter } from '@/client/components/mobile/mobile-session-sidebar/Footer';
6
- import { ToastStack } from '@/client/components/common/ToastStack';
7
- import { AboutModal, NewWorkspaceModal } from '@/client/components/layout/session-sidebar/Modals';
8
6
  import { useAndroidDownload } from '@/client/hooks/ui/updates/android-download';
9
- import { SchedulerModal } from '@/client/components/layout/session-sidebar/scheduler-modal';
10
- import { UpdatePopup } from '@/client/components/layout/update-popup';
11
7
  import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
12
8
  import { useKabmoEvent } from '@/client/hooks/ui/window-event';
13
9
  import { useToasts } from '@/client/hooks/ui/toasts';
10
+ import { useCancelWait } from '@/client/hooks/chat/waiting/cancel';
11
+ import { useWaitingDispatch } from '@/client/hooks/chat/waiting/dispatch';
14
12
  import { useUpdates } from '@/client/hooks/ui/updates';
15
13
  import { UPDATE_REQUEST_EVENT } from '@/shared/lib/updates/popup-state';
16
14
  import { useSessionSidebarController } from '@/client/hooks/chat/omp/session-sidebar-controller';
17
15
  import { useScheduledTaskCount } from '@/client/hooks/workspace/scheduled-tasks';
18
- import { cloneWorkspaceFolder, createWorkspaceFolder } from '@/client/hooks/workspace/new-workspace';
19
16
  import { MobileSessionListSkeleton } from '@/client/components/mobile/mobile-session-sidebar/Skeleton';
20
17
  import { useBatchSelection } from '@/client/hooks/workspace/batch-selection';
21
18
  import { useBatchDeleteRun } from '@/client/hooks/workspace/batch-delete-run';
19
+ import { phoneLeftStripVisible } from '@/shared/lib/workspace/mobile-dock';
20
+ import { dockBodyProps, useMobileDock } from '@/client/components/mobile/mobile-session-sidebar/use-dock';
21
+ import { MobileDockBody, MobileDockStrip } from '@/client/components/mobile/mobile-session-sidebar/Dock';
22
+ import { MobileDrawerDialogs } from '@/client/components/mobile/mobile-session-sidebar/Dialogs';
22
23
  import { BatchActionBar } from '@/client/components/common/batch-selection/BatchActionBar';
23
- import { BatchDeleteModal } from '@/client/components/common/batch-selection/BatchDeleteModal';
24
24
  import type { BatchBinding } from '@/client/components/common/batch-selection/types';
25
25
  import { liveSelection, selectableSessionIds } from '@/shared/lib/workspace/batch-select';
26
26
 
@@ -32,6 +32,12 @@ interface MobileSessionSidebarProps {
32
32
  onClose: () => void;
33
33
  onDesktopToggle?: () => void;
34
34
  appSettings?: Record<string, any>;
35
+ /** The active workspace's root, for a plugin panel placed on the dock. */
36
+ rootPath?: string;
37
+ /** Bumped after a write so a placed panel re-reads, like the right drawer. */
38
+ refreshKey?: number;
39
+ onRefresh?: () => void;
40
+ onOpenFile?: (file: unknown) => void;
35
41
  }
36
42
 
37
43
  export function MobileSessionSidebar({
@@ -41,7 +47,11 @@ export function MobileSessionSidebar({
41
47
  onNewSessionForFolder,
42
48
  onClose,
43
49
  onDesktopToggle,
44
- appSettings = {}
50
+ appSettings = {},
51
+ rootPath,
52
+ refreshKey = 0,
53
+ onRefresh,
54
+ onOpenFile,
45
55
  }: MobileSessionSidebarProps) {
46
56
  const {
47
57
  folders,
@@ -80,6 +90,8 @@ export function MobileSessionSidebar({
80
90
  window.__OMP_BOOTSTRAP__?.isAndroid,
81
91
  );
82
92
  const { toasts, pushToast, dismissToast } = useToasts();
93
+ const cancelWait = useCancelWait(pushToast);
94
+ useWaitingDispatch({ onNotice: pushToast });
83
95
 
84
96
  // The popup's "Update now" hands the run over rather than starting its own —
85
97
  // see the desktop sidebar. The phone must also bring the drawer forward, or
@@ -186,64 +198,103 @@ export function MobileSessionSidebar({
186
198
  else batch.begin('workspace');
187
199
  };
188
200
 
201
+ const dock = useMobileDock();
202
+ const showingSessions = dock.showingSessions;
203
+ const activeView = dock.activeView;
204
+ const dockBody = dockBodyProps({
205
+ sessionId: activeSessionId,
206
+ refreshKey,
207
+ rootPath,
208
+ onRefresh,
209
+ onOpenFile,
210
+ onClose,
211
+ });
212
+
189
213
  return (
190
214
  <div className="flex flex-col h-full w-full bg-canvas text-ink relative select-none">
191
-
192
- <MobileSessionHeader onClose={onClose} />
193
215
 
194
- <MobileSessionToolbar
195
- searchQuery={searchQuery}
196
- sortOption={sortOption}
197
- optionsOpen={optionsOpen}
198
- showArchived={showArchived}
199
- optionsRef={optionsRef}
200
- onNewSession={handleNewSessionAndClose}
201
- onNewWorkspace={() => setNewWorkspaceOpen(true)}
202
- onScheduler={() => setSchedulerOpen(true)}
203
- scheduledCount={scheduledCount}
204
- onRefresh={refreshNow}
205
- refreshing={isRefreshing}
206
- onToggleOptions={() => setOptionsOpen(!optionsOpen)}
207
- onSortChange={handleSortChange}
208
- onResetSort={() => handleSortChange('A-Z')}
209
- onToggleArchived={handleToggleArchived}
210
- onHideArchived={() => setShowArchived(false)}
211
- onSearchChange={setSearchQuery}
212
- onClearSearch={() => setSearchQuery('')}
213
- onSelectWorkspaces={startWorkspaceSelection}
214
- selecting={batch.mode !== null}
216
+ <MobileSessionHeader
217
+ onClose={onClose}
218
+ viewLabel={showingSessions ? 'Sessions' : dock.activePanel?.label ?? 'Panel'}
215
219
  />
216
220
 
217
- {initializing ? (
218
- <MobileSessionListSkeleton />
219
- ) : (
220
- <MobileSessionList
221
- folders={processedFolders}
222
- activeSessionId={activeSessionId}
223
- expandedFolders={expandedFolders}
224
- showArchived={showArchived}
225
- sessionStatus={sessionStatus}
226
- onSelectSession={handleSelectSession}
227
- onNewSessionForFolder={handleNewSessionForFolderAndClose}
228
- onToggleFolder={toggleFolder}
229
- onToast={pushToast}
230
- batch={batchBinding}
231
- onStartSessionSelection={(folderId) => batch.begin('session', folderId)}
232
- />
233
- )}
221
+ {/* The strip appears only once there is something to switch TO: with the
222
+ rail off, or with nothing placed on it, a row holding one chip would
223
+ be chrome that never changes. The exception is a stored view that no
224
+ longer resolves — the body is a notice, and without the strip the
225
+ session list would be unreachable (the desktop has the same escape in
226
+ its rail, which always holds Sessions). */}
227
+ {phoneLeftStripVisible(dock.enabled, dock.placed.length, showingSessions) ? (
228
+ <MobileDockStrip panels={dock.catalog} activeId={activeView} onSelect={dock.setActiveView} />
229
+ ) : null}
230
+
231
+ {/* The list is HIDDEN, never unmounted, while a panel is on screen — the
232
+ same rule the desktop dock follows (`LeftDockView`): its folder
233
+ expansion, scroll position and batch selection must survive a look at
234
+ another view, and a re-mount would also re-seed every expansion from
235
+ the server's flags. */}
236
+ <div className={showingSessions ? 'contents' : 'hidden'}>
237
+ <>
238
+ <MobileSessionToolbar
239
+ searchQuery={searchQuery}
240
+ sortOption={sortOption}
241
+ optionsOpen={optionsOpen}
242
+ showArchived={showArchived}
243
+ optionsRef={optionsRef}
244
+ onNewSession={handleNewSessionAndClose}
245
+ onNewWorkspace={() => setNewWorkspaceOpen(true)}
246
+ onScheduler={() => setSchedulerOpen(true)}
247
+ scheduledCount={scheduledCount}
248
+ onRefresh={refreshNow}
249
+ refreshing={isRefreshing}
250
+ onToggleOptions={() => setOptionsOpen(!optionsOpen)}
251
+ onSortChange={handleSortChange}
252
+ onResetSort={() => handleSortChange('A-Z')}
253
+ onToggleArchived={handleToggleArchived}
254
+ onHideArchived={() => setShowArchived(false)}
255
+ onSearchChange={setSearchQuery}
256
+ onClearSearch={() => setSearchQuery('')}
257
+ onSelectWorkspaces={startWorkspaceSelection}
258
+ selecting={batch.mode !== null}
259
+ />
234
260
 
235
- {batch.mode && (
236
- <BatchActionBar
237
- mode={batch.mode}
238
- count={batchSelected.length}
239
- total={batchAvailable.length}
240
- busy={batchDelete.isDeleting}
241
- scopeLabel={scopeFolder?.name}
242
- onSelectAll={() => batch.selectAllOf(batchAvailable)}
243
- onUnselectAll={() => batch.unselectAllOf(batchAvailable)}
244
- onDelete={() => batchDelete.request(batchSelected)}
245
- onCancel={batch.end}
246
- />
261
+ {initializing ? (
262
+ <MobileSessionListSkeleton />
263
+ ) : (
264
+ <MobileSessionList
265
+ folders={processedFolders}
266
+ activeSessionId={activeSessionId}
267
+ expandedFolders={expandedFolders}
268
+ showArchived={showArchived}
269
+ sessionStatus={sessionStatus}
270
+ onSelectSession={handleSelectSession}
271
+ onNewSessionForFolder={handleNewSessionForFolderAndClose}
272
+ onToggleFolder={toggleFolder}
273
+ onToast={pushToast}
274
+ batch={batchBinding}
275
+ onStartSessionSelection={(folderId) => batch.begin('session', folderId)}
276
+ onCancelWait={cancelWait}
277
+ />
278
+ )}
279
+
280
+ {batch.mode && (
281
+ <BatchActionBar
282
+ drawer
283
+ mode={batch.mode}
284
+ count={batchSelected.length}
285
+ total={batchAvailable.length}
286
+ busy={batchDelete.isDeleting}
287
+ scopeLabel={scopeFolder?.name}
288
+ onSelectAll={() => batch.selectAllOf(batchAvailable)}
289
+ onUnselectAll={() => batch.unselectAllOf(batchAvailable)}
290
+ onDelete={() => batchDelete.request(batchSelected)}
291
+ onCancel={batch.end}
292
+ />
293
+ )}
294
+ </>
295
+ </div>
296
+ {showingSessions ? null : (
297
+ <MobileDockBody panelKey={activeView} panel={dock.activePanel} body={dockBody} />
247
298
  )}
248
299
 
249
300
  <MobileSessionFooter
@@ -258,52 +309,24 @@ export function MobileSessionSidebar({
258
309
  }}
259
310
  />
260
311
 
261
- {/* Reusable Modals */}
262
- <NewWorkspaceModal
263
- isOpen={newWorkspaceOpen}
264
- onClose={() => setNewWorkspaceOpen(false)}
265
- onCreate={createWorkspaceFolder}
266
- onClone={cloneWorkspaceFolder}
267
- />
268
- <SchedulerModal
269
- isOpen={schedulerOpen}
270
- onClose={() => setSchedulerOpen(false)}
271
- activeSessionId={activeSessionId === null ? null : String(activeSessionId)}
272
- onToast={pushToast}
273
- />
274
- <AboutModal
275
- isOpen={aboutOpen}
276
- onClose={() => setAboutOpen(false)}
312
+ <MobileDrawerDialogs
313
+ activeSessionId={activeSessionId}
314
+ newWorkspaceOpen={newWorkspaceOpen}
315
+ onCloseNewWorkspace={() => setNewWorkspaceOpen(false)}
316
+ schedulerOpen={schedulerOpen}
317
+ onCloseScheduler={() => setSchedulerOpen(false)}
318
+ aboutOpen={aboutOpen}
319
+ onCloseAbout={() => setAboutOpen(false)}
277
320
  updates={updates}
278
- onToast={pushToast}
279
- />
280
-
281
- {/* "Update available" announcement — once per version, gated on the check.
282
- Its "Update now" hands the run to the About modal below. */}
283
- <UpdatePopup updates={updates} />
284
-
285
- {/* Batch delete confirmation — same dialog the desktop draws. */}
286
- <BatchDeleteModal
287
- mode={batch.mode ?? 'session'}
288
- count={batchDelete.pending.length}
321
+ pushToast={pushToast}
322
+ batchDelete={batchDelete}
323
+ batchMode={batch.mode}
324
+ onEndBatch={batch.end}
289
325
  scopeLabel={scopeFolder?.name}
290
- isDeleting={batchDelete.isDeleting}
291
- outcome={batchDelete.outcome}
292
- onClose={() => {
293
- const refusedCount = batchDelete.outcome?.refused.length ?? 0;
294
- batchDelete.cancel();
295
- if (batchDelete.outcome && refusedCount === 0) batch.end();
296
- }}
297
- onConfirm={() => {
298
- const mode = batch.mode ?? 'session';
299
- void batchDelete.confirm(mode).then((result) => {
300
- if (result && result.refused.length === 0) batch.end();
301
- });
302
- }}
326
+ toasts={toasts}
327
+ dismissToast={dismissToast}
303
328
  />
304
329
 
305
- <ToastStack toasts={toasts} onDismiss={dismissToast} />
306
-
307
330
  </div>
308
331
  );
309
332
  }
@@ -8,6 +8,7 @@ import { useSessionRenaming } from '@/client/hooks/workspace/renaming-sessions';
8
8
  import { StreamStatusDot } from '@/client/components/common/StreamStatusDot';
9
9
  import { BrandWordmark } from '@/client/components/common/brand-wordmark';
10
10
  import { HeaderControls } from '@/client/components/common/header-controls';
11
+ import { titlebarPaddingLeft, titlebarPaddingRight } from '@/shared/lib/ui/titlebar';
11
12
 
12
13
  interface MobileHeaderProps {
13
14
  activeSessionTitle: string;
@@ -91,8 +92,8 @@ export function MobileHeader({
91
92
  className="flex-shrink-0 bg-canvas border-b border-ink/10 flex items-center justify-between gap-2 px-3 z-20 titlebar-drag-region select-none"
92
93
  style={{
93
94
  height: 'calc(3.5rem + env(safe-area-inset-top, 0px))',
94
- paddingLeft: 'max(0.75rem, env(safe-area-inset-left, 0px), env(titlebar-area-x, 0px))',
95
- paddingRight: 'max(0.75rem, env(safe-area-inset-right, 0px), calc(100vw - env(titlebar-area-width, 100vw)))',
95
+ paddingLeft: titlebarPaddingLeft('0.75rem', true),
96
+ paddingRight: titlebarPaddingRight('0.75rem', true),
96
97
  paddingTop: 'env(safe-area-inset-top, 0px)',
97
98
  }}
98
99
  >
@@ -3,6 +3,7 @@ import { FileIcon } from '@/client/components/common/file-icon';
3
3
  import { DiffPanel } from '@/client/components/workspace/diff-panel';
4
4
  import { getGitStatusInfo } from '@/shared/lib/fs/git-status';
5
5
  import { diffTabId } from '@/shared/lib/workspace/file-tab-id';
6
+ import { titlebarPaddingLeft, titlebarPaddingRight } from '@/shared/lib/ui/titlebar';
6
7
 
7
8
  interface MobileFullDiffProps {
8
9
  diff: {
@@ -37,8 +38,8 @@ export function MobileFullDiff({ diff, onClose, onFileSaved }: MobileFullDiffPro
37
38
  style={{
38
39
  height: 'calc(3rem + env(safe-area-inset-top, 0px))',
39
40
  paddingTop: 'env(safe-area-inset-top, 0px)',
40
- paddingLeft: 'max(0.75rem, env(safe-area-inset-left, 0px))',
41
- paddingRight: 'max(0.75rem, env(safe-area-inset-right, 0px))',
41
+ paddingLeft: titlebarPaddingLeft('0.75rem', true),
42
+ paddingRight: titlebarPaddingRight('0.75rem', true),
42
43
  }}
43
44
  >
44
45
  <div className="flex items-center space-x-2 min-w-0 pr-2">
@@ -1,5 +1,6 @@
1
1
  import { ArrowLeft, Check, Copy, Download, Eye, EyeOff, Save, WrapText, ZoomIn, ZoomOut } from 'lucide-preact';
2
2
  import { FileIcon } from '@/client/components/common/file-icon';
3
+ import { titlebarPaddingLeft, titlebarPaddingRight } from '@/shared/lib/ui/titlebar';
3
4
 
4
5
  interface EditorHeaderProps {
5
6
  fileName: string;
@@ -47,8 +48,8 @@ export function EditorHeader({
47
48
  style={{
48
49
  height: 'calc(3rem + env(safe-area-inset-top, 0px))',
49
50
  paddingTop: 'env(safe-area-inset-top, 0px)',
50
- paddingLeft: 'max(0.75rem, env(safe-area-inset-left, 0px))',
51
- paddingRight: 'max(0.75rem, env(safe-area-inset-right, 0px))',
51
+ paddingLeft: titlebarPaddingLeft('0.75rem', true),
52
+ paddingRight: titlebarPaddingRight('0.75rem', true),
52
53
  }}
53
54
  >
54
55
  <div className="flex items-center space-x-2 min-w-0 pr-2">
@@ -0,0 +1,106 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The phone session drawer's dialogs.
8
+ *
9
+ * Split out of `SessionSidebar` for the file ceiling, and it is a clean seam:
10
+ * every one of these is app-level (a workspace, a schedule, the console's own
11
+ * version and update, a batch delete confirmation, the toast stack) and none of
12
+ * them reads the drawer's own view state. Keeping them here also means the
13
+ * drawer's body — session list or a placed panel — stays readable as one
14
+ * expression rather than being buried between modals.
15
+ */
16
+
17
+ import { ToastStack } from '@/client/components/common/ToastStack';
18
+ import { AboutModal, NewWorkspaceModal } from '@/client/components/layout/session-sidebar/Modals';
19
+ import { SchedulerModal } from '@/client/components/layout/session-sidebar/scheduler-modal';
20
+ import { UpdatePopup } from '@/client/components/layout/update-popup';
21
+ import { BatchDeleteModal } from '@/client/components/common/batch-selection/BatchDeleteModal';
22
+ import { createWorkspaceFolder, cloneWorkspaceFolder } from '@/client/hooks/workspace/new-workspace';
23
+ import type { BatchDeleteRun } from '@/client/hooks/workspace/batch-delete-run';
24
+ import type { UseUpdatesResult } from '@/client/hooks/ui/updates';
25
+ import type { ToastsHandle } from '@/client/hooks/ui/toasts';
26
+
27
+ interface MobileDrawerDialogsProps {
28
+ activeSessionId: number | string | null;
29
+ newWorkspaceOpen: boolean;
30
+ onCloseNewWorkspace: () => void;
31
+ schedulerOpen: boolean;
32
+ onCloseScheduler: () => void;
33
+ aboutOpen: boolean;
34
+ onCloseAbout: () => void;
35
+ updates: UseUpdatesResult;
36
+ pushToast: ToastsHandle['pushToast'];
37
+ batchDelete: BatchDeleteRun;
38
+ /** The batch mode the confirmation names, and the way out of it. */
39
+ batchMode: 'workspace' | 'session' | null;
40
+ onEndBatch: () => void;
41
+ scopeLabel?: string;
42
+ toasts: ToastsHandle['toasts'];
43
+ dismissToast: ToastsHandle['dismissToast'];
44
+ }
45
+
46
+ export function MobileDrawerDialogs({
47
+ activeSessionId,
48
+ newWorkspaceOpen,
49
+ onCloseNewWorkspace,
50
+ schedulerOpen,
51
+ onCloseScheduler,
52
+ aboutOpen,
53
+ onCloseAbout,
54
+ updates,
55
+ pushToast,
56
+ batchDelete,
57
+ batchMode,
58
+ onEndBatch,
59
+ scopeLabel,
60
+ toasts,
61
+ dismissToast,
62
+ }: MobileDrawerDialogsProps) {
63
+ return (
64
+ <>
65
+ <NewWorkspaceModal
66
+ isOpen={newWorkspaceOpen}
67
+ onClose={onCloseNewWorkspace}
68
+ onCreate={createWorkspaceFolder}
69
+ onClone={cloneWorkspaceFolder}
70
+ />
71
+ <SchedulerModal
72
+ isOpen={schedulerOpen}
73
+ onClose={onCloseScheduler}
74
+ activeSessionId={activeSessionId === null ? null : String(activeSessionId)}
75
+ onToast={pushToast}
76
+ />
77
+ <AboutModal isOpen={aboutOpen} onClose={onCloseAbout} updates={updates} onToast={pushToast} />
78
+
79
+ {/* "Update available" announcement — once per version, gated on the check.
80
+ Its "Update now" hands the run to the About modal. */}
81
+ <UpdatePopup updates={updates} />
82
+
83
+ {/* Batch delete confirmation — same dialog the desktop draws. */}
84
+ <BatchDeleteModal
85
+ mode={batchMode ?? 'session'}
86
+ count={batchDelete.pending.length}
87
+ scopeLabel={scopeLabel}
88
+ isDeleting={batchDelete.isDeleting}
89
+ outcome={batchDelete.outcome}
90
+ onClose={() => {
91
+ const refusedCount = batchDelete.outcome?.refused.length ?? 0;
92
+ batchDelete.cancel();
93
+ if (batchDelete.outcome && refusedCount === 0) onEndBatch();
94
+ }}
95
+ onConfirm={() => {
96
+ const mode = batchMode ?? 'session';
97
+ void batchDelete.confirm(mode).then((result) => {
98
+ if (result && result.refused.length === 0) onEndBatch();
99
+ });
100
+ }}
101
+ />
102
+
103
+ <ToastStack toasts={toasts} onDismiss={dismissToast} />
104
+ </>
105
+ );
106
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The phone's LEFT dock: the chip strip and the body of a placed panel.
8
+ *
9
+ * The desktop's left column is a dock — the session list plus whatever plugin
10
+ * the user placed beside it, switched by a vertical rail. A phone has no room
11
+ * for a rail, so the rail is the same horizontal chip strip the right drawer
12
+ * uses (`panel-chips.tsx`), and the body is the same `panel.render`.
13
+ *
14
+ * The session list is NOT rendered here: the phone's list is its own component
15
+ * (`MobileSessionList`), unlike the desktop's `SessionsView`, so the caller
16
+ * keeps drawing it and only hands over the OTHER views. That is also why the
17
+ * strip is drawn only while it has something else to switch to.
18
+ */
19
+
20
+ import { Suspense } from 'preact/compat';
21
+ import { PanelHostProvider } from '@kabmo-labs/kabmo-ui';
22
+ import type { ResolvedPanel } from '@/client/components/workspace/plugin-panel/catalog';
23
+ import { PluginPanelBody } from '@/client/components/workspace/plugin-panel/resolve';
24
+ import type { PanelBodyProps } from '@/client/components/workspace/panels/builtin';
25
+ import { PanelChips } from '@/client/components/mobile/panel-chips';
26
+
27
+ /** The strip row, drawn under the drawer's title. */
28
+ export function MobileDockStrip({
29
+ panels,
30
+ activeId,
31
+ onSelect,
32
+ }: {
33
+ panels: readonly ResolvedPanel[];
34
+ activeId: string;
35
+ onSelect: (id: string) => void;
36
+ }) {
37
+ return (
38
+ <div className="px-3 py-1.5 border-b border-ink/10 bg-canvas flex-shrink-0">
39
+ <PanelChips panels={panels} activeId={activeId} onSelect={onSelect} />
40
+ </div>
41
+ );
42
+ }
43
+
44
+ /**
45
+ * The body of one placed panel.
46
+ *
47
+ * A selection naming a panel that is switched off, uninstalled or still loading
48
+ * has no entry — `PluginPanelBody` says which, rather than leaving the drawer
49
+ * blank. That is the same notice the desktop dock draws.
50
+ */
51
+ export function MobileDockBody({
52
+ panelKey,
53
+ panel,
54
+ body,
55
+ }: {
56
+ panelKey: string;
57
+ panel?: ResolvedPanel;
58
+ body: PanelBodyProps;
59
+ }) {
60
+ if (!panel) {
61
+ return (
62
+ <div className="flex-1 min-h-0">
63
+ <PluginPanelBody panelKey={panelKey} />
64
+ </div>
65
+ );
66
+ }
67
+ return (
68
+ <div className="flex-1 min-h-0 overflow-hidden relative">
69
+ <Suspense
70
+ fallback={
71
+ <div className="h-full flex items-center justify-center text-ink/40">
72
+ <span className="text-xs font-mono">Loading…</span>
73
+ </div>
74
+ }
75
+ >
76
+ <PanelHostProvider value={body}>{panel.render(body)}</PanelHostProvider>
77
+ </Suspense>
78
+ </div>
79
+ );
80
+ }
@@ -1,14 +1,20 @@
1
1
  import { X } from 'lucide-preact';
2
2
  import { BrandWordmark } from '@/client/components/common/brand-wordmark';
3
+ import { titlebarPaddingLeft, titlebarPaddingRight } from '@/shared/lib/ui/titlebar';
3
4
 
4
- export function MobileSessionHeader({ onClose }: { onClose: () => void }) {
5
+ export function MobileSessionHeader({ onClose, viewLabel = 'Sessions' }: { onClose: () => void; viewLabel?: string }) {
5
6
  return (
6
7
  <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"
8
+ className="border-b border-ink/10 flex items-center justify-between px-3 flex-shrink-0 bg-canvas titlebar-drag-region select-none"
8
9
  style={{
9
10
  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))',
11
+ // `px-3`, the toolbar's own inset: the wordmark above and the New
12
+ // Session button below share one left edge, which the header's old
13
+ // 14px did not.
14
+ paddingLeft: titlebarPaddingLeft('0.75rem', true),
15
+ // The drawer is the full window width, so on a desktop showing the
16
+ // mobile layout its close button sits under the window controls too.
17
+ paddingRight: titlebarPaddingRight('0.75rem', true),
12
18
  paddingTop: 'env(safe-area-inset-top, 0px)',
13
19
  }}
14
20
  >
@@ -17,7 +23,7 @@ export function MobileSessionHeader({ onClose }: { onClose: () => void }) {
17
23
  <BrandWordmark className="text-[16px]" />
18
24
  </span>
19
25
  <span className="text-ink/25 text-xs select-none">/</span>
20
- <span className="text-[11px] font-semibold text-ink/60 uppercase tracking-wider font-mono">Sessions</span>
26
+ <span className="text-[11px] font-semibold text-ink/60 uppercase tracking-wider font-mono">{viewLabel}</span>
21
27
  </div>
22
28
 
23
29
  <button