@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.
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +412 -383
- package/dist/client/static/js/index-3qamd33d.css +1 -0
- package/dist/client/static/js/{index-28z316qk.js → index-cs3b557t.js} +792 -792
- package/package.json +1 -1
- package/src/client/components/common/batch-selection/BatchActionBar.tsx +8 -1
- package/src/client/components/common/session-actions-menu/index.tsx +23 -2
- package/src/client/components/layout/{RightActivityBar.test.ts → ActivityBar.test.ts} +5 -5
- package/src/client/components/layout/{RightActivityBar.tsx → ActivityBar.tsx} +87 -52
- package/src/client/components/layout/LeftActivityBar.test.ts +201 -0
- package/src/client/components/layout/LeftDock.tsx +119 -0
- package/src/client/components/layout/desktop-layout/LeftColumn.tsx +172 -0
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -3
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +22 -5
- package/src/client/components/layout/desktop-layout/index.tsx +88 -61
- package/src/client/components/layout/desktop-layout/left-column.test.tsx +192 -0
- package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +6 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +9 -0
- package/src/client/components/layout/session-sidebar/Header.tsx +2 -1
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +42 -6
- package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
- package/src/client/components/layout/session-sidebar/{index.tsx → SessionsView.tsx} +48 -72
- package/src/client/components/layout/session-sidebar/new-session-context.test.ts +55 -0
- package/src/client/components/mobile/LayoutWrapper.tsx +4 -0
- package/src/client/components/mobile/RightSidebar.tsx +30 -36
- package/src/client/components/mobile/SessionSidebar.tsx +124 -101
- 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 +11 -5
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +34 -44
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +9 -1
- package/src/client/components/mobile/mobile-session-sidebar/RevealRow.tsx +60 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +47 -14
- package/src/client/components/mobile/mobile-session-sidebar/Skeleton.tsx +2 -2
- 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/settings/categories/panel-plugins/InstalledList.tsx +2 -2
- package/src/client/components/settings/categories/panel-plugins/LeftDockSettings.tsx +268 -0
- package/src/client/components/settings/categories/panel-plugins/Section.tsx +8 -0
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +69 -1
- package/src/client/components/workspace/chat-timeline/MessageItem.gutter.test.ts +118 -0
- package/src/client/components/workspace/chat-timeline/MessageItem.tsx +13 -3
- package/src/client/components/workspace/chat-timeline/MessageList.tsx +1 -0
- package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +106 -0
- package/src/client/components/workspace/chat-timeline/SessionSkeleton.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/SubagentView.tsx +4 -1
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +5 -2
- package/src/client/components/workspace/chat-timeline/ThinkingSection.tsx +6 -2
- package/src/client/components/workspace/chat-timeline/TimelineBody.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/ToolCallingSection.test.ts +5 -5
- package/src/client/components/workspace/chat-timeline/ToolCallingSection.tsx +5 -2
- package/src/client/components/workspace/chat-timeline/chat-input/AttachmentToolbar.tsx +25 -1
- package/src/client/components/workspace/chat-timeline/chat-input/Toolbar.tsx +15 -10
- package/src/client/components/workspace/chat-timeline/chat-input/WaitingDependencyPicker.test.ts +84 -0
- package/src/client/components/workspace/chat-timeline/chat-input/WaitingDependencyPicker.tsx +185 -0
- package/src/client/components/workspace/chat-timeline/chat-input/compact.test.tsx +162 -0
- package/src/client/components/workspace/chat-timeline/chat-input/expansion.test.ts +22 -21
- package/src/client/components/workspace/chat-timeline/chat-input/expansion.ts +12 -20
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +54 -27
- package/src/client/components/workspace/chat-timeline/chat-input/props.ts +16 -0
- package/src/client/components/workspace/chat-timeline/index.tsx +15 -16
- package/src/client/components/workspace/editor/PluginRail.tsx +79 -0
- package/src/client/components/workspace/editor/index.tsx +1 -0
- package/src/client/components/workspace/editor/plugin-rail.test.tsx +126 -0
- package/src/client/components/workspace/model-dropdown/groups.test.ts +60 -0
- package/src/client/components/workspace/model-dropdown/groups.ts +7 -1
- 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/components/workspace/model-dropdown/use-catalog.ts +8 -2
- package/src/client/components/workspace/panels/builtin.tsx +8 -0
- package/src/client/components/workspace/plugin-panel/catalog.tsx +237 -0
- package/src/client/components/workspace/plugin-panel/resolve.tsx +19 -122
- package/src/client/hooks/chat/composer/compact.ts +119 -0
- package/src/client/hooks/chat/omp/pending-rows.ts +4 -0
- package/src/client/hooks/chat/omp/pending-sessions.ts +47 -0
- package/src/client/hooks/chat/omp/pending-sessions.waiting.test.ts +102 -0
- package/src/client/hooks/chat/omp/pending-waiting.ts +48 -0
- package/src/client/hooks/chat/omp/session-sidebar-controller.ts +29 -0
- package/src/client/hooks/chat/timeline/actions.ts +34 -35
- package/src/client/hooks/chat/timeline/composer-refs.ts +47 -0
- package/src/client/hooks/chat/timeline/enqueue-follow-up.ts +37 -0
- package/src/client/hooks/chat/timeline/index.ts +15 -19
- package/src/client/hooks/chat/timeline/local-commands.ts +29 -0
- package/src/client/hooks/chat/timeline/queue-rewind.test.ts +6 -0
- package/src/client/hooks/chat/timeline/run-identity.ts +45 -0
- package/src/client/hooks/chat/waiting/cancel.ts +34 -0
- package/src/client/hooks/chat/waiting/choice.ts +53 -0
- package/src/client/hooks/chat/waiting/dispatch.ts +91 -0
- package/src/client/hooks/chat/waiting/held.ts +42 -0
- package/src/client/hooks/chat/waiting/hold.test.ts +139 -0
- package/src/client/hooks/chat/waiting/hold.ts +156 -0
- package/src/client/hooks/chat/waiting/store.ts +100 -0
- package/src/client/hooks/ui/toasts.ts +12 -1
- package/src/client/hooks/workspace/left-dock.ts +132 -0
- package/src/client/hooks/workspace/panel-registry.ts +13 -0
- package/src/client/tailwind.css +22 -0
- package/src/server/db.server.test.ts +52 -1
- package/src/server/db.server.ts +49 -1
- package/src/server/index.ts +9 -0
- package/src/server/lib/assets/compressed-cache.server.test.ts +78 -0
- package/src/server/lib/assets/compressed-cache.server.ts +88 -0
- package/src/server/lib/db/schema.test.ts +1 -0
- package/src/server/lib/db/schema.ts +3 -0
- package/src/server/lib/fs/fs-path.test.ts +1 -1
- package/src/server/lib/omp/rpc/terminal-settle.ts +7 -0
- package/src/server/lib/omp/session/delete.server.ts +8 -0
- package/src/server/lib/omp/session/ownership.server.test.ts +45 -4
- package/src/server/lib/panels/registry.server.ts +2 -2
- package/src/server/lib/queue/delivery.server.ts +31 -39
- package/src/server/lib/queue/payload.ts +73 -0
- package/src/server/lib/realtime/signals.server.ts +10 -1
- package/src/server/lib/realtime/topics/catalog.server.ts +2 -0
- package/src/server/lib/realtime/topics/global.server.ts +5 -0
- package/src/server/lib/realtime/topics.server.ts +2 -0
- package/src/server/lib/util/with-timeout.ts +29 -0
- package/src/server/lib/waiting/row-clock.server.ts +37 -0
- package/src/server/lib/waiting/runtime.server.test.ts +278 -0
- package/src/server/lib/waiting/runtime.server.ts +313 -0
- package/src/server/lib/waiting/schema.server.ts +46 -0
- package/src/server/lib/waiting/settle.test.ts +84 -0
- package/src/server/lib/waiting/settle.ts +77 -0
- package/src/server/lib/waiting/store.server.test.ts +167 -0
- package/src/server/lib/waiting/store.server.ts +268 -0
- package/src/server/lib/waiting/sweep-trigger.ts +35 -0
- package/src/server/plugins/compress.ts +18 -6
- package/src/server/plugins/ssr.ts +16 -8
- package/src/server/plugins/static.test.ts +59 -0
- package/src/server/plugins/static.ts +56 -14
- package/src/server/routes/index.ts +2 -0
- package/src/server/routes/waiting/bindings.ts +14 -0
- package/src/server/routes/waiting/index.ts +123 -0
- package/src/server/routes/waiting/routes.test.ts +121 -0
- package/src/shared/lib/chat/timeline/gutter.test.ts +37 -0
- package/src/shared/lib/chat/timeline/gutter.ts +37 -0
- package/src/shared/lib/realtime/protocol.ts +10 -0
- package/src/shared/lib/ui/titlebar.test.ts +124 -0
- package/src/shared/lib/ui/titlebar.ts +54 -0
- package/src/shared/lib/workspace/left-dock-width.ts +78 -0
- package/src/shared/lib/workspace/left-dock.test.ts +203 -0
- package/src/shared/lib/workspace/left-dock.ts +197 -0
- package/src/shared/lib/workspace/mobile-dock.test.ts +65 -0
- package/src/shared/lib/workspace/mobile-dock.ts +55 -0
- package/src/shared/lib/workspace/panel-ids.test.ts +59 -10
- package/src/shared/lib/workspace/panel-ids.ts +114 -26
- package/src/shared/lib/workspace/sidebar-width.ts +4 -0
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/waiting.ts +54 -0
- package/src/shared/types/workspace.ts +8 -0
- package/src/test-support/isolated-db.ts +3 -4
- package/src/test-support/silent-socket.ts +69 -0
- package/dist/client/static/js/index-pdp5dwc8.css +0 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The left dock's view stack: the view on screen, plus the ones kept mounted.
|
|
8
|
+
*
|
|
9
|
+
* The left column used to be the session sidebar and nothing else. It is a DOCK
|
|
10
|
+
* now, exactly like the right column: a stack of views with one on screen.
|
|
11
|
+
*
|
|
12
|
+
* Two rules are load-bearing, and each is a bug if dropped:
|
|
13
|
+
*
|
|
14
|
+
* - **A view is never responsible for the column's furniture.** The Settings ·
|
|
15
|
+
* About strip, the update popup and the About modal belong to the COLUMN
|
|
16
|
+
* (`LeftColumn`), not to this stack: a plugin panel on the same dock must not
|
|
17
|
+
* take away the only visible way into Settings. That is also what lets the
|
|
18
|
+
* strip span the rail and the body together, which is where it belongs.
|
|
19
|
+
* - **The session list stays MOUNTED while another view is on screen**, hidden
|
|
20
|
+
* with CSS, the way the right stack keeps its views: its folder expansion,
|
|
21
|
+
* scroll position and batch selection must survive a look at another view. A
|
|
22
|
+
* plugin panel is mounted only while it is the active one — a hidden plugin is
|
|
23
|
+
* a component nobody is looking at.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { Suspense } from 'preact/compat';
|
|
27
|
+
import type { ReactNode } from 'preact/compat';
|
|
28
|
+
import { PluginPanelBody } from '@/client/components/workspace/plugin-panel/resolve';
|
|
29
|
+
import { usePanelCatalog } from '@/client/components/workspace/plugin-panel/catalog';
|
|
30
|
+
import type { PanelBodyProps } from '@/client/components/workspace/panels/builtin';
|
|
31
|
+
import { PanelHostProvider } from '@kabmo-labs/kabmo-ui';
|
|
32
|
+
import type { AgentStreamStatus } from '@/shared/lib/chat/omp/status';
|
|
33
|
+
|
|
34
|
+
interface LeftDockViewProps {
|
|
35
|
+
className?: string;
|
|
36
|
+
/** The view on screen. A stored id naming a vanished panel falls back. */
|
|
37
|
+
activeView: string;
|
|
38
|
+
onClose?: () => void;
|
|
39
|
+
appSettings?: Record<string, any>;
|
|
40
|
+
streamStatus?: AgentStreamStatus;
|
|
41
|
+
refreshKey?: number;
|
|
42
|
+
onRefresh?: () => void;
|
|
43
|
+
onOpenFile?: (file: unknown) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The views are `lazy()` (the session list is its own chunk), so the stack needs
|
|
48
|
+
* a boundary of its own: Preact THROWS the pending promise out of the tree when
|
|
49
|
+
* a lazy child suspends with no `<Suspense>` above it, which is a blank page
|
|
50
|
+
* rather than a loading state — measured with the whole app failing to mount.
|
|
51
|
+
*/
|
|
52
|
+
function DockSuspense({ children }: { children: ReactNode }) {
|
|
53
|
+
return (
|
|
54
|
+
<Suspense
|
|
55
|
+
fallback={
|
|
56
|
+
<div className="h-full flex items-center justify-center text-ink/40">
|
|
57
|
+
<span className="text-xs font-mono">Loading…</span>
|
|
58
|
+
</div>
|
|
59
|
+
}
|
|
60
|
+
>
|
|
61
|
+
{children}
|
|
62
|
+
</Suspense>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function LeftDockView({
|
|
67
|
+
className = '',
|
|
68
|
+
activeView,
|
|
69
|
+
onClose,
|
|
70
|
+
appSettings = {},
|
|
71
|
+
streamStatus,
|
|
72
|
+
refreshKey = 0,
|
|
73
|
+
onRefresh,
|
|
74
|
+
onOpenFile,
|
|
75
|
+
}: LeftDockViewProps) {
|
|
76
|
+
const catalog = usePanelCatalog('left');
|
|
77
|
+
const active = catalog.find((panel) => panel.id === activeView);
|
|
78
|
+
|
|
79
|
+
const body: PanelBodyProps = {
|
|
80
|
+
className: 'w-full h-full',
|
|
81
|
+
enabled: true,
|
|
82
|
+
active: true,
|
|
83
|
+
refreshKey,
|
|
84
|
+
...(onRefresh ? { onRefresh } : {}),
|
|
85
|
+
...(onOpenFile ? { onOpenFile } : {}),
|
|
86
|
+
...(onClose ? { onClose } : {}),
|
|
87
|
+
appSettings,
|
|
88
|
+
...(streamStatus ? { streamStatus } : {}),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<DockSuspense>
|
|
93
|
+
{/* `aside`, not a `div`: this IS the sidebar landmark — the session list
|
|
94
|
+
lives here — and the browser specs address it as one (`aside` scopes a
|
|
95
|
+
query to the sidebar rather than to the whole page). The dock body was
|
|
96
|
+
a `div` for one revision and two specs went red on it. */}
|
|
97
|
+
<aside className={`relative h-full ${className}`}>
|
|
98
|
+
{catalog.map((panel) => {
|
|
99
|
+
const isActive = panel.id === activeView;
|
|
100
|
+
if (!panel.builtin && !isActive) return null;
|
|
101
|
+
const viewBody: PanelBodyProps = { ...body, active: isActive, enabled: isActive };
|
|
102
|
+
return (
|
|
103
|
+
<div key={panel.id} className={isActive ? 'w-full h-full' : 'hidden'}>
|
|
104
|
+
<PanelHostProvider value={viewBody}>{panel.render(viewBody)}</PanelHostProvider>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
{/* The selection names a panel that is switched off, uninstalled, or
|
|
109
|
+
whose bundle has not loaded — a notice, never a blank column. The
|
|
110
|
+
session list is always drawable, so it is never this case. */}
|
|
111
|
+
{!active ? (
|
|
112
|
+
<div className="w-full h-full">
|
|
113
|
+
<PluginPanelBody panelKey={activeView} />
|
|
114
|
+
</div>
|
|
115
|
+
) : null}
|
|
116
|
+
</aside>
|
|
117
|
+
</DockSuspense>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The desktop's left column: the optional rail, the dock, its furniture, and
|
|
8
|
+
* its separator.
|
|
9
|
+
*
|
|
10
|
+
* Split out of `DesktopLayout` because the column has four parts that answer
|
|
11
|
+
* four different questions — whether the rail is drawn (a global preference),
|
|
12
|
+
* which view is on screen (per session), how wide the column is (per view), and
|
|
13
|
+
* what belongs to the COLUMN rather than to a view — and the layout component is
|
|
14
|
+
* where the group's own width arithmetic lives.
|
|
15
|
+
*
|
|
16
|
+
* Four rules are load-bearing, and each was a defect first:
|
|
17
|
+
*
|
|
18
|
+
* - **The rail is INSIDE the column's panel**, not a sibling of it. It used to
|
|
19
|
+
* be a sibling, which made it a panel of its own: the group's width arithmetic
|
|
20
|
+
* had to budget for a third fixed panel, and — the bug that was reported — the
|
|
21
|
+
* footer could only be as wide as the dock BODY, so turning the rail on left a
|
|
22
|
+
* strip under the rail with nothing in it.
|
|
23
|
+
* - **The rail sits BETWEEN the title row and the footer**, never above them.
|
|
24
|
+
* The column reads top-to-bottom as title · (rail | view) · strip, so the rail
|
|
25
|
+
* cannot run up into the app's titlebar and cannot outlive the strip.
|
|
26
|
+
* - **The footer spans the whole column.** Settings · About act on Kabmo, not on
|
|
27
|
+
* the view above them, so a plugin panel on the dock must not take them away
|
|
28
|
+
* and the strip must not shrink to one view's width.
|
|
29
|
+
* - **The rail exists only while the user turned it on** (`left-dock.ts`). Off —
|
|
30
|
+
* the default — means no rail and the column is the session list as it always
|
|
31
|
+
* was.
|
|
32
|
+
* - **The column's body is always mounted** (the panel collapses to zero width),
|
|
33
|
+
* so the session list's expansion, scroll and batch selection survive a
|
|
34
|
+
* collapse and the width transition has something to animate.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { useState } from 'preact/hooks';
|
|
38
|
+
import type { RefObject } from 'preact';
|
|
39
|
+
import { Panel, type PanelImperativeHandle } from '@/client/components/layout/desktop-layout/resizer';
|
|
40
|
+
import { ActivityBar } from '@/client/components/layout/ActivityBar';
|
|
41
|
+
import { LeftDockView } from '@/client/components/layout/LeftDock';
|
|
42
|
+
import { ResizeHandle } from '@/client/components/layout/desktop-layout/ResizeHandle';
|
|
43
|
+
import { AboutModal } from '@/client/components/layout/session-sidebar/about-modal';
|
|
44
|
+
import { UpdatePopup } from '@/client/components/layout/update-popup';
|
|
45
|
+
import { SettingsModal } from '@/client/components/settings/LazyModal';
|
|
46
|
+
import { SessionSidebarFooter } from '@/client/components/layout/session-sidebar/Footer';
|
|
47
|
+
import { SessionSidebarHeader } from '@/client/components/layout/session-sidebar/Header';
|
|
48
|
+
import { ToastStack } from '@/client/components/common/ToastStack';
|
|
49
|
+
import { useAndroidDownload } from '@/client/hooks/ui/updates/android-download';
|
|
50
|
+
import { useKabmoEvent } from '@/client/hooks/ui/window-event';
|
|
51
|
+
import { useToasts } from '@/client/hooks/ui/toasts';
|
|
52
|
+
import { useUpdates } from '@/client/hooks/ui/updates';
|
|
53
|
+
import { UPDATE_REQUEST_EVENT } from '@/shared/lib/updates/popup-state';
|
|
54
|
+
import { MAX_LEFT_PANEL_WIDTH } from '@/shared/lib/workspace/sidebar-width';
|
|
55
|
+
import type { AgentStreamStatus } from '@/shared/lib/chat/omp/status';
|
|
56
|
+
|
|
57
|
+
interface LeftColumnProps {
|
|
58
|
+
panelRef: RefObject<PanelImperativeHandle | null>;
|
|
59
|
+
open: boolean;
|
|
60
|
+
/** Draw the rail. A global preference, off by default. */
|
|
61
|
+
railEnabled: boolean;
|
|
62
|
+
/** The view the dock actually shows, after resolving the stored selection. */
|
|
63
|
+
view: string;
|
|
64
|
+
onSelectView: (view: string) => void;
|
|
65
|
+
onCollapse: () => void;
|
|
66
|
+
width: number;
|
|
67
|
+
minWidth: number;
|
|
68
|
+
hasActiveContext: boolean;
|
|
69
|
+
activeProjectPath?: string | null;
|
|
70
|
+
appSettings?: Record<string, any>;
|
|
71
|
+
streamStatus?: AgentStreamStatus;
|
|
72
|
+
refreshKey: number;
|
|
73
|
+
onRefresh: () => void;
|
|
74
|
+
onOpenFile: (file: unknown) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function LeftColumn({
|
|
78
|
+
panelRef,
|
|
79
|
+
open,
|
|
80
|
+
railEnabled,
|
|
81
|
+
view,
|
|
82
|
+
onSelectView,
|
|
83
|
+
onCollapse,
|
|
84
|
+
width,
|
|
85
|
+
minWidth,
|
|
86
|
+
hasActiveContext,
|
|
87
|
+
activeProjectPath,
|
|
88
|
+
appSettings = {},
|
|
89
|
+
streamStatus,
|
|
90
|
+
refreshKey,
|
|
91
|
+
onRefresh,
|
|
92
|
+
onOpenFile,
|
|
93
|
+
}: LeftColumnProps) {
|
|
94
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
95
|
+
const [infoOpen, setInfoOpen] = useState(false);
|
|
96
|
+
const updates = useUpdates();
|
|
97
|
+
const { toasts, pushToast, dismissToast } = useToasts();
|
|
98
|
+
const androidDownload = useAndroidDownload(
|
|
99
|
+
window.__OMP_BOOTSTRAP__?.shellVersion,
|
|
100
|
+
window.__OMP_BOOTSTRAP__?.isAndroid,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// The update popup's "Update now" hands the run to the About modal, which owns
|
|
104
|
+
// the live output. The column owns both, so a view switch cannot lose them.
|
|
105
|
+
useKabmoEvent(UPDATE_REQUEST_EVENT, () => {
|
|
106
|
+
setInfoOpen(true);
|
|
107
|
+
void updates.apply('kabmo').then((result) => {
|
|
108
|
+
if (!result) return;
|
|
109
|
+
pushToast(result.message, result.success && !result.manual ? 'success' : 'error');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<>
|
|
115
|
+
<Panel
|
|
116
|
+
panelRef={panelRef}
|
|
117
|
+
id="left-panel"
|
|
118
|
+
defaultSize={width}
|
|
119
|
+
minSize={minWidth}
|
|
120
|
+
maxSize={MAX_LEFT_PANEL_WIDTH}
|
|
121
|
+
collapsed={!open}
|
|
122
|
+
>
|
|
123
|
+
{/* The column is three ROWS, and the order is what makes it read as one
|
|
124
|
+
column: a title row across the whole width, then the rail beside the
|
|
125
|
+
view, then the strip across the whole width again. The rail must not
|
|
126
|
+
start above the title — it would run under the app's own titlebar —
|
|
127
|
+
and the strip must not stop at the view's edge. */}
|
|
128
|
+
<div className="flex flex-col h-full w-full bg-paper">
|
|
129
|
+
<SessionSidebarHeader streamStatus={streamStatus} onCollapse={onCollapse} />
|
|
130
|
+
<div className="flex flex-1 min-h-0 w-full">
|
|
131
|
+
{railEnabled ? (
|
|
132
|
+
<ActivityBar
|
|
133
|
+
side="left"
|
|
134
|
+
activePanel={view}
|
|
135
|
+
onChangePanel={onSelectView}
|
|
136
|
+
isPanelOpen={open}
|
|
137
|
+
hasActiveContext={hasActiveContext}
|
|
138
|
+
activeProjectPath={activeProjectPath}
|
|
139
|
+
/>
|
|
140
|
+
) : null}
|
|
141
|
+
<LeftDockView
|
|
142
|
+
className="flex-1 min-w-0 h-full"
|
|
143
|
+
activeView={view}
|
|
144
|
+
onClose={onCollapse}
|
|
145
|
+
appSettings={appSettings}
|
|
146
|
+
refreshKey={refreshKey}
|
|
147
|
+
onRefresh={onRefresh}
|
|
148
|
+
onOpenFile={onOpenFile}
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
<SessionSidebarFooter
|
|
152
|
+
onSettings={() => setSettingsOpen(true)}
|
|
153
|
+
onInfo={() => setInfoOpen(true)}
|
|
154
|
+
androidDownload={androidDownload}
|
|
155
|
+
updateAvailable={updates.hasUpdate}
|
|
156
|
+
onUpdateClick={() => {
|
|
157
|
+
setInfoOpen(true);
|
|
158
|
+
void updates.check();
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
</Panel>
|
|
163
|
+
|
|
164
|
+
{open ? <ResizeHandle /> : null}
|
|
165
|
+
|
|
166
|
+
<SettingsModal isOpen={settingsOpen} onClose={() => setSettingsOpen(false)} appSettings={appSettings} />
|
|
167
|
+
<AboutModal isOpen={infoOpen} onClose={() => setInfoOpen(false)} updates={updates} onToast={pushToast} />
|
|
168
|
+
<UpdatePopup updates={updates} />
|
|
169
|
+
<ToastStack toasts={toasts} onDismiss={dismissToast} />
|
|
170
|
+
</>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -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">
|
|
@@ -3,9 +3,10 @@ import { Suspense, lazy } from 'preact/compat';
|
|
|
3
3
|
import type { ReactNode, RefObject } from 'preact/compat';
|
|
4
4
|
import { Group, Panel, type PanelImperativeHandle } from '@/client/components/layout/desktop-layout/resizer';
|
|
5
5
|
import { ChatTimeline } from '@/client/components/workspace/chat-timeline/index';
|
|
6
|
-
import { usePanelCatalog, PluginPanelBody, type PanelBodyProps } from '@/client/components/workspace/plugin-panel/resolve';
|
|
6
|
+
import { usePanelCatalog, PluginPanelBody, usePanelSlotPlugins, type PanelBodyProps } from '@/client/components/workspace/plugin-panel/resolve';
|
|
7
|
+
import { EditorPluginRail } from '@/client/components/workspace/editor/PluginRail';
|
|
7
8
|
import { PanelHostProvider } from '@kabmo-labs/kabmo-ui';
|
|
8
|
-
import {
|
|
9
|
+
import { ActivityBar } from '@/client/components/layout/ActivityBar';
|
|
9
10
|
import { useEditorDrop, type EditorDropResult } from '@/client/hooks/editor/use-editor-drop';
|
|
10
11
|
import { EditorDropOverlay } from '@/client/components/workspace/editor/EditorDropOverlay';
|
|
11
12
|
import { ResizeHandle } from '@/client/components/layout/desktop-layout/ResizeHandle';
|
|
@@ -46,6 +47,9 @@ interface WorkspacePanelsProps {
|
|
|
46
47
|
showRightPanel: boolean;
|
|
47
48
|
/** A built-in view id or a plugin panel key (`plugin:<id>/<panel>`). */
|
|
48
49
|
activeRightPanel: string;
|
|
50
|
+
/** The `plugin:<id>` whose panel owns the editor column, or null. */
|
|
51
|
+
activeEditorPanelKey: string | null;
|
|
52
|
+
onSelectEditorPanel: (panelKey: string | null) => void;
|
|
49
53
|
rightPanelRef: RefObject<PanelImperativeHandle | null>;
|
|
50
54
|
/** Remembered width per panel, keyed the same way the layout reports them. */
|
|
51
55
|
panelWidths: PanelWidths;
|
|
@@ -77,6 +81,8 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
77
81
|
editorPanelRef,
|
|
78
82
|
showRightPanel,
|
|
79
83
|
activeRightPanel,
|
|
84
|
+
activeEditorPanelKey,
|
|
85
|
+
onSelectEditorPanel,
|
|
80
86
|
rightPanelRef,
|
|
81
87
|
panelWidths,
|
|
82
88
|
editorWidthMode,
|
|
@@ -127,6 +133,10 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
127
133
|
const catalog = usePanelCatalog();
|
|
128
134
|
const { sessionId } = useSessionStateContext();
|
|
129
135
|
const activePanel = catalog.find((panel) => panel.id === activeRightPanel);
|
|
136
|
+
// The EDITOR column's plugin slot (`panel`): a takeover, so it has no tab in
|
|
137
|
+
// the strip and this rail is its only switch. Empty until a plugin registers
|
|
138
|
+
// one, which is why a Kabmo without one looks exactly as it did.
|
|
139
|
+
const editorPlugins = usePanelSlotPlugins();
|
|
130
140
|
|
|
131
141
|
// Separators present in this group, which also consume width.
|
|
132
142
|
const handleCount = (showEditor ? 1 : 0) + (showRightPanel ? 1 : 0);
|
|
@@ -256,12 +266,18 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
256
266
|
|
|
257
267
|
{showEditor && <ResizeHandle />}
|
|
258
268
|
<Panel panelRef={editorPanelRef} id="editor-panel" defaultSize={editorWidth} minSize={MIN_EDITOR_PANEL_WIDTH} maxSize={editorMaxSize} collapsed={!showEditor}>
|
|
259
|
-
<div className="
|
|
269
|
+
<div className="flex w-full h-full" {...drop.dropProps}>
|
|
260
270
|
<EditorDropOverlay visible={drop.isDragging} />
|
|
271
|
+
<EditorPluginRail
|
|
272
|
+
activeKey={activeEditorPanelKey}
|
|
273
|
+
onSelect={onSelectEditorPanel}
|
|
274
|
+
plugins={editorPlugins}
|
|
275
|
+
/>
|
|
276
|
+
<div className="relative flex-1 min-w-0 h-full">
|
|
261
277
|
<PanelSuspense>
|
|
262
278
|
<Editor
|
|
263
279
|
className="w-full h-full"
|
|
264
|
-
activeEditorPanelKey={
|
|
280
|
+
activeEditorPanelKey={activeEditorPanelKey}
|
|
265
281
|
openedFiles={openedFiles}
|
|
266
282
|
activeFileId={activeFileId}
|
|
267
283
|
onSelectFile={onSetActiveFileId}
|
|
@@ -271,6 +287,7 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
271
287
|
onFileSaved={onRefreshWorkspace}
|
|
272
288
|
/>
|
|
273
289
|
</PanelSuspense>
|
|
290
|
+
</div>
|
|
274
291
|
</div>
|
|
275
292
|
</Panel>
|
|
276
293
|
|
|
@@ -320,7 +337,7 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
320
337
|
</Panel>
|
|
321
338
|
</Group>
|
|
322
339
|
|
|
323
|
-
<
|
|
340
|
+
<ActivityBar side="right" activePanel={activeRightPanel} onChangePanel={onChangeRightPanel} isPanelOpen={showRightPanel} hasActiveContext={hasActiveContext} activeProjectPath={activeProjectPath} />
|
|
324
341
|
</div>
|
|
325
342
|
);
|
|
326
343
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useMemo, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { useSearchParams } from '@/client/lib/router/search-params';
|
|
3
3
|
import { Group, Panel, type PanelImperativeHandle } from '@/client/components/layout/desktop-layout/resizer';
|
|
4
|
-
import {
|
|
4
|
+
import { LeftColumn } from '@/client/components/layout/desktop-layout/LeftColumn';
|
|
5
5
|
import { SettingsModal } from '@/client/components/settings/LazyModal';
|
|
6
6
|
import type { SettingsCategoryId } from '@/shared/types';
|
|
7
7
|
import { activeProjectForSession } from '@/shared/lib/workspace/active-project';
|
|
@@ -9,13 +9,16 @@ import { useFileTabs } from '@/client/hooks/workspace/file-tabs';
|
|
|
9
9
|
import { useSessionState } from '@/client/hooks/workspace/session-state';
|
|
10
10
|
import { TopNavbar } from '@/client/components/layout/desktop-layout/TopNavbar';
|
|
11
11
|
import { WorkspacePanels } from '@/client/components/layout/desktop-layout/WorkspacePanels';
|
|
12
|
+
import { usePanelSlotPlugins } from '@/client/components/workspace/plugin-panel/resolve';
|
|
12
13
|
import { useAgentStreamStatus } from '@/client/hooks/chat/omp/status';
|
|
13
14
|
import { usePanelWidths } from '@/client/hooks/workspace/panel-widths';
|
|
14
|
-
import { useSidebarWidth } from '@/client/hooks/workspace/sidebar-width';
|
|
15
15
|
import { useSidebarData } from '@/client/hooks/chat/omp/session-list';
|
|
16
|
-
import {
|
|
16
|
+
import { MIN_LEFT_PANEL_WIDTH } from '@/shared/lib/workspace/sidebar-width';
|
|
17
|
+
import { MIN_LEFT_DOCK_PLUGIN_WIDTH } from '@/shared/lib/workspace/left-dock-width';
|
|
18
|
+
import { SESSIONS_VIEW_ID } from '@/shared/lib/workspace/left-dock';
|
|
19
|
+
import { useLeftDock, useLeftDockWidth } from '@/client/hooks/workspace/left-dock';
|
|
20
|
+
import { useLeftDockView } from '@/client/components/workspace/plugin-panel/catalog';
|
|
17
21
|
import type { EditorWidthMode } from '@/shared/lib/workspace/panel-widths';
|
|
18
|
-
import { ResizeHandle } from '@/client/components/layout/desktop-layout/ResizeHandle';
|
|
19
22
|
import { useKabmoEvent, useWindowEvent } from '@/client/hooks/ui/window-event';
|
|
20
23
|
import { PluginBootstrap } from '@/client/lib/plugins/bootstrap';
|
|
21
24
|
import { WORKSPACE_KEY_BINDINGS } from '@/shared/lib/workspace/keymap';
|
|
@@ -38,10 +41,22 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
38
41
|
const [showRightPanel, setShowRightPanel] = useSessionState<boolean>('layout.showRightPanel', appSettings.showRightPanel ?? true);
|
|
39
42
|
const [activeRightPanel, setActiveRightPanel] = useSessionState<string>('layout.activeRightPanel', (appSettings.activeRightPanel as string) ?? 'files');
|
|
40
43
|
const [showLeftPanel, setShowLeftPanel] = useSessionState<boolean>('layout.showLeftPanel', appSettings.showLeftPanel ?? true);
|
|
44
|
+
const [activeLeftView, setActiveLeftView] = useSessionState<string>('layout.activeLeftPanel', SESSIONS_VIEW_ID);
|
|
45
|
+
// Which plugin panel, if any, owns the editor column. Per session, like every
|
|
46
|
+
// other layout choice: two chats may be working on different things.
|
|
47
|
+
const [activeEditorPanelKey, setActiveEditorPanelKey] = useSessionState<string | null>('layout.activeEditorPanel', null);
|
|
41
48
|
const { widths: panelWidths, commitWidths } = usePanelWidths();
|
|
42
|
-
// The
|
|
43
|
-
// lists the same
|
|
44
|
-
const
|
|
49
|
+
// The dock's width is per VIEW and global, like the right panel's order: it
|
|
50
|
+
// lists the same views in every chat.
|
|
51
|
+
const dock = useLeftDock();
|
|
52
|
+
// What the dock actually shows: a stored id naming a panel that has moved to
|
|
53
|
+
// the right column falls back to the session list, while one naming nothing at
|
|
54
|
+
// all is left for the dock to explain.
|
|
55
|
+
const leftView = useLeftDockView(activeLeftView, dock.enabled);
|
|
56
|
+
const [sidebarWidth, commitSidebarWidth] = useLeftDockWidth(leftView);
|
|
57
|
+
// A plugin view placed on the dock declares its own floor; the session list
|
|
58
|
+
// keeps the column's own. Resolved here because the resizer clamps to it.
|
|
59
|
+
const leftDockMinWidth = leftView === SESSIONS_VIEW_ID ? MIN_LEFT_PANEL_WIDTH : MIN_LEFT_DOCK_PLUGIN_WIDTH;
|
|
45
60
|
|
|
46
61
|
const editorPanelRef = useRef<PanelImperativeHandle | null>(null);
|
|
47
62
|
const rightPanelRef = useRef<PanelImperativeHandle | null>(null);
|
|
@@ -72,7 +87,17 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
72
87
|
convertDiffToEditor,
|
|
73
88
|
} = useFileTabs(activeProjectPath, sessionId || '', handleOpenTab);
|
|
74
89
|
|
|
75
|
-
|
|
90
|
+
// Three ways the column has content: a file, a chosen takeover, and a
|
|
91
|
+
// takeover that merely exists. Only the first two OPEN it — installing a
|
|
92
|
+
// plugin must not push a column onto the screen — but the third is what the
|
|
93
|
+
// toggle has to open on to, since the rail lives inside the column.
|
|
94
|
+
const editorTakeovers = usePanelSlotPlugins();
|
|
95
|
+
// The user's own switch wins over every default, in both directions.
|
|
96
|
+
const editorHasContent =
|
|
97
|
+
openedFiles.length > 0 || activeEditorPanelKey !== null || editorTakeovers.length > 0;
|
|
98
|
+
const showEditor =
|
|
99
|
+
editorHasContent &&
|
|
100
|
+
(userToggledEditor ?? (openedFiles.length > 0 || activeEditorPanelKey !== null));
|
|
76
101
|
|
|
77
102
|
// A diff wants room a source file does not, so the editor panel keeps a
|
|
78
103
|
// separate remembered width per tab kind.
|
|
@@ -81,13 +106,9 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
81
106
|
[openedFiles, activeFileId],
|
|
82
107
|
);
|
|
83
108
|
|
|
84
|
-
// Width restoration is declarative:
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// an imperative push — a right-panel view switch and an editor source↔diff
|
|
88
|
-
// switch, both of which keep the panel mounted while only its width slot
|
|
89
|
-
// changes — and it cannot go stale, because there is no cached layout to
|
|
90
|
-
// disagree with the props.
|
|
109
|
+
// Width restoration is declarative: each panel's `defaultSize` comes from the
|
|
110
|
+
// remembered width for its current slot and `Panel` adopts a change, so a
|
|
111
|
+
// view switch needs no imperative push and no cached layout can go stale.
|
|
91
112
|
|
|
92
113
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
93
114
|
const [settingsCategory, setSettingsCategory] = useState<SettingsCategoryId>('appearance');
|
|
@@ -114,13 +135,9 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
114
135
|
const { toasts, pushToast, dismissToast } = useToasts();
|
|
115
136
|
|
|
116
137
|
/**
|
|
117
|
-
* A drop
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* The TARGET itself is not here: it belongs to the chat and editor panels
|
|
122
|
-
* (`WorkspacePanels`), because the sidebar and the right panel are panels in
|
|
123
|
-
* the same group and a layout-wide target would accept a drop over them too.
|
|
138
|
+
* A refused drop names its reason ("outside every workspace", "not text") —
|
|
139
|
+
* a silent no-op reads as a broken target. The TARGET is `WorkspacePanels`':
|
|
140
|
+
* a layout-wide one would accept a drop over the sidebar and right panel too.
|
|
124
141
|
*/
|
|
125
142
|
const handleDropResult = useCallback(
|
|
126
143
|
(result: EditorDropResult) => pushToast(result.message, result.ok ? 'success' : 'error'),
|
|
@@ -142,21 +159,21 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
142
159
|
: undefined;
|
|
143
160
|
|
|
144
161
|
/**
|
|
145
|
-
* The workspace
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* `⌘S` is deliberately absent here: the editor panel already owns it (it is
|
|
149
|
-
* the only surface with a buffer to save), and binding it twice would save
|
|
150
|
-
* the file twice.
|
|
162
|
+
* The workspace shortcuts, from the shared keymap so the toggles' tooltips
|
|
163
|
+
* name the bound chord. `⌘S` is absent: the editor owns it, and binding it
|
|
164
|
+
* twice would save the file twice.
|
|
151
165
|
*/
|
|
152
166
|
useWindowEvent('keydown', (e) => {
|
|
153
167
|
const command = resolveBinding(WORKSPACE_KEY_BINDINGS, e);
|
|
154
168
|
if (!command) return;
|
|
155
169
|
if (command === 'saveFile') return;
|
|
156
170
|
if (command === 'toggleEditorPanel' && !showEditor) {
|
|
157
|
-
// The editor panel
|
|
158
|
-
// already folds in the user's own toggle.
|
|
159
|
-
|
|
171
|
+
// The editor panel needs something to show — a file or a takeover — and
|
|
172
|
+
// `showEditor` already folds in the user's own toggle.
|
|
173
|
+
// The toggle is also the only way to REACH a takeover while the column is
|
|
174
|
+
// closed, since the rail lives inside the column: with no file and no
|
|
175
|
+
// takeover there would be nothing to open on to, and with one there is.
|
|
176
|
+
if (openedFiles.length === 0 && !activeEditorPanelKey && editorTakeovers.length === 0) return;
|
|
160
177
|
}
|
|
161
178
|
e.preventDefault();
|
|
162
179
|
if (command === 'toggleSidebar') setShowLeftPanel((previous) => !previous);
|
|
@@ -181,34 +198,17 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
181
198
|
setSettingsOpen(true);
|
|
182
199
|
});
|
|
183
200
|
|
|
201
|
+
// Clicking the ACTIVE view on an open panel collapses that panel; any other
|
|
202
|
+
// click selects and opens. One handler for both columns' rails.
|
|
184
203
|
const handleChangeRightPanel = (panel: string) => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (activeRightPanel === panel && showRightPanel) {
|
|
189
|
-
nextShow = false;
|
|
190
|
-
} else {
|
|
191
|
-
nextActive = panel;
|
|
192
|
-
nextShow = true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
setShowRightPanel(nextShow);
|
|
196
|
-
setActiveRightPanel(nextActive);
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const handleToggleLeftPanel = (show: boolean) => {
|
|
200
|
-
setShowLeftPanel(show);
|
|
204
|
+
const collapsing = activeRightPanel === panel && showRightPanel;
|
|
205
|
+
setShowRightPanel(!collapsing);
|
|
206
|
+
if (!collapsing) setActiveRightPanel(panel);
|
|
201
207
|
};
|
|
202
208
|
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const handleToggleEditor = () => {
|
|
208
|
-
setUserToggledEditor(!showEditor);
|
|
209
|
-
// The editor panel keeps its own remembered width per tab kind, so a
|
|
210
|
-
// dragged width survives the toggle without being re-applied here.
|
|
211
|
-
};
|
|
209
|
+
const handleToggleLeftPanel = (show: boolean) => setShowLeftPanel(show);
|
|
210
|
+
const handleToggleRightPanel = () => setShowRightPanel(!showRightPanel);
|
|
211
|
+
const handleToggleEditor = () => setUserToggledEditor(!showEditor);
|
|
212
212
|
|
|
213
213
|
return (
|
|
214
214
|
<div
|
|
@@ -216,7 +216,8 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
216
216
|
className="relative flex flex-col h-screen w-screen overflow-hidden bg-canvas text-ink font-sans selection:bg-ink selection:text-canvas"
|
|
217
217
|
>
|
|
218
218
|
|
|
219
|
-
{/* Main Workspace:
|
|
219
|
+
{/* Main Workspace: the resizable LEFT dock (rail + view, see
|
|
220
|
+
`LeftColumn`) plus the right stack. */}
|
|
220
221
|
<div className="flex flex-1 overflow-hidden">
|
|
221
222
|
<Group
|
|
222
223
|
orientation="horizontal"
|
|
@@ -226,10 +227,29 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
226
227
|
if (left != null && left > 0) commitSidebarWidth(Math.round(left));
|
|
227
228
|
}}
|
|
228
229
|
>
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
<LeftColumn
|
|
231
|
+
panelRef={leftPanelRef}
|
|
232
|
+
open={showLeftPanel}
|
|
233
|
+
railEnabled={dock.enabled}
|
|
234
|
+
view={leftView}
|
|
235
|
+
onSelectView={(panel) => {
|
|
236
|
+
// A rail click means "show this view" — never "toggle" it. A
|
|
237
|
+
// toggle would hide the rail the user is aiming at, and the dock
|
|
238
|
+
// has its own collapse control in the header.
|
|
239
|
+
setActiveLeftView(panel);
|
|
240
|
+
if (!showLeftPanel) handleToggleLeftPanel(true);
|
|
241
|
+
}}
|
|
242
|
+
onCollapse={() => handleToggleLeftPanel(false)}
|
|
243
|
+
width={sidebarWidth}
|
|
244
|
+
minWidth={leftDockMinWidth}
|
|
245
|
+
hasActiveContext={hasActiveContext}
|
|
246
|
+
activeProjectPath={activeProjectPath}
|
|
247
|
+
appSettings={appSettings}
|
|
248
|
+
streamStatus={streamStatus}
|
|
249
|
+
refreshKey={refreshKey}
|
|
250
|
+
onRefresh={handleRefreshWorkspace}
|
|
251
|
+
onOpenFile={(file) => handleOpenFile(file as Parameters<typeof handleOpenFile>[0])}
|
|
252
|
+
/>
|
|
233
253
|
|
|
234
254
|
{/* Right stack: top navbar + resizable workspace (the group's filler) */}
|
|
235
255
|
<Panel id="main-right-stack" filler minSize={0}>
|
|
@@ -255,6 +275,13 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
255
275
|
editorPanelRef={editorPanelRef}
|
|
256
276
|
showRightPanel={showRightPanel}
|
|
257
277
|
activeRightPanel={activeRightPanel}
|
|
278
|
+
activeEditorPanelKey={activeEditorPanelKey}
|
|
279
|
+
onSelectEditorPanel={(panelKey) => {
|
|
280
|
+
setActiveEditorPanelKey(panelKey);
|
|
281
|
+
// Choosing a takeover must OPEN the column, or the click would
|
|
282
|
+
// land on a panel that is still collapsed.
|
|
283
|
+
if (panelKey) setUserToggledEditor(true);
|
|
284
|
+
}}
|
|
258
285
|
rightPanelRef={rightPanelRef}
|
|
259
286
|
panelWidths={panelWidths}
|
|
260
287
|
editorWidthMode={editorWidthMode}
|