@kabmo-labs/kabmo 0.8.0 → 0.9.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/README.md +7 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +314 -314
- package/dist/client/static/js/{index-qwpvd9fv.js → index-bpptfkz2.js} +922 -892
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +9 -1
- package/src/client/components/common/code-surface/documents.test.ts +120 -0
- package/src/client/components/common/code-surface/documents.ts +201 -0
- package/src/client/components/common/code-surface/extensions.test.ts +110 -0
- package/src/client/components/common/code-surface/extensions.ts +162 -0
- package/src/client/components/common/code-surface/handle.ts +102 -0
- package/src/client/components/common/code-surface/index.tsx +253 -314
- package/src/client/components/common/code-surface/keymap.test.ts +133 -0
- package/src/client/components/common/code-surface/keymap.ts +267 -0
- package/src/client/components/common/code-surface/marks.ts +74 -0
- package/src/client/components/common/code-surface/shiki.ts +234 -0
- package/src/client/components/common/code-surface/sync.test.ts +185 -0
- package/src/client/components/common/code-surface/sync.ts +58 -0
- package/src/client/components/common/code-surface/theme.test.ts +202 -0
- package/src/client/components/common/code-surface/theme.ts +111 -0
- package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
- package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
- package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
- package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
- package/src/client/components/common/shell-update-button/index.tsx +125 -0
- package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
- package/src/client/components/common/toolbar-row/index.tsx +189 -0
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +47 -67
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/mobile/LayoutWrapper.tsx +1 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
- package/src/client/components/settings/Modal.tsx +8 -0
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
- package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +8 -2
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +16 -8
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +8 -11
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
- package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
- package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
- package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
- package/src/client/components/workspace/diff-panel/index.tsx +76 -145
- package/src/client/components/workspace/diff-panel/merge.ts +315 -0
- package/src/client/components/workspace/editor/Body.tsx +4 -7
- package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
- package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
- package/src/client/components/workspace/editor/FilePane.tsx +4 -7
- package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
- package/src/client/components/workspace/editor/index.tsx +7 -9
- package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
- package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
- package/src/client/components/workspace/search-panel/Body.tsx +1 -1
- package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
- package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
- package/src/client/hooks/chat/omp/reconnect.ts +187 -0
- package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
- package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
- package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
- package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
- package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
- package/src/client/hooks/chat/omp/session-list.tsx +69 -9
- package/src/client/hooks/chat/timeline/generating.ts +19 -2
- package/src/client/hooks/chat/timeline/index.ts +19 -24
- package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
- package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
- package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
- package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
- package/src/client/hooks/chat/timeline/session-load.ts +69 -37
- package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
- package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
- package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
- package/src/client/hooks/code/deferred-highlight.ts +129 -0
- package/src/client/hooks/editor/find-state.ts +1 -1
- package/src/client/hooks/editor/theme.test.ts +206 -0
- package/src/client/hooks/editor/theme.ts +82 -0
- package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
- package/src/client/hooks/editor/use-editor-drop.ts +67 -14
- package/src/client/hooks/editor/use-editor-find.ts +7 -7
- package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
- package/src/client/hooks/editor/use-outline-view.ts +94 -7
- package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
- package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
- package/src/client/hooks/editor/use-search-reveal.ts +1 -1
- package/src/client/hooks/ui/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +2 -0
- package/src/client/hooks/ui/updates/shell-download.ts +55 -14
- package/src/client/hooks/ui/updates/shell-offer.test.ts +26 -0
- package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
- package/src/client/hooks/ui/updates/shell-update.ts +119 -0
- package/src/client/hooks/workspace/diff-file.ts +29 -10
- package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
- package/src/client/hooks/workspace/editor-view.ts +61 -0
- package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +20 -5
- package/src/client/lib/desktop/bridge.ts +45 -0
- package/src/client/lib/search-reveal.ts +1 -1
- package/src/client/main.tsx +5 -1
- package/src/client/tailwind.css +37 -8
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/fs/worktree/create.ts +2 -3
- package/src/server/lib/fs/worktree/list.ts +13 -0
- package/src/server/lib/fs/worktree/remove.ts +3 -3
- package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
- package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
- package/src/server/lib/lifecycle/port-guard.ts +39 -7
- package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
- package/src/server/lib/lifecycle/probe.test.ts +77 -164
- package/src/server/lib/lifecycle/probe.ts +121 -13
- package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
- package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
- package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
- package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
- package/src/server/lib/omp/session/telemetry.test.ts +33 -0
- package/src/server/lib/realtime/signals.server.ts +10 -0
- package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
- package/src/server/lib/realtime/topics.server.ts +16 -0
- package/src/server/lib/updates/shell-assets.ts +94 -3
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +33 -2
- package/src/server/lib/updates/shell-pages.ts +17 -5
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +10 -27
- package/src/server/lib/updates/shell-release.ts +107 -111
- package/src/server/routes/fs/git-merge.test.ts +11 -1
- package/src/server/routes/fs/read.test.ts +24 -7
- package/src/server/routes/fs/scope.test.ts +15 -10
- package/src/server/routes/fs/scope.ts +3 -14
- package/src/server/routes/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +29 -7
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/list.ts +11 -1
- package/src/server/routes/worktrees/routes.test.ts +17 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
- package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
- package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
- package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
- package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
- package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
- package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
- package/src/shared/lib/code/editor/find-scan.ts +177 -0
- package/src/shared/lib/code/editor/keymap.ts +3 -3
- package/src/shared/lib/code/editor/lines.test.ts +7 -55
- package/src/shared/lib/code/editor/lines.ts +8 -48
- package/src/shared/lib/code/editor/marks.test.ts +4 -5
- package/src/shared/lib/code/editor/marks.ts +13 -25
- package/src/shared/lib/code/editor/theme-loader.ts +97 -0
- package/src/shared/lib/code/editor/themes.generated.ts +156 -0
- package/src/shared/lib/code/editor/themes.test.ts +165 -0
- package/src/shared/lib/code/editor/themes.ts +165 -0
- package/src/shared/lib/code/highlight-memo.test.ts +103 -0
- package/src/shared/lib/code/highlight-memo.ts +149 -0
- package/src/shared/lib/code/highlighter-warmup.ts +112 -0
- package/src/shared/lib/code/highlighter.ts +3 -1
- package/src/shared/lib/code/idle.ts +37 -0
- package/src/shared/lib/code/outline/rows.test.ts +96 -0
- package/src/shared/lib/code/outline/rows.ts +94 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
- package/src/shared/lib/code/syntax-highlight.ts +2 -3
- package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
- package/src/shared/lib/fs/file-kind.ts +47 -13
- package/src/shared/lib/fs/file-url.test.ts +52 -0
- package/src/shared/lib/fs/file-url.ts +37 -0
- package/src/shared/lib/fs/patch-sides.test.ts +114 -0
- package/src/shared/lib/fs/patch-sides.ts +139 -0
- package/src/shared/lib/realtime/protocol.ts +16 -0
- package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
- package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
- package/src/shared/lib/updates/shell-arch.ts +54 -0
- package/src/shared/lib/updates/shell-ua.ts +4 -4
- package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
- package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
- package/src/shared/lib/workspace/panel-widths.ts +25 -5
- package/src/shared/lib/workspace/session-state/cache.ts +159 -0
- package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
- package/src/shared/lib/workspace/session-state/merge.ts +100 -0
- package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
- package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
- package/src/shared/lib/workspace/session-state/store.ts +92 -130
- package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-gtbpewr7.css +0 -1
- package/src/client/components/common/code-editor/handler.test.ts +0 -274
- package/src/client/components/common/code-editor/handler.ts +0 -175
- package/src/client/components/common/code-editor/index.tsx +0 -314
- package/src/client/components/common/code-surface/measure.ts +0 -148
- package/src/client/components/workspace/chat-timeline/target-picker/index.test.tsx +0 -227
- package/src/client/components/workspace/chat-timeline/target-picker/index.tsx +0 -260
- package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
- package/src/client/hooks/editor/scroll-container.test.ts +0 -102
- package/src/client/hooks/editor/scroll-container.ts +0 -26
- package/src/client/hooks/editor/use-code-window.ts +0 -100
- package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
- package/src/client/hooks/editor/use-editor-history.ts +0 -134
- package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
- package/src/client/hooks/editor/use-occurrences.ts +0 -98
- package/src/shared/lib/code/editor/commands.test.ts +0 -155
- package/src/shared/lib/code/editor/commands.ts +0 -260
- package/src/shared/lib/code/editor/edits.test.ts +0 -100
- package/src/shared/lib/code/editor/edits.ts +0 -122
- package/src/shared/lib/code/editor/history.test.ts +0 -118
- package/src/shared/lib/code/editor/history.ts +0 -101
- package/src/shared/lib/code/editor/layers.test.ts +0 -77
- package/src/shared/lib/code/editor/layers.ts +0 -67
- package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
- package/src/shared/lib/code/editor/line-edits.ts +0 -159
- package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
- package/src/shared/lib/code/editor/occurrences.ts +0 -111
- package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
- package/src/shared/lib/code/editor/reset-css.ts +0 -32
- package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
- package/src/shared/lib/code/editor/text-commands.ts +0 -154
- package/src/shared/lib/code/lazy-window.test.ts +0 -161
- package/src/shared/lib/code/lazy-window.ts +0 -156
- package/src/shared/lib/code/windowed-highlight.ts +0 -114
- package/src/shared/lib/fs/diff-parser.test.ts +0 -116
- package/src/shared/lib/fs/diff-parser.ts +0 -213
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One toolbar row that never becomes two.
|
|
8
|
+
*
|
|
9
|
+
* The editor and diff toolbars carry ten-plus controls and the editor panel is
|
|
10
|
+
* 320px wide on a 1440px screen. WRAPPING was the previous answer and it is the
|
|
11
|
+
* wrong one: a second row moves the document down by the toolbar's own height
|
|
12
|
+
* and reads as a second toolbar. A horizontal scroll is worse — a clipped
|
|
13
|
+
* control is invisible. So the clusters that do not fit move into a `⋯` menu and
|
|
14
|
+
* every control stays one click away.
|
|
15
|
+
*
|
|
16
|
+
* Widths are MEASURED, never tabulated: the row lays every cluster out once and
|
|
17
|
+
* reads each one's rendered width, then hides what does not fit. A table of
|
|
18
|
+
* guessed pixel widths drifts the moment a label, an icon or a font changes, and
|
|
19
|
+
* a wrong guess here is a control that is either clipped or needlessly buried.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
|
23
|
+
import type { ComponentChildren } from 'preact';
|
|
24
|
+
import { Ellipsis, type LucideIcon } from 'lucide-preact';
|
|
25
|
+
import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
26
|
+
import { toolbarHasOverflow, toolbarVisibleClusters } from '@/shared/lib/ui/toolbar-overflow';
|
|
27
|
+
|
|
28
|
+
/** One row the `⋯` menu offers when its cluster is not on the row. */
|
|
29
|
+
export interface ToolbarOverflowAction {
|
|
30
|
+
label: string;
|
|
31
|
+
icon: LucideIcon;
|
|
32
|
+
onSelect: () => void;
|
|
33
|
+
/** Rendered as a pressed row (word wrap on, a mode already active). */
|
|
34
|
+
active?: boolean;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A logical group of controls that hides as a unit.
|
|
40
|
+
*
|
|
41
|
+
* A group rather than a single button because the clusters are already grouped
|
|
42
|
+
* in the toolbars (zoom is two buttons, copy/download is a pair, the width modes
|
|
43
|
+
* are a pair) and splitting them would let half of a pair stay on the row.
|
|
44
|
+
*/
|
|
45
|
+
export interface ToolbarCluster {
|
|
46
|
+
id: string;
|
|
47
|
+
/**
|
|
48
|
+
* Never leave the row, whatever its width.
|
|
49
|
+
*
|
|
50
|
+
* The visual order is not the priority order: Save is drawn first because that
|
|
51
|
+
* is where a reader looks for it, while the width modes sit last and are the
|
|
52
|
+
* reason the panel exists. Pinning says which ones a menu row must not be the
|
|
53
|
+
* only way to reach.
|
|
54
|
+
*/
|
|
55
|
+
pinned?: boolean;
|
|
56
|
+
/** What the `⋯` menu offers when this cluster is hidden. */
|
|
57
|
+
actions: ToolbarOverflowAction[];
|
|
58
|
+
children: ComponentChildren;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ToolbarRowProps {
|
|
62
|
+
clusters: ToolbarCluster[];
|
|
63
|
+
className?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function ToolbarRow({ clusters, className = '' }: ToolbarRowProps) {
|
|
67
|
+
const rowRef = useRef<HTMLDivElement>(null);
|
|
68
|
+
/** Measured widths, tagged with the cluster set they describe. */
|
|
69
|
+
const [metrics, setMetrics] = useState<{ signature: string; widths: number[] } | null>(null);
|
|
70
|
+
const [available, setAvailable] = useState(0);
|
|
71
|
+
|
|
72
|
+
// The set changes when a conditional cluster appears or disappears (a
|
|
73
|
+
// previewable file opened, a byte viewer took over), and stale widths would
|
|
74
|
+
// then hide the wrong cluster. Rendering every cluster until they are measured
|
|
75
|
+
// is what gives the ruler something to measure — and the layout effect runs
|
|
76
|
+
// BEFORE paint, so the unresolved frame is never seen.
|
|
77
|
+
const signature = clusters.map((cluster) => `${cluster.id}:${cluster.pinned ? 1 : 0}`).join('|');
|
|
78
|
+
const ready = metrics?.signature === signature;
|
|
79
|
+
const widths = ready ? metrics.widths : [];
|
|
80
|
+
const pinned = clusters.map((cluster) => cluster.pinned === true);
|
|
81
|
+
const visible = widths.length > 0
|
|
82
|
+
? toolbarVisibleClusters({ widths, pinned, available })
|
|
83
|
+
: clusters.map(() => true);
|
|
84
|
+
|
|
85
|
+
useLayoutEffect(() => {
|
|
86
|
+
const row = rowRef.current;
|
|
87
|
+
if (!row) return;
|
|
88
|
+
if (!ready) {
|
|
89
|
+
const children = [...row.querySelectorAll<HTMLElement>('[data-toolbar-cluster]')];
|
|
90
|
+
if (children.length === clusters.length) {
|
|
91
|
+
setMetrics({ signature, widths: children.map((child) => child.getBoundingClientRect().width) });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
setAvailable(row.clientWidth);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// The row's width changes on a window resize, a panel drag and a layout mode
|
|
98
|
+
// switch — none of which are `window` events.
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const row = rowRef.current;
|
|
101
|
+
if (!row || typeof ResizeObserver === 'undefined') return;
|
|
102
|
+
const observer = new ResizeObserver(() => setAvailable(row.clientWidth));
|
|
103
|
+
observer.observe(row);
|
|
104
|
+
return () => observer.disconnect();
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
const hiddenActions = clusters.filter((_, index) => !visible[index]).flatMap((cluster) => cluster.actions);
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div ref={rowRef} className={`flex flex-nowrap items-center justify-end gap-x-1 overflow-hidden ${className}`}>
|
|
111
|
+
{clusters.map((cluster, index) =>
|
|
112
|
+
visible[index] ? (
|
|
113
|
+
<div key={cluster.id} data-toolbar-cluster="" className="flex items-center flex-shrink-0">
|
|
114
|
+
{cluster.children}
|
|
115
|
+
</div>
|
|
116
|
+
) : null,
|
|
117
|
+
)}
|
|
118
|
+
{toolbarHasOverflow(visible) && <OverflowMenu actions={hiddenActions} />}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The `⋯` trigger and its menu.
|
|
125
|
+
*
|
|
126
|
+
* A menu rather than a horizontal scroll: a clipped action is invisible, and the
|
|
127
|
+
* diff toolbar already learned that a scroll strip with no visible scrollbar
|
|
128
|
+
* reads as "there is nothing more here" (measured: 361px of content in a 296px
|
|
129
|
+
* box, with `Stage` entirely outside it).
|
|
130
|
+
*/
|
|
131
|
+
function OverflowMenu({ actions }: { actions: ToolbarOverflowAction[] }) {
|
|
132
|
+
const [open, setOpen] = useState(false);
|
|
133
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
134
|
+
useOnClickOutside(rootRef, () => setOpen(false));
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!open) return;
|
|
138
|
+
const onKeyDown = (event: globalThis.KeyboardEvent) => {
|
|
139
|
+
if (event.key === 'Escape') setOpen(false);
|
|
140
|
+
};
|
|
141
|
+
document.addEventListener('keydown', onKeyDown);
|
|
142
|
+
return () => document.removeEventListener('keydown', onKeyDown);
|
|
143
|
+
}, [open]);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="relative flex-shrink-0" ref={rootRef}>
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
title="More actions"
|
|
150
|
+
aria-label="More actions"
|
|
151
|
+
aria-haspopup="menu"
|
|
152
|
+
aria-expanded={open}
|
|
153
|
+
onClick={() => setOpen(!open)}
|
|
154
|
+
className={`flex items-center justify-center p-1.5 rounded transition-colors cursor-pointer ${
|
|
155
|
+
open ? 'bg-ink/10 text-ink' : 'text-ink/50 hover:text-ink hover:bg-ink/5'
|
|
156
|
+
}`}
|
|
157
|
+
>
|
|
158
|
+
<Ellipsis size={14} />
|
|
159
|
+
</button>
|
|
160
|
+
|
|
161
|
+
{open && (
|
|
162
|
+
<div
|
|
163
|
+
role="menu"
|
|
164
|
+
className="absolute right-0 top-full mt-1 w-52 bg-paper border border-ink/15 rounded-lg shadow-lg z-50 py-1 text-xs"
|
|
165
|
+
>
|
|
166
|
+
{actions.map((action) => (
|
|
167
|
+
<button
|
|
168
|
+
key={action.label}
|
|
169
|
+
type="button"
|
|
170
|
+
role="menuitem"
|
|
171
|
+
disabled={action.disabled}
|
|
172
|
+
aria-pressed={action.active}
|
|
173
|
+
onClick={() => {
|
|
174
|
+
setOpen(false);
|
|
175
|
+
action.onSelect();
|
|
176
|
+
}}
|
|
177
|
+
className={`w-full text-left flex items-center gap-2 px-3 py-1.5 transition-colors disabled:cursor-default disabled:opacity-40 ${
|
|
178
|
+
action.active ? 'bg-ink/5 text-ink font-medium' : 'text-ink/75 hover:bg-ink/5 hover:text-ink'
|
|
179
|
+
}`}
|
|
180
|
+
>
|
|
181
|
+
<action.icon size={13} className="flex-shrink-0" />
|
|
182
|
+
<span className="truncate">{action.label}</span>
|
|
183
|
+
</button>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -13,18 +13,13 @@ import { ResizeHandle } from '@/client/components/layout/desktop-layout/ResizeHa
|
|
|
13
13
|
import { useAvailableWidth } from '@/client/hooks/workspace/available-width';
|
|
14
14
|
import { useSessionStateContext } from '@/client/hooks/workspace/session-state/context';
|
|
15
15
|
import {
|
|
16
|
-
DEFAULT_EDITOR_FRACTIONS,
|
|
17
|
-
DEFAULT_EDITOR_WIDTHS,
|
|
18
16
|
HANDLE_WIDTH,
|
|
19
17
|
MIN_EDITOR_PANEL_WIDTH,
|
|
20
18
|
fillerMinWidth,
|
|
21
|
-
pairMaxSize,
|
|
22
|
-
panelFraction,
|
|
23
|
-
resolvePanelWidth,
|
|
24
19
|
type EditorWidthMode,
|
|
25
20
|
type PanelWidths,
|
|
26
21
|
} from '@/shared/lib/workspace/panel-widths';
|
|
27
|
-
import {
|
|
22
|
+
import { resolveWorkspacePanelWidths } from '@/shared/lib/workspace/workspace-panel-widths';
|
|
28
23
|
|
|
29
24
|
const Editor = lazy(() => import('@/client/components/workspace/editor/index').then((m) => ({ default: m.Editor })));
|
|
30
25
|
|
|
@@ -43,6 +38,13 @@ function PanelSuspense({ children }: { children: ReactNode }) {
|
|
|
43
38
|
interface WorkspacePanelsProps {
|
|
44
39
|
appSettings?: Record<string, any>;
|
|
45
40
|
showEditor: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The chat column is collapsed: the editor's compact/full view took its
|
|
43
|
+
* width. The chat STAYS MOUNTED (it owns the live stream and the composer)
|
|
44
|
+
* but is sized to nothing and marked inert, so a run in progress is not
|
|
45
|
+
* thrown away by widening a document.
|
|
46
|
+
*/
|
|
47
|
+
chatHidden?: boolean;
|
|
46
48
|
editorPanelRef: RefObject<PanelImperativeHandle | null>;
|
|
47
49
|
showRightPanel: boolean;
|
|
48
50
|
/** A built-in view id or a plugin panel key (`plugin:<id>/<panel>`). */
|
|
@@ -78,6 +80,7 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
78
80
|
const {
|
|
79
81
|
appSettings,
|
|
80
82
|
showEditor,
|
|
83
|
+
chatHidden = false,
|
|
81
84
|
editorPanelRef,
|
|
82
85
|
showRightPanel,
|
|
83
86
|
activeRightPanel,
|
|
@@ -138,75 +141,27 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
138
141
|
// one, which is why a Kabmo without one looks exactly as it did.
|
|
139
142
|
const editorPlugins = usePanelSlotPlugins();
|
|
140
143
|
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// The editor and the right panel are a COUPLED pair (see `panel-widths.ts`):
|
|
152
|
-
// they share `PAIR_MAX_TOTAL_FRACTION` and each is capped by `PAIR_MAX_FRACTION`.
|
|
153
|
-
// The right panel is resolved first, so it reserves the editor's WANTED share
|
|
154
|
-
// (the editor has no result yet); the editor then reserves the right panel's
|
|
155
|
-
// ACTUAL width. That asymmetry is deliberate: an editor asking for more than
|
|
156
|
-
// its default makes the right panel give way first, and the pair still sums
|
|
157
|
-
// to the total.
|
|
158
|
-
const editorFraction = panelFraction(panelWidths[editorWidthMode], DEFAULT_EDITOR_FRACTIONS[editorWidthMode], available);
|
|
159
|
-
const editorReservePx = available != null && available > 0 ? Math.round(editorFraction * available) : 0;
|
|
160
|
-
|
|
161
|
-
// Resolve the RIGHT panel first, then the editor against the width it
|
|
162
|
-
// actually got. The px order is the other half of the contract: each panel's
|
|
163
|
-
// group budget is `available − chat floor − the sibling's reserved width −
|
|
164
|
-
// handles`, so reserving only a sibling's FLOOR (the previous shape) let the
|
|
165
|
-
// two declared widths sum past the group, and flexbox then shrank BOTH —
|
|
166
|
-
// measured at 1440 with both open: the editor asked for 507 and rendered 477,
|
|
167
|
-
// the right panel asked for 245 and rendered 230. The right panel is resolved
|
|
168
|
-
// first because it is the smaller, view-specific slot: it keeps its own width
|
|
169
|
-
// and the editor absorbs the remainder.
|
|
170
|
-
const rightWidth = resolvePanelWidth({
|
|
171
|
-
stored: panelWidths.right?.[activeRightPanel],
|
|
172
|
-
defaultFraction: activePanel?.defaultFraction ?? DEFAULT_RIGHT_PANEL_FRACTION,
|
|
173
|
-
defaultPx: activePanel?.defaultWidth ?? MIN_EDITOR_PANEL_WIDTH,
|
|
174
|
-
available,
|
|
175
|
-
min: rightPanelMin,
|
|
176
|
-
// The pair cap: the right panel may not exceed `total − the editor's wanted share`.
|
|
177
|
-
pairReservePx: showEditor ? editorReservePx : 0,
|
|
178
|
-
// The editor renders before the right panel and has not been resolved yet,
|
|
179
|
-
// so only its floor can be reserved here.
|
|
180
|
-
siblingWidth: showEditor ? MIN_EDITOR_PANEL_WIDTH : 0,
|
|
181
|
-
handleCount,
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const editorWidth = resolvePanelWidth({
|
|
185
|
-
stored: panelWidths[editorWidthMode],
|
|
186
|
-
defaultFraction: DEFAULT_EDITOR_FRACTIONS[editorWidthMode],
|
|
187
|
-
defaultPx: DEFAULT_EDITOR_WIDTHS[editorWidthMode],
|
|
188
|
-
available,
|
|
189
|
-
min: MIN_EDITOR_PANEL_WIDTH,
|
|
190
|
-
// The pair cap: the editor may not exceed `total − the right panel's width`.
|
|
191
|
-
pairReservePx: showRightPanel ? rightWidth : 0,
|
|
192
|
-
// The right panel is resolved above; reserve the width it actually took so
|
|
193
|
-
// the editor's group budget cannot push the pair past the group.
|
|
194
|
-
siblingWidth: showRightPanel ? rightWidth : 0,
|
|
144
|
+
// The active view's own floor and defaults are read here and handed to the
|
|
145
|
+
// resolver: a view that is switched off (or a plugin that has not loaded) has
|
|
146
|
+
// no entry, and the right panel falls back to the editor's own floor — it
|
|
147
|
+
// renders the notice, not a view.
|
|
148
|
+
const {
|
|
149
|
+
editor: editorWidth,
|
|
150
|
+
right: rightWidth,
|
|
151
|
+
editorMax: editorMaxSize,
|
|
152
|
+
rightMax: rightMaxSize,
|
|
153
|
+
rightMin: rightPanelMin,
|
|
195
154
|
handleCount,
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
* `undefined` before the group is measured (no cap yet).
|
|
207
|
-
*/
|
|
208
|
-
const editorMaxSize = pairMaxSize({ available, siblingMin: rightPanelMin, siblingOpen: showRightPanel });
|
|
209
|
-
const rightMaxSize = pairMaxSize({ available, siblingMin: MIN_EDITOR_PANEL_WIDTH, siblingOpen: showEditor });
|
|
155
|
+
} = resolveWorkspacePanelWidths({
|
|
156
|
+
available,
|
|
157
|
+
panelWidths,
|
|
158
|
+
editorWidthMode,
|
|
159
|
+
activeRightPanel,
|
|
160
|
+
activePanel,
|
|
161
|
+
showEditor,
|
|
162
|
+
showRightPanel,
|
|
163
|
+
chatHidden,
|
|
164
|
+
});
|
|
210
165
|
|
|
211
166
|
/**
|
|
212
167
|
* Widths of the fixed panels as they are right now, keyed by slot: the
|
|
@@ -222,7 +177,11 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
222
177
|
const patch: PanelWidths = {};
|
|
223
178
|
const editor = editorPanelRef.current?.getSize()?.inPixels;
|
|
224
179
|
const right = rightPanelRef.current?.getSize()?.inPixels;
|
|
225
|
-
|
|
180
|
+
// With the chat hidden the editor's width is whatever is left over, not a
|
|
181
|
+
// choice — persisting it would overwrite the width the user set for the
|
|
182
|
+
// ordinary layout with the one this mode produced. A drag made here still
|
|
183
|
+
// moves the RIGHT panel, which is the width that was actually chosen.
|
|
184
|
+
if (!chatHidden && editor != null && editor > 0) {
|
|
226
185
|
patch[editorWidthMode] = { px: Math.round(editor), ...(available ? { fraction: editor / available } : {}) };
|
|
227
186
|
}
|
|
228
187
|
if (right != null && right > 0) {
|
|
@@ -250,6 +209,10 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
250
209
|
<Panel
|
|
251
210
|
id="center-panel"
|
|
252
211
|
filler
|
|
212
|
+
// Collapsed — not unmounted — in compact/full view: the chat holds the
|
|
213
|
+
// live run, the composer draft and the scroll position, and widening a
|
|
214
|
+
// document must not discard any of them.
|
|
215
|
+
collapsed={chatHidden}
|
|
253
216
|
minSize={fillerMinWidth({
|
|
254
217
|
available,
|
|
255
218
|
reservedPx:
|
|
@@ -258,15 +221,15 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
258
221
|
handleCount * HANDLE_WIDTH,
|
|
259
222
|
})}
|
|
260
223
|
>
|
|
261
|
-
<div className="relative w-full h-full" {...drop.dropProps}>
|
|
224
|
+
<div className="relative w-full h-full" {...drop.dropProps} {...drop.captureDropProps} inert={chatHidden}>
|
|
262
225
|
<EditorDropOverlay visible={drop.isDragging} />
|
|
263
226
|
<ChatTimeline className="w-full h-full" appSettings={appSettings} onSessionTitle={onSessionTitle} />
|
|
264
227
|
</div>
|
|
265
228
|
</Panel>
|
|
266
229
|
|
|
267
|
-
{showEditor && <ResizeHandle />}
|
|
230
|
+
{showEditor && !chatHidden && <ResizeHandle />}
|
|
268
231
|
<Panel panelRef={editorPanelRef} id="editor-panel" defaultSize={editorWidth} minSize={MIN_EDITOR_PANEL_WIDTH} maxSize={editorMaxSize} collapsed={!showEditor}>
|
|
269
|
-
<div className="flex w-full h-full" {...drop.dropProps}>
|
|
232
|
+
<div className="flex w-full h-full" {...drop.dropProps} {...drop.captureDropProps}>
|
|
270
233
|
<EditorDropOverlay visible={drop.isDragging} />
|
|
271
234
|
<EditorPluginRail
|
|
272
235
|
activeKey={activeEditorPanelKey}
|
|
@@ -3,7 +3,6 @@ import { useSearchParams } from '@/client/lib/router/search-params';
|
|
|
3
3
|
import { Group, Panel, type PanelImperativeHandle } from '@/client/components/layout/desktop-layout/resizer';
|
|
4
4
|
import { LeftColumn } from '@/client/components/layout/desktop-layout/LeftColumn';
|
|
5
5
|
import { SettingsModal } from '@/client/components/settings/LazyModal';
|
|
6
|
-
import type { SettingsCategoryId } from '@/shared/types';
|
|
7
6
|
import { activeProjectForSession, effectiveRootFor } from '@/shared/lib/workspace/active-project';
|
|
8
7
|
import { useFileTabs } from '@/client/hooks/workspace/file-tabs';
|
|
9
8
|
import { useSessionState } from '@/client/hooks/workspace/session-state';
|
|
@@ -18,17 +17,17 @@ import { MIN_LEFT_DOCK_PLUGIN_WIDTH } from '@/shared/lib/workspace/left-dock-wid
|
|
|
18
17
|
import { SESSIONS_VIEW_ID } from '@/shared/lib/workspace/left-dock';
|
|
19
18
|
import { useOpenPanelRequest } from '@/client/hooks/workspace/open-panel';
|
|
20
19
|
import { useLeftDock, useLeftDockWidth } from '@/client/hooks/workspace/left-dock';
|
|
20
|
+
import { useEditorViewLayout } from '@/client/hooks/workspace/editor-view';
|
|
21
21
|
import { useLeftDockView } from '@/client/components/workspace/plugin-panel/catalog';
|
|
22
22
|
import type { EditorWidthMode } from '@/shared/lib/workspace/panel-widths';
|
|
23
|
-
import {
|
|
23
|
+
import { useSettingsModal } from '@/client/hooks/ui/settings-modal';
|
|
24
24
|
import { useWorkspaceKeybindings } from '@/client/hooks/workspace/keybindings';
|
|
25
25
|
import { PluginBootstrap } from '@/client/lib/plugins/bootstrap';
|
|
26
26
|
import { useToasts } from '@/client/hooks/ui/toasts';
|
|
27
27
|
import type { EditorDropResult } from '@/client/hooks/editor/use-editor-drop';
|
|
28
28
|
import { ToastStack } from '@/client/components/common/ToastStack';
|
|
29
29
|
import { SessionDeleteModal } from '@/client/components/common/session-delete-modal';
|
|
30
|
-
import {
|
|
31
|
-
import { useSessionDelete } from '@/client/hooks/workspace/session-delete';
|
|
30
|
+
import { useNavbarSessionActions } from '@/client/hooks/workspace/navbar-session-actions';
|
|
32
31
|
|
|
33
32
|
interface DesktopLayoutProps {
|
|
34
33
|
sessionId: string | null;
|
|
@@ -112,32 +111,30 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
112
111
|
[openedFiles, activeFileId],
|
|
113
112
|
);
|
|
114
113
|
|
|
114
|
+
/**
|
|
115
|
+
* The editor's WIDTH mode (`compact` / `full`), entered from either toolbar.
|
|
116
|
+
*
|
|
117
|
+
* It collapses columns around the editor and is resolved here, not in the
|
|
118
|
+
* panels, because the sidebar is a sibling of the whole workspace group: a
|
|
119
|
+
* full view that left it standing would not be the width it promises.
|
|
120
|
+
*
|
|
121
|
+
* The mode is derived from `showEditor`, so closing the column returns the
|
|
122
|
+
* chat and the sidebar without writing anything — reopening it comes back to
|
|
123
|
+
* the width the user chose.
|
|
124
|
+
*/
|
|
125
|
+
const editorView = useEditorViewLayout(showEditor);
|
|
126
|
+
const sidebarOpen = showLeftPanel && !editorView.sidebarHidden;
|
|
127
|
+
|
|
115
128
|
// Width restoration is declarative: each panel's `defaultSize` comes from the
|
|
116
129
|
// remembered width for its current slot and `Panel` adopts a change, so a
|
|
117
130
|
// view switch needs no imperative push and no cached layout can go stale.
|
|
118
131
|
|
|
119
|
-
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
120
|
-
const [settingsCategory, setSettingsCategory] = useState<SettingsCategoryId>('appearance');
|
|
121
|
-
const [autoOpenAddProvider, setAutoOpenAddProvider] = useState(false);
|
|
122
132
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
123
133
|
const handleRefreshWorkspace = () => setRefreshKey(k => k + 1);
|
|
124
134
|
const [sessionTitle, setSessionTitle] = useState<string | null>(null);
|
|
125
135
|
const streamStatus = useAgentStreamStatus();
|
|
136
|
+
const settings = useSettingsModal();
|
|
126
137
|
|
|
127
|
-
// The navbar's title menu acts on the session on screen. Its row may live in
|
|
128
|
-
// a folder that is not expanded in the sidebar, so the session is found by id
|
|
129
|
-
// across every folder rather than by walking the visible list. No row means
|
|
130
|
-
// nothing to act on — a session the sidebar does not know (a chat running
|
|
131
|
-
// outside every registered workspace) gets no menu rather than dead items.
|
|
132
|
-
const activeSession = useMemo(() => {
|
|
133
|
-
if (!sessionId) return null;
|
|
134
|
-
for (const folder of folders) {
|
|
135
|
-
const found = (folder.sessions || []).find((s) => String(s.id) === String(sessionId));
|
|
136
|
-
if (found) return found;
|
|
137
|
-
}
|
|
138
|
-
return null;
|
|
139
|
-
}, [folders, sessionId]);
|
|
140
|
-
const sidebar = useSidebarData();
|
|
141
138
|
const { toasts, pushToast, dismissToast } = useToasts();
|
|
142
139
|
|
|
143
140
|
/**
|
|
@@ -149,20 +146,19 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
149
146
|
(result: EditorDropResult) => pushToast(result.message, result.ok ? 'success' : 'error'),
|
|
150
147
|
[pushToast],
|
|
151
148
|
);
|
|
152
|
-
const {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
: undefined;
|
|
149
|
+
const { menu: sessionActions, sessionDelete } = useNavbarSessionActions(folders, sessionId, pushToast);
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Ask for the sidebar (or put it away).
|
|
153
|
+
*
|
|
154
|
+
* Full view hides the sidebar, so asking for it must leave that mode —
|
|
155
|
+
* otherwise the toggle flips a stored flag the layout is ignoring and the
|
|
156
|
+
* click does nothing. `compact` is the mode that keeps the wider editor.
|
|
157
|
+
*/
|
|
158
|
+
const handleToggleLeftPanel = (show: boolean) => {
|
|
159
|
+
if (show) editorView.revealSidebar();
|
|
160
|
+
setShowLeftPanel(show);
|
|
161
|
+
};
|
|
166
162
|
|
|
167
163
|
// The workspace shortcuts, from the shared keymap so the toggles' tooltips
|
|
168
164
|
// name the bound chord. `⌘S` is absent: the editor owns it, and binding it
|
|
@@ -170,26 +166,13 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
170
166
|
useWorkspaceKeybindings({
|
|
171
167
|
showEditor,
|
|
172
168
|
editorHasContent,
|
|
173
|
-
|
|
169
|
+
// Through the handler, not the raw flag: in full view the sidebar is hidden
|
|
170
|
+
// by the editor's WIDTH mode, so flipping `showLeftPanel` alone would change
|
|
171
|
+
// a value the layout is ignoring and the chord would do nothing.
|
|
172
|
+
toggleSidebar: () => handleToggleLeftPanel(!sidebarOpen),
|
|
174
173
|
toggleEditor: () => setUserToggledEditor((previous) => !(previous ?? showEditor)),
|
|
175
174
|
toggleRightPanel: () => setShowRightPanel((previous) => !previous),
|
|
176
|
-
openSettings: () =>
|
|
177
|
-
setSettingsCategory('appearance');
|
|
178
|
-
setAutoOpenAddProvider(false);
|
|
179
|
-
setSettingsOpen((previous) => !previous);
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// Global event listener for settings triggers
|
|
184
|
-
useKabmoEvent('omp:open-settings', (e) => {
|
|
185
|
-
const customEvent = e as CustomEvent<{ category?: SettingsCategoryId; autoOpenAdd?: boolean }>;
|
|
186
|
-
if (customEvent.detail?.category) {
|
|
187
|
-
setSettingsCategory(customEvent.detail.category);
|
|
188
|
-
} else {
|
|
189
|
-
setSettingsCategory('appearance');
|
|
190
|
-
}
|
|
191
|
-
setAutoOpenAddProvider(!!customEvent.detail?.autoOpenAdd);
|
|
192
|
-
setSettingsOpen(true);
|
|
175
|
+
openSettings: () => (settings.isOpen ? settings.close() : settings.open()),
|
|
193
176
|
});
|
|
194
177
|
|
|
195
178
|
// Clicking the ACTIVE view on an open panel collapses that panel; any other
|
|
@@ -200,7 +183,6 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
200
183
|
if (!collapsing) setActiveRightPanel(panel);
|
|
201
184
|
};
|
|
202
185
|
|
|
203
|
-
const handleToggleLeftPanel = (show: boolean) => setShowLeftPanel(show);
|
|
204
186
|
const handleToggleRightPanel = () => setShowRightPanel(!showRightPanel);
|
|
205
187
|
const handleToggleEditor = () => setUserToggledEditor(!showEditor);
|
|
206
188
|
|
|
@@ -243,7 +225,7 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
243
225
|
>
|
|
244
226
|
<LeftColumn
|
|
245
227
|
panelRef={leftPanelRef}
|
|
246
|
-
open={
|
|
228
|
+
open={sidebarOpen}
|
|
247
229
|
railEnabled={dock.enabled}
|
|
248
230
|
view={leftView}
|
|
249
231
|
onSelectView={(panel) => {
|
|
@@ -251,7 +233,7 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
251
233
|
// toggle would hide the rail the user is aiming at, and the dock
|
|
252
234
|
// has its own collapse control in the header.
|
|
253
235
|
setActiveLeftView(panel);
|
|
254
|
-
if (!
|
|
236
|
+
if (!sidebarOpen) handleToggleLeftPanel(true);
|
|
255
237
|
}}
|
|
256
238
|
onCollapse={() => handleToggleLeftPanel(false)}
|
|
257
239
|
width={sidebarWidth}
|
|
@@ -272,20 +254,21 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
272
254
|
<TopNavbar
|
|
273
255
|
sessionTitle={sessionTitle}
|
|
274
256
|
sessionId={sessionId}
|
|
275
|
-
showLeftPanel={
|
|
257
|
+
showLeftPanel={sidebarOpen}
|
|
276
258
|
showEditor={showEditor}
|
|
277
259
|
showRightPanel={showRightPanel}
|
|
278
260
|
streamStatus={streamStatus}
|
|
279
261
|
onSwitchToMobile={onSwitchToMobile}
|
|
280
262
|
onToggleEditor={handleToggleEditor}
|
|
281
263
|
onToggleRightPanel={handleToggleRightPanel}
|
|
282
|
-
onToggleLeftPanel={() => handleToggleLeftPanel(!
|
|
264
|
+
onToggleLeftPanel={() => handleToggleLeftPanel(!sidebarOpen)}
|
|
283
265
|
sessionActions={sessionActions}
|
|
284
266
|
/>
|
|
285
267
|
|
|
286
268
|
<WorkspacePanels
|
|
287
269
|
appSettings={appSettings}
|
|
288
270
|
showEditor={showEditor}
|
|
271
|
+
chatHidden={editorView.chatHidden}
|
|
289
272
|
editorPanelRef={editorPanelRef}
|
|
290
273
|
showRightPanel={showRightPanel}
|
|
291
274
|
activeRightPanel={activeRightPanel}
|
|
@@ -321,15 +304,12 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
321
304
|
</div>
|
|
322
305
|
|
|
323
306
|
{/* Global Desktop Settings Modal */}
|
|
324
|
-
<SettingsModal
|
|
325
|
-
isOpen={
|
|
326
|
-
onClose={
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
initialCategory={settingsCategory}
|
|
331
|
-
autoOpenAddProvider={autoOpenAddProvider}
|
|
332
|
-
appSettings={appSettings}
|
|
307
|
+
<SettingsModal
|
|
308
|
+
isOpen={settings.isOpen}
|
|
309
|
+
onClose={settings.close}
|
|
310
|
+
initialCategory={settings.category}
|
|
311
|
+
autoOpenAddProvider={settings.autoOpenAddProvider}
|
|
312
|
+
appSettings={appSettings}
|
|
333
313
|
/>
|
|
334
314
|
|
|
335
315
|
{/* The navbar title menu's delete path and the rename-with-AI refusal
|
|
@@ -212,11 +212,14 @@ export function Panel(props: {
|
|
|
212
212
|
|
|
213
213
|
const baseStyle: CSSProperties = filler
|
|
214
214
|
? {
|
|
215
|
-
|
|
215
|
+
// A collapsed filler keeps its DOM (and therefore its state: the chat
|
|
216
|
+
// owns the live stream and the composer) but takes no width, so the
|
|
217
|
+
// sibling panels' own widths are the whole row.
|
|
218
|
+
flex: collapsed ? '0 0 0%' : '1 1 0%',
|
|
216
219
|
// The filler keeps its floor on the group's axis (and none across it,
|
|
217
220
|
// where it is sized by the container): see the group model note above.
|
|
218
|
-
minWidth: orientation === 'horizontal' ? minPx : 0,
|
|
219
|
-
minHeight: orientation === 'horizontal' ? 0 : minPx,
|
|
221
|
+
minWidth: orientation === 'horizontal' ? (collapsed ? 0 : minPx) : 0,
|
|
222
|
+
minHeight: orientation === 'horizontal' ? 0 : collapsed ? 0 : minPx,
|
|
220
223
|
overflow: 'hidden',
|
|
221
224
|
position: 'relative',
|
|
222
225
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
type ShellDownload,
|
|
5
|
-
} from '@/client/hooks/ui/updates/shell-download';
|
|
1
|
+
import { Info, Settings } from 'lucide-preact';
|
|
2
|
+
import type { ShellDownload } from '@/client/hooks/ui/updates/shell-download';
|
|
3
|
+
import { ShellUpdateButton } from '@/client/components/common/shell-update-button';
|
|
6
4
|
|
|
7
5
|
interface SessionSidebarFooterProps {
|
|
8
6
|
onSettings: () => void;
|
|
@@ -50,16 +48,11 @@ export function SessionSidebarFooter({ onSettings, onInfo, updateAvailable, onUp
|
|
|
50
48
|
// an installed shell that is behind (an update), or an Android browser
|
|
51
49
|
// whose reader may not know the shell exists (an install). A desktop
|
|
52
50
|
// browser and an iOS browser get neither — see `useShellDownload`.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
aria-label={shellDownloadLabel(shellDownload)}
|
|
59
|
-
className="p-1 rounded hover:text-ink hover:bg-ink/5 cursor-pointer transition-colors"
|
|
60
|
-
>
|
|
61
|
-
<Download size={16} />
|
|
62
|
-
</a>
|
|
51
|
+
//
|
|
52
|
+
// A desktop shell that reported its architecture gets a BUTTON: the
|
|
53
|
+
// shell installs the artifact itself, and a link there would be a file
|
|
54
|
+
// in Downloads instead of an updated app.
|
|
55
|
+
<ShellUpdateButton download={shellDownload} variant="icon" />
|
|
63
56
|
)}
|
|
64
57
|
</div>
|
|
65
58
|
{updateAvailable && (
|
|
@@ -259,6 +259,7 @@ export function MobileLayoutWrapper({ onDesktopToggle, appSettings = {} }: Mobil
|
|
|
259
259
|
<div
|
|
260
260
|
className={`flex-1 min-h-0 w-full relative ${currentScreen === 'main' ? 'block' : 'hidden'}`}
|
|
261
261
|
{...editorDrop.dropProps}
|
|
262
|
+
{...editorDrop.captureDropProps}
|
|
262
263
|
>
|
|
263
264
|
<EditorDropOverlay visible={editorDrop.isDragging} />
|
|
264
265
|
<MobileMainView
|
|
@@ -69,6 +69,9 @@ export function MobileFullDiff({ diff, onClose, onFileSaved }: MobileFullDiffPro
|
|
|
69
69
|
<div className="flex-1 min-h-0">
|
|
70
70
|
<DiffPanel
|
|
71
71
|
className="h-full"
|
|
72
|
+
// A full-screen phone overlay has no chat column or sidebar beside
|
|
73
|
+
// it, so the width modes would widen into nothing.
|
|
74
|
+
viewModeButtons={false}
|
|
72
75
|
filePath={diff.path}
|
|
73
76
|
status={status}
|
|
74
77
|
isStaged={isStaged}
|