ompchamber 1.0.0 → 1.1.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/static/css/index.b859cc1cc5.css +2 -0
- package/dist/client/static/js/async/{5b.0d6ae50fe8.js → 5b.a5ccbc19dc.js} +2 -2
- package/dist/client/static/js/index.6e065017eb.js +33 -0
- package/package.json +1 -1
- package/src/client/components/common/ComposerPicker.tsx +16 -4
- package/src/client/components/common/ComposerTextarea.tsx +1 -0
- package/src/client/components/layout/RightActivityBar.tsx +1 -1
- package/src/client/components/layout/desktop-layout/ResizeHandle.tsx +2 -2
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +13 -2
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +107 -56
- package/src/client/components/layout/desktop-layout/index.tsx +25 -96
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +79 -118
- package/src/client/components/layout/desktop-layout/resizer/separator-drag.ts +140 -0
- package/src/client/components/layout/session-sidebar/Header.tsx +1 -1
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +26 -12
- package/src/client/components/layout/session-sidebar/SubagentList.tsx +6 -4
- package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +3 -0
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +3 -0
- package/src/client/components/workspace/chat-timeline/SubagentView.tsx +3 -1
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +23 -3
- package/src/client/components/workspace/chat-timeline/index.tsx +3 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Goal.tsx +3 -3
- package/src/client/components/workspace/model-dropdown/groups.ts +35 -2
- package/src/client/components/workspace/model-dropdown/index.tsx +48 -7
- package/src/client/components/workspace/model-dropdown/use-catalog.ts +28 -4
- package/src/client/hooks/chat/composer/index.ts +28 -8
- package/src/client/hooks/chat/timeline/actions.ts +24 -3
- package/src/client/hooks/chat/timeline/deferred-model.test.ts +219 -0
- package/src/client/hooks/chat/timeline/deferred-model.ts +92 -0
- package/src/client/hooks/chat/timeline/index.ts +13 -1
- package/src/client/hooks/chat/timeline/send.ts +16 -5
- package/src/client/hooks/workspace/available-width.ts +33 -0
- package/src/client/hooks/workspace/panel-widths.ts +34 -37
- package/src/server/lib/models/preferences.server.ts +110 -0
- package/src/server/lib/models/registry.server.ts +206 -0
- package/src/server/lib/omp/rpc/utility.ts +7 -1
- package/src/server/lib/queue/delivery.server.test.ts +203 -0
- package/src/server/lib/queue/delivery.server.ts +38 -8
- package/src/server/routes/models/root.ts +88 -215
- package/src/server/routes/settings/commands.ts +59 -18
- package/src/shared/lib/chat/composer/client.ts +17 -8
- package/src/shared/lib/chat/composer/commands.ts +290 -0
- package/src/shared/lib/chat/composer/composer.test.ts +157 -0
- package/src/shared/lib/chat/composer/filter.ts +21 -7
- package/src/shared/lib/chat/composer/trigger.ts +109 -20
- package/src/shared/lib/models/preferences.test.ts +131 -0
- package/src/shared/lib/models/preferences.ts +88 -0
- package/src/shared/lib/models/provider-models.ts +8 -1
- package/src/shared/lib/omp/subagent/label.test.ts +94 -0
- package/src/shared/lib/omp/subagent/label.ts +84 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +113 -18
- package/src/shared/lib/workspace/panel-widths.ts +161 -30
- package/src/shared/lib/workspace/right-panels.ts +50 -11
- package/src/shared/lib/workspace/session-state/store.ts +3 -0
- package/src/shared/types/chat.ts +38 -65
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/models.ts +101 -0
- package/src/shared/types/omp/subagent.ts +4 -0
- package/src/shared/types/settings/command.ts +13 -0
- package/dist/client/static/css/index.e880517fcd.css +0 -2
- package/dist/client/static/js/index.0de15c5e17.js +0 -33
- /package/dist/client/static/js/async/{5b.0d6ae50fe8.js.LICENSE.txt → 5b.a5ccbc19dc.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{index.0de15c5e17.js.LICENSE.txt → index.6e065017eb.js.LICENSE.txt} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ompchamber",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Developer web console and diagnostic chamber for AI Oh-My-Pi — Elysia + Preact on Bun.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Wijanarko Putra Rajeb <wijanarko.rajeb@gmail.com>",
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'preact/hooks';
|
|
2
2
|
import type { ReactElement } from 'preact/compat';
|
|
3
|
-
import type { ComposerMatchItem, ComposerPickItem, ComposerPickKind } from '@/shared/types';
|
|
3
|
+
import type { ComposerMatchItem, ComposerPickItem, ComposerPickKind, ComposerTriggerPhase } from '@/shared/types';
|
|
4
4
|
|
|
5
5
|
export interface ComposerPickerProps {
|
|
6
6
|
open: boolean;
|
|
7
7
|
kind: ComposerPickKind;
|
|
8
|
+
/** `args` swaps the header for the subcommand hint of the typed command. */
|
|
9
|
+
phase?: ComposerTriggerPhase;
|
|
8
10
|
items: ComposerMatchItem[];
|
|
9
11
|
activeIndex: number;
|
|
10
12
|
loading: boolean;
|
|
@@ -37,6 +39,7 @@ function OptionName({ item }: { item: ComposerMatchItem }) {
|
|
|
37
39
|
export function ComposerPicker({
|
|
38
40
|
open,
|
|
39
41
|
kind,
|
|
42
|
+
phase = 'name',
|
|
40
43
|
items,
|
|
41
44
|
activeIndex,
|
|
42
45
|
loading,
|
|
@@ -56,9 +59,18 @@ export function ComposerPicker({
|
|
|
56
59
|
if (!open) return null;
|
|
57
60
|
|
|
58
61
|
const isMention = kind === 'mention';
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
+
const isArgs = !isMention && phase === 'args';
|
|
63
|
+
const headerText = isMention
|
|
64
|
+
? 'Type to search files and agents'
|
|
65
|
+
: isArgs
|
|
66
|
+
? 'Pick a subcommand'
|
|
67
|
+
: 'Type to search commands and skills';
|
|
68
|
+
const ariaLabel = isMention ? 'Files and agents' : isArgs ? 'Subcommands' : 'Commands and skills';
|
|
69
|
+
const emptyText = isMention
|
|
70
|
+
? 'No matching files or agents'
|
|
71
|
+
: isArgs
|
|
72
|
+
? 'No matching subcommands'
|
|
73
|
+
: 'No matching commands or skills';
|
|
62
74
|
|
|
63
75
|
return (
|
|
64
76
|
<div
|
|
@@ -117,6 +117,7 @@ export function ComposerTextarea({
|
|
|
117
117
|
<ComposerPicker
|
|
118
118
|
open={composer.isOpen}
|
|
119
119
|
kind={composer.trigger?.kind ?? 'mention'}
|
|
120
|
+
phase={composer.trigger?.phase ?? 'name'}
|
|
120
121
|
items={composer.matches}
|
|
121
122
|
activeIndex={composer.activeIndex}
|
|
122
123
|
loading={composer.loading}
|
|
@@ -34,7 +34,7 @@ export function RightActivityBar({ activePanel, onChangePanel, isPanelOpen, hasA
|
|
|
34
34
|
const hasGitChanges = changes.length > 0;
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
|
-
<nav className="w-
|
|
37
|
+
<nav className="w-11 flex-shrink-0 border-l border-ink/10 bg-paper flex flex-col items-center py-3 space-y-2 z-10">
|
|
38
38
|
{/* Top Icons */}
|
|
39
39
|
<div className="flex flex-col items-center space-y-1 w-full">
|
|
40
40
|
{RIGHT_PANEL_TYPES.map((panel) => {
|
|
@@ -3,8 +3,8 @@ import { Separator } from '@/client/components/layout/desktop-layout/resizer';
|
|
|
3
3
|
/** Hairline drag handle shared by both resizable groups in the desktop layout. */
|
|
4
4
|
export function ResizeHandle() {
|
|
5
5
|
return (
|
|
6
|
-
<Separator className="relative w-
|
|
7
|
-
<div className="h-full w-[1px] bg-ink/10 group-hover:bg-ink/40 group-active:bg-ink/60 group-hover:w-
|
|
6
|
+
<Separator className="relative w-[3px] outline-none group flex justify-center cursor-col-resize z-10">
|
|
7
|
+
<div className="h-full w-[1px] bg-ink/10 group-hover:bg-ink/40 group-active:bg-ink/60 group-hover:w-[3px] transition-all" />
|
|
8
8
|
</Separator>
|
|
9
9
|
);
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LayoutTemplate, MoreHorizontal, PanelRight, PanelRightClose, Smartphone } from 'lucide-preact';
|
|
1
|
+
import { LayoutTemplate, MoreHorizontal, PanelLeft, PanelRight, PanelRightClose, Smartphone } from 'lucide-preact';
|
|
2
2
|
import { PWAInstallButton } from '@/client/components/common/PWAInstallButton';
|
|
3
3
|
import { StreamStatusDot } from '@/client/components/common/StreamStatusDot';
|
|
4
4
|
import type { AgentStreamStatus } from '@/shared/lib/chat/omp/status';
|
|
@@ -13,6 +13,7 @@ interface TopNavbarProps {
|
|
|
13
13
|
onSwitchToMobile?: () => void;
|
|
14
14
|
onToggleEditor: () => void;
|
|
15
15
|
onToggleRightPanel: () => void;
|
|
16
|
+
onToggleLeftPanel: () => void;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function TopNavbar({
|
|
@@ -24,10 +25,11 @@ export function TopNavbar({
|
|
|
24
25
|
onSwitchToMobile,
|
|
25
26
|
onToggleEditor,
|
|
26
27
|
onToggleRightPanel,
|
|
28
|
+
onToggleLeftPanel,
|
|
27
29
|
}: TopNavbarProps) {
|
|
28
30
|
return (
|
|
29
31
|
<header
|
|
30
|
-
className="h-
|
|
32
|
+
className="h-14 flex-shrink-0 border-b border-ink/10 bg-paper flex items-center justify-between pr-4 z-20 titlebar-drag-region select-none"
|
|
31
33
|
style={{
|
|
32
34
|
paddingLeft: !showLeftPanel ? 'max(1rem, env(titlebar-area-x, 0px))' : undefined,
|
|
33
35
|
paddingRight: 'max(1rem, calc(100vw - env(titlebar-area-width, 100vw)))',
|
|
@@ -36,6 +38,15 @@ export function TopNavbar({
|
|
|
36
38
|
<div className="flex items-center space-x-2 px-4 min-w-0">
|
|
37
39
|
{!showLeftPanel && (
|
|
38
40
|
<>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
onClick={onToggleLeftPanel}
|
|
44
|
+
className="p-1.5 rounded hover:bg-ink/10 transition-colors text-ink/60 hover:text-ink cursor-pointer flex-shrink-0 titlebar-no-drag"
|
|
45
|
+
title="Expand Sidebar"
|
|
46
|
+
aria-label="Expand Sidebar"
|
|
47
|
+
>
|
|
48
|
+
<PanelLeft size={16} />
|
|
49
|
+
</button>
|
|
39
50
|
<span className="font-bold text-sm tracking-tight hidden sm:flex items-center flex-shrink-0">
|
|
40
51
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-orange-600 to-amber-500 font-extrabold text-[15px] tracking-tighter">OMP</span>
|
|
41
52
|
<span className="ml-[1px]">Chamber</span>
|
|
@@ -6,15 +6,32 @@ import { ChatTimeline } from '@/client/components/workspace/chat-timeline/index'
|
|
|
6
6
|
import { getDesktopPanelView } from '@/client/components/common/lazy-panels';
|
|
7
7
|
import { RightActivityBar } from '@/client/components/layout/RightActivityBar';
|
|
8
8
|
import { ResizeHandle } from '@/client/components/layout/desktop-layout/ResizeHandle';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { useAvailableWidth } from '@/client/hooks/workspace/available-width';
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_RIGHT_PANEL_FRACTIONS,
|
|
12
|
+
DEFAULT_RIGHT_PANEL_WIDTHS,
|
|
13
|
+
MIN_RIGHT_PANEL_WIDTHS,
|
|
14
|
+
RIGHT_PANEL_TYPES,
|
|
15
|
+
type RightPanelType,
|
|
16
|
+
} from '@/shared/lib/workspace/right-panels';
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_EDITOR_FRACTIONS,
|
|
19
|
+
DEFAULT_EDITOR_WIDTHS,
|
|
20
|
+
MIN_CHAT_PANEL_WIDTH,
|
|
21
|
+
MIN_EDITOR_PANEL_WIDTH,
|
|
22
|
+
resolvePanelWidth,
|
|
23
|
+
type EditorWidthMode,
|
|
24
|
+
type PanelWidths,
|
|
25
|
+
} from '@/shared/lib/workspace/panel-widths';
|
|
11
26
|
|
|
12
27
|
const Editor = lazy(() => import('@/client/components/workspace/editor/index').then((m) => ({ default: m.Editor })));
|
|
13
28
|
|
|
14
29
|
function PanelSuspense({ children }: { children: ReactNode }) {
|
|
15
30
|
return (
|
|
16
31
|
<Suspense fallback={
|
|
17
|
-
<div className="w-full h-full flex items-center justify-center text-ink/40
|
|
32
|
+
<div className="w-full h-full flex items-center justify-center text-ink/40">
|
|
33
|
+
<span className="text-xs font-mono">Loading…</span>
|
|
34
|
+
</div>
|
|
18
35
|
}>
|
|
19
36
|
{children}
|
|
20
37
|
</Suspense>
|
|
@@ -72,9 +89,12 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
72
89
|
onPanelWidths,
|
|
73
90
|
} = props;
|
|
74
91
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
92
|
+
// The group's own area: the base every stored fraction is a share of, and the
|
|
93
|
+
// budget the chat floor is subtracted from. Measured through the group's
|
|
94
|
+
// element, not a wrapper — the wrapper also spans the activity bar, which is
|
|
95
|
+
// not area any panel may take.
|
|
96
|
+
const groupRef = useRef<HTMLDivElement>(null);
|
|
97
|
+
const available = useAvailableWidth(groupRef);
|
|
78
98
|
|
|
79
99
|
// One cached lazy per view, scoped to this layout instance: switching the
|
|
80
100
|
// right panel toggles CSS visibility instead of unmounting, so a view's
|
|
@@ -86,18 +106,55 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
86
106
|
[viewScope],
|
|
87
107
|
);
|
|
88
108
|
|
|
109
|
+
// Separators present in this group, which also consume width.
|
|
110
|
+
const handleCount = (showEditor ? 1 : 0) + (showRightPanel ? 1 : 0);
|
|
111
|
+
|
|
112
|
+
const editorWidth = resolvePanelWidth({
|
|
113
|
+
stored: panelWidths[editorWidthMode],
|
|
114
|
+
defaultFraction: DEFAULT_EDITOR_FRACTIONS[editorWidthMode],
|
|
115
|
+
defaultPx: DEFAULT_EDITOR_WIDTHS[editorWidthMode],
|
|
116
|
+
available,
|
|
117
|
+
min: MIN_EDITOR_PANEL_WIDTH,
|
|
118
|
+
// The right panel renders after the editor, so it absorbs the remainder.
|
|
119
|
+
// Reserving this view's own floor is enough: the editor's share is what
|
|
120
|
+
// the ceiling subtracts.
|
|
121
|
+
siblingMin: showRightPanel ? MIN_RIGHT_PANEL_WIDTHS[activeRightPanel] : 0,
|
|
122
|
+
handleCount,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const rightWidth = resolvePanelWidth({
|
|
126
|
+
stored: panelWidths.right?.[activeRightPanel],
|
|
127
|
+
defaultFraction: DEFAULT_RIGHT_PANEL_FRACTIONS[activeRightPanel],
|
|
128
|
+
defaultPx: DEFAULT_RIGHT_PANEL_WIDTHS[activeRightPanel],
|
|
129
|
+
available,
|
|
130
|
+
min: MIN_RIGHT_PANEL_WIDTHS[activeRightPanel],
|
|
131
|
+
// The editor took its share first; only its floor is reserved here.
|
|
132
|
+
siblingMin: showEditor ? MIN_EDITOR_PANEL_WIDTH : 0,
|
|
133
|
+
handleCount,
|
|
134
|
+
});
|
|
135
|
+
|
|
89
136
|
/**
|
|
90
137
|
* Widths of the fixed panels as they are right now, keyed by slot: the
|
|
91
138
|
* editor (source or diff, whichever is showing) and the right panel under
|
|
92
139
|
* the view it currently holds. The chat column is the group's filler, so it
|
|
93
140
|
* has no width of its own to report.
|
|
141
|
+
*
|
|
142
|
+
* Each is stored as a share of the area it was measured in, so the layout
|
|
143
|
+
* follows a window resize instead of holding a pixel value that was only
|
|
144
|
+
* right on the display it was dragged on.
|
|
94
145
|
*/
|
|
95
146
|
const readPanelWidths = (): PanelWidths => {
|
|
96
147
|
const patch: PanelWidths = {};
|
|
97
148
|
const editor = editorPanelRef.current?.getSize()?.inPixels;
|
|
98
149
|
const right = rightPanelRef.current?.getSize()?.inPixels;
|
|
99
|
-
if (editor != null && editor > 0)
|
|
100
|
-
|
|
150
|
+
if (editor != null && editor > 0) {
|
|
151
|
+
patch[editorWidthMode] = { px: Math.round(editor), ...(available ? { fraction: editor / available } : {}) };
|
|
152
|
+
}
|
|
153
|
+
if (right != null && right > 0) {
|
|
154
|
+
patch.right = {
|
|
155
|
+
[activeRightPanel]: { px: Math.round(right), ...(available ? { fraction: right / available } : {}) },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
101
158
|
return patch;
|
|
102
159
|
};
|
|
103
160
|
|
|
@@ -106,6 +163,8 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
106
163
|
<Group
|
|
107
164
|
orientation="horizontal"
|
|
108
165
|
id="ompchamber-layout"
|
|
166
|
+
className="flex-1 min-w-0"
|
|
167
|
+
groupRef={groupRef}
|
|
109
168
|
onLayoutChanged={() => {
|
|
110
169
|
// Persist only real drags: imperative restores and constraint
|
|
111
170
|
// recomputes would otherwise rewrite the saved widths with the
|
|
@@ -117,55 +176,47 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
117
176
|
<ChatTimeline className="w-full h-full" appSettings={appSettings} onSessionTitle={onSessionTitle} />
|
|
118
177
|
</Panel>
|
|
119
178
|
|
|
120
|
-
{showEditor &&
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
</PanelSuspense>
|
|
135
|
-
</Panel>
|
|
136
|
-
</>
|
|
137
|
-
)}
|
|
179
|
+
{showEditor && <ResizeHandle />}
|
|
180
|
+
<Panel panelRef={editorPanelRef} id="editor-panel" defaultSize={editorWidth} minSize={MIN_EDITOR_PANEL_WIDTH} collapsed={!showEditor}>
|
|
181
|
+
<PanelSuspense>
|
|
182
|
+
<Editor
|
|
183
|
+
className="w-full h-full"
|
|
184
|
+
openedFiles={openedFiles}
|
|
185
|
+
activeFileId={activeFileId}
|
|
186
|
+
onSelectFile={onSetActiveFileId}
|
|
187
|
+
onCloseFile={onCloseFile}
|
|
188
|
+
refreshKey={refreshKey}
|
|
189
|
+
onFileSaved={onRefreshWorkspace}
|
|
190
|
+
/>
|
|
191
|
+
</PanelSuspense>
|
|
192
|
+
</Panel>
|
|
138
193
|
|
|
139
|
-
{showRightPanel &&
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
</PanelSuspense>
|
|
166
|
-
</Panel>
|
|
167
|
-
</>
|
|
168
|
-
)}
|
|
194
|
+
{showRightPanel && <ResizeHandle />}
|
|
195
|
+
<Panel panelRef={rightPanelRef} id="right-panel" defaultSize={rightWidth} minSize={MIN_RIGHT_PANEL_WIDTHS[activeRightPanel]} collapsed={!showRightPanel}>
|
|
196
|
+
<PanelSuspense>
|
|
197
|
+
{rightViews.map(({ view, Comp }) => {
|
|
198
|
+
const isActiveView = activeRightPanel === view;
|
|
199
|
+
const base = {
|
|
200
|
+
className: 'w-full h-full',
|
|
201
|
+
// The terminal must stay enabled while hidden: swapping it
|
|
202
|
+
// to the placeholder mid-command would unmount the live
|
|
203
|
+
// stream. Everything else polls/re-reads only while shown.
|
|
204
|
+
enabled: view === 'terminal' ? hasActiveContext : hasActiveContext && isActiveView,
|
|
205
|
+
active: isActiveView,
|
|
206
|
+
refreshKey,
|
|
207
|
+
rootPath: activeProjectPath ?? undefined,
|
|
208
|
+
onRefresh: onRefreshWorkspace,
|
|
209
|
+
onOpenFile,
|
|
210
|
+
onClose: onToggleRightPanel,
|
|
211
|
+
};
|
|
212
|
+
return (
|
|
213
|
+
<div key={view} className={isActiveView ? 'w-full h-full' : 'hidden'}>
|
|
214
|
+
<Comp {...base} />
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
})}
|
|
218
|
+
</PanelSuspense>
|
|
219
|
+
</Panel>
|
|
169
220
|
</Group>
|
|
170
221
|
|
|
171
222
|
<RightActivityBar activePanel={activeRightPanel} onChangePanel={onChangeRightPanel} isPanelOpen={showRightPanel} hasActiveContext={hasActiveContext} activeProjectPath={activeProjectPath} refreshKey={refreshKey} />
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from 'preact/hooks';
|
|
3
2
|
import { useSearchParams } from '@/client/lib/router/search-params';
|
|
4
3
|
import { Group, Panel, type PanelImperativeHandle } from '@/client/components/layout/desktop-layout/resizer';
|
|
5
4
|
import { SessionSidebar } from '@/client/components/layout/session-sidebar/index';
|
|
6
|
-
import {
|
|
5
|
+
import type { RightPanelType } from '@/shared/lib/workspace/right-panels';
|
|
7
6
|
import { SettingsModal } from '@/client/components/settings/LazyModal';
|
|
8
|
-
import { PanelLeft } from 'lucide-preact';
|
|
9
7
|
import type { SettingsCategoryId } from '@/shared/types';
|
|
10
8
|
import { activeProjectForSession } from '@/shared/lib/workspace/active-project';
|
|
11
9
|
import { useFileTabs } from '@/client/hooks/workspace/file-tabs';
|
|
@@ -15,9 +13,8 @@ import { WorkspacePanels } from '@/client/components/layout/desktop-layout/Works
|
|
|
15
13
|
import { useAgentStreamStatus } from '@/client/hooks/chat/omp/status';
|
|
16
14
|
import { usePanelWidths } from '@/client/hooks/workspace/panel-widths';
|
|
17
15
|
import { useSidebarData } from '@/client/hooks/chat/omp/session-list';
|
|
18
|
-
import {
|
|
16
|
+
import { DEFAULT_LEFT_PANEL_WIDTH, MAX_LEFT_PANEL_WIDTH, MIN_LEFT_PANEL_WIDTH, type EditorWidthMode } from '@/shared/lib/workspace/panel-widths';
|
|
19
17
|
import { ResizeHandle } from '@/client/components/layout/desktop-layout/ResizeHandle';
|
|
20
|
-
import { writeSetting } from '@/shared/lib/settings/client';
|
|
21
18
|
import { useChamberEvent, useWindowEvent } from '@/client/hooks/ui/window-event';
|
|
22
19
|
|
|
23
20
|
interface DesktopLayoutProps {
|
|
@@ -26,29 +23,17 @@ interface DesktopLayoutProps {
|
|
|
26
23
|
appSettings?: Record<string, any>;
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
/**
|
|
30
|
-
* Push a remembered width onto a mounted panel. Panels report nothing while
|
|
31
|
-
* unmounted, so a missing width (never resized) or a missing handle (panel
|
|
32
|
-
* closed) simply leaves the panel at whatever its `defaultSize` derived.
|
|
33
|
-
*/
|
|
34
|
-
function applyWidth(
|
|
35
|
-
ref: RefObject<PanelImperativeHandle | null>,
|
|
36
|
-
px: number | undefined,
|
|
37
|
-
) {
|
|
38
|
-
if (px != null) ref.current?.resize(px);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
26
|
export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }: DesktopLayoutProps) {
|
|
42
27
|
const { folders } = useSidebarData();
|
|
43
28
|
const [showRightPanel, setShowRightPanel] = useSessionState<boolean>('layout.showRightPanel', appSettings.showRightPanel ?? true);
|
|
44
29
|
const [activeRightPanel, setActiveRightPanel] = useSessionState<RightPanelType>('layout.activeRightPanel', (appSettings.activeRightPanel as RightPanelType) ?? 'files');
|
|
45
|
-
const [showLeftPanel, setShowLeftPanel] =
|
|
46
|
-
const { widths: panelWidths,
|
|
30
|
+
const [showLeftPanel, setShowLeftPanel] = useSessionState<boolean>('layout.showLeftPanel', appSettings.showLeftPanel ?? true);
|
|
31
|
+
const { widths: panelWidths, commitWidths } = usePanelWidths(appSettings, activeRightPanel);
|
|
47
32
|
|
|
48
33
|
const editorPanelRef = useRef<PanelImperativeHandle | null>(null);
|
|
49
34
|
const rightPanelRef = useRef<PanelImperativeHandle | null>(null);
|
|
50
35
|
const leftPanelRef = useRef<PanelImperativeHandle | null>(null);
|
|
51
|
-
const [userToggledEditor, setUserToggledEditor] =
|
|
36
|
+
const [userToggledEditor, setUserToggledEditor] = useSessionState<boolean | null>('layout.userToggledEditor', appSettings.userToggledEditor ?? null);
|
|
52
37
|
|
|
53
38
|
const [searchParams] = useSearchParams();
|
|
54
39
|
const folderIdParam = searchParams.get('folderId');
|
|
@@ -63,8 +48,7 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
63
48
|
|
|
64
49
|
const handleOpenTab = useCallback(() => {
|
|
65
50
|
setUserToggledEditor(true);
|
|
66
|
-
|
|
67
|
-
}, []);
|
|
51
|
+
}, [setUserToggledEditor]);
|
|
68
52
|
|
|
69
53
|
const {
|
|
70
54
|
openedFiles,
|
|
@@ -83,42 +67,13 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
83
67
|
[openedFiles, activeFileId],
|
|
84
68
|
);
|
|
85
69
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
useEffect(() => {
|
|
94
|
-
const frame = requestAnimationFrame(() => {
|
|
95
|
-
const widths = widthsRef.current;
|
|
96
|
-
if (widths) applyWidth(leftPanelRef, widths.left);
|
|
97
|
-
});
|
|
98
|
-
return () => cancelAnimationFrame(frame);
|
|
99
|
-
}, [showLeftPanel, widthsRef]);
|
|
100
|
-
|
|
101
|
-
// The right panel survives activity-bar view switches (only CSS hides the
|
|
102
|
-
// outgoing view), so a mounted panel never re-reads its defaultSize. A view
|
|
103
|
-
// that has been resized gets that width pushed here; one that never has gets
|
|
104
|
-
// the width it is supposed to open at, which the element would otherwise
|
|
105
|
-
// have taken from the view it was mounted under.
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
const frame = requestAnimationFrame(() => {
|
|
108
|
-
applyWidth(rightPanelRef, widthsRef.current?.right?.[activeRightPanel] ?? DEFAULT_RIGHT_PANEL_WIDTHS[activeRightPanel]);
|
|
109
|
-
});
|
|
110
|
-
return () => cancelAnimationFrame(frame);
|
|
111
|
-
}, [showRightPanel, activeRightPanel, widthsRef]);
|
|
112
|
-
|
|
113
|
-
// Same story for the editor across a source↔diff tab switch: the panel
|
|
114
|
-
// stays mounted, so the width its tab kind remembers — or opens at, for a
|
|
115
|
-
// diff, which is wider than a source view — is re-applied here.
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
const frame = requestAnimationFrame(() => {
|
|
118
|
-
applyWidth(editorPanelRef, widthsRef.current?.[editorWidthMode] ?? DEFAULT_PANEL_WIDTHS[editorWidthMode]);
|
|
119
|
-
});
|
|
120
|
-
return () => cancelAnimationFrame(frame);
|
|
121
|
-
}, [showEditor, editorWidthMode, widthsRef]);
|
|
70
|
+
// Width restoration is declarative: every panel's `defaultSize` is derived
|
|
71
|
+
// from the remembered width for its current slot, and `Panel` adopts a
|
|
72
|
+
// changed `defaultSize` itself. That covers the two cases that used to need
|
|
73
|
+
// an imperative push — a right-panel view switch and an editor source↔diff
|
|
74
|
+
// switch, both of which keep the panel mounted while only its width slot
|
|
75
|
+
// changes — and it cannot go stale, because there is no cached layout to
|
|
76
|
+
// disagree with the props.
|
|
122
77
|
|
|
123
78
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
124
79
|
const [settingsCategory, setSettingsCategory] = useState<SettingsCategoryId>('appearance');
|
|
@@ -153,41 +108,30 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
153
108
|
const handleChangeRightPanel = (panel: RightPanelType) => {
|
|
154
109
|
let nextShow = showRightPanel;
|
|
155
110
|
let nextActive = activeRightPanel;
|
|
156
|
-
|
|
111
|
+
|
|
157
112
|
if (activeRightPanel === panel && showRightPanel) {
|
|
158
113
|
nextShow = false;
|
|
159
114
|
} else {
|
|
160
115
|
nextActive = panel;
|
|
161
116
|
nextShow = true;
|
|
162
117
|
}
|
|
163
|
-
|
|
118
|
+
|
|
164
119
|
setShowRightPanel(nextShow);
|
|
165
120
|
setActiveRightPanel(nextActive);
|
|
166
|
-
|
|
167
|
-
// The width is not set here: the restore effect re-applies whichever width
|
|
168
|
-
// this view remembers, and a view that opens for the first time derives it
|
|
169
|
-
// from its own defaultSize. Reopening the same view keeps its width.
|
|
170
|
-
writeSetting('showRightPanel', nextShow);
|
|
171
|
-
writeSetting('activeRightPanel', nextActive);
|
|
172
121
|
};
|
|
173
122
|
|
|
174
123
|
const handleToggleLeftPanel = (show: boolean) => {
|
|
175
124
|
setShowLeftPanel(show);
|
|
176
|
-
saveSetting('showLeftPanel', show);
|
|
177
125
|
};
|
|
178
126
|
|
|
179
127
|
const handleToggleRightPanel = () => {
|
|
180
|
-
|
|
181
|
-
setShowRightPanel(nextShow);
|
|
182
|
-
saveSetting('showRightPanel', nextShow);
|
|
128
|
+
setShowRightPanel(!showRightPanel);
|
|
183
129
|
};
|
|
184
130
|
|
|
185
131
|
const handleToggleEditor = () => {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// Reopening the editor re-applies the width its tab kind remembers (see the
|
|
190
|
-
// restore effect), so a dragged width survives the toggle.
|
|
132
|
+
setUserToggledEditor(!showEditor);
|
|
133
|
+
// The editor panel keeps its own remembered width per tab kind, so a
|
|
134
|
+
// dragged width survives the toggle without being re-applied here.
|
|
191
135
|
};
|
|
192
136
|
|
|
193
137
|
return (
|
|
@@ -195,18 +139,6 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
195
139
|
|
|
196
140
|
{/* Main Workspace: full-height left sidebar (resizable) + right stack */}
|
|
197
141
|
<div className="flex flex-1 overflow-hidden">
|
|
198
|
-
{!showLeftPanel && (
|
|
199
|
-
<div
|
|
200
|
-
className="w-12 h-full bg-paper border-r border-ink/10 flex flex-col items-center py-3 flex-shrink-0 cursor-pointer hover:bg-ink/5 transition-colors titlebar-no-drag"
|
|
201
|
-
style={{ paddingTop: 'max(0.75rem, env(titlebar-area-height, 0px))' }}
|
|
202
|
-
onClick={() => handleToggleLeftPanel(true)}
|
|
203
|
-
title="Expand Sidebar"
|
|
204
|
-
>
|
|
205
|
-
<PanelLeft size={16} className="text-ink/60" />
|
|
206
|
-
<div className="w-[1px] flex-1 bg-ink/10 my-4" />
|
|
207
|
-
</div>
|
|
208
|
-
)}
|
|
209
|
-
|
|
210
142
|
<Group
|
|
211
143
|
orientation="horizontal"
|
|
212
144
|
id="ompchamber-main"
|
|
@@ -215,14 +147,10 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
215
147
|
if (left != null && left > 0) commitWidths({ left: Math.round(left) });
|
|
216
148
|
}}
|
|
217
149
|
>
|
|
218
|
-
{
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
</Panel>
|
|
223
|
-
<ResizeHandle />
|
|
224
|
-
</>
|
|
225
|
-
)}
|
|
150
|
+
<Panel panelRef={leftPanelRef} id="left-panel" defaultSize={panelWidths.left ?? DEFAULT_LEFT_PANEL_WIDTH} minSize={MIN_LEFT_PANEL_WIDTH} maxSize={MAX_LEFT_PANEL_WIDTH} collapsed={!showLeftPanel}>
|
|
151
|
+
<SessionSidebar className="w-full h-full" onClose={() => handleToggleLeftPanel(false)} appSettings={appSettings} />
|
|
152
|
+
</Panel>
|
|
153
|
+
{showLeftPanel && <ResizeHandle />}
|
|
226
154
|
|
|
227
155
|
{/* Right stack: top navbar + resizable workspace (the group's filler) */}
|
|
228
156
|
<Panel id="main-right-stack" filler minSize={0}>
|
|
@@ -236,6 +164,7 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
236
164
|
onSwitchToMobile={onSwitchToMobile}
|
|
237
165
|
onToggleEditor={handleToggleEditor}
|
|
238
166
|
onToggleRightPanel={handleToggleRightPanel}
|
|
167
|
+
onToggleLeftPanel={() => handleToggleLeftPanel(!showLeftPanel)}
|
|
239
168
|
/>
|
|
240
169
|
|
|
241
170
|
<WorkspacePanels
|