From 935bd21eb3b0ed7f051ea9e6ea2dab5761bd031b Mon Sep 17 00:00:00 2001 From: Angus Peart Date: Fri, 16 Oct 2026 16:08:41 -0700 Subject: [PATCH] fix: open new chats in separate tabs and show them immediately --- .../design/chat-creation/new-chat-history.png | Bin 0 -> 22506 bytes program/plugins/hotkeys-composer.client.ts | 8 +- program/plugins/sidebar.client.tsx | 43 +++++- program/plugins/sidebar.css | 6 + program/plugins/ui/history.tsx | 7 +- program/plugins/workspace-layout-api.ts | 16 ++- program/plugins/workspace-layout.client.tsx | 27 +++- tests/chat-history.test.ts | 17 +++ tests/fixtures/workspace-chat-creation.cjs | 129 ++++++++++++++++++ tests/hotkeys.test.ts | 23 +++- tests/sidebar.test.ts | 40 ++++++ tests/workspace-chat-creation-browser.test.ts | 16 +++ 12 files changed, 310 insertions(+), 22 deletions(-) create mode 100644 docs/design/chat-creation/new-chat-history.png create mode 100644 tests/fixtures/workspace-chat-creation.cjs create mode 100644 tests/workspace-chat-creation-browser.test.ts diff --git a/docs/design/chat-creation/new-chat-history.png b/docs/design/chat-creation/new-chat-history.png new file mode 100644 index 0000000000000000000000000000000000000000..37105d70d826b95f90318298ab39ecedb98edbb2 GIT binary patch literal 22506 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 AG5`Po literal 0 HcmV?d00001 diff --git a/program/plugins/hotkeys-composer.client.ts b/program/plugins/hotkeys-composer.client.ts index f87afd4..d56bfe2 100644 --- a/program/plugins/hotkeys-composer.client.ts +++ b/program/plugins/hotkeys-composer.client.ts @@ -83,13 +83,13 @@ const hotkeysComposer: Plugin = (ctx) => { ctx.clientHotkeys.registerAction(ctx, { id: 'chat.new', label: 'New chat', - detail: 'Start a blank task in the current workspace.', + detail: 'Open a new chat tab.', category: 'Navigation', binding: { kind: 'leader', key: 'n' }, - enabled: () => ctx.clientConversation.snapshot().turn.tag === 'idle', + enabled: () => ctx.clientWorkspaceLayout.available(), run: () => { ctx.clientUi.overlays.closeAll() - ctx.clientConversation.newThread() + ctx.clientWorkspaceLayout.newTab() }, }) @@ -106,6 +106,6 @@ const hotkeysComposer: Plugin = (ctx) => { }) } -hotkeysComposer.inject = ['clientHotkeys', 'clientComposer', 'clientConversation', 'clientUi'] +hotkeysComposer.inject = ['clientHotkeys', 'clientComposer', 'clientConversation', 'clientUi', 'clientWorkspaceLayout'] export default hotkeysComposer diff --git a/program/plugins/sidebar.client.tsx b/program/plugins/sidebar.client.tsx index 585357c..e0fcaf1 100644 --- a/program/plugins/sidebar.client.tsx +++ b/program/plugins/sidebar.client.tsx @@ -35,6 +35,8 @@ import { threadWorkStatus, } from './thread-status-api.js' import { HistoryPanel } from './ui/history.js' +import type { HistoryState } from './ui/history-model.js' +import { requestWorkspaceNewChat, type WorkspaceTabTarget } from './workspace-layout-api.js' import { NewWorkspaceDialog } from './ui/project-settings.js' import { useStoreSelector } from './ui/store-selector.js' import styles from './sidebar.css' @@ -282,6 +284,28 @@ interface SidebarHistoryDataSnapshot { projects: ClientSessionSnapshot['projects'] } +/** Keep chats visible while the global history list catches up with a new pane. */ +export function historyWithOpenWorkspaceThreads( + history: HistoryState, + tabs: readonly WorkspaceTabTarget[], +): HistoryState { + const entries = new Map(history.entries.map((thread) => [thread.id, thread])) + let changed = false + for (const tab of tabs) { + for (const thread of tab.threads ?? []) { + const listed = entries.get(thread.id) + if (!listed) { + entries.set(thread.id, thread) + changed = true + } else if (thread.projectId && !listed.projectId) { + entries.set(thread.id, { ...listed, projectId: thread.projectId }) + changed = true + } + } + } + return changed ? { ...history, entries: [...entries.values()] } : history +} + function sidebarHistoryDataSnapshot(state: ClientSessionSnapshot): SidebarHistoryDataSnapshot { return { history: state.history, projects: state.projects } } @@ -469,7 +493,6 @@ const sidebarClient: BrowserPlugin = (ctx, config) => { } function NewThreadSurface({ surface }: ClientSurfaceProps): ReactNode { - const turn = useStoreSelector(session, turnTag) const { labeled, buttonClass } = appearance(surface) const label = surface.label ?? 'New chat' return ( @@ -480,11 +503,9 @@ const sidebarClient: BrowserPlugin = (ctx, config) => { data-hotkey-action="chat.new" title={label} aria-label={label} - disabled={turn !== 'idle'} onClick={() => { overlays.closeAll() - session.newThread() - if (controller.snapshot().mode === 'floating') controller.setCollapsed(true) + if (requestWorkspaceNewChat() && controller.snapshot().mode === 'floating') controller.setCollapsed(true) }} > @@ -517,6 +538,7 @@ const sidebarClient: BrowserPlugin = (ctx, config) => { } function HistorySurface({ surface }: ClientSurfaceProps): ReactNode { + const [newThreadProblem, setNewThreadProblem] = useState('') const historyState = useStoreSelector( historySession, sidebarHistoryDataSnapshot, @@ -527,6 +549,7 @@ const sidebarClient: BrowserPlugin = (ctx, config) => { sidebarSelectionSnapshot, sidebarSelectionSnapshotEqual, ) + useSyncExternalStore(ctx.clientWorkspaceLayout.subscribe, ctx.clientWorkspaceLayout.snapshot) const statusState = useSyncExternalStore(threadStatus.subscribe, threadStatus.snapshot) useSyncExternalStore(ctx.clientUi.subscribe, ctx.clientUi.snapshot) const EntryDecoration = ctx.clientUi.component( @@ -535,11 +558,13 @@ const sidebarClient: BrowserPlugin = (ctx, config) => { return ( threadWorkStatus(statusState, threadId)} @@ -547,8 +572,12 @@ const sidebarClient: BrowserPlugin = (ctx, config) => { onNewWorkspace={() => overlays.open('new-workspace')} onClose={() => controller.setCollapsed(true)} onNewThread={(project) => { + if (!requestWorkspaceNewChat(project.id)) { + setNewThreadProblem('Workspace tabs are not ready. Try again in a moment.') + return + } + setNewThreadProblem('') overlays.closeAll() - session.newThread(project) if (controller.snapshot().mode === 'floating') controller.setCollapsed(true) }} onOpen={(thread) => { @@ -686,7 +715,7 @@ const sidebarClient: BrowserPlugin = (ctx, config) => { } } -sidebarClient.inject = ['clientUi', 'clientSession', 'clientSessionRouter'] +sidebarClient.inject = ['clientUi', 'clientSession', 'clientSessionRouter', 'clientWorkspaceLayout'] sidebarClient.provide = 'clientSidebar' sidebarClient.resources = { provides: { diff --git a/program/plugins/sidebar.css b/program/plugins/sidebar.css index ecb528b..7647f40 100644 --- a/program/plugins/sidebar.css +++ b/program/plugins/sidebar.css @@ -448,3 +448,9 @@ html[data-alto-theme='dark'] .history-entry.active { box-shadow: var(--shadow-popover); } } + +.history-new-thread-error { + margin: var(--space-2) var(--space-3); + color: var(--danger); + font-size: var(--type-caption); +} diff --git a/program/plugins/ui/history.tsx b/program/plugins/ui/history.tsx index c0f8140..4ffde4a 100644 --- a/program/plugins/ui/history.tsx +++ b/program/plugins/ui/history.tsx @@ -152,6 +152,8 @@ export function HistoryPanel({ activeProjectId, projects, disabled, + newThreadDisabled = disabled, + newThreadProblem, showAge = false, emptyText = 'No conversations yet', statusFor, @@ -169,6 +171,8 @@ export function HistoryPanel({ activeProjectId?: string projects: LocalProject[] disabled: boolean + newThreadDisabled?: boolean + newThreadProblem?: string showAge?: boolean emptyText?: string statusFor?: (threadId: string) => ThreadWorkStatus | undefined @@ -273,6 +277,7 @@ export function HistoryPanel({ )} )} + {newThreadProblem &&

{newThreadProblem}

}
{groups.map((group) => { const project = group.project @@ -309,7 +314,7 @@ export function HistoryPanel({ type="button" title={`New chat in ${group.label}`} aria-label={`New chat in ${group.label}`} - disabled={disabled} + disabled={newThreadDisabled} onClick={() => onNewThread(project)} > diff --git a/program/plugins/workspace-layout-api.ts b/program/plugins/workspace-layout-api.ts index 69e7ccf..06d8508 100644 --- a/program/plugins/workspace-layout-api.ts +++ b/program/plugins/workspace-layout-api.ts @@ -1,5 +1,5 @@ import type { ComponentType, Dispatch, KeyboardEvent, MouseEvent, ReactNode, RefObject, SetStateAction } from 'react' -import type { ThreadSummary } from '../../src/shared/protocol.js' +import type { LocalProject, ThreadSummary } from '../../src/shared/protocol.js' import type { ClientSessionService } from './session-api.js' import type { WorkspaceOpenPaneRequest, @@ -148,6 +148,18 @@ export interface WorkspaceTabAction { run(tab: WorkspaceView): void } +export const WORKSPACE_NEW_CHAT_EVENT = 'alto:workspace-new-chat' + +/** Request a tab from the mounted compositor and report whether it handled the click. */ +export function requestWorkspaceNewChat(projectId?: string): boolean { + const event = new CustomEvent<{ projectId?: string }>(WORKSPACE_NEW_CHAT_EVENT, { + cancelable: true, + detail: projectId ? { projectId } : {}, + }) + window.dispatchEvent(event) + return event.defaultPrevented +} + export interface ClientWorkspaceLayoutService { subscribe(listener: () => void): () => void snapshot(): number @@ -167,7 +179,7 @@ export interface ClientWorkspaceLayoutService { tabs(): readonly WorkspaceTabTarget[] moveActiveTab(direction: 1 | -1): void /** Open a full-size tab; omit kind for the normal new-chat behavior. */ - newTab(kind?: string, thread?: ThreadSummary): void + newTab(kind?: string, thread?: ThreadSummary, project?: LocalProject | null): void showThreadInPane(workspaceId: string, paneId: string, thread: ThreadSummary): void openPane( request: Omit, diff --git a/program/plugins/workspace-layout.client.tsx b/program/plugins/workspace-layout.client.tsx index 064c20e..6068b5c 100644 --- a/program/plugins/workspace-layout.client.tsx +++ b/program/plugins/workspace-layout.client.tsx @@ -79,6 +79,7 @@ import type { WorkspaceTabStrip, WorkspaceTabStripProps, } from './workspace-layout-api.js' +import { WORKSPACE_NEW_CHAT_EVENT } from './workspace-layout-api.js' import type { WorkspaceOpenPaneRequest, WorkspaceOpenPaneResult, @@ -228,7 +229,7 @@ interface WorkspaceLayoutController { selectTab(index: number): void tabs(): readonly WorkspaceTabTarget[] moveActiveTab(direction: 1 | -1): void - newTab(kind?: string, thread?: ThreadSummary): void + newTab(kind?: string, thread?: ThreadSummary, project?: LocalProject | null): void showThreadInPane(workspaceId: string, paneId: string, thread: ThreadSummary): void openPane( request: Omit, @@ -368,8 +369,9 @@ export class WorkspaceLayoutRegistry implements ClientWorkspaceLayoutService { this.controller?.moveActiveTab(direction) } - newTab(kind?: string, thread?: ThreadSummary): void { - if (thread) this.controller?.newTab(kind, thread) + newTab(kind?: string, thread?: ThreadSummary, project?: LocalProject | null): void { + if (project !== undefined) this.controller?.newTab(kind, thread, project) + else if (thread) this.controller?.newTab(kind, thread) else this.controller?.newTab(kind) } @@ -1592,7 +1594,7 @@ function WorkspaceLayoutControllerBridge({ selectTab: (index: number) => void tabs: () => readonly WorkspaceTabTarget[] moveActiveTab: (direction: 1 | -1) => void - newTab: (kind?: string, thread?: ThreadSummary) => void + newTab: (kind?: string, thread?: ThreadSummary, project?: LocalProject | null) => void showThreadInPane: (workspaceId: string, paneId: string, thread: ThreadSummary) => void openPane: ( request: Omit, @@ -2642,6 +2644,21 @@ function WorkspaceLayout({ })) } + useEffect(() => { + const newChat = (event: Event): void => { + const request = event as CustomEvent<{ projectId?: string }> + const projectId = request.detail?.projectId + const project = projectId + ? globalSession.snapshot().projects.find((candidate) => candidate.id === projectId) + : undefined + if (projectId && !project) return + addView(project) + request.preventDefault() + } + window.addEventListener(WORKSPACE_NEW_CHAT_EVENT, newChat) + return () => window.removeEventListener(WORKSPACE_NEW_CHAT_EVENT, newChat) + }, [addView, globalSession]) + const splitFocused = (direction: WorkspaceSplitDirection, kind?: string): void => { registry.showPage() const paneId = activeView.focusedPaneId @@ -2981,7 +2998,7 @@ function WorkspaceLayout({ paneKinds: workspaceViewPaneKinds(view), }))} moveActiveTab={moveActiveTab} - newTab={(kind, thread) => addView(undefined, kind, thread)} + newTab={(kind, thread, project) => addView(project, kind, thread)} showThreadInPane={showThreadInPane} openPane={openPane} splitFocused={splitFocused} diff --git a/tests/chat-history.test.ts b/tests/chat-history.test.ts index 277903e..ec020af 100644 --- a/tests/chat-history.test.ts +++ b/tests/chat-history.test.ts @@ -30,6 +30,23 @@ describe('chat history pages', () => { expect(newTab).toHaveBeenCalledTimes(2) }) + it('forwards an explicit project or projectless chat without changing the selected thread', () => { + const registry = new WorkspaceLayoutRegistry() + const newTab = vi.fn() + const unbind = registry.bindController({ tabs: () => [], paneTargets: () => [], newTab } as unknown as Parameters[0]) + const project = { id: 'atlas', name: 'Atlas', primaryRoot: '/repo/atlas', roots: ['/repo/atlas'] } + const thread = chat('existing', 10) + registry.newTab(undefined, undefined, project) + registry.newTab(undefined, undefined, null) + registry.newTab(undefined, thread) + expect(newTab.mock.calls).toEqual([ + [undefined, undefined, project], + [undefined, undefined, null], + [undefined, thread], + ]) + unbind() + }) + it('requests one cursor page of top-level chats and uses the shared summary parser and workspace classifier', async () => { const request = vi.fn().mockResolvedValue({ data: [ { id: 'a', name: 'Actual chat name', preview: 'First message', cwd: '/repo', createdAt: 10, updatedAt: 20 }, diff --git a/tests/fixtures/workspace-chat-creation.cjs b/tests/fixtures/workspace-chat-creation.cjs new file mode 100644 index 0000000..5fcfe97 --- /dev/null +++ b/tests/fixtures/workspace-chat-creation.cjs @@ -0,0 +1,129 @@ +const { app, BrowserWindow } = require('./background-electron.cjs') +const { build } = require('esbuild') +const assert = require('node:assert/strict') +const fs = require('node:fs/promises') +const http = require('node:http') +const path = require('node:path') + +async function check() { + await app.whenReady() + const bundle = await build({ + stdin: { resolveDir: process.cwd(), sourcefile: 'workspace-chat-creation.tsx', loader: 'tsx', contents: ` + import React,{useEffect,useRef} from 'react'; + import {createRoot} from 'react-dom/client'; + import {flushSync} from 'react-dom'; + import layoutPlugin from './program/plugins/workspace-layout.client'; + import sidebarPlugin from './program/plugins/sidebar.client'; + const project={id:'atlas',name:'Atlas',primaryRoot:'/repo/atlas',roots:['/repo/atlas']}; + const original={id:'working',title:'Agent is working',preview:'',cwd:project.primaryRoot,createdAt:1,updatedAt:1,projectId:project.id}; + let saved={version:2,activeViewId:'working-tab',views:[{id:'working-tab',name:original.title,workspace:original.cwd,projectId:project.id, + focusedPaneId:'working-pane',root:{type:'pane',id:'working-pane',workspace:original.cwd,projectId:project.id,thread:original}}]}; + localStorage.setItem('codex-cordis.workspace-layout',JSON.stringify(saved)); + const state={connected:true,session:{workspace:original.cwd,permissionMode:'ask'},projects:[project],threads:[original],threadId:original.id, + activeProjectId:project.id,projectScope:'workspace',skills:[],turn:{tag:'running'},activities:[],history:{tag:'loading',entries:[]}, + harness:{extensions:{'session.workspace':'/tmp/alto-scratch'},codex:{status:'ready',models:[]},ui:{surfaces:[],contributions:[],regions:[]}}}; + const globalSession={snapshot:()=>state,subscribe:()=>()=>{},newThread:()=>{throw Error('Replaced the working chat')},openThread:()=>{throw Error('Replaced the working chat')}}; + const host={snapshot:()=>({connectionEpoch:1}),subscribe:()=>()=>{},call:async(method,payload)=>{ + if(method.endsWith('.read'))return structuredClone(saved); + if(method.endsWith('.write'))saved=structuredClone(payload); + }}; + const surfaces=new Map(),sessions=[];let registry,layoutRoot; + const Composer=({autoFocus,session})=>{const input=useRef(null);useEffect(()=>{if(autoFocus)input.current.focus()},[autoFocus]); + return