Group toolbar actions by UI region

This commit is contained in:
murashit 2026-06-27 09:00:12 +09:00
parent 5abc9cf2c3
commit b47a43e763
9 changed files with 324 additions and 186 deletions

View file

@ -11,7 +11,7 @@ import type { ChatPanelGoalSurface } from "../panel/surface/goal-projection";
import { MessageStreamPresenter } from "../panel/surface/message-stream-presenter";
import type { ChatMessageScrollController } from "../panel/surface/message-stream-scroll";
import type { ChatPanelToolbarSurface } from "../panel/surface/toolbar-projection";
import { createChatPanelToolbarActions, type ToolbarPanelActions } from "../panel/toolbar-actions";
import { createToolbarUiActions, type ToolbarPanelActions } from "../panel/toolbar-actions";
import type { ToolbarActions } from "../ui/toolbar";
import type { ChatPanelConnectionBundle } from "./connection-bundle";
import type { ChatPanelEnvironment } from "./environment";
@ -33,7 +33,7 @@ export interface ChatPanelSurfacesInput {
goals: ChatPanelGoalActions;
rename: ThreadRenameEditorActions;
threadActions: ChatPanelThreadActions;
toolbarPanels: ToolbarPanelActions;
toolbarPanelActions: ToolbarPanelActions;
navigation: ChatPanelThreadNavigationActions;
reconnect: () => Promise<void>;
history: HistoryController;
@ -55,7 +55,7 @@ export function createChatPanelSurfaces(host: ChatPanelSurfacesHost, input: Chat
goals,
rename,
threadActions,
toolbarPanels,
toolbarPanelActions,
navigation,
reconnect,
history,
@ -63,12 +63,12 @@ export function createChatPanelSurfaces(host: ChatPanelSurfacesHost, input: Chat
turnActions,
} = input;
const { environment, stateStore } = host;
const toolbarActions = createChatPanelToolbarActions({
const toolbarActions = createToolbarUiActions({
connectionController,
reconnectPanel: reconnect,
threadActions,
goals,
toolbarPanels,
toolbarPanel: toolbarPanelActions,
rename,
navigation,
});

View file

@ -138,7 +138,7 @@ type ChatPanelThreadNavigationActions = ReturnType<typeof createThreadNavigation
interface ChatPanelThreadActionParts {
actions: ChatPanelThreadActions;
toolbarPanels: ToolbarPanelActions;
toolbarPanelActions: ToolbarPanelActions;
navigation: ChatPanelThreadNavigationActions;
}
@ -281,7 +281,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
goals,
rename,
threadActions: threadActionParts.actions,
toolbarPanels: threadActionParts.toolbarPanels,
toolbarPanelActions: threadActionParts.toolbarPanelActions,
navigation: threadActionParts.navigation,
reconnect: composerAndTurn.reconnect,
history,
@ -324,7 +324,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
rename,
},
toolbar: {
panels: threadActionParts.toolbarPanels,
panels: threadActionParts.toolbarPanelActions,
actions: surfaces.toolbarActions,
},
composer: {
@ -667,7 +667,7 @@ function createThreadActionParts(
},
};
const actions = createThreadManagementActions(threadManagementHost);
const toolbarPanels = createToolbarPanelActions({
const toolbarPanelActions = createToolbarPanelActions({
stateStore,
threadActions: actions,
});
@ -675,7 +675,7 @@ function createThreadActionParts(
stateStore,
identity,
closeForThreadSelection: () => {
toolbarPanels.closeForThreadSelection();
toolbarPanelActions.closeForThreadSelection();
},
focusThreadInOpenView: (threadId) => environment.plugin.workspace.focusThreadInOpenView(threadId),
resumeThread: (threadId) => resume.resumeThread(threadId),
@ -684,7 +684,7 @@ function createThreadActionParts(
composerController.focus();
},
});
return { actions, toolbarPanels, navigation };
return { actions, toolbarPanelActions, navigation };
}
function createComposerAndTurnActions(

View file

@ -25,12 +25,12 @@ export interface ToolbarPanelActions {
closeOnOutsidePointer(context: ToolbarOutsidePointerContext): void;
}
export interface ChatPanelToolbarActionDependencies {
export interface ToolbarUiActionDependencies {
connectionController: ChatConnectionController;
reconnectPanel: () => Promise<void>;
threadActions: ThreadManagementActions;
goals: GoalActions;
toolbarPanels: ToolbarPanelActions;
toolbarPanel: ToolbarPanelActions;
rename: ThreadRenameEditorActions;
navigation: ThreadNavigationActions;
}
@ -120,58 +120,70 @@ export function createToolbarPanelActions(host: ToolbarPanelActionsHost): Toolba
};
}
export function createChatPanelToolbarActions(deps: ChatPanelToolbarActionDependencies): ToolbarActions {
export function createToolbarUiActions(deps: ToolbarUiActionDependencies): ToolbarActions {
return {
startNewThread: () => {
void deps.navigation.startNewThread();
primary: {
toggleChatActions: () => {
deps.toolbarPanel.toggleChatActions();
},
toggleHistory: () => {
deps.toolbarPanel.toggleHistory();
},
toggleStatusPanel: () => {
deps.toolbarPanel.toggleStatus();
},
},
toggleChatActions: () => {
deps.toolbarPanels.toggleChatActions();
chat: {
startNewThread: () => {
void deps.navigation.startNewThread();
},
compactConversation: () => {
void deps.threadActions.compactActiveThread();
},
setGoal: () => {
deps.goals.startEditingCurrent();
},
},
compactConversation: () => {
void deps.threadActions.compactActiveThread();
status: {
connect: () => {
void deps.reconnectPanel();
},
refreshStatus: () => {
void deps.connectionController.refreshStatusPanel();
},
copyDebugDetails: (details) => {
void copyTextWithNotice(details, "Copied debug details.", "Could not copy debug details.");
},
},
setGoal: () => {
deps.goals.startEditingCurrent();
},
toggleHistory: () => {
deps.toolbarPanels.toggleHistory();
},
toggleStatusPanel: () => {
deps.toolbarPanels.toggleStatus();
},
connect: () => {
void deps.reconnectPanel();
},
refreshStatus: () => {
void deps.connectionController.refreshStatusPanel();
},
copyDebugDetails: (details) => {
void copyTextWithNotice(details, "Copied debug details.", "Could not copy debug details.");
},
resumeThread: (threadId) => {
void deps.navigation.selectThreadFromToolbar(threadId);
},
startArchiveThread: (threadId) => {
deps.toolbarPanels.startArchive(threadId);
},
archiveThread: (threadId, saveMarkdown) => {
void deps.toolbarPanels.archiveThread(threadId, saveMarkdown);
},
startRenameThread: (threadId) => {
deps.rename.start(threadId);
},
updateRenameDraft: (threadId, value) => {
deps.rename.updateDraft(threadId, value);
},
saveRenameThread: (threadId, value) => {
void deps.rename.save(threadId, value);
},
cancelRenameThread: (threadId) => {
deps.rename.cancel(threadId);
},
autoNameThread: (threadId) => {
void deps.rename.autoNameDraft(threadId);
threads: {
resume: (threadId) => {
void deps.navigation.selectThreadFromToolbar(threadId);
},
archive: {
start: (threadId) => {
deps.toolbarPanel.startArchive(threadId);
},
confirm: (threadId, saveMarkdown) => {
void deps.toolbarPanel.archiveThread(threadId, saveMarkdown);
},
},
rename: {
start: (threadId) => {
deps.rename.start(threadId);
},
updateDraft: (threadId, value) => {
deps.rename.updateDraft(threadId, value);
},
save: (threadId, value) => {
void deps.rename.save(threadId, value);
},
cancel: (threadId) => {
deps.rename.cancel(threadId);
},
autoName: (threadId) => {
void deps.rename.autoNameDraft(threadId);
},
},
},
};
}

View file

@ -45,24 +45,44 @@ export interface ToolbarViewModel {
toolInventory: ToolbarDiagnosticSection[];
}
export interface ToolbarActions {
startNewThread: () => void;
interface ToolbarPrimaryActions {
toggleHistory: () => void;
toggleChatActions: () => void;
toggleStatusPanel: () => void;
}
interface ToolbarChatActions {
startNewThread: () => void;
compactConversation: () => void;
setGoal: () => void;
toggleHistory: () => void;
toggleStatusPanel: () => void;
}
interface ToolbarStatusActions {
connect: () => void;
refreshStatus: () => void;
copyDebugDetails: (details: string) => void;
resumeThread: (threadId: string) => void;
startArchiveThread: (threadId: string) => void;
archiveThread: (threadId: string, saveMarkdown: boolean) => void;
startRenameThread: (threadId: string) => void;
updateRenameDraft: (threadId: string, value: string) => void;
saveRenameThread: (threadId: string, value: string) => void;
cancelRenameThread: (threadId: string) => void;
autoNameThread: (threadId: string) => void;
}
interface ToolbarThreadActions {
resume: (threadId: string) => void;
archive: {
start: (threadId: string) => void;
confirm: (threadId: string, saveMarkdown: boolean) => void;
};
rename: {
start: (threadId: string) => void;
updateDraft: (threadId: string, value: string) => void;
save: (threadId: string, value: string) => void;
cancel: (threadId: string) => void;
autoName: (threadId: string) => void;
};
}
export interface ToolbarActions {
primary: ToolbarPrimaryActions;
chat: ToolbarChatActions;
status: ToolbarStatusActions;
threads: ToolbarThreadActions;
}
export function Toolbar({ model, actions }: { model: ToolbarViewModel; actions: ToolbarActions }): UiNode {
@ -74,16 +94,16 @@ export function Toolbar({ model, actions }: { model: ToolbarViewModel; actions:
icon="history"
label={model.historyOpen ? "Hide thread list" : "Show thread list"}
className={["codex-panel__history-toggle", model.historyOpen ? "is-active" : ""].filter(Boolean).join(" ")}
onClick={actions.toggleHistory}
onClick={actions.primary.toggleHistory}
/>
<ToolbarIconButton
icon="messages-square"
label={model.chatActionsOpen ? "Hide chat actions" : "Show chat actions"}
className={["codex-panel__new-chat", model.chatActionsOpen ? "is-active" : ""].filter(Boolean).join(" ")}
disabled={model.newChatDisabled}
onClick={actions.toggleChatActions}
onClick={actions.primary.toggleChatActions}
/>
<StatusButton model={model} actions={actions} />
<StatusButton model={model} actions={actions.primary} />
</div>
</div>
<ToolbarPanel model={model} actions={actions} />
@ -111,7 +131,7 @@ function ToolbarIconButton({
);
}
function StatusButton({ model, actions }: { model: ToolbarViewModel; actions: ToolbarActions }): UiNode {
function StatusButton({ model, actions }: { model: ToolbarViewModel; actions: ToolbarPrimaryActions }): UiNode {
return (
<ToolbarIconButton
icon="waypoints"
@ -131,14 +151,14 @@ function ToolbarPanel({ model, actions }: { model: ToolbarViewModel; actions: To
.join(" ")}
data-codex-panel-toolbar-panel={model.openPanel}
>
{model.openPanel === "history" ? <ThreadList threads={model.threads} actions={actions} /> : null}
{model.openPanel === "chat-actions" ? <ChatActionsPanel model={model} actions={actions} /> : null}
{model.openPanel === "status" ? <StatusPanel model={model} actions={actions} /> : null}
{model.openPanel === "history" ? <ThreadList threads={model.threads} actions={actions.threads} /> : null}
{model.openPanel === "chat-actions" ? <ChatActionsPanel model={model} actions={actions.chat} /> : null}
{model.openPanel === "status" ? <StatusPanel model={model} actions={actions.status} /> : null}
</div>
);
}
function ChatActionsPanel({ model, actions }: { model: ToolbarViewModel; actions: ToolbarActions }): UiNode {
function ChatActionsPanel({ model, actions }: { model: ToolbarViewModel; actions: ToolbarChatActions }): UiNode {
return (
<div className="codex-panel__chat-actions-panel-items">
<ToolbarPanelItem
@ -157,7 +177,7 @@ function ChatActionsPanel({ model, actions }: { model: ToolbarViewModel; actions
);
}
function StatusPanel({ model, actions }: { model: ToolbarViewModel; actions: ToolbarActions }): UiNode {
function StatusPanel({ model, actions }: { model: ToolbarViewModel; actions: ToolbarStatusActions }): UiNode {
return (
<>
<div className="codex-panel__status-panel-items">
@ -270,7 +290,7 @@ function diagnosticRowClassName(level: NonNullable<ToolbarDiagnosticRow["level"]
return "codex-panel__connection-diagnostics-row";
}
function ThreadList({ threads, actions }: { threads: ToolbarThreadRow[]; actions: ToolbarActions }): UiNode {
function ThreadList({ threads, actions }: { threads: ToolbarThreadRow[]; actions: ToolbarThreadActions }): UiNode {
if (threads.length === 0) {
return (
<div className="codex-panel__threads">
@ -292,7 +312,7 @@ function ThreadList({ threads, actions }: { threads: ToolbarThreadRow[]; actions
);
}
function ThreadListRow({ thread, actions }: { thread: ToolbarThreadRow; actions: ToolbarActions }): UiNode {
function ThreadListRow({ thread, actions }: { thread: ToolbarThreadRow; actions: ToolbarThreadActions }): UiNode {
const archiveConfirm = archiveConfirmState(thread);
return (
<div
@ -319,7 +339,7 @@ function ThreadListRow({ thread, actions }: { thread: ToolbarThreadRow; actions:
disabled={thread.disabled}
className="codex-panel__thread"
onClick={() => {
actions.resumeThread(thread.threadId);
actions.resume(thread.threadId);
}}
/>
{!archiveConfirm.active ? (
@ -330,7 +350,7 @@ function ThreadListRow({ thread, actions }: { thread: ToolbarThreadRow; actions:
disabled={thread.disabled}
onClick={(event) => {
event.stopPropagation();
actions.startRenameThread(thread.threadId);
actions.rename.start(thread.threadId);
}}
/>
) : null}
@ -341,7 +361,7 @@ function ThreadListRow({ thread, actions }: { thread: ToolbarThreadRow; actions:
);
}
function ArchiveControls({ thread, actions }: { thread: ToolbarThreadRow; actions: ToolbarActions }): UiNode {
function ArchiveControls({ thread, actions }: { thread: ToolbarThreadRow; actions: ToolbarThreadActions }): UiNode {
const archiveConfirm = archiveConfirmState(thread);
if (!archiveConfirm.active) {
return (
@ -352,7 +372,7 @@ function ArchiveControls({ thread, actions }: { thread: ToolbarThreadRow; action
disabled={thread.disabled}
onClick={(event) => {
event.stopPropagation();
actions.startArchiveThread(thread.threadId);
actions.archive.start(thread.threadId);
}}
/>
);
@ -375,7 +395,7 @@ function ArchiveModeButton({
thread: ToolbarThreadRow;
saveMarkdown: boolean;
primary: boolean;
actions: ToolbarActions;
actions: ToolbarThreadActions;
}): UiNode {
const label = saveMarkdown ? "Save and archive thread" : "Archive thread without saving";
return (
@ -386,13 +406,13 @@ function ArchiveModeButton({
disabled={thread.disabled}
onClick={(event) => {
event.stopPropagation();
actions.archiveThread(thread.threadId, saveMarkdown);
actions.archive.confirm(thread.threadId, saveMarkdown);
}}
/>
);
}
function ThreadRenameRow({ thread, actions }: { thread: ToolbarThreadRow; actions: ToolbarActions }): UiNode {
function ThreadRenameRow({ thread, actions }: { thread: ToolbarThreadRow; actions: ToolbarThreadActions }): UiNode {
const inputRef = useRef<HTMLInputElement | null>(null);
const generating = thread.rename?.generating ?? false;
const draft = thread.rename?.draft ?? thread.title;
@ -419,21 +439,21 @@ function ThreadRenameRow({ thread, actions }: { thread: ToolbarThreadRow; action
type="text"
value={draft}
onInput={(event) => {
actions.updateRenameDraft(thread.threadId, event.currentTarget.value);
actions.rename.updateDraft(thread.threadId, event.currentTarget.value);
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
if (!event.isComposing && !generating) actions.saveRenameThread(thread.threadId, event.currentTarget.value);
if (!event.isComposing && !generating) actions.rename.save(thread.threadId, event.currentTarget.value);
return;
}
if (event.key === "Escape") {
event.preventDefault();
actions.cancelRenameThread(thread.threadId);
actions.rename.cancel(thread.threadId);
}
}}
onBlur={(event) => {
if (!generating) actions.saveRenameThread(thread.threadId, event.currentTarget.value);
if (!generating) actions.rename.save(thread.threadId, event.currentTarget.value);
}}
/>
</div>
@ -450,7 +470,7 @@ function ThreadRenameRow({ thread, actions }: { thread: ToolbarThreadRow; action
}}
onClick={(event) => {
event.stopPropagation();
actions.autoNameThread(thread.threadId);
actions.rename.autoName(thread.threadId);
}}
/>
</>

View file

@ -470,23 +470,35 @@ const noOpMessageStreamScrollController: MessageStreamScrollControllerBinding =
function toolbarActionsFixture(): ChatPanelShellParts["toolbar"]["actions"] {
return {
startNewThread: vi.fn(),
toggleChatActions: vi.fn(),
compactConversation: vi.fn(),
setGoal: vi.fn(),
toggleHistory: vi.fn(),
toggleStatusPanel: vi.fn(),
connect: vi.fn(),
refreshStatus: vi.fn(),
copyDebugDetails: vi.fn(),
resumeThread: vi.fn(),
startArchiveThread: vi.fn(),
archiveThread: vi.fn(),
startRenameThread: vi.fn(),
updateRenameDraft: vi.fn(),
saveRenameThread: vi.fn(),
cancelRenameThread: vi.fn(),
autoNameThread: vi.fn(),
primary: {
toggleHistory: vi.fn(),
toggleChatActions: vi.fn(),
toggleStatusPanel: vi.fn(),
},
chat: {
startNewThread: vi.fn(),
compactConversation: vi.fn(),
setGoal: vi.fn(),
},
status: {
connect: vi.fn(),
refreshStatus: vi.fn(),
copyDebugDetails: vi.fn(),
},
threads: {
resume: vi.fn(),
archive: {
start: vi.fn(),
confirm: vi.fn(),
},
rename: {
start: vi.fn(),
updateDraft: vi.fn(),
save: vi.fn(),
cancel: vi.fn(),
autoName: vi.fn(),
},
},
};
}

View file

@ -445,26 +445,41 @@ function toolbarSurfaceFixture(overrides: { archiveExportEnabled?: boolean } = {
};
}
function toolbarActionsFixture(overrides: Partial<ToolbarActions> = {}): ToolbarActions {
interface ToolbarActionOverrides {
copyDebugDetails?: (details: string) => void;
}
function toolbarActionsFixture(overrides: ToolbarActionOverrides = {}): ToolbarActions {
return {
startNewThread: () => undefined,
toggleChatActions: () => undefined,
compactConversation: () => undefined,
setGoal: () => undefined,
toggleHistory: () => undefined,
toggleStatusPanel: () => undefined,
connect: () => undefined,
refreshStatus: () => undefined,
copyDebugDetails: () => undefined,
resumeThread: () => undefined,
startArchiveThread: () => undefined,
archiveThread: () => undefined,
startRenameThread: () => undefined,
updateRenameDraft: () => undefined,
saveRenameThread: () => undefined,
cancelRenameThread: () => undefined,
autoNameThread: () => undefined,
...overrides,
primary: {
toggleHistory: () => undefined,
toggleChatActions: () => undefined,
toggleStatusPanel: () => undefined,
},
chat: {
startNewThread: () => undefined,
compactConversation: () => undefined,
setGoal: () => undefined,
},
status: {
connect: () => undefined,
refreshStatus: () => undefined,
copyDebugDetails: overrides.copyDebugDetails ?? (() => undefined),
},
threads: {
resume: () => undefined,
archive: {
start: () => undefined,
confirm: () => undefined,
},
rename: {
start: () => undefined,
updateDraft: () => undefined,
save: () => undefined,
cancel: () => undefined,
autoName: () => undefined,
},
},
};
}

View file

@ -5,7 +5,7 @@ import { describe, expect, it, vi } from "vitest";
import { createChatState } from "../../../../src/features/chat/application/state/root-reducer";
import { createChatStateStore } from "../../../../src/features/chat/application/state/store";
import type { ThreadManagementActions } from "../../../../src/features/chat/application/threads/thread-management-actions";
import { createChatPanelToolbarActions, createToolbarPanelActions } from "../../../../src/features/chat/panel/toolbar-actions";
import { createToolbarPanelActions, createToolbarUiActions } from "../../../../src/features/chat/panel/toolbar-actions";
describe("createToolbarPanelActions", () => {
it("tracks archive confirmation and delegates archive actions", async () => {
@ -45,39 +45,75 @@ describe("createToolbarPanelActions", () => {
});
});
describe("createChatPanelToolbarActions", () => {
describe("createToolbarUiActions", () => {
it("maps primary toolbar controls to panel actions", () => {
const deps = toolbarActionDeps();
const actions = createToolbarUiActions(deps);
actions.primary.toggleHistory();
actions.primary.toggleChatActions();
actions.primary.toggleStatusPanel();
expect(vi.mocked(deps.toolbarPanel.toggleHistory)).toHaveBeenCalledOnce();
expect(vi.mocked(deps.toolbarPanel.toggleChatActions)).toHaveBeenCalledOnce();
expect(vi.mocked(deps.toolbarPanel.toggleStatus)).toHaveBeenCalledOnce();
});
it("starts a new thread through thread navigation", () => {
const deps = toolbarActionDeps();
const actions = createChatPanelToolbarActions(deps);
const actions = createToolbarUiActions(deps);
actions.startNewThread();
actions.chat.startNewThread();
expect(vi.mocked(deps.navigation.startNewThread)).toHaveBeenCalledOnce();
});
it("delegates compacting the active thread", () => {
const deps = toolbarActionDeps();
const actions = createChatPanelToolbarActions(deps);
const actions = createToolbarUiActions(deps);
actions.compactConversation();
actions.chat.compactConversation();
expect(vi.mocked(deps.threadActions.compactActiveThread)).toHaveBeenCalledOnce();
});
it("starts the goal editor from the active goal", () => {
const deps = toolbarActionDeps();
const actions = createChatPanelToolbarActions(deps);
const actions = createToolbarUiActions(deps);
actions.setGoal();
actions.chat.setGoal();
expect(vi.mocked(deps.goals.startEditingCurrent)).toHaveBeenCalledOnce();
});
it("maps thread list actions to navigation, archive, and rename actions", () => {
const deps = toolbarActionDeps();
const actions = createToolbarUiActions(deps);
actions.threads.resume("thread");
actions.threads.archive.start("thread");
actions.threads.archive.confirm("thread", true);
actions.threads.rename.start("thread");
actions.threads.rename.updateDraft("thread", "Draft");
actions.threads.rename.save("thread", "Saved");
actions.threads.rename.cancel("thread");
actions.threads.rename.autoName("thread");
expect(vi.mocked(deps.navigation.selectThreadFromToolbar)).toHaveBeenCalledWith("thread");
expect(vi.mocked(deps.toolbarPanel.startArchive)).toHaveBeenCalledWith("thread");
expect(vi.mocked(deps.toolbarPanel.archiveThread)).toHaveBeenCalledWith("thread", true);
expect(vi.mocked(deps.rename.start)).toHaveBeenCalledWith("thread");
expect(vi.mocked(deps.rename.updateDraft)).toHaveBeenCalledWith("thread", "Draft");
expect(vi.mocked(deps.rename.save)).toHaveBeenCalledWith("thread", "Saved");
expect(vi.mocked(deps.rename.cancel)).toHaveBeenCalledWith("thread");
expect(vi.mocked(deps.rename.autoNameDraft)).toHaveBeenCalledWith("thread");
});
});
function toolbarActionDeps(): Parameters<typeof createChatPanelToolbarActions>[0] {
function toolbarActionDeps(): Parameters<typeof createToolbarUiActions>[0] {
return {
connectionController: { refreshStatusPanel: vi.fn() } as unknown as Parameters<
typeof createChatPanelToolbarActions
typeof createToolbarUiActions
>[0]["connectionController"],
reconnectPanel: vi.fn(),
threadActions: {
@ -87,24 +123,24 @@ function toolbarActionDeps(): Parameters<typeof createChatPanelToolbarActions>[0
} as unknown as ThreadManagementActions,
goals: {
startEditingCurrent: vi.fn(),
} as unknown as Parameters<typeof createChatPanelToolbarActions>[0]["goals"],
toolbarPanels: {
} as unknown as Parameters<typeof createToolbarUiActions>[0]["goals"],
toolbarPanel: {
toggleChatActions: vi.fn(),
toggleHistory: vi.fn(),
toggleStatus: vi.fn(),
startArchive: vi.fn(),
archiveThread: vi.fn().mockResolvedValue(undefined),
} as unknown as Parameters<typeof createChatPanelToolbarActions>[0]["toolbarPanels"],
} as unknown as Parameters<typeof createToolbarUiActions>[0]["toolbarPanel"],
rename: {
start: vi.fn(),
updateDraft: vi.fn(),
save: vi.fn().mockResolvedValue(undefined),
cancel: vi.fn(),
autoNameDraft: vi.fn().mockResolvedValue(undefined),
} as unknown as Parameters<typeof createChatPanelToolbarActions>[0]["rename"],
} as unknown as Parameters<typeof createToolbarUiActions>[0]["rename"],
navigation: {
startNewThread: vi.fn().mockResolvedValue(undefined),
selectThreadFromToolbar: vi.fn().mockResolvedValue(undefined),
} as unknown as Parameters<typeof createChatPanelToolbarActions>[0]["navigation"],
} as unknown as Parameters<typeof createToolbarUiActions>[0]["navigation"],
};
}

View file

@ -68,31 +68,43 @@ function toolbarSurface(_store: ReturnType<typeof createChatStateStore>, _toolba
};
}
function shellParts(store: ReturnType<typeof createChatStateStore>, toolbarActions: ToolbarPanelActions): ChatPanelShellParts {
const surface = surfaceFixture(store, toolbarActions);
function shellParts(store: ReturnType<typeof createChatStateStore>, toolbarPanelActions: ToolbarPanelActions): ChatPanelShellParts {
const surface = surfaceFixture(store, toolbarPanelActions);
return {
toolbar: {
surface: surface.toolbar,
actions: {
startNewThread: vi.fn(),
toggleChatActions: vi.fn(),
compactConversation: vi.fn(),
setGoal: vi.fn(),
toggleHistory: vi.fn(),
toggleStatusPanel: vi.fn(),
connect: vi.fn(),
refreshStatus: vi.fn(),
copyDebugDetails: vi.fn(),
resumeThread: vi.fn(),
startArchiveThread: (threadId) => {
toolbarActions.startArchive(threadId);
primary: {
toggleHistory: vi.fn(),
toggleChatActions: vi.fn(),
toggleStatusPanel: vi.fn(),
},
chat: {
startNewThread: vi.fn(),
compactConversation: vi.fn(),
setGoal: vi.fn(),
},
status: {
connect: vi.fn(),
refreshStatus: vi.fn(),
copyDebugDetails: vi.fn(),
},
threads: {
resume: vi.fn(),
archive: {
start: (threadId) => {
toolbarPanelActions.startArchive(threadId);
},
confirm: vi.fn(),
},
rename: {
start: vi.fn(),
updateDraft: vi.fn(),
save: vi.fn(),
cancel: vi.fn(),
autoName: vi.fn(),
},
},
archiveThread: vi.fn(),
startRenameThread: vi.fn(),
updateRenameDraft: vi.fn(),
saveRenameThread: vi.fn(),
cancelRenameThread: vi.fn(),
autoNameThread: vi.fn(),
},
},
goal: surface.goal,

View file

@ -368,25 +368,56 @@ function toolbarModel(overrides: Partial<ToolbarViewModel> = {}): ToolbarViewMod
};
}
function toolbarActions(overrides: Partial<ToolbarActions> = {}): ToolbarActions {
interface ToolbarActionOverrides {
toggleHistory?: () => void;
startNewThread?: () => void;
toggleChatActions?: () => void;
compactConversation?: () => void;
setGoal?: () => void;
toggleStatusPanel?: () => void;
connect?: () => void;
refreshStatus?: () => void;
copyDebugDetails?: (details: string) => void;
resumeThread?: (threadId: string) => void;
startArchiveThread?: (threadId: string) => void;
archiveThread?: (threadId: string, saveMarkdown: boolean) => void;
startRenameThread?: (threadId: string) => void;
updateRenameDraft?: (threadId: string, value: string) => void;
saveRenameThread?: (threadId: string, value: string) => void;
cancelRenameThread?: (threadId: string) => void;
autoNameThread?: (threadId: string) => void;
}
function toolbarActions(overrides: ToolbarActionOverrides = {}): ToolbarActions {
return {
toggleHistory: vi.fn(),
startNewThread: vi.fn(),
toggleChatActions: vi.fn(),
compactConversation: vi.fn(),
setGoal: vi.fn(),
toggleStatusPanel: vi.fn(),
connect: vi.fn(),
refreshStatus: vi.fn(),
copyDebugDetails: vi.fn(),
resumeThread: vi.fn(),
startArchiveThread: vi.fn(),
archiveThread: vi.fn(),
startRenameThread: vi.fn(),
updateRenameDraft: vi.fn(),
saveRenameThread: vi.fn(),
cancelRenameThread: vi.fn(),
autoNameThread: vi.fn(),
...overrides,
primary: {
toggleHistory: overrides.toggleHistory ?? vi.fn(),
toggleChatActions: overrides.toggleChatActions ?? vi.fn(),
toggleStatusPanel: overrides.toggleStatusPanel ?? vi.fn(),
},
chat: {
startNewThread: overrides.startNewThread ?? vi.fn(),
compactConversation: overrides.compactConversation ?? vi.fn(),
setGoal: overrides.setGoal ?? vi.fn(),
},
status: {
connect: overrides.connect ?? vi.fn(),
refreshStatus: overrides.refreshStatus ?? vi.fn(),
copyDebugDetails: overrides.copyDebugDetails ?? vi.fn(),
},
threads: {
resume: overrides.resumeThread ?? vi.fn(),
archive: {
start: overrides.startArchiveThread ?? vi.fn(),
confirm: overrides.archiveThread ?? vi.fn(),
},
rename: {
start: overrides.startRenameThread ?? vi.fn(),
updateDraft: overrides.updateRenameDraft ?? vi.fn(),
save: overrides.saveRenameThread ?? vi.fn(),
cancel: overrides.cancelRenameThread ?? vi.fn(),
autoName: overrides.autoNameThread ?? vi.fn(),
},
},
};
}