Skip to content

Commit c5f1d03

Browse files
committed
fix(desktop): keep side tools as one docked rail
1 parent f0dc27e commit c5f1d03

10 files changed

Lines changed: 52 additions & 89 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ Detailed data and runtime contracts: @docs/agents/repository-contracts.md and
8181
- Desktop app-shell chrome must keep its complete geometry and state recipe in
8282
an always-loaded owner stylesheet. Never place global shell component rules
8383
in a removable feature stylesheet; pin the owner import with a contract test.
84+
- Desktop thread side tools have one presentation: a right-docked rail. The
85+
header control toggles that rail directly; do not add an overlay variant.
8486
- Mobile route state, presentation mapping, formatting, and business-rule
8587
transformations should live in `GaryxMobileCore` with SwiftPM tests.
8688
- Message, transcript, and tool-row display is server-render-state first:

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ Detailed data and runtime contracts: @docs/agents/repository-contracts.md and
8181
- Desktop app-shell chrome must keep its complete geometry and state recipe in
8282
an always-loaded owner stylesheet. Never place global shell component rules
8383
in a removable feature stylesheet; pin the owner import with a contract test.
84+
- Desktop thread side tools have one presentation: a right-docked rail. The
85+
header control toggles that rail directly; do not add an overlay variant.
8486
- Mobile route state, presentation mapping, formatting, and business-rule
8587
transformations should live in `GaryxMobileCore` with SwiftPM tests.
8688
- Message, transcript, and tool-row display is server-render-state first:

desktop/garyx-desktop/src/renderer/src/app-shell/AppShell.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,7 +1180,6 @@ export function AppShell() {
11801180
sidebarCollapsed,
11811181
sidebarResizing,
11821182
sidebarWidth,
1183-
sideToolsDocked,
11841183
sideToolsPanelWidth,
11851184
sideToolsPanelWidthCustomizedRef,
11861185
sideToolsPanelWidthRef,
@@ -3821,9 +3820,6 @@ export function AppShell() {
38213820
isSkillsView ? "skills-view" : null,
38223821
isTasksView ? "tasks-view" : null,
38233822
showConversationSideTools ? "with-side-tools" : null,
3824-
showConversationSideTools
3825-
? sideToolsDocked ? "side-tools-docked" : "side-tools-overlay"
3826-
: null,
38273823
sideToolsResizing ? "side-tools-resizing" : null,
38283824
]
38293825
.filter(Boolean)

desktop/garyx-desktop/src/renderer/src/app-shell/diagnostics-helpers.test.mjs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
33

44
import {
55
buildThreadLogLines,
6+
clampSideToolsPanelWidth,
67
defaultSideToolsPanelWidth,
78
} from './diagnostics-helpers.ts';
89

@@ -18,11 +19,13 @@ test('still parses legacy RFC3339 thread-log stamps without a timezone suffix',
1819
assert.equal(line.text, 'WARN [run] legacy');
1920
});
2021

21-
test('defaults side tools to a bounded rail instead of consuming most of the window', () => {
22-
assert.equal(defaultSideToolsPanelWidth(1800), 720);
22+
test('defaults side tools to the single measured Codex-style right rail', () => {
23+
assert.equal(defaultSideToolsPanelWidth(1800), 320);
24+
assert.equal(defaultSideToolsPanelWidth(1235), 320);
25+
assert.equal(defaultSideToolsPanelWidth(900), 320);
2326
});
2427

25-
test('clamps side tools width to keep the main message column usable', () => {
26-
assert.equal(defaultSideToolsPanelWidth(1235), 520);
27-
assert.equal(defaultSideToolsPanelWidth(900), 520);
28+
test('clamps a customized side-tools rail to the measured canvas', () => {
29+
assert.equal(clampSideToolsPanelWidth(520, 736), 320);
30+
assert.equal(clampSideToolsPanelWidth(685, 1235), 685);
2831
});

desktop/garyx-desktop/src/renderer/src/app-shell/diagnostics-helpers.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@ export const MAX_GATEWAY_THREAD_LOG_LINES = 100;
1313
export const THREAD_LOG_PANEL_MIN_WIDTH = 280;
1414
export const THREAD_LOG_PANEL_MAX_WIDTH = 760;
1515
const DEFAULT_THREAD_LOG_PANEL_WIDTH = 360;
16-
export const SIDE_TOOLS_PANEL_MIN_WIDTH = 520;
16+
export const SIDE_TOOLS_PANEL_MIN_WIDTH = 320;
1717
export const SIDE_TOOLS_PANEL_MAX_WIDTH = 1180;
18-
export const SIDE_TOOLS_PANEL_DEFAULT_RATIO = 0.42;
19-
const SIDE_TOOLS_PANEL_DEFAULT_MAX_WIDTH = 720;
20-
const DEFAULT_SIDE_TOOLS_PANEL_WIDTH = 520;
18+
const DEFAULT_SIDE_TOOLS_PANEL_WIDTH = 320;
2119
const GATEWAY_OFFLINE_THRESHOLD = 3;
2220

2321
export function keepRecentThreadLogLines(
@@ -112,16 +110,7 @@ export function clampSideToolsPanelWidth(
112110
}
113111

114112
export function defaultSideToolsPanelWidth(layoutWidth?: number | null): number {
115-
const baseWidth = layoutWidth && layoutWidth > 0
116-
? Math.min(
117-
SIDE_TOOLS_PANEL_DEFAULT_MAX_WIDTH,
118-
Math.max(
119-
DEFAULT_SIDE_TOOLS_PANEL_WIDTH,
120-
layoutWidth * SIDE_TOOLS_PANEL_DEFAULT_RATIO,
121-
),
122-
)
123-
: DEFAULT_SIDE_TOOLS_PANEL_WIDTH;
124-
return clampSideToolsPanelWidth(baseWidth, layoutWidth);
113+
return clampSideToolsPanelWidth(DEFAULT_SIDE_TOOLS_PANEL_WIDTH, layoutWidth);
125114
}
126115

127116
export function computeGatewayIndicator(input: {

desktop/garyx-desktop/src/renderer/src/app-shell/responsive-layout-model.test.mjs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,25 +80,25 @@ test("task tree docks only after the full 736px reading column still fits", () =
8080
assert.equal(isDockedTaskTree(TASK_TREE_DOCK_MIN_WIDTH), true);
8181
});
8282

83-
test("right panels overlay before they can crush the primary thread", () => {
83+
test("thread logs overlay before they can crush the primary thread", () => {
8484
assert.equal(
8585
isDockedSidePanel({
86-
canvasWidth: 1069,
87-
panelWidth: 520,
86+
canvasWidth: 829,
87+
panelWidth: 280,
8888
}),
8989
false,
9090
);
9191
assert.equal(
9292
isDockedSidePanel({
93-
canvasWidth: 1070,
94-
panelWidth: 520,
93+
canvasWidth: 830,
94+
panelWidth: 280,
9595
}),
9696
true,
9797
);
9898
assert.equal(
9999
isDockedSidePanel({
100100
canvasWidth: 736,
101-
panelWidth: 520,
101+
panelWidth: 280,
102102
}),
103103
false,
104104
);

desktop/garyx-desktop/src/renderer/src/app-shell/useLayoutResizeController.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ export function useLayoutResizeController({
136136
}, [secondaryRailOpen]);
137137
const threadLayoutRef = useRef<HTMLDivElement | null>(null);
138138
const conversationRef = useRef<HTMLElement | null>(null);
139-
const [conversationWidth, setConversationWidth] = useState(0);
140139
const [threadLayoutWidth, setThreadLayoutWidth] = useState(0);
141140
const threadLogsPanelWidthRef = useRef(
142141
DEFAULT_DESKTOP_SETTINGS.threadLogsPanelWidth,
@@ -154,14 +153,9 @@ export function useLayoutResizeController({
154153

155154
const desktopStateReady = desktopState !== null;
156155
useLayoutEffect(() => {
157-
const conversation = conversationRef.current;
158156
const threadLayout = threadLayoutRef.current;
159157
const syncMeasuredWidths = () => {
160-
const nextConversationWidth = conversation?.clientWidth || 0;
161158
const nextThreadLayoutWidth = threadLayout?.clientWidth || 0;
162-
setConversationWidth((current) =>
163-
current === nextConversationWidth ? current : nextConversationWidth,
164-
);
165159
setThreadLayoutWidth((current) =>
166160
current === nextThreadLayoutWidth ? current : nextThreadLayoutWidth,
167161
);
@@ -176,9 +170,6 @@ export function useLayoutResizeController({
176170
}
177171

178172
const observer = new ResizeObserver(syncMeasuredWidths);
179-
if (conversation) {
180-
observer.observe(conversation);
181-
}
182173
if (threadLayout) {
183174
observer.observe(threadLayout);
184175
}
@@ -352,7 +343,6 @@ export function useLayoutResizeController({
352343
const measuredThreadLayoutWidth = currentThreadLayoutWidth() || 0;
353344
const measuredConversationWidth = currentConversationWidth() || 0;
354345
setThreadLayoutWidth(measuredThreadLayoutWidth);
355-
setConversationWidth(measuredConversationWidth);
356346
const nextWidth = clampThreadLogsPanelWidth(
357347
threadLogsPanelWidthRef.current,
358348
measuredThreadLayoutWidth,
@@ -539,10 +529,6 @@ export function useLayoutResizeController({
539529
};
540530
}, [sideToolsResizing]);
541531

542-
const sideToolsDocked = isDockedSidePanel({
543-
canvasWidth: conversationWidth,
544-
panelWidth: sideToolsPanelWidth,
545-
});
546532
const threadLogsDocked = isDockedSidePanel({
547533
canvasWidth: threadLayoutWidth,
548534
panelWidth: threadLogsPanelWidth,
@@ -562,7 +548,6 @@ export function useLayoutResizeController({
562548
sidebarCollapsed,
563549
sidebarResizing,
564550
sidebarWidth,
565-
sideToolsDocked,
566551
sideToolsPanelWidth,
567552
sideToolsPanelWidthCustomizedRef,
568553
sideToolsPanelWidthRef,

desktop/garyx-desktop/src/renderer/src/responsive-layout-design.test.mjs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,30 @@ test("task tree only reserves a rail when the full reading column fits", () => {
6060
assert.ok(css.includes("max-height: calc(100% - 24px)"));
6161
});
6262

63-
test("right panels use measured dock or overlay state instead of a viewport guess", () => {
63+
test("side tools use one right-docked presentation at every width", () => {
6464
const appShellSource = read("app-shell/AppShell.tsx");
65-
const threadPageSource = read("app-shell/components/ThreadPage.tsx");
6665
const controllerSource = read("app-shell/useLayoutResizeController.ts");
6766
const workspaceRailsCss = read("styles/workspace-rails.css");
67+
const browserCss = read("styles/browser.css");
68+
69+
assert.ok(!appShellSource.includes("sideToolsDocked"));
70+
assert.ok(!controllerSource.includes("sideToolsDocked"));
71+
assert.ok(workspaceRailsCss.includes(".conversation.with-side-tools {"));
72+
assert.ok(workspaceRailsCss.includes("minmax(320px, var(--side-tools-panel-width, 320px))"));
73+
assert.ok(!workspaceRailsCss.includes("side-tools-docked"));
74+
assert.ok(!workspaceRailsCss.includes("side-tools-overlay"));
75+
assert.ok(!browserCss.includes(".conversation.with-side-tools {"));
76+
assert.ok(!browserCss.includes(".thread-side-tools-panel {"));
77+
});
78+
79+
test("thread logs use measured dock or overlay state instead of a viewport guess", () => {
80+
const threadPageSource = read("app-shell/components/ThreadPage.tsx");
81+
const controllerSource = read("app-shell/useLayoutResizeController.ts");
6882
const conversationCss = read("styles/conversation.css");
6983
const browserCss = read("styles/browser.css");
7084

71-
assert.ok(appShellSource.includes('sideToolsDocked ? "side-tools-docked" : "side-tools-overlay"'));
7285
assert.ok(threadPageSource.includes('threadLogsDocked ? "log-panel-docked" : "log-panel-overlay"'));
7386
assert.ok(controllerSource.includes("new ResizeObserver(syncMeasuredWidths)"));
74-
assert.ok(workspaceRailsCss.includes(".conversation.with-side-tools.side-tools-overlay"));
7587
assert.ok(conversationCss.includes(".thread-layout.with-log-panel.log-panel-overlay"));
76-
assert.ok(!browserCss.includes(".conversation.with-side-tools {"));
77-
assert.ok(!browserCss.includes(".thread-side-tools-panel {"));
7888
assert.ok(!browserCss.includes(".thread-layout.with-log-panel {"));
7989
});

desktop/garyx-desktop/src/renderer/src/styles/workspace-rails.css

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,18 +1219,11 @@
12191219
.conversation.with-side-tools {
12201220
--side-tools-resizer-width: 10px;
12211221
position: relative;
1222-
grid-template-rows: auto minmax(0, 1fr);
1223-
}
1224-
1225-
.conversation.with-side-tools.side-tools-docked {
12261222
grid-template-columns:
12271223
minmax(0, 1fr)
12281224
var(--side-tools-resizer-width)
1229-
minmax(520px, var(--side-tools-panel-width, 520px));
1230-
}
1231-
1232-
.conversation.with-side-tools.side-tools-overlay {
1233-
grid-template-columns: minmax(0, 1fr);
1225+
minmax(320px, var(--side-tools-panel-width, 320px));
1226+
grid-template-rows: auto minmax(0, 1fr);
12341227
}
12351228

12361229
.conversation.with-side-tools .conversation-header {
@@ -1243,35 +1236,16 @@
12431236
grid-row: 2;
12441237
}
12451238

1246-
.conversation.with-side-tools.side-tools-docked .side-tools-resizer {
1239+
.conversation.with-side-tools .side-tools-resizer {
12471240
grid-column: 2;
12481241
grid-row: 1 / span 2;
12491242
}
12501243

1251-
.conversation.with-side-tools.side-tools-docked .thread-side-tools-panel {
1244+
.conversation.with-side-tools .thread-side-tools-panel {
12521245
grid-column: 3;
12531246
grid-row: 1 / span 2;
12541247
}
12551248

1256-
.conversation.with-side-tools.side-tools-overlay .side-tools-resizer {
1257-
display: none;
1258-
}
1259-
1260-
.conversation.with-side-tools.side-tools-overlay .thread-side-tools-panel {
1261-
position: absolute;
1262-
top: calc(var(--inset-toolbar) + 12px);
1263-
right: 12px;
1264-
bottom: 12px;
1265-
z-index: 45;
1266-
width: min(var(--side-tools-panel-width, 520px), calc(100% - 24px));
1267-
height: auto;
1268-
max-height: none;
1269-
border: 0.5px solid var(--color-token-border);
1270-
border-radius: 16px;
1271-
background: var(--color-token-bg-primary);
1272-
box-shadow: var(--shadow-lg);
1273-
}
1274-
12751249
.conversation.side-tools-resizing {
12761250
cursor: col-resize;
12771251
}

docs/agents/desktop-ui.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,17 @@
7070
least 1088px wide, leaving the full reading column intact. Below that width,
7171
expose the tree through the 28px conversation-header control and show it as
7272
a dismissible overlay with Escape, outside-click, and focus-return behavior.
73-
- Right-side tools and logs choose docked versus overlay presentation from the
74-
measured conversation canvas, the actual panel width, a 10px resizer, and a
75-
540px minimum primary thread. Do not use a viewport-only media query: open
76-
rails and user-resized sidebars change the available canvas. A constrained
77-
panel floats below the toolbar with 12px edge insets; it never stacks under
78-
the transcript or compresses the composer below the minimum primary width.
79-
- Floating task trees and right panels use the available height between their
80-
top and bottom insets. Do not cap them to a percentage of viewport height on
81-
tall windows or turn the app shell into document-height vertical stacking.
73+
- Side tools have one presentation: a right-docked rail, approximately 320px
74+
by default. The conversation-header control toggles that rail directly; do
75+
not add a second inset/overlay form. The rail may still be resized and must
76+
keep its contents responsive within the available width.
77+
- Thread logs choose docked versus overlay presentation from the measured
78+
thread canvas, the actual panel width, a 10px resizer, and a 540px minimum
79+
primary thread. Do not use a viewport-only media query: open rails and
80+
user-resized sidebars change the available canvas.
81+
- Floating task trees and log panels use the available height between their top
82+
and bottom insets. Do not cap them to a percentage of viewport height on tall
83+
windows or turn the app shell into document-height vertical stacking.
8284
- Validate responsive changes in the installed app around the actual seams:
8385
640, 720/721, 980/981, 1332/1333, and a wide desktop width. Restore the
8486
user's original window size after CDP measurement.

0 commit comments

Comments
 (0)