Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@
"react": 1
},
"importSpecifiers": 80,
"nonTriviaTokens": 9576
"nonTriviaTokens": 9575
},
"src/renderer/use-app-shell-session-list.ts": {
"importDeclarations": 4,
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/main/__tests__/workbar-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ describe('Workbar feature boundary', () => {
}
assert.equal(appShell.includes('<WorkbarShellRoot>'), true);
assert.equal(appShell.includes('<WorkbarProvider'), true);
assert.equal(provider.includes('useWorkbarController(props.input)'), true);
assert.equal(appShell.includes('<WorkbarLayoutContainer>'), true);
assert.equal(provider.includes('useWorkbarController({ ...props.input, layoutContainerRef })'), true);
assert.equal(provider.includes('<div className="maka-detail-with-artifacts" ref={ref}>'), true);
assert.equal(productionEntry.includes('useWorkbarController'), false);
});

Expand Down
140 changes: 135 additions & 5 deletions apps/desktop/src/main/__tests__/workbar-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import { createSessionCatalogController, selectAuthoritativeSessionIds } from '.
import { sessionIdSetsEqual } from '../../renderer/features/conversation/index.js';
import assert from 'node:assert/strict';
import { afterEach, describe, it } from 'node:test';
import {
SHELL_CONTENT_AREA_GAP_PX,
shellWorkbarGridRoom,
} from '../../renderer/application/contracts/shell-layout-contract.js';
import { SHELL_WINDOW_MIN_WIDTH } from '../../shared/shell-layout-contract.js';
import {
createSessionWorkbarPanelsState,
createSessionWorkbarTabsState,
Expand All @@ -32,9 +37,15 @@ import {
reduceWorkbarLayout,
reduceWorkbarPanels,
SESSION_BOTTOM_PANEL_MAX_HEIGHT,
SESSION_CONVERSATION_MIN_WIDTH,
SESSION_WORKBAR_DEFAULT_WIDTH,
SESSION_WORKBAR_MIN_WIDTH,
sessionWorkbarCeiling,
sessionWorkbarDisplayWidth,
sessionWorkbarMaxWidth,
terminalSessionWorkbarTabId,
WORKBAR_TOOL_DEFINITIONS,
type WorkbarLayoutState,
} from '../../renderer/features/workbar/testing.js';

function installMemoryLocalStorage(initial: Record<string, string> = {}) {
Expand Down Expand Up @@ -90,15 +101,16 @@ describe('Workbar topology', () => {
});

it('routes panel visibility and dimensions through the layout reducer', () => {
let state = {
let state: WorkbarLayoutState = {
panels: createSessionWorkbarPanelsState(),
activeSessionId: 'session-a' as string | undefined,
collapsedBySession: {} as Record<string, boolean>,
compact: false,
compactCollapsed: {},
spaceCollapsed: false,
bottomOpen: false,
rightWidth: 480,
rightWidthPreference: 480,
rightWidthCeiling: undefined,
bottomHeight: 300,
};
state = reduceWorkbarLayout(state, {
Expand All @@ -112,7 +124,7 @@ describe('Workbar topology', () => {
placement: 'right',
size: 12,
});
assert.equal(state.rightWidth, SESSION_WORKBAR_MIN_WIDTH);
assert.equal(sessionWorkbarDisplayWidth(state), SESSION_WORKBAR_MIN_WIDTH);
state = reduceWorkbarLayout(state, {
type: 'open',
placement: 'bottom',
Expand All @@ -133,6 +145,122 @@ describe('Workbar topology', () => {
assert.equal(state.bottomOpen, false);
});

it('sizes the right rail from the measured container, not a fixed cap', () => {
cleanups.push(installMemoryLocalStorage());
// 900 is a width the old 600px cap would have truncated.
const base: WorkbarLayoutState = {
panels: createSessionWorkbarPanelsState(),
activeSessionId: 'session-a',
collapsedBySession: {},
compact: false,
compactCollapsed: {},
spaceCollapsed: false,
bottomOpen: false,
rightWidthPreference: 900,
// The hook measures the container; a test states the ceiling the layout
// would have produced, which is `sessionWorkbarCeiling`'s job.
rightWidthCeiling: undefined,
bottomHeight: 300,
};
let state = reduceWorkbarLayout(base, {
type: 'activate-session',
sessionId: 'session-a',
});
// Unmeasured: the default is the safe display, but the preference survives.
assert.equal(sessionWorkbarDisplayWidth(state), SESSION_WORKBAR_DEFAULT_WIDTH);
assert.equal(state.rightWidthPreference, 900);

state = reduceWorkbarLayout(state, { type: 'measure-right-ceiling', ceiling: 1108 });
assert.equal(sessionWorkbarMaxWidth(state), 1108);
assert.equal(sessionWorkbarDisplayWidth(state), 900);

// A narrower window narrows the display without rewriting the preference.
state = reduceWorkbarLayout(state, { type: 'measure-right-ceiling', ceiling: 620 });
assert.equal(sessionWorkbarDisplayWidth(state), 620);
assert.equal(state.rightWidthPreference, 900);
persistWorkbarLayout(state, 'right-size');
assert.equal(localStorage.getItem('maka-session-workbar-width-v1'), '900');

// Space comes back: so does the width the user chose.
state = reduceWorkbarLayout(state, { type: 'measure-right-ceiling', ceiling: 1400 });
assert.equal(sessionWorkbarDisplayWidth(state), 900);

// Dragging while narrowed starts from what is displayed, so the constrained
// width becomes the new preference instead of snapping back to 900.
state = reduceWorkbarLayout(state, { type: 'measure-right-ceiling', ceiling: 620 });
state = reduceWorkbarLayout(state, { type: 'resize', placement: 'right', size: 700 });
assert.equal(state.rightWidthPreference, 620);
assert.equal(sessionWorkbarDisplayWidth(state), 620);
state = reduceWorkbarLayout(state, { type: 'resize', placement: 'right', size: 300 });
assert.equal(sessionWorkbarDisplayWidth(state), SESSION_WORKBAR_MIN_WIDTH);
});

it('lets an explicitly opened Workbar fit below its drag minimum at the window floor', () => {
cleanups.push(installMemoryLocalStorage());
let state: WorkbarLayoutState = {
panels: createSessionWorkbarPanelsState(),
activeSessionId: 'session-a',
collapsedBySession: {},
compact: true,
compactCollapsed: { 'session-a': false },
spaceCollapsed: false,
bottomOpen: false,
rightWidthPreference: 900,
rightWidthCeiling: undefined,
bottomHeight: 300,
};
// The measured ceiling and the shell's CSS cap share the 400px conversation
// floor and two 4px seams (one outside and one inside the detail grid).
assert.equal(sessionWorkbarCeiling(1600, 12), 1188);
assert.equal(
1600 - 12 - sessionWorkbarCeiling(1600, 12),
SESSION_CONVERSATION_MIN_WIDTH,
);
const compactRoom = shellWorkbarGridRoom(SHELL_WINDOW_MIN_WIDTH, 0);
assert.equal(compactRoom, 192);
assert.equal(
sessionWorkbarCeiling(
SHELL_WINDOW_MIN_WIDTH - SHELL_CONTENT_AREA_GAP_PX,
SHELL_CONTENT_AREA_GAP_PX,
),
compactRoom,
);
state = reduceWorkbarLayout(state, { type: 'measure-right-ceiling', ceiling: compactRoom });
assert.equal(isSessionWorkbarCollapsed(state), false);
assert.equal(sessionWorkbarMaxWidth(state), 192);
assert.equal(sessionWorkbarDisplayWidth(state), 192);
assert.equal(state.rightWidthPreference, 900);
assert.equal(reduceWorkbarLayout(state, { type: 'resize', placement: 'right', size: 192 }), state);
state = reduceWorkbarLayout(state, { type: 'measure-right-ceiling', ceiling: 1188 });
state = reduceWorkbarLayout(state, { type: 'set-compact', compact: false });
assert.equal(isSessionWorkbarCollapsed(state), false);
assert.equal(sessionWorkbarDisplayWidth(state), 900);
});

it('ignores an unreadable or unchanged measurement', () => {
cleanups.push(installMemoryLocalStorage());
const state: WorkbarLayoutState = {
panels: createSessionWorkbarPanelsState(),
activeSessionId: 'session-a',
collapsedBySession: {},
compact: false,
compactCollapsed: {},
spaceCollapsed: false,
bottomOpen: false,
rightWidthPreference: 480,
rightWidthCeiling: 1108,
bottomHeight: 300,
};
assert.equal(
reduceWorkbarLayout(state, { type: 'measure-right-ceiling', ceiling: 1108 }),
state,
);
assert.equal(
reduceWorkbarLayout(state, { type: 'measure-right-ceiling', ceiling: Number.NaN }),
state,
);
});

it('keeps static tabs and removes dynamic metadata from persistence', () => {
const terminalRef = 'run:1';
const state = createSessionWorkbarPanelsState(
Expand Down Expand Up @@ -242,7 +370,8 @@ describe('Workbar topology', () => {
compactCollapsed: {},
spaceCollapsed: false,
bottomOpen: true,
rightWidth: 544,
rightWidthPreference: 544,
rightWidthCeiling: undefined,
bottomHeight: 388,
};
persistWorkbarLayout(layout);
Expand Down Expand Up @@ -272,7 +401,8 @@ describe('Workbar topology', () => {
compactCollapsed: {},
spaceCollapsed: false,
bottomOpen: true,
rightWidth: 544,
rightWidthPreference: 544,
rightWidthCeiling: undefined,
bottomHeight: 388,
});
});
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/renderer/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ import * as Conversation from './features/conversation';
import { deriveWorkspaceReadinessRecovery } from './workspace-readiness-recovery';
import { AgentGraphPanel } from './agent-graph-panel';
import { ChatComposerRegion } from './chat-composer-region';
import { WorkbarHost, WorkbarProvider, WorkbarShellRoot, type WorkbarShellProjection } from './features/workbar';
import { WorkbarHost, WorkbarLayoutContainer, WorkbarProvider, WorkbarShellRoot, type WorkbarShellProjection } from './features/workbar';
import { AppUpdateProvider } from './features/app-update/index.js';
import * as Goals from './features/goals';
import * as ModuleHub from './features/module-hub';
Expand Down Expand Up @@ -1757,7 +1757,7 @@ function AppShellContent({
which is correct: those surfaces shouldn't be a
navigation entry point. */}
<MakaUriContext.Provider value={dispatchMakaUri}>
<div className="maka-detail-with-artifacts">
<WorkbarLayoutContainer>
<div className="mainColumn" data-home-surface={homeSurfaceActive ? 'true' : undefined}
inert={switchingSession || undefined}
aria-busy={switchingSession || undefined}>
Expand Down Expand Up @@ -2030,7 +2030,7 @@ function AppShellContent({
</div>
{/* Collapse hides the Workbar surface without unmounting its tools. */}
<WorkbarHost togglePosition={workbarTogglePosition} />
</div>
</WorkbarLayoutContainer>
</MakaUriContext.Provider>
</AppShellDetailPanel>
</AstryxAppShell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
useRef,
useState,
type ComponentProps,
type RefObject,
} from 'react';
import type { ClientCapabilityResponse } from '@maka/core/client-capability-grant';
import { useMediaQuery } from '@astryxdesign/core/hooks';
Expand Down Expand Up @@ -122,6 +123,11 @@ export interface UseWorkbarControllerInput {
authoritativeSessionIds: ReadonlySet<string> | undefined;
shellObscured: boolean;
modelChoices: readonly ChatModelChoice[];
/**
* The grid container holding both the conversation column and the rail. The
* WorkbarProvider attaches the ref through WorkbarLayoutContainer.
*/
layoutContainerRef?: RefObject<HTMLElement | null>;
/** Toast surface owned by the shell composition zone. */
toastApi: ToastApi;
composerRef?: { current: Pick<ComposerHandle, 'focus' | 'setDraft'> | null };
Expand Down Expand Up @@ -197,7 +203,12 @@ export function useWorkbarController(
const terminalCopy = getDesktopConversationCopy(locale).terminalPanel;
const { browser, sideChat, terminal, workBoard } = useWorkbarServices();
const compact = useMediaQuery(SHELL_WORKBAR_COMPACT_QUERY);
const layout = useWorkbarLayoutState(input.layoutSessionId, input.authoritativeSessionIds, compact);
const layout = useWorkbarLayoutState(
input.layoutSessionId,
input.authoritativeSessionIds,
compact,
input.layoutContainerRef,
);
const sideConversations = useSideConversationWorkspace();
const [pendingSideChatClose, setPendingSideChatClose] = useState<
Array<{ placement: SessionWorkbarPlacement; tab: SessionWorkbarTab }>
Expand Down
Loading