fix(chat): yield the work status card to the context panel in project-less chats
The visibility hook looked the context panel up under the directory the card reports about. A managed Chat reports about none, so the lookup ran against an empty key and answered "closed" while the panel was open. It now resolves the key the way the rail and the panel do, through useEffectiveDirectory. The reporting directory is no longer an input to this hook at all.
This commit is contained in:
@@ -761,7 +761,6 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
|
||||
// row that holds both columns, so its width never depends on the panel's
|
||||
// own visibility.
|
||||
const { rowRef: workStatusRowRef, visible: workStatusVisible, fits: workStatusFits } = useWorkStatusVisibility({
|
||||
directory: workStatusDirectory,
|
||||
isMobile,
|
||||
isVSCode,
|
||||
});
|
||||
|
||||
@@ -45,7 +45,11 @@ exactly as it already does when the context panel opens.
|
||||
|
||||
- the user switched it off;
|
||||
- the runtime is mobile or VS Code;
|
||||
- the context panel is open for the active directory;
|
||||
- the context panel is open for the directory the app is effectively on —
|
||||
looked up through `useEffectiveDirectory` and `normalizeContextPanelDirectoryKey`,
|
||||
the same key the rail and the panel use. It is deliberately **not** the
|
||||
directory this panel reports about: a managed Chat reports about none, and
|
||||
that empty key answered "closed" for a context panel that was plainly open;
|
||||
- the row cannot fit `WORK_STATUS_MIN_CHAT_WIDTH` of transcript alongside
|
||||
`WORK_STATUS_PANEL_WIDTH` of panel.
|
||||
|
||||
|
||||
@@ -10,14 +10,16 @@ type PanelState = {
|
||||
|
||||
let panelByDirectory: Record<string, PanelState> = {};
|
||||
let panelEnabled = true;
|
||||
let effectiveDirectory: string | undefined = '/repo';
|
||||
|
||||
mock.module('@/stores/useUIStore', () => ({
|
||||
useUIStore: (selector: (state: unknown) => unknown) =>
|
||||
selector({ contextPanelByDirectory: panelByDirectory, workStatusPanelEnabled: panelEnabled }),
|
||||
normalizeContextPanelDirectoryKey: (value: string) => value,
|
||||
}));
|
||||
|
||||
mock.module('@/lib/pathNormalization', () => ({
|
||||
normalizePath: (value?: string | null) => value ?? null,
|
||||
mock.module('@/hooks/useEffectiveDirectory', () => ({
|
||||
useEffectiveDirectory: () => effectiveDirectory,
|
||||
}));
|
||||
|
||||
const { useWorkStatusVisibility, WORK_STATUS_REQUIRED_ROW_WIDTH: REQUIRED } = await import(
|
||||
@@ -88,7 +90,7 @@ const installMinimalDom = () => {
|
||||
};
|
||||
};
|
||||
|
||||
type Args = { directory: string | null; isMobile: boolean; isVSCode: boolean };
|
||||
type Args = { isMobile: boolean; isVSCode: boolean };
|
||||
|
||||
/**
|
||||
* Renders the hook with a stand-in row node, attached through the returned
|
||||
@@ -130,6 +132,7 @@ const renderVisibility = (args: Args, rowWidth: number) => {
|
||||
beforeEach(() => {
|
||||
panelByDirectory = {};
|
||||
panelEnabled = true;
|
||||
effectiveDirectory = '/repo';
|
||||
observed = [];
|
||||
notify = null;
|
||||
});
|
||||
@@ -142,7 +145,7 @@ afterEach(() => {
|
||||
describe('useWorkStatusVisibility', () => {
|
||||
test('shows the panel when the row can afford both columns', () => {
|
||||
const { result, teardown } = renderVisibility(
|
||||
{ directory: '/repo', isMobile: false, isVSCode: false },
|
||||
{ isMobile: false, isVSCode: false },
|
||||
REQUIRED,
|
||||
);
|
||||
expect(result.visible).toBe(true);
|
||||
@@ -151,7 +154,7 @@ describe('useWorkStatusVisibility', () => {
|
||||
|
||||
test('hides the panel when the row cannot afford both columns', () => {
|
||||
const { result, teardown } = renderVisibility(
|
||||
{ directory: '/repo', isMobile: false, isVSCode: false },
|
||||
{ isMobile: false, isVSCode: false },
|
||||
REQUIRED - 1,
|
||||
);
|
||||
expect(result.visible).toBe(false);
|
||||
@@ -173,7 +176,6 @@ describe('useWorkStatusVisibility', () => {
|
||||
|
||||
const Probe: React.FC = () => {
|
||||
const { rowRef, visible } = useWorkStatusVisibility({
|
||||
directory: '/repo',
|
||||
isMobile: false,
|
||||
isVSCode: false,
|
||||
});
|
||||
@@ -199,7 +201,7 @@ describe('useWorkStatusVisibility', () => {
|
||||
// In the app this is the chat area (chat + context panel); here `closest`
|
||||
// finds nothing, so the hook falls back to the row it was given.
|
||||
const { rowNode, teardown } = renderVisibility(
|
||||
{ directory: '/repo', isMobile: false, isVSCode: false },
|
||||
{ isMobile: false, isVSCode: false },
|
||||
REQUIRED,
|
||||
);
|
||||
expect(observed).toHaveLength(1);
|
||||
@@ -209,7 +211,7 @@ describe('useWorkStatusVisibility', () => {
|
||||
|
||||
test('reacts to a live resize across the threshold', () => {
|
||||
const { result, teardown } = renderVisibility(
|
||||
{ directory: '/repo', isMobile: false, isVSCode: false },
|
||||
{ isMobile: false, isVSCode: false },
|
||||
REQUIRED,
|
||||
);
|
||||
expect(result.visible).toBe(true);
|
||||
@@ -228,7 +230,7 @@ describe('useWorkStatusVisibility', () => {
|
||||
'/repo': { isOpen: true, tabs: [{ id: 'tab-1', mode: 'git' }], activeTabId: 'tab-1' },
|
||||
};
|
||||
const { result, rowNode, teardown } = renderVisibility(
|
||||
{ directory: '/repo', isMobile: false, isVSCode: false },
|
||||
{ isMobile: false, isVSCode: false },
|
||||
REQUIRED,
|
||||
);
|
||||
expect(result.visible).toBe(false);
|
||||
@@ -236,11 +238,23 @@ describe('useWorkStatusVisibility', () => {
|
||||
teardown();
|
||||
});
|
||||
|
||||
test('yields to the open context panel even when the chat reports on no project', () => {
|
||||
// A Chat session carries no repository, so the panel describes no
|
||||
// directory. The context panel is still keyed by the directory the app is
|
||||
// on, and looking it up under the chat's empty one answered "closed".
|
||||
panelByDirectory = {
|
||||
'/repo': { isOpen: true, tabs: [{ id: 'tab-1', mode: 'git' }], activeTabId: 'tab-1' },
|
||||
};
|
||||
const { result, teardown } = renderVisibility({ isMobile: false, isVSCode: false }, REQUIRED);
|
||||
expect(result.visible).toBe(false);
|
||||
teardown();
|
||||
});
|
||||
|
||||
test('ignores an open context panel that has no resolvable tab', () => {
|
||||
// ContextPanel renders nothing in that state, so it displaces nothing.
|
||||
panelByDirectory = { '/repo': { isOpen: true, tabs: [], activeTabId: null } };
|
||||
const { result, teardown } = renderVisibility(
|
||||
{ directory: '/repo', isMobile: false, isVSCode: false },
|
||||
{ isMobile: false, isVSCode: false },
|
||||
REQUIRED,
|
||||
);
|
||||
expect(result.visible).toBe(true);
|
||||
@@ -264,7 +278,6 @@ describe('useWorkStatusVisibility', () => {
|
||||
const Probe: React.FC = () => {
|
||||
const [attached, setAttached] = React.useState(false);
|
||||
const { rowRef, visible } = useWorkStatusVisibility({
|
||||
directory: '/repo',
|
||||
isMobile: false,
|
||||
isVSCode: false,
|
||||
});
|
||||
@@ -292,7 +305,7 @@ describe('useWorkStatusVisibility', () => {
|
||||
// there is room for it.
|
||||
panelEnabled = false;
|
||||
const { result, teardown } = renderVisibility(
|
||||
{ directory: '/repo', isMobile: false, isVSCode: false },
|
||||
{ isMobile: false, isVSCode: false },
|
||||
REQUIRED * 2,
|
||||
);
|
||||
expect(result.visible).toBe(false);
|
||||
@@ -302,7 +315,7 @@ describe('useWorkStatusVisibility', () => {
|
||||
|
||||
test('reports no fit when the row is too narrow, whatever the switch says', () => {
|
||||
const { result, teardown } = renderVisibility(
|
||||
{ directory: '/repo', isMobile: false, isVSCode: false },
|
||||
{ isMobile: false, isVSCode: false },
|
||||
REQUIRED - 1,
|
||||
);
|
||||
expect(result.fits).toBe(false);
|
||||
@@ -312,7 +325,7 @@ describe('useWorkStatusVisibility', () => {
|
||||
|
||||
test('stays hidden on mobile and in VS Code regardless of width', () => {
|
||||
const mobile = renderVisibility(
|
||||
{ directory: '/repo', isMobile: true, isVSCode: false },
|
||||
{ isMobile: true, isVSCode: false },
|
||||
REQUIRED * 2,
|
||||
);
|
||||
expect(mobile.result.visible).toBe(false);
|
||||
@@ -320,7 +333,7 @@ describe('useWorkStatusVisibility', () => {
|
||||
|
||||
observed = [];
|
||||
const vscode = renderVisibility(
|
||||
{ directory: '/repo', isMobile: false, isVSCode: true },
|
||||
{ isMobile: false, isVSCode: true },
|
||||
REQUIRED * 2,
|
||||
);
|
||||
expect(vscode.result.visible).toBe(false);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { normalizePath } from '@/lib/pathNormalization';
|
||||
import { normalizeContextPanelDirectoryKey, useUIStore } from '@/stores/useUIStore';
|
||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||
|
||||
/**
|
||||
* Fixed panel width. The panel is not user-resizable: it is an object inside
|
||||
@@ -23,7 +23,6 @@ export const WORK_STATUS_REQUIRED_ROW_WIDTH =
|
||||
WORK_STATUS_PANEL_WIDTH + WORK_STATUS_PANEL_GUTTER + WORK_STATUS_MIN_CHAT_WIDTH;
|
||||
|
||||
type Options = {
|
||||
directory: string | null | undefined;
|
||||
isMobile: boolean;
|
||||
isVSCode: boolean;
|
||||
};
|
||||
@@ -52,12 +51,20 @@ type Result = {
|
||||
* panel, oscillating forever. The row width is independent of the panel, so it
|
||||
* is the only stable input.
|
||||
*/
|
||||
export const useWorkStatusVisibility = ({ directory, isMobile, isVSCode }: Options): Result => {
|
||||
export const useWorkStatusVisibility = ({ isMobile, isVSCode }: Options): Result => {
|
||||
const [rowNode, setRowNode] = React.useState<HTMLDivElement | null>(null);
|
||||
const [rowWidth, setRowWidth] = React.useState<number | null>(null);
|
||||
const rowRef = React.useCallback((node: HTMLDivElement | null) => { setRowNode(node); }, []);
|
||||
|
||||
const directoryKey = React.useMemo(() => normalizePath(directory ?? null), [directory]);
|
||||
// Keyed exactly like the rail and the panel itself: whichever directory the
|
||||
// app is effectively on, not the directory this panel reports about. A chat
|
||||
// with no project reports on nothing, and looking the context panel up under
|
||||
// that empty key answered "closed" while it was plainly open on screen.
|
||||
const effectiveDirectory = useEffectiveDirectory();
|
||||
const directoryKey = React.useMemo(
|
||||
() => (effectiveDirectory ? normalizeContextPanelDirectoryKey(effectiveDirectory) : ''),
|
||||
[effectiveDirectory],
|
||||
);
|
||||
|
||||
// Mirrors ContextPanel's own derivation: a panel with `isOpen` but no
|
||||
// resolvable active tab renders nothing, and must not displace this panel.
|
||||
|
||||
Reference in New Issue
Block a user