@kabmo-labs/kabmo 0.4.0 → 0.5.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.
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/static/js/{index-twsk4wfa.js → index-za950td7.js} +665 -665
- package/package.json +1 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -3
- package/src/client/components/layout/session-sidebar/Header.tsx +2 -1
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -1
- package/src/client/components/mobile/LayoutWrapper.tsx +4 -0
- package/src/client/components/mobile/RightSidebar.tsx +23 -32
- package/src/client/components/mobile/SessionSidebar.tsx +120 -103
- package/src/client/components/mobile/mobile-main-view/Header.tsx +3 -2
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -2
- package/src/client/components/mobile/mobile-right-sidebar/Header.tsx +3 -2
- package/src/client/components/mobile/mobile-session-sidebar/Dialogs.tsx +106 -0
- package/src/client/components/mobile/mobile-session-sidebar/Dock.tsx +80 -0
- package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +7 -4
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +6 -1
- package/src/client/components/mobile/mobile-session-sidebar/use-dock.ts +89 -0
- package/src/client/components/mobile/panel-chips.test.tsx +155 -0
- package/src/client/components/mobile/panel-chips.tsx +74 -0
- package/src/client/components/workspace/chat-timeline/chat-input/Toolbar.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/chat-input/compact.test.tsx +12 -4
- package/src/client/components/workspace/model-dropdown/index.tsx +3 -3
- package/src/client/components/workspace/model-dropdown/trigger.test.ts +123 -0
- package/src/client/hooks/chat/timeline/queue-rewind.test.ts +6 -0
- package/src/client/hooks/ui/toasts.ts +12 -1
- package/src/shared/lib/ui/titlebar.test.ts +124 -0
- package/src/shared/lib/ui/titlebar.ts +54 -0
- package/src/shared/lib/workspace/mobile-dock.test.ts +65 -0
- package/src/shared/lib/workspace/mobile-dock.ts +55 -0
- package/src/test-support/silent-socket.ts +69 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kabmo-labs/kabmo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Web console and diagnostic workbench for AI coding agents — Elysia + Preact on Bun.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Wijanarko Putra Rajeb <wijanarko.rajeb@gmail.com>",
|
|
@@ -7,6 +7,7 @@ import { useInlineRename } from '@/client/hooks/ui/inline-rename';
|
|
|
7
7
|
import { useSessionRenaming } from '@/client/hooks/workspace/renaming-sessions';
|
|
8
8
|
import type { AgentStreamStatus } from '@/shared/lib/chat/omp/status';
|
|
9
9
|
import { withChord } from '@/client/lib/ui/with-chord';
|
|
10
|
+
import { titlebarPaddingLeft, titlebarPaddingRight } from '@/shared/lib/ui/titlebar';
|
|
10
11
|
|
|
11
12
|
interface TopNavbarProps {
|
|
12
13
|
sessionTitle: string | null;
|
|
@@ -59,10 +60,11 @@ export function TopNavbar({
|
|
|
59
60
|
);
|
|
60
61
|
return (
|
|
61
62
|
<header
|
|
62
|
-
className="h-10 flex-shrink-0 border-b border-ink/10 bg-paper flex items-center justify-between
|
|
63
|
+
className="h-10 flex-shrink-0 border-b border-ink/10 bg-paper flex items-center justify-between z-20 titlebar-drag-region select-none"
|
|
63
64
|
style={{
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
// The sidebar's own header carries the left inset while it is open.
|
|
66
|
+
paddingLeft: !showLeftPanel ? titlebarPaddingLeft('1rem') : undefined,
|
|
67
|
+
paddingRight: titlebarPaddingRight('1rem'),
|
|
66
68
|
}}
|
|
67
69
|
>
|
|
68
70
|
<div className="flex items-center space-x-2 px-4 min-w-0">
|
|
@@ -2,6 +2,7 @@ import { PanelLeftClose } from 'lucide-preact';
|
|
|
2
2
|
import { BrandWordmark } from '@/client/components/common/brand-wordmark';
|
|
3
3
|
import { StreamStatusDot } from '@/client/components/common/StreamStatusDot';
|
|
4
4
|
import { withChord } from '@/client/lib/ui/with-chord';
|
|
5
|
+
import { titlebarPaddingLeft } from '@/shared/lib/ui/titlebar';
|
|
5
6
|
import type { AgentStreamStatus } from '@/shared/lib/chat/omp/status';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -29,7 +30,7 @@ export function SessionSidebarHeader({
|
|
|
29
30
|
return (
|
|
30
31
|
<div
|
|
31
32
|
className="h-10 flex-shrink-0 flex items-center justify-between px-4 border-b border-ink/10 titlebar-drag-region select-none"
|
|
32
|
-
style={{ paddingLeft: '
|
|
33
|
+
style={{ paddingLeft: titlebarPaddingLeft('1rem') }}
|
|
33
34
|
>
|
|
34
35
|
{/* The stream indicator badges the wordmark's top-right corner, the
|
|
35
36
|
same placement the phone's header uses — it reads as part of the
|
|
@@ -179,7 +179,18 @@ export function SessionItem({
|
|
|
179
179
|
<Hourglass size={12} className="animate-pulse text-ink/60" />
|
|
180
180
|
</span>
|
|
181
181
|
) : status === 'stream' ? (
|
|
182
|
-
|
|
182
|
+
// The run spinner is the ONE thing on this row that says a run
|
|
183
|
+
// is in flight, and it was an unlabelled icon: a screen reader
|
|
184
|
+
// got a bare `svg`, and a test had no accessible way to ask
|
|
185
|
+
// "is this session running". `waiting-for.e2e.ts` needs exactly
|
|
186
|
+
// that as its gate — the wait picker only appears once this
|
|
187
|
+
// session's `stream` row has reached the client, so asserting
|
|
188
|
+
// the picker without asserting the row was a race measured on
|
|
189
|
+
// the Windows runner (the dependency's 20s run expired while the
|
|
190
|
+
// picker's own 20s timeout was still counting).
|
|
191
|
+
<span role="status" aria-label="Running">
|
|
192
|
+
<Loader2 size={12} className="animate-spin" />
|
|
193
|
+
</span>
|
|
183
194
|
) : (
|
|
184
195
|
<Check size={12} />
|
|
185
196
|
)}
|
|
@@ -239,6 +239,10 @@ export function MobileLayoutWrapper({ onDesktopToggle, appSettings = {} }: Mobil
|
|
|
239
239
|
onClose={() => setCurrentScreen('main')}
|
|
240
240
|
onDesktopToggle={onDesktopToggle}
|
|
241
241
|
appSettings={appSettings}
|
|
242
|
+
rootPath={activeProjectPath ?? undefined}
|
|
243
|
+
refreshKey={refreshKey}
|
|
244
|
+
onRefresh={handleWorkspaceChanged}
|
|
245
|
+
onOpenFile={handleOpenFile}
|
|
242
246
|
/>
|
|
243
247
|
</div>
|
|
244
248
|
|
|
@@ -5,9 +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 {
|
|
9
|
-
import { PANEL_MARK_SIZE } from '@/shared/lib/workspace/right-panels';
|
|
8
|
+
import { useLeftDock } from '@/client/hooks/workspace/left-dock';
|
|
10
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';
|
|
11
13
|
|
|
12
14
|
interface MobileRightSidebarProps {
|
|
13
15
|
enabled?: boolean;
|
|
@@ -50,8 +52,17 @@ export function MobileRightSidebar({
|
|
|
50
52
|
// panel on the left hide it from every phone. The dock's own session list is
|
|
51
53
|
// filtered out below: the phone already has that as its own drawer screen,
|
|
52
54
|
// and a second copy inside this one would be two lists of the same sessions.
|
|
53
|
-
|
|
54
|
-
|
|
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];
|
|
55
66
|
// Same source the desktop activity bar uses — the Source Control view's own
|
|
56
67
|
// repo pick, read shared so switching repos moves this dot too. Polls only
|
|
57
68
|
// while this drawer is the mounted screen (the poll is visibility-gated).
|
|
@@ -84,37 +95,17 @@ export function MobileRightSidebar({
|
|
|
84
95
|
style={{
|
|
85
96
|
height: 'calc(3.5rem + env(safe-area-inset-top, 0px))',
|
|
86
97
|
paddingTop: 'env(safe-area-inset-top, 0px)',
|
|
87
|
-
paddingLeft: '
|
|
88
|
-
paddingRight: '
|
|
98
|
+
paddingLeft: titlebarPaddingLeft('0.75rem', true),
|
|
99
|
+
paddingRight: titlebarPaddingRight('0.75rem', true),
|
|
89
100
|
}}
|
|
90
101
|
>
|
|
91
102
|
|
|
92
|
-
<
|
|
93
|
-
{catalog
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
className={`relative flex items-center transition-all cursor-pointer ${
|
|
99
|
-
activeTab === panel.id
|
|
100
|
-
? 'space-x-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold bg-ink text-canvas shadow-sm'
|
|
101
|
-
: 'p-2 rounded-lg text-ink/70 hover:bg-ink/5'
|
|
102
|
-
}`}
|
|
103
|
-
title={panel.title}
|
|
104
|
-
>
|
|
105
|
-
{panel.builtin ? panel.icon : <PluginMark name={panel.name} iconUrl={panel.iconUrl} size={PANEL_MARK_SIZE} />}
|
|
106
|
-
{activeTab === panel.id && (
|
|
107
|
-
<span className={panel.id === 'git' ? 'tracking-wide' : undefined}>{panel.label}</span>
|
|
108
|
-
)}
|
|
109
|
-
{panel.id === 'git' && hasGitChanges && (
|
|
110
|
-
<span
|
|
111
|
-
className="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-info"
|
|
112
|
-
title="Uncommitted changes"
|
|
113
|
-
/>
|
|
114
|
-
)}
|
|
115
|
-
</button>
|
|
116
|
-
))}
|
|
117
|
-
</div>
|
|
103
|
+
<PanelChips
|
|
104
|
+
panels={catalog}
|
|
105
|
+
activeId={activePanel?.id ?? activeTab}
|
|
106
|
+
onSelect={setActiveTab}
|
|
107
|
+
hasGitChanges={hasGitChanges}
|
|
108
|
+
/>
|
|
118
109
|
|
|
119
110
|
<button
|
|
120
111
|
type="button"
|
|
@@ -3,11 +3,7 @@ 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';
|
|
@@ -17,12 +13,14 @@ import { useUpdates } from '@/client/hooks/ui/updates';
|
|
|
17
13
|
import { UPDATE_REQUEST_EVENT } from '@/shared/lib/updates/popup-state';
|
|
18
14
|
import { useSessionSidebarController } from '@/client/hooks/chat/omp/session-sidebar-controller';
|
|
19
15
|
import { useScheduledTaskCount } from '@/client/hooks/workspace/scheduled-tasks';
|
|
20
|
-
import { cloneWorkspaceFolder, createWorkspaceFolder } from '@/client/hooks/workspace/new-workspace';
|
|
21
16
|
import { MobileSessionListSkeleton } from '@/client/components/mobile/mobile-session-sidebar/Skeleton';
|
|
22
17
|
import { useBatchSelection } from '@/client/hooks/workspace/batch-selection';
|
|
23
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';
|
|
24
23
|
import { BatchActionBar } from '@/client/components/common/batch-selection/BatchActionBar';
|
|
25
|
-
import { BatchDeleteModal } from '@/client/components/common/batch-selection/BatchDeleteModal';
|
|
26
24
|
import type { BatchBinding } from '@/client/components/common/batch-selection/types';
|
|
27
25
|
import { liveSelection, selectableSessionIds } from '@/shared/lib/workspace/batch-select';
|
|
28
26
|
|
|
@@ -34,6 +32,12 @@ interface MobileSessionSidebarProps {
|
|
|
34
32
|
onClose: () => void;
|
|
35
33
|
onDesktopToggle?: () => void;
|
|
36
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;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export function MobileSessionSidebar({
|
|
@@ -43,7 +47,11 @@ export function MobileSessionSidebar({
|
|
|
43
47
|
onNewSessionForFolder,
|
|
44
48
|
onClose,
|
|
45
49
|
onDesktopToggle,
|
|
46
|
-
appSettings = {}
|
|
50
|
+
appSettings = {},
|
|
51
|
+
rootPath,
|
|
52
|
+
refreshKey = 0,
|
|
53
|
+
onRefresh,
|
|
54
|
+
onOpenFile,
|
|
47
55
|
}: MobileSessionSidebarProps) {
|
|
48
56
|
const {
|
|
49
57
|
folders,
|
|
@@ -190,66 +198,103 @@ export function MobileSessionSidebar({
|
|
|
190
198
|
else batch.begin('workspace');
|
|
191
199
|
};
|
|
192
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
|
+
|
|
193
213
|
return (
|
|
194
214
|
<div className="flex flex-col h-full w-full bg-canvas text-ink relative select-none">
|
|
195
|
-
|
|
196
|
-
<MobileSessionHeader onClose={onClose} />
|
|
197
215
|
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
optionsOpen={optionsOpen}
|
|
202
|
-
showArchived={showArchived}
|
|
203
|
-
optionsRef={optionsRef}
|
|
204
|
-
onNewSession={handleNewSessionAndClose}
|
|
205
|
-
onNewWorkspace={() => setNewWorkspaceOpen(true)}
|
|
206
|
-
onScheduler={() => setSchedulerOpen(true)}
|
|
207
|
-
scheduledCount={scheduledCount}
|
|
208
|
-
onRefresh={refreshNow}
|
|
209
|
-
refreshing={isRefreshing}
|
|
210
|
-
onToggleOptions={() => setOptionsOpen(!optionsOpen)}
|
|
211
|
-
onSortChange={handleSortChange}
|
|
212
|
-
onResetSort={() => handleSortChange('A-Z')}
|
|
213
|
-
onToggleArchived={handleToggleArchived}
|
|
214
|
-
onHideArchived={() => setShowArchived(false)}
|
|
215
|
-
onSearchChange={setSearchQuery}
|
|
216
|
-
onClearSearch={() => setSearchQuery('')}
|
|
217
|
-
onSelectWorkspaces={startWorkspaceSelection}
|
|
218
|
-
selecting={batch.mode !== null}
|
|
216
|
+
<MobileSessionHeader
|
|
217
|
+
onClose={onClose}
|
|
218
|
+
viewLabel={showingSessions ? 'Sessions' : dock.activePanel?.label ?? 'Panel'}
|
|
219
219
|
/>
|
|
220
220
|
|
|
221
|
-
{
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
/>
|
|
239
260
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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} />
|
|
253
298
|
)}
|
|
254
299
|
|
|
255
300
|
<MobileSessionFooter
|
|
@@ -264,52 +309,24 @@ export function MobileSessionSidebar({
|
|
|
264
309
|
}}
|
|
265
310
|
/>
|
|
266
311
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
isOpen={schedulerOpen}
|
|
276
|
-
onClose={() => setSchedulerOpen(false)}
|
|
277
|
-
activeSessionId={activeSessionId === null ? null : String(activeSessionId)}
|
|
278
|
-
onToast={pushToast}
|
|
279
|
-
/>
|
|
280
|
-
<AboutModal
|
|
281
|
-
isOpen={aboutOpen}
|
|
282
|
-
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)}
|
|
283
320
|
updates={updates}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
Its "Update now" hands the run to the About modal below. */}
|
|
289
|
-
<UpdatePopup updates={updates} />
|
|
290
|
-
|
|
291
|
-
{/* Batch delete confirmation — same dialog the desktop draws. */}
|
|
292
|
-
<BatchDeleteModal
|
|
293
|
-
mode={batch.mode ?? 'session'}
|
|
294
|
-
count={batchDelete.pending.length}
|
|
321
|
+
pushToast={pushToast}
|
|
322
|
+
batchDelete={batchDelete}
|
|
323
|
+
batchMode={batch.mode}
|
|
324
|
+
onEndBatch={batch.end}
|
|
295
325
|
scopeLabel={scopeFolder?.name}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
onClose={() => {
|
|
299
|
-
const refusedCount = batchDelete.outcome?.refused.length ?? 0;
|
|
300
|
-
batchDelete.cancel();
|
|
301
|
-
if (batchDelete.outcome && refusedCount === 0) batch.end();
|
|
302
|
-
}}
|
|
303
|
-
onConfirm={() => {
|
|
304
|
-
const mode = batch.mode ?? 'session';
|
|
305
|
-
void batchDelete.confirm(mode).then((result) => {
|
|
306
|
-
if (result && result.refused.length === 0) batch.end();
|
|
307
|
-
});
|
|
308
|
-
}}
|
|
326
|
+
toasts={toasts}
|
|
327
|
+
dismissToast={dismissToast}
|
|
309
328
|
/>
|
|
310
329
|
|
|
311
|
-
<ToastStack toasts={toasts} onDismiss={dismissToast} />
|
|
312
|
-
|
|
313
330
|
</div>
|
|
314
331
|
);
|
|
315
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: '
|
|
95
|
-
paddingRight: '
|
|
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: '
|
|
41
|
-
paddingRight: '
|
|
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: '
|
|
51
|
-
paddingRight: '
|
|
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
|
+
}
|