@@ -20,6 +20,7 @@ import {
2020 hasUnseenCompletion ,
2121 isContextMenuPointerDown ,
2222 isSidebarNestedLinkClick ,
23+ isSidebarThreadWorking ,
2324 isTrailingDoubleClick ,
2425 orderItemsByPreferredIds ,
2526 resolveProjectStatusIndicator ,
@@ -32,6 +33,7 @@ import {
3233 shouldClearThreadSelectionOnMouseDown ,
3334 shouldRecedeSidebarThread ,
3435 sortLogicalProjectsForSidebar ,
36+ sortInboxThreadsByReturn ,
3537 resolveSidebarDropTarget ,
3638 pinOrderKeyBetween ,
3739 planSidebarThreadDrop ,
@@ -2459,3 +2461,168 @@ describe("navigation after parking a thread", () => {
24592461 } ,
24602462 ) ;
24612463} ) ;
2464+
2465+ describe ( "Working shelf (beta)" , ( ) => {
2466+ const session = {
2467+ threadId : ThreadId . make ( "thread-1" ) ,
2468+ status : "running" as const ,
2469+ providerName : "Codex" ,
2470+ providerInstanceId : ProviderInstanceId . make ( "codex" ) ,
2471+ runtimeMode : DEFAULT_RUNTIME_MODE ,
2472+ activeTurnId : "turn-1" as never ,
2473+ lastError : null ,
2474+ updatedAt : "2026-03-09T10:00:00.000Z" ,
2475+ } ;
2476+ const idle = {
2477+ hasActionableProposedPlan : false ,
2478+ hasPendingApprovals : false ,
2479+ hasPendingUserInput : false ,
2480+ interactionMode : "default" as const ,
2481+ latestTurn : makeLatestTurn ( ) ,
2482+ session : { ...session , status : "ready" as const } ,
2483+ } ;
2484+
2485+ it ( "folds away running and monitoring threads only" , ( ) => {
2486+ expect ( isSidebarThreadWorking ( { ...idle , session } ) ) . toBe ( true ) ;
2487+ expect ( isSidebarThreadWorking ( { ...idle , backgroundLiveness : "monitoring" } ) ) . toBe ( true ) ;
2488+ expect ( isSidebarThreadWorking ( idle ) ) . toBe ( false ) ;
2489+ expect ( isSidebarThreadWorking ( { ...idle , session, hasPendingApprovals : true } ) ) . toBe ( false ) ;
2490+ expect ( isSidebarThreadWorking ( { ...idle , session, hasPendingUserInput : true } ) ) . toBe ( false ) ;
2491+ expect (
2492+ isSidebarThreadWorking ( {
2493+ ...idle ,
2494+ backgroundLiveness : "working" ,
2495+ session : { ...session , status : "error" as const , lastError : "boom" } ,
2496+ } ) ,
2497+ ) . toBe ( false ) ;
2498+ } ) ;
2499+
2500+ it ( "keeps a ready plan in the inbox while background work runs" , ( ) => {
2501+ expect (
2502+ isSidebarThreadWorking ( {
2503+ ...idle ,
2504+ interactionMode : "plan" ,
2505+ hasActionableProposedPlan : true ,
2506+ backgroundLiveness : "working" ,
2507+ } ) ,
2508+ ) . toBe ( false ) ;
2509+ } ) ;
2510+
2511+ describe ( "sortInboxThreadsByReturn" , ( ) => {
2512+ const thread = (
2513+ id : string ,
2514+ input : { createdAt : string ; completedAt ?: string | null ; unsettledAt ?: string } ,
2515+ ) => ( {
2516+ id : ThreadId . make ( id ) ,
2517+ environmentId : localEnvironmentId ,
2518+ createdAt : input . createdAt ,
2519+ unsettledAt : input . unsettledAt ?? null ,
2520+ latestTurn :
2521+ input . completedAt === undefined
2522+ ? null
2523+ : { ...makeLatestTurn ( { completedAt : input . completedAt } ) , requestedAt : input . createdAt } ,
2524+ } ) ;
2525+
2526+ it ( "puts the thread that finished last on top, whatever its age" , ( ) => {
2527+ const sorted = sortInboxThreadsByReturn ( [
2528+ thread ( "new" , { createdAt : "2026-03-09T11:00:00.000Z" } ) ,
2529+ thread ( "old-finished-now" , {
2530+ createdAt : "2026-03-01T09:00:00.000Z" ,
2531+ completedAt : "2026-03-09T12:00:00.000Z" ,
2532+ } ) ,
2533+ thread ( "reopened" , {
2534+ createdAt : "2026-03-02T09:00:00.000Z" ,
2535+ unsettledAt : "2026-03-09T11:30:00.000Z" ,
2536+ } ) ,
2537+ ] ) ;
2538+ expect ( sorted . map ( ( entry ) => entry . id ) ) . toEqual ( [ "old-finished-now" , "reopened" , "new" ] ) ;
2539+ } ) ;
2540+
2541+ it ( "counts a return the server does not stamp, like an approval request" , ( ) => {
2542+ const waiting = thread ( "asks-approval" , {
2543+ createdAt : "2026-03-09T09:00:00.000Z" ,
2544+ completedAt : null ,
2545+ } ) ;
2546+ const finished = thread ( "finished" , {
2547+ createdAt : "2026-03-09T09:30:00.000Z" ,
2548+ completedAt : "2026-03-09T11:00:00.000Z" ,
2549+ } ) ;
2550+ expect ( sortInboxThreadsByReturn ( [ finished , waiting ] ) . map ( ( entry ) => entry . id ) ) . toEqual ( [
2551+ "finished" ,
2552+ "asks-approval" ,
2553+ ] ) ;
2554+ expect (
2555+ sortInboxThreadsByReturn ( [ finished , waiting ] , ( entry ) =>
2556+ entry === waiting ? Date . parse ( "2026-03-09T11:05:00.000Z" ) : undefined ,
2557+ ) . map ( ( entry ) => entry . id ) ,
2558+ ) . toEqual ( [ "asks-approval" , "finished" ] ) ;
2559+ } ) ;
2560+ } ) ;
2561+
2562+ describe ( "dragging" , ( ) => {
2563+ const marker = ( name : SidebarListMarker ) : SidebarListItem => ( { kind : "marker" , marker : name } ) ;
2564+ const row = ( key : string , section : SidebarSection ) : SidebarListItem => ( {
2565+ kind : "thread" ,
2566+ key,
2567+ section,
2568+ } ) ;
2569+ // Pinned p1 | Active a1 a2 | Working w1 | Settled s1
2570+ const items : readonly SidebarListItem [ ] = [
2571+ marker ( "pinned-header" ) ,
2572+ row ( "p1" , "pinned" ) ,
2573+ marker ( "pinned-divider" ) ,
2574+ row ( "a1" , "active" ) ,
2575+ row ( "a2" , "active" ) ,
2576+ marker ( "working-header" ) ,
2577+ row ( "w1" , "working" ) ,
2578+ marker ( "settled-header" ) ,
2579+ row ( "s1" , "settled" ) ,
2580+ ] ;
2581+
2582+ it ( "never drops into the Working shelf, and keeps it out of the inbox order" , ( ) => {
2583+ expect ( resolveSidebarDropTarget ( items , "a1" , "w1" ) ) . toBeNull ( ) ;
2584+ expect ( resolveSidebarDropTarget ( items , "p1" , "a2" ) ) . toEqual ( {
2585+ section : "active" ,
2586+ pinnedOrder : [ ] ,
2587+ activeOrder : [ "a1" , "a2" , "p1" ] ,
2588+ } ) ;
2589+ expect ( resolveSidebarDropVerb ( "active" , "working" ) ) . toBeNull ( ) ;
2590+ } ) ;
2591+
2592+ it ( "only changes lifecycle when the inbox is time-ordered" , ( ) => {
2593+ const base = {
2594+ pinnedOrder : [ "p1" ] ,
2595+ pinnedKeysById : new Map ( [ [ "p1" , "m" ] ] ) ,
2596+ activeOrder : [ "a1" , "a2" ] ,
2597+ activeKeysById : new Map ( [
2598+ [ "a1" , "f" ] ,
2599+ [ "a2" , "t" ] ,
2600+ ] ) ,
2601+ activeTimeOrdered : true ,
2602+ } ;
2603+ expect (
2604+ planSidebarThreadDrop ( {
2605+ ...base ,
2606+ activeKey : "a1" ,
2607+ activeSection : "active" ,
2608+ target : { section : "active" , pinnedOrder : [ "p1" ] , activeOrder : [ "a2" , "a1" ] } ,
2609+ } ) ,
2610+ ) . toEqual ( { kind : "none" } ) ;
2611+ expect (
2612+ planSidebarThreadDrop ( {
2613+ ...base ,
2614+ activeKey : "p1" ,
2615+ activeSection : "pinned" ,
2616+ target : { section : "active" , pinnedOrder : [ ] , activeOrder : [ "a1" , "p1" , "a2" ] } ,
2617+ } ) ,
2618+ ) . toEqual ( {
2619+ kind : "move-active" ,
2620+ order : null ,
2621+ assignments : [ ] ,
2622+ unpin : true ,
2623+ unsettle : false ,
2624+ unsnooze : false ,
2625+ } ) ;
2626+ } ) ;
2627+ } ) ;
2628+ } ) ;
0 commit comments