@@ -2110,6 +2110,48 @@ describe("ChatView timeline estimator parity (full app)", () => {
21102110 }
21112111 } ) ;
21122112
2113+ it ( "keeps the compact chat header on one row" , async ( ) => {
2114+ const mounted = await mountChatView ( {
2115+ viewport : COMPACT_FOOTER_VIEWPORT ,
2116+ snapshot : createSnapshotForTargetUser ( {
2117+ targetMessageId : "msg-user-compact-header" as MessageId ,
2118+ targetText : "keep the compact header aligned" ,
2119+ } ) ,
2120+ } ) ;
2121+
2122+ try {
2123+ const chatHeader = await waitForElement (
2124+ ( ) => document . querySelector < HTMLElement > ( "[data-chat-header]" ) ,
2125+ "Unable to find chat header." ,
2126+ ) ;
2127+ const threadTitle = await waitForElement (
2128+ ( ) => chatHeader . querySelector < HTMLElement > ( "h2" ) ,
2129+ "Unable to find thread title." ,
2130+ ) ;
2131+ const headerActions = await waitForElement (
2132+ ( ) => document . querySelector < HTMLElement > ( "[data-chat-header-actions]" ) ,
2133+ "Unable to find chat header actions." ,
2134+ ) ;
2135+
2136+ const headerRect = chatHeader . getBoundingClientRect ( ) ;
2137+ const titleRect = threadTitle . getBoundingClientRect ( ) ;
2138+ const actionsRect = headerActions . getBoundingClientRect ( ) ;
2139+ const headerCenter = headerRect . top + headerRect . height / 2 ;
2140+
2141+ expect ( headerRect . height ) . toBe ( 52 ) ;
2142+ expect ( titleRect . top ) . toBeGreaterThanOrEqual ( headerRect . top ) ;
2143+ expect ( titleRect . bottom ) . toBeLessThanOrEqual ( headerRect . bottom ) ;
2144+ expect ( actionsRect . top ) . toBeGreaterThanOrEqual ( headerRect . top ) ;
2145+ expect ( actionsRect . bottom ) . toBeLessThanOrEqual ( headerRect . bottom ) ;
2146+ expect ( Math . abs ( titleRect . top + titleRect . height / 2 - headerCenter ) ) . toBeLessThanOrEqual ( 1 ) ;
2147+ expect ( Math . abs ( actionsRect . top + actionsRect . height / 2 - headerCenter ) ) . toBeLessThanOrEqual (
2148+ 1 ,
2149+ ) ;
2150+ } finally {
2151+ await mounted . cleanup ( ) ;
2152+ }
2153+ } ) ;
2154+
21132155 it ( "keeps panel toggles fixed and can maximize the right panel" , async ( ) => {
21142156 const mounted = await mountChatView ( {
21152157 viewport : WIDE_FOOTER_VIEWPORT ,
@@ -2276,6 +2318,151 @@ describe("ChatView timeline estimator parity (full app)", () => {
22762318 }
22772319 } ) ;
22782320
2321+ it ( "renders the plan surface in the inline right panel" , async ( ) => {
2322+ useRightPanelStore . getState ( ) . open ( THREAD_REF , "plan" ) ;
2323+
2324+ const mounted = await mountChatView ( {
2325+ viewport : WIDE_FOOTER_VIEWPORT ,
2326+ snapshot : createSnapshotForTargetUser ( {
2327+ targetMessageId : "msg-user-inline-plan-panel" as MessageId ,
2328+ targetText : "show the inline plan panel" ,
2329+ } ) ,
2330+ } ) ;
2331+
2332+ try {
2333+ await waitForElement (
2334+ ( ) =>
2335+ Array . from ( document . querySelectorAll < HTMLElement > ( "p" ) ) . find (
2336+ ( element ) => element . textContent ?. trim ( ) === "No active plan yet." ,
2337+ ) ?? null ,
2338+ "Unable to find inline plan panel content." ,
2339+ ) ;
2340+
2341+ expect (
2342+ document . querySelector < HTMLElement > ( "[data-right-panel-tabbar]" ) ?. textContent ,
2343+ ) . toContain ( "Plan" ) ;
2344+ expect ( document . body . textContent ) . toContain ( "Plans will appear here when generated." ) ;
2345+ } finally {
2346+ await mounted . cleanup ( ) ;
2347+ }
2348+ } ) ;
2349+
2350+ it ( "renders the shared panel toggles in the responsive right-panel sheet" , async ( ) => {
2351+ useRightPanelStore . getState ( ) . open ( THREAD_REF , "plan" ) ;
2352+ useRightPanelStore . getState ( ) . openTerminal ( THREAD_REF , DEFAULT_TERMINAL_ID ) ;
2353+ useRightPanelStore . getState ( ) . activateSurface ( THREAD_REF , "plan" ) ;
2354+ const baseSnapshot = createSnapshotForTargetUser ( {
2355+ targetMessageId : "msg-user-responsive-plan-panel-controls" as MessageId ,
2356+ targetText : "show responsive plan panel controls" ,
2357+ } ) ;
2358+ const snapshot : OrchestrationReadModel = {
2359+ ...baseSnapshot ,
2360+ threads : baseSnapshot . threads . map ( ( thread ) =>
2361+ thread . id === THREAD_ID
2362+ ? {
2363+ ...thread ,
2364+ activities : [
2365+ {
2366+ id : EventId . make ( "activity-responsive-panel-plan" ) ,
2367+ tone : "info" ,
2368+ kind : "turn.plan.updated" ,
2369+ summary : "Plan updated" ,
2370+ payload : {
2371+ explanation : "Claude Tasks" ,
2372+ plan : [ { step : "Keep terminal navigation available" , status : "inProgress" } ] ,
2373+ } ,
2374+ turnId : null ,
2375+ sequence : 1 ,
2376+ createdAt : isoAt ( 1_000 ) ,
2377+ } ,
2378+ ] ,
2379+ }
2380+ : thread ,
2381+ ) ,
2382+ } ;
2383+
2384+ const mounted = await mountChatView ( {
2385+ viewport : COMPACT_FOOTER_VIEWPORT ,
2386+ snapshot,
2387+ } ) ;
2388+
2389+ try {
2390+ const sheet = await waitForElement (
2391+ ( ) => document . querySelector < HTMLElement > ( '[data-slot="sheet-popup"]' ) ,
2392+ "Unable to find responsive right-panel sheet." ,
2393+ ) ;
2394+ const controls = await waitForElement (
2395+ ( ) => sheet . querySelector < HTMLElement > ( "[data-panel-layout-controls]" ) ,
2396+ "Unable to find shared controls in the responsive right-panel sheet." ,
2397+ ) ;
2398+ const tabbar = await waitForElement (
2399+ ( ) => sheet . querySelector < HTMLElement > ( "[data-right-panel-tabbar]" ) ,
2400+ "Unable to find responsive right-panel tabbar." ,
2401+ ) ;
2402+ const controlButtons = Array . from ( controls . querySelectorAll < HTMLButtonElement > ( "button" ) ) ;
2403+ const tabbarRect = tabbar . getBoundingClientRect ( ) ;
2404+ const controlsRect = controls . getBoundingClientRect ( ) ;
2405+
2406+ expect ( controlButtons . map ( ( button ) => button . getAttribute ( "aria-label" ) ) ) . toEqual ( [
2407+ "Toggle terminal drawer" ,
2408+ "Toggle right panel" ,
2409+ ] ) ;
2410+ expect ( tabbarRect . height ) . toBe ( 52 ) ;
2411+ expect ( controlsRect . height ) . toBe ( 52 ) ;
2412+ expect ( controlsRect . top ) . toBe ( tabbarRect . top ) ;
2413+ expect ( window . innerWidth - controlsRect . right ) . toBe ( 12 ) ;
2414+ for ( const button of controlButtons ) {
2415+ const rect = button . getBoundingClientRect ( ) ;
2416+ const buttonCenter = rect . top + rect . height / 2 ;
2417+ const tabbarCenter = tabbarRect . top + tabbarRect . height / 2 ;
2418+ expect ( rect . width ) . toBe ( 32 ) ;
2419+ expect ( rect . height ) . toBe ( 32 ) ;
2420+ expect ( Math . abs ( buttonCenter - tabbarCenter ) ) . toBeLessThanOrEqual ( 1 ) ;
2421+ }
2422+ expect (
2423+ controlButtons [ 1 ] ! . getBoundingClientRect ( ) . left -
2424+ controlButtons [ 0 ] ! . getBoundingClientRect ( ) . right ,
2425+ ) . toBe ( 4 ) ;
2426+ expect ( sheet . querySelector ( 'button[aria-label="Maximize panel"]' ) ) . toBeNull ( ) ;
2427+ expect ( sheet . querySelector ( 'button[aria-label="Close tasks sidebar"]' ) ) . toBeNull ( ) ;
2428+
2429+ const terminalTab = Array . from (
2430+ sheet . querySelectorAll < HTMLButtonElement > ( "[data-right-panel-tab-list] button" ) ,
2431+ ) . find ( ( button ) => button . textContent ?. includes ( "Terminal" ) ) ;
2432+ terminalTab ?. click ( ) ;
2433+
2434+ await vi . waitFor ( ( ) => {
2435+ expect (
2436+ selectThreadRightPanelState ( useRightPanelStore . getState ( ) . byThreadKey , THREAD_REF )
2437+ . activeSurfaceId ,
2438+ ) . toBe ( `terminal:${ DEFAULT_TERMINAL_ID } ` ) ;
2439+ expect ( sheet . querySelector ( '[data-terminal-owner="right-panel"]' ) ) . not . toBeNull ( ) ;
2440+ expect ( sheet . textContent ) . not . toContain ( "Claude Tasks" ) ;
2441+ } ) ;
2442+
2443+ sheet . querySelector < HTMLButtonElement > ( 'button[aria-label="Close Plan"]' ) ?. click ( ) ;
2444+
2445+ await vi . waitFor ( ( ) => {
2446+ const panelState = selectThreadRightPanelState (
2447+ useRightPanelStore . getState ( ) . byThreadKey ,
2448+ THREAD_REF ,
2449+ ) ;
2450+ expect ( panelState . surfaces . some ( ( surface ) => surface . kind === "plan" ) ) . toBe ( false ) ;
2451+ expect ( panelState . activeSurfaceId ) . toBe ( `terminal:${ DEFAULT_TERMINAL_ID } ` ) ;
2452+ } ) ;
2453+
2454+ controls . querySelector < HTMLButtonElement > ( 'button[aria-label="Toggle right panel"]' ) ?. click ( ) ;
2455+
2456+ await vi . waitFor ( ( ) => {
2457+ expect (
2458+ selectThreadRightPanelState ( useRightPanelStore . getState ( ) . byThreadKey , THREAD_REF ) . isOpen ,
2459+ ) . toBe ( false ) ;
2460+ } ) ;
2461+ } finally {
2462+ await mounted . cleanup ( ) ;
2463+ }
2464+ } ) ;
2465+
22792466 it ( "loads file previews from the active thread worktree" , async ( ) => {
22802467 const worktreePath = "/repo/worktrees/file-preview-thread" ;
22812468 const snapshot = createSnapshotForTargetUser ( {
0 commit comments