@av-pi-studio/web-client 0.0.74 → 0.0.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/web/assets/{BinaryFallbackViewer-B1PBcT3N.js → BinaryFallbackViewer-BcXz7eRe.js} +2 -2
  2. package/dist/web/assets/{BinaryFallbackViewer-B1PBcT3N.js.map → BinaryFallbackViewer-BcXz7eRe.js.map} +1 -1
  3. package/dist/web/assets/ChatPanel-BGaHXRkd.js +3 -0
  4. package/dist/web/assets/ChatPanel-BGaHXRkd.js.map +1 -0
  5. package/dist/web/assets/ChatPanel-BSD-8ded.css +1 -0
  6. package/dist/web/assets/{CodeView-_wBYU-zK.js → CodeView-BwpO1rFh.js} +2 -2
  7. package/dist/web/assets/{CodeView-_wBYU-zK.js.map → CodeView-BwpO1rFh.js.map} +1 -1
  8. package/dist/web/assets/{FilePanel-CqiLosrb.js → FilePanel-Cg9NsETY.js} +2 -2
  9. package/dist/web/assets/{FilePanel-CqiLosrb.js.map → FilePanel-Cg9NsETY.js.map} +1 -1
  10. package/dist/web/assets/{ImageViewer-WToUsK2Y.js → ImageViewer-BJp-etHM.js} +2 -2
  11. package/dist/web/assets/{ImageViewer-WToUsK2Y.js.map → ImageViewer-BJp-etHM.js.map} +1 -1
  12. package/dist/web/assets/{MarkdownFileViewer-Do69vE6s.js → MarkdownFileViewer-BzB1d4TY.js} +2 -2
  13. package/dist/web/assets/{MarkdownFileViewer-Do69vE6s.js.map → MarkdownFileViewer-BzB1d4TY.js.map} +1 -1
  14. package/dist/web/assets/{MoleculeViewerPanel-CKaIka-k.js → MoleculeViewerPanel-DrXqIoI5.js} +2 -2
  15. package/dist/web/assets/{MoleculeViewerPanel-CKaIka-k.js.map → MoleculeViewerPanel-DrXqIoI5.js.map} +1 -1
  16. package/dist/web/assets/{TerminalPanel-B7ujFxCG.js → TerminalPanel-CqbvRBBr.js} +2 -2
  17. package/dist/web/assets/{TerminalPanel-B7ujFxCG.js.map → TerminalPanel-CqbvRBBr.js.map} +1 -1
  18. package/dist/web/assets/{TextViewer-CJGwYjCh.js → TextViewer-Bd-xuyEa.js} +2 -2
  19. package/dist/web/assets/{TextViewer-CJGwYjCh.js.map → TextViewer-Bd-xuyEa.js.map} +1 -1
  20. package/dist/web/assets/{VideoViewer-CYXPkYLV.js → VideoViewer-q6x3q8Rf.js} +2 -2
  21. package/dist/web/assets/{VideoViewer-CYXPkYLV.js.map → VideoViewer-q6x3q8Rf.js.map} +1 -1
  22. package/dist/web/assets/index-CO_ZzVSZ.js +18 -0
  23. package/dist/web/assets/index-CO_ZzVSZ.js.map +1 -0
  24. package/dist/web/assets/index-D5SH-IYM.css +1 -0
  25. package/dist/web/assets/{markdown-q0cxuHgm.js → markdown-DZVzjFwV.js} +2 -2
  26. package/dist/web/assets/{markdown-q0cxuHgm.js.map → markdown-DZVzjFwV.js.map} +1 -1
  27. package/dist/web/assets/{use-file-download-BCG_kqpD.js → use-file-download-f9giWuuK.js} +2 -2
  28. package/dist/web/assets/{use-file-download-BCG_kqpD.js.map → use-file-download-f9giWuuK.js.map} +1 -1
  29. package/dist/web/assets/{use-file-watch-CZgwolg7.js → use-file-watch-CR6oYkRD.js} +2 -2
  30. package/dist/web/assets/{use-file-watch-CZgwolg7.js.map → use-file-watch-CR6oYkRD.js.map} +1 -1
  31. package/dist/web/assets/{vendor-icons-DDiHMdgo.js → vendor-icons-4MWXLMX2.js} +51 -66
  32. package/dist/web/assets/vendor-icons-4MWXLMX2.js.map +1 -0
  33. package/dist/web/index.html +3 -3
  34. package/package.json +3 -3
  35. package/dist/web/assets/ChatPanel-DuiC5ipg.js +0 -3
  36. package/dist/web/assets/ChatPanel-DuiC5ipg.js.map +0 -1
  37. package/dist/web/assets/ChatPanel-qE6vr3IJ.css +0 -1
  38. package/dist/web/assets/index-BLK6SFvN.css +0 -1
  39. package/dist/web/assets/index-Cz7AgOgN.js +0 -18
  40. package/dist/web/assets/index-Cz7AgOgN.js.map +0 -1
  41. package/dist/web/assets/vendor-icons-DDiHMdgo.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"mappings":";o0DAIO,MAAMA,GAAc,IAAIC,GCCxB,SAASC,GAASC,EAAkB,CACzC,MAAMC,EAAID,EAAI,OAAO,QAAQ,KAAM,EAAE,EAC/BE,EACJD,EAAE,SAAW,EACTA,EACG,MAAM,EAAE,EACR,IAAKE,GAAMA,EAAIA,CAAC,EAChB,KAAK,EAAE,EACVF,EACN,GAAIC,EAAK,SAAW,GAAK,eAAe,KAAKA,CAAI,EAC/C,MAAM,IAAI,MAAM,sBAAsBF,CAAG,EAAE,EAE7C,MAAO,CACL,EAAG,SAASE,EAAK,MAAM,EAAG,CAAC,EAAG,EAAE,EAChC,EAAG,SAASA,EAAK,MAAM,EAAG,CAAC,EAAG,EAAE,EAChC,EAAG,SAASA,EAAK,MAAM,EAAG,CAAC,EAAG,EAAE,EAEpC,CAEA,SAASE,GAASC,EAAmB,CACnC,OAAO,KAAK,IAAI,EAAG,KAAK,IAAI,IAAK,KAAK,MAAMA,CAAC,CAAC,CAAC,CACjD,CAEO,SAASC,GAAS,CAAE,EAAAC,EAAG,EAAAC,EAAG,EAAAC,GAAkB,CACjD,MAAMC,EAAML,GAAcD,GAASC,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,EAAG,GAAG,EAClE,MAAO,IAAIK,EAAGH,CAAC,CAAC,GAAGG,EAAGF,CAAC,CAAC,GAAGE,EAAGD,CAAC,CAAC,EAClC,CAGO,SAASE,GAAkBX,EAAqB,CACrD,KAAM,CAAE,EAAAO,EAAG,EAAAC,EAAG,EAAAC,CAAA,EAAMV,GAASC,CAAG,EAC1BY,EAAWC,GAAc,CAC7B,MAAMC,EAAID,EAAI,IACd,OAAOC,GAAK,OAAUA,EAAI,QAAUA,EAAI,MAAS,QAAU,GAC7D,EACA,MAAO,OAASF,EAAQL,CAAC,EAAI,MAASK,EAAQJ,CAAC,EAAI,MAASI,EAAQH,CAAC,CACvE,CAGO,SAASM,GAAmBf,EAAagB,EAAO,UAAWC,EAAQ,UAAmB,CAC3F,OAAON,GAAkBX,CAAG,EAAI,IAAOgB,EAAOC,CAChD,CAGO,SAASC,GAAQlB,EAAamB,EAAwB,CAC3D,KAAM,CAAE,EAAAZ,EAAG,EAAAC,EAAG,EAAAC,CAAA,EAAMV,GAASC,CAAG,EAC1BoB,EAAI,KAAK,IAAI,EAAG,KAAK,IAAI,EAAGD,CAAM,CAAC,EACzC,OAAOb,GAAS,CACd,EAAGC,GAAK,IAAMA,GAAKa,EACnB,EAAGZ,GAAK,IAAMA,GAAKY,EACnB,EAAGX,GAAK,IAAMA,GAAKW,CAAA,CACpB,CACH,CASO,SAASC,GAAWC,EAAwB,CACjD,MAAMrB,EAAIqB,EAAM,OAAO,QAAQ,KAAM,EAAE,EACvC,OAAQrB,EAAE,SAAW,GAAKA,EAAE,SAAW,IAAM,CAAC,eAAe,KAAKA,CAAC,CACrE,CClDA,MAAMsB,GAAQ,CACZC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,KACkB,CAClB,GAAMV,EACN,IAAOC,EACP,IAAOC,EACP,IAAOC,EACP,IAAOC,EACP,IAAOC,EACP,IAAOC,EACP,IAAOC,EACP,IAAOC,EACP,IAAOC,EACP,IAAOC,CACT,GAEaC,EAAU,CACrB,KAAMZ,GACJ,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,WAEF,KAAMA,GACJ,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,WAEF,MAAOA,GACL,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,WAEF,KAAMA,GACJ,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,WAEF,MAAOA,GACL,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,WAEF,IAAKA,GACH,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,WAEF,KAAMA,GACJ,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,WAEF,MAAOA,GACL,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,WAEF,OAAQA,GACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,WAEF,OAAQA,GACN,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,WAEF,OAAQA,GACN,U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","names":["queryClient","QueryClient","hexToRgb","hex","h","full","c","clamp255","n","rgbToHex","r","g","b","to","relativeLuminance","channel","v","s","contrastForeground","dark","light","lighten","amount","a","isHexColor","value","scale","s50","s100","s200","s300","s400","s500","s600","s700","s800","s900","s950","palette","DARK_STATUS","LIGHT_STATUS","darkSyntax","lightSyntax","terminalFrom","surface0","foreground","accent","buildDarkColors","tint","accentForeground","buildLightColors","THEME_SWATCHES","THEME_NAMES","buildVariantColors","name","_variants","colorScheme","THEME_VARIANTS","spacing","baseFontSize","fontWeight","borderRadius","borderWidth","opacity","iconSize","baseLineHeight","DEFAULT_UI_FONT","DEFAULT_MONO_FONT","buildShadows","color","buildTheme","variant","FONT_SIZE_BASE","FONT_SIZE_MIN","FONT_SIZE_MAX","applyAppearance","base","settings","themeName","baseTheme","uiFont","monoFont","fontSize","k","getTheme","DEFAULT_THEME_NAME","REM_BASE_PX","pxToRem","px","rem","flattenThemeToVars","theme","vars","flattenColors","key","val","colors","_palette","_syntax","_terminal","semanticColors","flattenSyntax","syntax","camelToKebab","str","m","applyVarsToRoot","style","prop","scheme","applyThemeToDOM","AssetRefSchema","z.string","BrandColorsSchema","z.object","BrandAssetsSchema","BrandLinksSchema","BrandLegalSchema","resolveAccentColors","accentBright","swatch","injectBrandAccent","resolved","STORAGE_KEY","createAppearanceController","store","brandConfig","state","loadInitial","mediaCleanup","resolveThemeName","mode","buildResolved","persist","serialized","raw","parsed","resolvedTheme","update","controller","patch","mq","handler","ThemeBoundary","children","controllerRef","useRef","useEffect","localKvStore","_a","AppProviders","jsx","QueryClientProvider","BUTTON_MIN_HEIGHT","BUTTON_ICON_SIZE","BUTTON_PADDING_H","BUTTON_FONT_SIZE","resolveButtonState","opts","disabled","loading","Button","size","leftIcon","trailing","iconOnly","className","rest","isDisabled","iconPx","jsxs","clsx","styles","Loader2","TextInput","forwardRef","ref","TextArea","isoTimestampSchema","wireTimestampSchema","z.union","z.number","clientTypeSchema","z.enum","helloSchema","z.literal","z.record","z.boolean","serverInfoPayloadSchema","z.unknown","statusSchema","pingSchema","pongSchema","agentStatusEnum","imageAttachmentSchema","agentAttachmentsSchema","z.array","agentSessionConfigSchema","createAgentRequestSchema","createAgentResponseSchema","agentUpdateSchema","agentStatusMessageSchema","agentListSchema","agentDeletedSchema","agentArchivedSchema","toolCallDetailSchema","z.discriminatedUnion","agentStreamEventSchema","agentStreamSchema","timelineDirectionEnum","sourceSeqRangeSchema","fetchAgentTimelineRequestSchema","fetchAgentTimelineResponseSchema","agentPermissionRequestSchema","agentPermissionResolvedSchema","respondToPermissionRequestSchema","respondToPermissionResponseSchema","legacyRespondToPermissionSchema","rewindModeSchema","agentRewindRequestSchema","agentRewindResponseSchema","agentTokenUsageSchema","agentContextUsageSchema","agentSessionStatsRequestSchema","agentSessionStatsResponseSchema","agentCompactRequestSchema","agentCompactResponseSchema","agentNewSessionRequestSchema","agentNewSessionResponseSchema","agentSwitchSessionRequestSchema","agentSwitchSessionResponseSchema","agentForkRequestSchema","agentForkResponseSchema","agentForkMessagesRequestSchema","agentForkMessagesResponseSchema","agentCloneRequestSchema","agentCloneResponseSchema","agentSetSessionNameRequestSchema","agentSetSessionNameResponseSchema","agentExportHtmlRequestSchema","agentExportHtmlResponseSchema","agentSetModelRequestSchema","agentSetModelResponseSchema","agentCycleModelRequestSchema","agentCycleModelResponseSchema","agentLastAssistantTextRequestSchema","agentLastAssistantTextResponseSchema","steerAgentRequestSchema","steerAgentResponseSchema","followUpAgentRequestSchema","followUpAgentResponseSchema","agentCommandDescriptorSchema","agentListCommandsRequestSchema","agentListCommandsResponseSchema","extensionEntryInfoSchema","extensionPackInfoSchema","extensionSyncFailureSchema","extensionSyncReportSchema","extensionsLastSyncSummarySchema","extensionPacksListRequestSchema","extensionPacksListResponseSchema","extensionPacksSetRequestSchema","extensionPacksSetResponseSchema","rpcErrorSchema","sessionMessageSchema","sessionMessageBaseSchema","sessionEnvelopeSchema","CLIENT_CAPS","TerminalOpcode","OPCODE_BY_VALUE","terminalResizeSchema","TerminalFrameError","message","textEncoder","textDecoder","assertSlot","slot","frameWithPayload","opcodeValue","payload","out","encodeTerminalFrame","frame","json","decodeTerminalFrame","bytes","opcode","result","FileTransferOpcode","fileTransferBeginSchema","FileTransferFrameError","assertStream","stream","encodeFileTransferFrame","f","decodeFileTransferFrame","tryDecodeFileTransferFrame","SLOT_SPACE","nextFreeSlot","inUse","cursor","start","i","providerIdSchema","colorTierSchema","agentCapabilityFlagsSchema","providerModeSchema","reasonString","reason","createWebSocketTransport","factory","ws","self","url","resolve","reject","create","err","ev","data","_b","_c","_d","code","CHARS","encodeBase64","b0","hasB1","hasB2","b1","b2","decodeBase64","input","clean","buffer","bits","ch","parseFrame","NONCE_LENGTH","sealBytes","sharedKey","nonce","nacl","ciphertext","combined","openBytes","seal","plaintext","open","opened","wireAppMessages","role","attachment","transport","events","getSharedKey","isReady","closed","createClientChannel","daemonPublicKey","ephemeral","ready","plumbing","SESSION_ID_BYTES","deriveRelaySessionId","publicKey","digest","relayDialUrl","relay","host","createRelayTransport","sessionId","socket","messageHandlers","closeHandlers","parsePairingUrl","hashIndex","fragment","params","publicKeyB64","relayEndpoint","PROTOCOL_VERSION","randomId","RpcError","requestId","RpcTimeoutError","DaemonClient","options","__publicField","flag","handshake","info","type","timeoutMs","timeout","timer","next","_code","pending","buf","opcodeByte","envelope","msg","PiStudioClient","daemon","req","agentId","AgentHandle","workspaceId","WorkspaceHandle","ProviderHandle","packs","TimelineHandle","prompt","customInstructions","sessionPath","entryId","outputPath","provider","modelId","cwd","UPLOAD_CHUNK_BYTES","EXPIRED_TOKEN","concat","chunks","total","sum","offset","chunk","FileTransferClient","path","error","tokenResponse","requestResponse","transferId","response","ReconnectionManager","cb","ms","attempt","jitterAmount","delay","normalizeDaemonUrl","schemeMatch","resolveConnectTarget","offer","WORKER_SOURCE","createFallbackBackend","handle","createWorkerBackend","blob","worker","callbacks","nextId","e","id","backend","getBackend","createWorkerTimers","MAX_INLINE_IMAGE_ENTRIES","cache","evictOverBound","evictedPath","record","acquireInlineImage","download","existing","file","releaseInlineImage","clearInlineImageCache","generateClientId","useConnectionStore","set","get","target","u","client","setTimer","clearTimer","reconnection","MIN_SIDEBAR_WIDTH","MAX_SIDEBAR_WIDTH","DEFAULT_LEFT_SIDEBAR_WIDTH","DEFAULT_RIGHT_SIDEBAR_WIDTH","clampSidebarWidth","width","useUiStore","password","x","y","isDirectory","background","tab","cwds","deltaX","LABEL","ConnectionStatus","status","Toolbar","connect","disconnect","setHost","setPassword","leftSidebarCollapsed","toggleLeftSidebar","rightSidebarCollapsed","toggleRightSidebar","connected","PanelLeftOpen","PanelLeftClose","PanelRightOpen","PanelRightClose","EMPTY_TIMELINE","rowSeq","nextRowId","toolDetailText","tool","toolCallKey","callId","finalizeRow","rows","index","row","finalizeStreamingRows","onTurnStarted","onAssistantMessage","text","final","onReasoning","mergeTool","prev","merged","onToolCall","event","existingIndex","onTurnCompleted","onTurnFailed","finalized","onTurnCanceled","onError","onUserMessage","images","messageId","idx","onQueueUpdate","steering","changed","applyStreamEvent","addOptimisticUserMessage","clientMessageId","queued","markUserMessageFailed","sessionSeq","newSessionId","useSessionStore","entry","model","modelProvider","title","timeline","applyStreamEventToTimeline","lastAssistant","addOptimisticUserMessageToTimeline","markUserMessageFailedInTimeline","_removed","order","activeSessionId","defaultModelCache","resolveDefaultModel","cached","promise","res","materializing","ensureMaterialized","inFlight","current","fresh","config","discardIfEmpty","MAX_PANE_DEPTH","directionOfRegion","region","leafIds","tree","hasLeaf","paneId","child","paneDepth","node","depth","found","parentSplitOf","directChildIndex","split","canSplit","targetPaneId","parent","splitPane","newPaneId","direction","before","added","nest","transform","half","at","sizes","removePane","drop","kept","_","keptTotal","slotTotal","normalizePaneTree","normalized","grandChild","j","MIN_PANE_FRACTION","paneRects","rects","walk","rect","childRects","childRect","dividers","after","nodeAtPath","resizeAtDivider","splitPath","boundaryIndex","deltaFraction","giveable","takeable","delta","resized","rebuild","effectiveTree","occupied","vacant","PANE_LAYOUT_VERSION","parsePaneTree","parseNode","seenIds","HYDRATION_SOURCES","mintPaneId","createPaneLayout","tabsInPane","layout","nearestSibling","leavingTabId","tabs","remaining","hasPendingClaim","withPaneRemoved","activeByPane","leaves","placeTab","tabId","pane","source","placement","moved","withUserAction","settleWorkspace","settled","repaired","first","isPaneActiveTab","useLayoutStore","fn","activate","placed","targetTabs","created","loaded","activeWorkspaceCwd","layouts","identity","fromClaim","claimed","pendingPlacement","wanted","hydrationSources","required","tabsInWorkspace","t","workspaceOrder","syncActiveFromLayout","activeTabId","useTabStore","syncActiveSession","tabIdentity","fromId","toId","fromIdx","toIdx","label","prefix","matches","staged","useIsTabVisible","tabIds","slotOrToken","terminalCount","openNewTerminal","workspaceCwd","moleculeCount","openNewMolecule","openNewChat","closeTab","Panel","EmptyState","useExplorerStore","expandedByRoot","remembered","expanded","parentPath","kind","selected","from","toParent","rewrite","p","cachedHomeDir","homeDirListeners","onHomeDirResolved","listener","resolveHome","peekCachedHomeDir","resolveTildePath","useHomeDir","homeDir","setHomeDir","useState","IconButton","hoverBase","statusDotColor","requiresAttention","attentionReason","showInactive","STATUS_DOT_SIZE","COLOR_TOKEN_VAR","StatusDot","colorToken","spinning","MAP","toDotStatus","SessionItem","session","active","draggable","onSelect","onOpenMenu","onDragStartRow","MoreVertical","MenuCursorTrigger","MenuContent","minWidth","sideOffset","mergedStyle","DropdownMenu.Portal","DropdownMenu.Content","MenuItem","danger","DropdownMenu.Item","MenuSeparator","props","DropdownMenu.Separator","SessionContextMenu","menu","closeSessionMenu","triggerRef","setOpen","canStop","handleOpenChange","rename","stop","removeLocal","archive","remove","DropdownMenu.Root","DropdownMenu.Trigger","Pencil","Square","ArchiveIcon","Trash2","WorkspaceGroupHeader","sessionCount","collapsed","onToggleCollapsed","ChevronRight","ChevronDown","FolderClosed","normalizeCwd","groupSessionsByWorkspace","sessions","groups","bucket","sessionsInGroup","workspaceLabel","collapseInactiveWorkspaces","activeCwd","WorkspaceContextMenu","closeWorkspaceMenu","groupSessions","newConversation","deleteWorkspace","count","Plus","openChatTab","CENTER_BAND","resolveDropRegion","pointer","body","dx","dy","containsPoint","bounds","effectiveDropRegion","isNoOpDrop","EXTERNAL_DRAG_MIME","KINDS","externalDragKind","types","pathDragStartHandler","readExternalDrag","read","resolveExternalDropRegion","SessionList","openSessionMenu","openWorkspaceMenu","setCwd","openCwdPicker","collapsedWorkspaces","toggleWorkspaceCollapsed","handleSelect","handleDragStart","groupCwd","FolderOpen","group","SPINNER_SIZE_PX","SPINNER_BORDER_PX","Spinner","ariaLabel","borderPx","TARGET_RANK","dropTarget","pointerOf","activator","paneOfTarget","overId","usePaneDrag","draggedId","setDraggedId","preview","setPreview","draggedTab","sensors","useSensors","useSensor","PointerSensor","collisionDetection","useMemo","args","ranked","pointerWithin","hit","container","resolveTarget","useCallback","over","onDragStart","onDragMove","onDragCancel","onDragEnd","tabStore","moveTab","splitWithTab","targetPane","last","ChatPanel","lazy","FilePanel","TerminalPanel","MoleculeViewerPanel","PANEL_BY_KIND","STRIP_HEIGHT","occupiedPaneRects","percent","fraction","fixed","paneStyle","dropPreviewStyle","bodyTop","bodyHeight","halfHeight","paneChrome","paneDividers","rendered","resolveOwningPaneId","panelBoxes","PaneDividers","hostRef","resizeDivider","lastRef","startDrag","divider","horizontal","extentPx","onMove","move","position","deltaPx","onUp","TextViewer","__vitePreload","MarkdownFileViewer","ImageViewer","VideoViewer","BinaryFallbackViewer","VIEWER_BY_KIND","EXT_TO_VIEWER","MIME_PREFIX_TO_VIEWER","extOf","dot","MOLECULE_EXTENSIONS","MOLECULE_FILENAMES","isMoleculeFile","detectViewerKind","byExt","mime","openFileTab","openMoleculeTab","openTextTab","PANE_DROP_ATTR","paneDropProps","paneUnderPointer","zone","existingTabId","applyExternalDrop","useExternalPaneDrop","outcomeAt","onDragOver","onDragLeave","onDrop","outcome","ICON_BY_KIND","MessageSquare","FileText","GitCompare","TerminalSquare","Atom","TabItem","Icon","attributes","listeners","setNodeRef","transition","isDragging","useSortable","CSS","X","NewTabMenu","openInPane","SplitActions","refusal","rightRefusal","downRefusal","Fragment","Columns2","Rows2","TabStrip","focused","focusPane","activeInPane","paneTabs","useDroppable","SortableContext","horizontalListSortingStrategy","DropPreview","DragChip","PaneDropZone","TabPanelHost","hasTabsInWorkspace","areaRef","boxes","chrome","drag","externalDrop","DndContext","box","Suspense","DragOverlay","rpcKeys","objectUrl","normalizeEntry","sortEntries","entries","fetchExplorerListing","useExplorer","enabled","useQuery","useExplorerTree","paths","results","useQueries","map","isFileChangedMessage","createExplorerWatcher","onChanged","subscribed","unsubscribeMessages","useExplorerWatch","useQueryClient","watcherRef","clientRef","sortedKey","transferByDaemon","transferFor","transfer","saveBlob","fileName","mimeType","anchor","useFileTransfer","upload","dir","saveToDisk","EMPTY_BRANCH_META","useGitStore","projection","changes","resolveWorkspacePath","dirOf","relativeToRoot","root","collapseDotSegments","segments","stack","segment","resolveMoveTarget","sourcePath","rootPath","destinationDir","basename","resolveUploadTarget","ERROR_MESSAGES","moveEntry","destination","withClosedDiffs","closedDiffs","rowKey","joinPath","pushChildren","dirPath","draft","renamingPath","pushEntry","isExpanded","flattenTree","rootExpanded","NO_STATUS","mergeFile","previous","contributionToParents","buildGitStatusLookup","exact","ancestors","untrackedDirs","change","isCollapsedDir","relative","absolute","contribution","direct","NOTHING_IGNORED","buildIgnoredMatcher","ignored","dirs","isDir","TreeDraftRow","draftKind","indentStyle","onSubmit","onCancel","setName","Folder","FileIcon","trimmed","TreeRenameRow","setValue","inputRef","end","INDENT_PX","GIT_STATUS_CLASS","TreeNode","gitStatus","hidden","isRoot","onToggle","onOpenFile","onContextMenu","onSubmitDraft","onCancelDraft","onSubmitRename","onCancelRename","onDragEndRow","indent","copyText","textarea","deleteEntry","FileContextMenu","closeFileMenu","startDraft","startRename","openFile","openInMolviewer","openAsText","copyAbsolutePath","copyRelativePath","startNew","FilePlus","FolderPlus","Copy","ExternalLink","DownloadIcon","CreateEntryError","createEntry","ROW_HEIGHT_PX","MOVE_MIME","OS_FILE_TYPE","FileExplorer","setRoot","toggle","cancelDraft","renaming","cancelRename","setSelected","activeTab","openFileMenu","setStatus","dropTargetRowPath","setDropTargetRowPath","fileInputRef","scrollRef","dragSourceRef","autoExpandRef","activeFilePath","createTargetDir","lastSeededCwd","gitStatusOf","isIgnored","virtualizer","useVirtualizer","handleOpenFile","handleDragStartRow","handleDragEndRow","clearDropState","scheduleAutoExpand","applyMove","requestedDestination","hadTab","moveDropped","submitDraft","submitRename","newName","finalPath","uploadFiles","files","existingListing","clashes","names","handleDrop","handleDragOver","isInternalMove","Upload","virtualRow","isRootRow","BADGE_LABEL","ChangesPanel","openTab","handleClick","RightSidebar","rightSidebarTab","setRightSidebarTab","handleRefresh","RotateCw","useStatsStore","partial","POLL_INTERVAL_MS","shouldRepollOnStatusChange","applySessionStats","_e","_f","useSessionStats","sessionStatus","inFlightRef","prevStatusRef","cancelled","poll","interval","prevStatus","isCheckoutStatusUpdate","useCheckoutStatus","applyProjection","setSubscribedCwd","PLACEHOLDER","formatTokens","formatPercent","pct","formatCost","dp","formatCwd","home","formatBranchMeta","ahead","behind","parts","StatusBar","gitAvailable","branch","detached","conflictCount","dirtyCount","stats","cwdText","branchMeta","GitBranch","contextText","Gauge","Coins","DollarSign","seg","checked","onChange","Check","placeholder","opt","DialogClose","RadixDialog.Close","Dialog","onOpenChange","footer","bare","RadixDialog.Root","RadixDialog.Portal","RadixDialog.Overlay","RadixDialog.Content","RadixDialog.Title","ResizeHandle","side","onResize","startXRef","handlePointerMove","handlePointerUp","handlePointerDown","openWorkspace","OpenWorkspaceDialog","closeDialog","uiCwd","setPathState","inputValue","setInputValue","creating","setCreating","setNewName","createError","setCreateError","createBusy","setCreateBusy","seed","resetCreate","isLoading","isError","dirEntries","navigate","handleUp","handleGo","handleCreate","handleOpen","ArrowUp","WorkspacePage","leftSidebarWidth","rightSidebarWidth","resizeLeftSidebar","resizeRightSidebar","useConnectionBoot","ranRef","pairingUrl","qs","pair","shouldConnect","ui","pw","PANE_LAYOUT_WRITE_DEBOUNCE_MS","loadPaneLayout","workspaces","empty","workspace","validateWorkspace","candidate","panes","stringEntries","activePaneId","cancelPendingWrite","schedulePaneLayoutWrite","writePaneLayout","flushPaneLayoutWrite","identities","installPaneLayoutPersistence","lastSignature","identitySignature","onIdentityMaybeChanged","signature","unsubscribeLayout","unsubscribeTabs","unsubscribeSessions","DIFF_IDENTITY","reopenClientTabs","tabFromIdentity","suffix","common","molecule","diff","installActiveWorkspaceRestore","isHydrated","restoreActiveWorkspace","unsubscribe","switched","adoptChatFromWorkspace","local","terminals","EMPTY_RECORD","usePaneLayoutBoot","loadedRef","replayedRef","uninstall","flush","isTimelineItem","flattenTimelineItems","items","fetchTimelineEvents","fetchPage","page","hasStringModel","useSessionRestore","restoredRef","runSessionRestore","restoreAgents","agents","sessionStore","agent","isEmptyDraft","firstCwd","viewCwd","workspaceGroups","inView","claimedChatIdentities","reopened","hasRecord","runTerminalRestore","claimedTerminalIdentities","useTerminalRestore","useShortcuts","onKeyDown","mod","Boot","App","resolveResumeAction","PROBE_TIMEOUT_MS","attachResumeTriggers","probeInFlight","onResume","action","onVisibilityChange","createRoot","StrictMode"],"ignoreList":[],"sources":["../../../src/lib/connection/query-client.ts","../../../src/theme/color-utils.ts","../../../src/theme/palette.ts","../../../src/theme/colors.ts","../../../src/theme/variants.ts","../../../src/theme/tokens.ts","../../../src/theme/theme.ts","../../../src/theme/css-bridge.ts","../../../src/brand/config.ts","../../../src/brand/theme-injection.ts","../../../src/theme/appearance-store.ts","../../../src/theme/ThemeBoundary.tsx","../../../src/providers/kv-store.ts","../../../src/providers/AppProviders.tsx","../../../src/ui/button.ts","../../../src/components/primitives/Button.tsx","../../../src/components/primitives/TextInput.tsx","../../../../protocol/src/validation.ts","../../../../protocol/src/messages.ts","../../../../protocol/src/client-capabilities.ts","../../../../protocol/src/binary-frames/terminal-stream-protocol.ts","../../../../protocol/src/binary-frames/file-transfer-protocol.ts","../../../../protocol/src/binary-frames/slots.ts","../../../../protocol/src/provider-manifest.ts","../../../../client/src/transport.ts","../../../../relay/dist/base64.js","../../../../relay/dist/channel.js","../../../../relay/dist/session-id.js","../../../../client/src/relay-transport.ts","../../../../client/src/pairing.ts","../../../../client/src/daemon-client.ts","../../../../client/src/pistudio-client.ts","../../../../client/src/file-transfer-client.ts","../../../../client/src/reconnect.ts","../../../src/lib/connection/normalize-url.ts","../../../src/lib/connection/resolve-connect-target.ts","../../../src/lib/connection/worker-timers.ts","../../../src/lib/inline-image-cache.ts","../../../src/lib/connection/connection-store.ts","../../../src/stores/ui-store.ts","../../../src/features/connection/ConnectionStatus.tsx","../../../src/features/connection/Toolbar.tsx","../../../src/timeline/row-model.ts","../../../src/timeline/reducer.ts","../../../src/stores/session-store.ts","../../../src/stores/materialize.ts","../../../src/features/workspace/pane-tree.ts","../../../src/stores/layout-store.ts","../../../src/stores/tab-store.ts","../../../src/components/primitives/Panel.tsx","../../../src/components/primitives/EmptyState.tsx","../../../src/stores/explorer-store.ts","../../../src/hooks/use-home-dir.ts","../../../src/components/primitives/IconButton.tsx","../../../src/ui/status-dot.ts","../../../src/components/primitives/StatusDot.tsx","../../../src/features/sessions/status-map.ts","../../../src/features/sessions/SessionItem.tsx","../../../src/components/primitives/Menu.tsx","../../../src/features/sessions/SessionContextMenu.tsx","../../../src/features/sessions/WorkspaceGroupHeader.tsx","../../../src/features/sessions/workspace-grouping.ts","../../../src/features/sessions/WorkspaceContextMenu.tsx","../../../src/features/sessions/open-chat-tab.ts","../../../src/features/workspace/pane-dnd.ts","../../../src/features/workspace/external-drag.ts","../../../src/features/sessions/SessionList.tsx","../../../src/components/primitives/Spinner.tsx","../../../src/hooks/use-pane-drag.ts","../../../src/features/workspace/panel-registry.ts","../../../src/features/workspace/pane-layout-view.ts","../../../src/features/workspace/PaneDividers.tsx","../../../src/features/files/viewer-registry.ts","../../../src/features/files/open-file-tab.ts","../../../src/hooks/use-external-pane-drop.ts","../../../src/features/workspace/TabStrip.tsx","../../../src/features/workspace/DropPreview.tsx","../../../src/features/workspace/TabPanelHost.tsx","../../../src/lib/connection/rpc-keys.ts","../../../src/hooks/use-explorer.ts","../../../src/hooks/use-explorer-tree.ts","../../../src/hooks/use-explorer-watch.ts","../../../src/hooks/file-transfer-instance.ts","../../../src/hooks/use-file-transfer.ts","../../../src/stores/git-store.ts","../../../src/lib/paths.ts","../../../src/features/files/move-target.ts","../../../src/features/files/move-entry.ts","../../../src/features/files/move-status.ts","../../../src/features/files/file-tree.ts","../../../src/features/files/git-status-index.ts","../../../src/features/files/TreeDraftRow.tsx","../../../src/features/files/TreeRenameRow.tsx","../../../src/features/files/TreeNode.tsx","../../../src/lib/clipboard.ts","../../../src/features/files/delete-entry.ts","../../../src/features/files/FileContextMenu.tsx","../../../src/features/files/create-entry.ts","../../../src/features/files/FileExplorer.tsx","../../../src/features/git/ChangesPanel.tsx","../../../src/features/files/RightSidebar.tsx","../../../src/stores/stats-store.ts","../../../src/hooks/use-session-stats.ts","../../../src/hooks/use-checkout-status.ts","../../../src/features/workspace/status-bar-format.ts","../../../src/features/workspace/StatusBar.tsx","../../../src/components/primitives/Checkbox.tsx","../../../src/components/primitives/Select.tsx","../../../src/components/primitives/Dialog.tsx","../../../src/components/primitives/ResizeHandle.tsx","../../../src/features/sessions/open-workspace.ts","../../../src/features/workspace-picker/OpenWorkspaceDialog.tsx","../../../src/routes/WorkspacePage.tsx","../../../src/hooks/use-connection.ts","../../../src/lib/pane-layout-persistence.ts","../../../src/features/workspace/reopen-client-tabs.ts","../../../src/features/workspace/restore-active-workspace.ts","../../../src/hooks/use-pane-layout.ts","../../../src/lib/protocol/events.ts","../../../src/lib/protocol/timeline-paging.ts","../../../src/hooks/use-session-restore.ts","../../../src/hooks/use-terminal-restore.ts","../../../src/hooks/use-shortcuts.ts","../../../src/app.tsx","../../../src/lib/connection/resume-action.ts","../../../src/lib/connection/resume-triggers.ts","../../../src/main.tsx"],"sourcesContent":["import { QueryClient } from \"@tanstack/react-query\";\n\n// Shared TanStack Query cache. Tuning (staleTime, retry, invalidation on the\n// `filesChanged` signal) is refined as data-fetching hooks land.\nexport const queryClient = new QueryClient();\n","// Small pure color helpers used by the theme builder and (later) brand accent derivation.\n// No external deps; operates on `#rrggbb` / `#rgb` hex strings.\n\nexport type Rgb = { r: number; g: number; b: number };\n\nexport function hexToRgb(hex: string): Rgb {\n const h = hex.trim().replace(/^#/, \"\");\n const full =\n h.length === 3\n ? h\n .split(\"\")\n .map((c) => c + c)\n .join(\"\")\n : h;\n if (full.length !== 6 || /[^0-9a-fA-F]/.test(full)) {\n throw new Error(`Invalid hex color: ${hex}`);\n }\n return {\n r: parseInt(full.slice(0, 2), 16),\n g: parseInt(full.slice(2, 4), 16),\n b: parseInt(full.slice(4, 6), 16),\n };\n}\n\nfunction clamp255(n: number): number {\n return Math.max(0, Math.min(255, Math.round(n)));\n}\n\nexport function rgbToHex({ r, g, b }: Rgb): string {\n const to = (n: number) => clamp255(n).toString(16).padStart(2, \"0\");\n return `#${to(r)}${to(g)}${to(b)}`;\n}\n\n// Relative luminance per WCAG (0 = black, 1 = white).\nexport function relativeLuminance(hex: string): number {\n const { r, g, b } = hexToRgb(hex);\n const channel = (v: number) => {\n const s = v / 255;\n return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n };\n return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);\n}\n\n// Contrast-safe text color to place ON TOP of `hex`.\nexport function contrastForeground(hex: string, dark = \"#18181b\", light = \"#ffffff\"): string {\n return relativeLuminance(hex) > 0.45 ? dark : light;\n}\n\n// Mix `hex` toward white by `amount` (0..1). Used to derive an accentBright when omitted.\nexport function lighten(hex: string, amount: number): string {\n const { r, g, b } = hexToRgb(hex);\n const a = Math.max(0, Math.min(1, amount));\n return rgbToHex({\n r: r + (255 - r) * a,\n g: g + (255 - g) * a,\n b: b + (255 - b) * a,\n });\n}\n\n// Mix `hex` toward black by `amount` (0..1).\nexport function darken(hex: string, amount: number): string {\n const { r, g, b } = hexToRgb(hex);\n const a = Math.max(0, Math.min(1, amount));\n return rgbToHex({ r: r * (1 - a), g: g * (1 - a), b: b * (1 - a) });\n}\n\nexport function isHexColor(value: string): boolean {\n const h = value.trim().replace(/^#/, \"\");\n return (h.length === 3 || h.length === 6) && !/[^0-9a-fA-F]/.test(h);\n}\n","// Design-system raw color palette.\n// Tailwind-style numeric scales referenced by design-system.md § Colors (`colors.palette`).\n// Components reach into these only for FIXED signal colors that must not shift per theme.\n\nexport type PaletteShade =\n | \"50\"\n | \"100\"\n | \"200\"\n | \"300\"\n | \"400\"\n | \"500\"\n | \"600\"\n | \"700\"\n | \"800\"\n | \"900\"\n | \"950\";\n\nexport type PaletteScale = Readonly<Record<PaletteShade, string>>;\n\nconst scale = (\n s50: string,\n s100: string,\n s200: string,\n s300: string,\n s400: string,\n s500: string,\n s600: string,\n s700: string,\n s800: string,\n s900: string,\n s950: string,\n): PaletteScale => ({\n \"50\": s50,\n \"100\": s100,\n \"200\": s200,\n \"300\": s300,\n \"400\": s400,\n \"500\": s500,\n \"600\": s600,\n \"700\": s700,\n \"800\": s800,\n \"900\": s900,\n \"950\": s950,\n});\n\nexport const palette = {\n zinc: scale(\n \"#fafafa\",\n \"#f4f4f5\",\n \"#e4e4e7\",\n \"#d4d4d8\",\n \"#a1a1aa\",\n \"#71717a\",\n \"#52525b\",\n \"#3f3f46\",\n \"#27272a\",\n \"#18181b\",\n \"#09090b\",\n ),\n gray: scale(\n \"#f9fafb\",\n \"#f3f4f6\",\n \"#e5e7eb\",\n \"#d1d5db\",\n \"#9ca3af\",\n \"#6b7280\",\n \"#4b5563\",\n \"#374151\",\n \"#1f2937\",\n \"#111827\",\n \"#030712\",\n ),\n slate: scale(\n \"#f8fafc\",\n \"#f1f5f9\",\n \"#e2e8f0\",\n \"#cbd5e1\",\n \"#94a3b8\",\n \"#64748b\",\n \"#475569\",\n \"#334155\",\n \"#1e293b\",\n \"#0f172a\",\n \"#020617\",\n ),\n blue: scale(\n \"#eff6ff\",\n \"#dbeafe\",\n \"#bfdbfe\",\n \"#93c5fd\",\n \"#60a5fa\",\n \"#3b82f6\",\n \"#2563eb\",\n \"#1d4ed8\",\n \"#1e40af\",\n \"#1e3a8a\",\n \"#172554\",\n ),\n green: scale(\n \"#f0fdf4\",\n \"#dcfce7\",\n \"#bbf7d0\",\n \"#86efac\",\n \"#4ade80\",\n \"#22c55e\",\n \"#16a34a\",\n \"#15803d\",\n \"#166534\",\n \"#14532d\",\n \"#052e16\",\n ),\n red: scale(\n \"#fef2f2\",\n \"#fee2e2\",\n \"#fecaca\",\n \"#fca5a5\",\n \"#f87171\",\n \"#ef4444\",\n \"#dc2626\",\n \"#b91c1c\",\n \"#991b1b\",\n \"#7f1d1d\",\n \"#450a0a\",\n ),\n teal: scale(\n \"#f0fdfa\",\n \"#ccfbf1\",\n \"#99f6e4\",\n \"#5eead4\",\n \"#2dd4bf\",\n \"#14b8a6\",\n \"#0d9488\",\n \"#0f766e\",\n \"#115e59\",\n \"#134e4a\",\n \"#042f2e\",\n ),\n amber: scale(\n \"#fffbeb\",\n \"#fef3c7\",\n \"#fde68a\",\n \"#fcd34d\",\n \"#fbbf24\",\n \"#f59e0b\",\n \"#d97706\",\n \"#b45309\",\n \"#92400e\",\n \"#78350f\",\n \"#451a03\",\n ),\n yellow: scale(\n \"#fefce8\",\n \"#fef9c3\",\n \"#fef08a\",\n \"#fde047\",\n \"#facc15\",\n \"#eab308\",\n \"#ca8a04\",\n \"#a16207\",\n \"#854d0e\",\n \"#713f12\",\n \"#422006\",\n ),\n purple: scale(\n \"#faf5ff\",\n \"#f3e8ff\",\n \"#e9d5ff\",\n \"#d8b4fe\",\n \"#c084fc\",\n \"#a855f7\",\n \"#9333ea\",\n \"#7e22ce\",\n \"#6b21a8\",\n \"#581c87\",\n \"#3b0764\",\n ),\n orange: scale(\n \"#fff7ed\",\n \"#ffedd5\",\n \"#fed7aa\",\n \"#fdba74\",\n \"#fb923c\",\n \"#f97316\",\n \"#ea580c\",\n \"#c2410c\",\n \"#9a3412\",\n \"#7c2d12\",\n \"#431407\",\n ),\n white: \"#ffffff\",\n black: \"#000000\",\n} as const;\n\nexport type Palette = typeof palette;\n","// Layer-based semantic color system + syntax/terminal maps per design-system.md § Colors.\n\nimport { contrastForeground, lighten } from \"./color-utils.js\";\nimport { palette } from \"./palette.js\";\n\n// Highlight token → color map (see timeline-rendering.md § Syntax highlighting).\nexport type SyntaxColors = {\n keyword: string;\n string: string;\n number: string;\n boolean: string;\n comment: string;\n function: string;\n variable: string;\n type: string;\n class: string;\n constant: string;\n operator: string;\n punctuation: string;\n tag: string;\n attribute: string;\n property: string;\n regexp: string;\n escape: string;\n heading: string;\n link: string;\n deleted: string;\n inserted: string;\n};\n\n// Full xterm theme: bg/fg/cursor/selection + 16 ANSI colors.\nexport type TerminalTheme = {\n background: string;\n foreground: string;\n cursor: string;\n cursorAccent: string;\n selectionBackground: string;\n black: string;\n red: string;\n green: string;\n yellow: string;\n blue: string;\n magenta: string;\n cyan: string;\n white: string;\n brightBlack: string;\n brightRed: string;\n brightGreen: string;\n brightYellow: string;\n brightBlue: string;\n brightMagenta: string;\n brightCyan: string;\n brightWhite: string;\n};\n\nexport type ThemeColors = {\n // Surfaces (elevation ramp)\n surface0: string;\n surface1: string;\n surface2: string;\n surface3: string;\n surface4: string;\n // Surfaces (special)\n surfaceDiffEmpty: string;\n surfaceSidebar: string;\n surfaceSidebarHover: string;\n surfaceWorkspace: string;\n // Text\n foreground: string;\n foregroundMuted: string;\n // Brand\n accent: string;\n accentBright: string;\n accentForeground: string;\n // Semantic\n destructive: string;\n destructiveForeground: string;\n success: string;\n successForeground: string;\n // Borders\n border: string;\n borderAccent: string;\n // Status\n statusSuccess: string;\n statusDanger: string;\n statusWarning: string;\n statusMerged: string;\n // Diff\n diffAddition: string;\n diffDeletion: string;\n // Controls\n scrollbarHandle: string;\n // Legacy aliases (migration only; new code uses semantic tokens above)\n background: string;\n popover: string;\n popoverForeground: string;\n primary: string;\n secondary: string;\n muted: string;\n mutedForeground: string;\n input: string;\n ring: string;\n // Nested raw scales\n palette: typeof palette;\n syntax: SyntaxColors;\n terminal: TerminalTheme;\n};\n\n// Signal status colors — semantic success/danger/warning/merged. Kept a step\n// darker than the raw palette so they read as signals, not neon. Per Paseo\n// (`~/DEV/paseo/packages/app/src/styles/theme.ts`) light and dark differ.\nconst DARK_STATUS = {\n statusSuccess: \"#16a34a\", // green-600\n statusDanger: \"#dc2626\", // red-600\n statusWarning: \"#f59e0b\", // amber-500\n statusMerged: \"#9333ea\", // purple-600\n} as const;\nconst LIGHT_STATUS = {\n statusSuccess: \"#15803d\", // green-700\n statusDanger: \"#b91c1c\", // red-700\n statusWarning: \"#d97706\", // amber-600\n statusMerged: \"#7c3aed\", // purple-600\n} as const;\n\nfunction darkSyntax(): SyntaxColors {\n return {\n keyword: palette.purple[\"300\"],\n string: palette.green[\"300\"],\n number: palette.orange[\"300\"],\n boolean: palette.orange[\"300\"],\n comment: palette.zinc[\"500\"],\n function: palette.blue[\"300\"],\n variable: palette.zinc[\"200\"],\n type: palette.teal[\"300\"],\n class: palette.amber[\"300\"],\n constant: palette.orange[\"300\"],\n operator: palette.zinc[\"400\"],\n punctuation: palette.zinc[\"400\"],\n tag: palette.red[\"300\"],\n attribute: palette.amber[\"300\"],\n property: palette.blue[\"200\"],\n regexp: palette.teal[\"300\"],\n escape: palette.orange[\"200\"],\n heading: palette.blue[\"300\"],\n link: palette.blue[\"400\"],\n deleted: palette.red[\"400\"],\n inserted: palette.green[\"400\"],\n };\n}\n\nfunction lightSyntax(): SyntaxColors {\n return {\n keyword: palette.purple[\"700\"],\n string: palette.green[\"700\"],\n number: palette.orange[\"700\"],\n boolean: palette.orange[\"700\"],\n comment: palette.zinc[\"500\"],\n function: palette.blue[\"700\"],\n variable: palette.zinc[\"800\"],\n type: palette.teal[\"700\"],\n class: palette.amber[\"700\"],\n constant: palette.orange[\"700\"],\n operator: palette.zinc[\"600\"],\n punctuation: palette.zinc[\"600\"],\n tag: palette.red[\"700\"],\n attribute: palette.amber[\"700\"],\n property: palette.blue[\"700\"],\n regexp: palette.teal[\"700\"],\n escape: palette.orange[\"700\"],\n heading: palette.blue[\"700\"],\n link: palette.blue[\"600\"],\n deleted: palette.red[\"700\"],\n inserted: palette.green[\"700\"],\n };\n}\n\n// Tint config consumed by the dark-theme builder.\nexport type DarkTintConfig = {\n surface0: string;\n surface1: string;\n surface2: string;\n surface3: string;\n surface4: string;\n surfaceDiffEmpty: string;\n surfaceSidebar: string;\n surfaceSidebarHover: string;\n mutedForeground: string;\n scrollbarHandle: string;\n border: string;\n borderAccent: string;\n accent: string;\n accentBright: string;\n destructive: string;\n};\n\nfunction terminalFrom(surface0: string, foreground: string, accent: string): TerminalTheme {\n return {\n background: surface0,\n foreground,\n cursor: accent,\n cursorAccent: surface0,\n selectionBackground: \"rgba(255,255,255,0.18)\",\n black: palette.zinc[\"900\"],\n red: palette.red[\"500\"],\n green: palette.green[\"500\"],\n yellow: palette.amber[\"500\"],\n blue: palette.blue[\"500\"],\n magenta: palette.purple[\"500\"],\n cyan: palette.teal[\"500\"],\n white: palette.zinc[\"300\"],\n brightBlack: palette.zinc[\"600\"],\n brightRed: palette.red[\"400\"],\n brightGreen: palette.green[\"400\"],\n brightYellow: palette.amber[\"400\"],\n brightBlue: palette.blue[\"400\"],\n brightMagenta: palette.purple[\"400\"],\n brightCyan: palette.teal[\"400\"],\n brightWhite: palette.zinc[\"50\"],\n };\n}\n\n// Build a full dark theme color set from a tint config.\nexport function buildDarkColors(tint: DarkTintConfig): ThemeColors {\n // Paseo dark foreground is zinc-50 (#fafafa), not zinc-100.\n const foreground = palette.zinc[\"50\"];\n const accentForeground = contrastForeground(tint.accent);\n return {\n surface0: tint.surface0,\n surface1: tint.surface1,\n surface2: tint.surface2,\n surface3: tint.surface3,\n surface4: tint.surface4,\n surfaceDiffEmpty: tint.surfaceDiffEmpty,\n surfaceSidebar: tint.surfaceSidebar,\n surfaceSidebarHover: tint.surfaceSidebarHover,\n // Paseo dark: the workspace surface is surface1 (not the app background surface0).\n surfaceWorkspace: tint.surface1,\n foreground,\n foregroundMuted: tint.mutedForeground,\n accent: tint.accent,\n accentBright: tint.accentBright,\n accentForeground,\n destructive: tint.destructive,\n destructiveForeground: palette.white,\n // Paseo: success uses the theme accent in dark.\n success: tint.accent,\n successForeground: palette.white,\n border: tint.border,\n borderAccent: tint.borderAccent,\n ...DARK_STATUS,\n diffAddition: palette.green[\"400\"], // #4ade80\n diffDeletion: palette.red[\"500\"], // #ef4444\n scrollbarHandle: tint.scrollbarHandle,\n // Legacy aliases → nearest semantic token\n background: tint.surface0,\n popover: tint.surface2,\n popoverForeground: foreground,\n primary: tint.accent,\n secondary: tint.surface2,\n muted: tint.surface2,\n mutedForeground: tint.mutedForeground,\n input: tint.surface2,\n ring: tint.accent,\n palette,\n syntax: darkSyntax(),\n terminal: terminalFrom(tint.surface0, foreground, tint.accent),\n };\n}\n\n// The single light theme (built directly, not via the dark builder).\n// Values mirror Paseo's `lightSemanticColors` (`~/DEV/paseo/.../styles/theme.ts`).\nexport function buildLightColors(): ThemeColors {\n const foreground = \"#1a1a1e\";\n const accent = \"#253e6f\";\n return {\n surface0: \"#ffffff\",\n surface1: \"#fafafa\",\n surface2: \"#f4f4f5\",\n surface3: \"#e4e4e7\",\n surface4: \"#d4d4d8\",\n surfaceDiffEmpty: \"#f6f6f6\",\n surfaceSidebar: \"#f4f4f5\",\n surfaceSidebarHover: \"#e9e9ec\",\n surfaceWorkspace: \"#ffffff\",\n foreground,\n foregroundMuted: \"#71717a\",\n accent,\n accentBright: \"#2f4f8e\",\n accentForeground: contrastForeground(accent),\n destructive: \"#b04138\",\n destructiveForeground: \"#ffffff\",\n success: accent,\n successForeground: \"#ffffff\",\n border: \"#e4e4e7\",\n borderAccent: \"#ececf1\",\n ...LIGHT_STATUS,\n diffAddition: \"#15803d\", // green-700\n diffDeletion: \"#b91c1c\", // red-700\n scrollbarHandle: \"#3f3f46\", // zinc-700\n background: \"#ffffff\",\n popover: \"#ffffff\",\n popoverForeground: foreground,\n primary: accent,\n secondary: \"#f4f4f5\",\n muted: \"#f4f4f5\",\n mutedForeground: \"#71717a\",\n input: \"#f4f4f5\",\n ring: accent,\n palette,\n syntax: lightSyntax(),\n terminal: {\n ...terminalFrom(\"#ffffff\", foreground, accent),\n selectionBackground: \"rgba(0,0,0,0.15)\",\n white: \"#ffffff\",\n brightWhite: \"#fafafa\",\n },\n };\n}\n\nexport { lighten };\n","// Six named theme variants per design-system.md § Theme variants.\n// One light + five dark tints sharing the buildDarkColors builder.\n\nimport { buildDarkColors, buildLightColors, type ThemeColors } from \"./colors.js\";\n\n// theme-name → swatch color map (for Appearance picker).\nexport const THEME_SWATCHES: Readonly<Record<ThemeName, string>> = {\n light: \"#ffffff\",\n dark: \"#3b62b0\",\n zinc: \"#808080\",\n midnight: \"#4A6BA8\",\n claude: \"#D97757\",\n ghostty: \"#8caaee\",\n};\n\nexport type ThemeName = \"light\" | \"dark\" | \"zinc\" | \"midnight\" | \"claude\" | \"ghostty\";\nexport const THEME_NAMES: readonly ThemeName[] = [\n \"light\",\n \"dark\",\n \"zinc\",\n \"midnight\",\n \"claude\",\n \"ghostty\",\n];\n\nfunction buildVariantColors(name: ThemeName): ThemeColors {\n if (name === \"light\") return buildLightColors();\n\n switch (name) {\n // Paseo — subtle teal-green tint (default). Exact values from Paseo `paseoDarkColors`.\n case \"dark\":\n return buildDarkColors({\n surface0: \"#181B1A\",\n surface1: \"#1E2120\",\n surface2: \"#272A29\",\n surface3: \"#434645\",\n surface4: \"#595B5B\",\n surfaceDiffEmpty: \"#252827\",\n surfaceSidebar: \"#141716\",\n surfaceSidebarHover: \"#1c1f1e\",\n mutedForeground: \"#A1A5A4\",\n scrollbarHandle: \"#717574\",\n border: \"#252B2A\",\n borderAccent: \"#2f3135\",\n accent: \"#2e5cb8\",\n accentBright: \"#a2b4d7\",\n destructive: \"#c64f43\",\n });\n\n // Zinc — neutral gray, no tint. Exact values from Paseo `zincDarkColors`.\n case \"zinc\":\n return buildDarkColors({\n surface0: \"#18181b\",\n surface1: \"#1f1f22\",\n surface2: \"#27272a\",\n surface3: \"#3f3f46\",\n surface4: \"#52525b\",\n surfaceDiffEmpty: \"#242427\",\n surfaceSidebar: \"#131316\",\n surfaceSidebarHover: \"#1b1b1e\",\n mutedForeground: \"#a1a1aa\",\n scrollbarHandle: \"#71717a\",\n border: \"#27272a\",\n borderAccent: \"#303036\",\n accent: \"#e4e4e7\",\n accentBright: \"#fafafa\",\n destructive: \"#c44a4a\",\n });\n\n // Midnight — subtle blue tint. Exact values from Paseo `midnightDarkColors`.\n case \"midnight\":\n return buildDarkColors({\n surface0: \"#161820\",\n surface1: \"#1c1e27\",\n surface2: \"#252731\",\n surface3: \"#3c3e4c\",\n surface4: \"#535564\",\n surfaceDiffEmpty: \"#222430\",\n surfaceSidebar: \"#121420\",\n surfaceSidebarHover: \"#1a1c28\",\n mutedForeground: \"#9a9db0\",\n scrollbarHandle: \"#6b6e82\",\n border: \"#242636\",\n borderAccent: \"#2e3040\",\n accent: \"#3b6fcf\",\n accentBright: \"#7eaaeb\",\n destructive: \"#c44a52\",\n });\n\n // Claude — warm neutral with subtle orange undertone. From Paseo `claudeDarkColors`.\n case \"claude\":\n return buildDarkColors({\n surface0: \"#1f1f1e\",\n surface1: \"#262523\",\n surface2: \"#2f2d2b\",\n surface3: \"#4a4745\",\n surface4: \"#605d5b\",\n surfaceDiffEmpty: \"#2a2826\",\n surfaceSidebar: \"#1a1918\",\n surfaceSidebarHover: \"#222120\",\n mutedForeground: \"#ada9a5\",\n scrollbarHandle: \"#78746f\",\n border: \"#2c2a27\",\n borderAccent: \"#36332f\",\n accent: \"#d97757\",\n accentBright: \"#e89a7f\",\n destructive: \"#cf513e\",\n });\n\n // Ghostty — blue-tinted dark. Exact values from Paseo `ghosttyDarkColors`.\n case \"ghostty\":\n return buildDarkColors({\n surface0: \"#282c34\",\n surface1: \"#2f333d\",\n surface2: \"#383c48\",\n surface3: \"#4a4f5e\",\n surface4: \"#5b6175\",\n surfaceDiffEmpty: \"#323643\",\n surfaceSidebar: \"#21252d\",\n surfaceSidebarHover: \"#292d36\",\n mutedForeground: \"#c8ccd8\",\n scrollbarHandle: \"#a0a4b2\",\n border: \"#353a47\",\n borderAccent: \"#3f4454\",\n accent: \"#89b4fa\",\n accentBright: \"#b4d0fc\",\n destructive: \"#c44a55\",\n });\n }\n}\n\nexport type ThemeVariant = {\n name: ThemeName;\n colorScheme: \"light\" | \"dark\";\n colors: ThemeColors;\n swatch: string;\n};\n\nconst _variants: Record<ThemeName, ThemeVariant> = {} as Record<ThemeName, ThemeVariant>;\nfor (const name of THEME_NAMES) {\n const colorScheme: \"light\" | \"dark\" = name === \"light\" ? \"light\" : \"dark\";\n _variants[name] = {\n name,\n colorScheme,\n colors: buildVariantColors(name),\n swatch: THEME_SWATCHES[name],\n };\n}\nexport const THEME_VARIANTS: Readonly<Record<ThemeName, ThemeVariant>> = _variants;\n","// Theme scale tokens (theme-invariant unless patched by Appearance settings).\n// Exact values per design-system.md § Scales.\n\n// spacing: key = literal px value (dot-free by construction — a CSS custom-property name\n// cannot contain a literal `.`; an earlier \"key = px / 4\" scheme used fractional keys like\n// \"1.5\"/\"7.5\", which silently emitted INVALID custom properties `--pi-spacing-1.5` that no\n// browser ever resolves, so every padding/margin/gap using one collapsed to nothing app-wide.\n// Covers every value actually in use across the app — analogous to fontSize's \"dense ladder\n// covering the rungs the UI actually uses\" rather than forcing existing, deliberately-tuned\n// spacing onto a sparser grid and visibly nudging it.\nexport const spacing = {\n \"0\": 0,\n \"1\": 1,\n \"2\": 2,\n \"3\": 3,\n \"4\": 4,\n \"5\": 5,\n \"6\": 6,\n \"7\": 7,\n \"8\": 8,\n \"10\": 10,\n \"12\": 12,\n \"14\": 14,\n \"16\": 16,\n \"20\": 20,\n \"24\": 24,\n \"30\": 30,\n \"32\": 32,\n \"48\": 48,\n \"64\": 64,\n \"80\": 80,\n \"96\": 96,\n \"128\": 128,\n} as const;\nexport type Spacing = typeof spacing;\nexport type SpacingKey = keyof Spacing;\n\n// fontSize — a dense, mostly 1px-step ladder covering the rungs the UI actually uses, plus the\n// document base and the display sizes above it. Every `font-size` in the app resolves to one of\n// these rungs through `var(--pi-font-size-*)`; CSS modules never hardcode a literal, so this\n// table is the ONE lever for the app's text size (packages/web-client/AGENTS.md § Invariants).\n// Values are px and are typed as `number` (not literals) so `applyAppearance` can patch them at\n// runtime; `theme/css-bridge.ts` emits them as rem against the untouched 16px root.\nexport type FontSizeKey =\n | \"4xs\"\n | \"3xs\"\n | \"2xs\"\n | \"xs\"\n | \"code\"\n | \"sm\"\n | \"md\"\n | \"base\"\n | \"lg\"\n | \"xl\"\n | \"2xl\"\n | \"3xl\"\n | \"4xl\";\n\n// Tuning history, so nobody re-walks it: the original ladder (9/10/11/12/13/14/16) read too small,\n// a ~1.25x pass overshot, ~1.125x still ran slightly large. This is ~1.06x — the micro rungs stay\n// where 1.125x put them (below `4xs`=10 the badges stop being legible at all), and everything from\n// `xs` up steps back down by 1px. Retuning the app's text size means editing THIS TABLE ONLY.\nexport const baseFontSize: Readonly<Record<FontSizeKey, number>> = {\n \"4xs\": 10, // micro badges (queued, command-kind)\n \"3xs\": 11, // micro meta (author label, git status badge)\n \"2xs\": 12, // secondary meta (versions, counts, line numbers, hunk headers)\n xs: 13, // dense UI text — the most common rung\n code: 13, // code/diff surfaces; tracks its own base, not the prose one\n sm: 14, // primary UI text, row titles, tab labels\n md: 15, // prose and form controls\n base: 17, // document base — inputs, screen titles, composer (FONT_SIZE_BASE anchor)\n lg: 19,\n xl: 21,\n \"2xl\": 23,\n \"3xl\": 28,\n \"4xl\": 36,\n};\n\n// fontWeight — RN-style string weights.\nexport const fontWeight = {\n normal: \"400\",\n medium: \"500\",\n semibold: \"600\",\n bold: \"700\",\n} as const;\nexport type FontWeight = typeof fontWeight;\n\nexport const borderRadius = {\n none: 0,\n sm: 2,\n base: 4,\n md: 6,\n lg: 8,\n xl: 12,\n \"2xl\": 16,\n full: 9999,\n} as const;\nexport type BorderRadius = typeof borderRadius;\n\nexport const borderWidth = {\n \"0\": 0,\n \"1\": 1,\n \"2\": 2,\n} as const;\nexport type BorderWidth = typeof borderWidth;\n\nexport const opacity = {\n \"0\": 0,\n \"50\": 0.5,\n \"100\": 1,\n} as const;\nexport type Opacity = typeof opacity;\n\nexport const iconSize = {\n xs: 12,\n sm: 14,\n md: 16,\n lg: 20,\n} as const;\nexport type IconSize = typeof iconSize;\n\n// lineHeight.diff is reserved for code/diff; prose uses fontSize.base * ~1.4 at render time.\nexport const baseLineHeight = { diff: 22 } as const;\n\n// Default platform font stacks (seeds; appearance can override the `ui` stack).\nexport const DEFAULT_UI_FONT =\n 'system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif';\nexport const DEFAULT_MONO_FONT =\n 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", monospace';\n\nexport type Shadow = {\n color: string;\n offsetX: number;\n offsetY: number;\n radius: number;\n elevation: number;\n};\n\nexport type ShadowSet = { sm: Shadow; md: Shadow; lg: Shadow };\n\n// Shadow builder — dark themes use a stronger alpha-tinted color.\nexport function buildShadows(colorScheme: \"light\" | \"dark\"): ShadowSet {\n const color = colorScheme === \"dark\" ? \"rgba(0,0,0,0.55)\" : \"rgba(24,24,27,0.12)\";\n return {\n sm: { color, offsetX: 0, offsetY: 1, radius: 2, elevation: 1 },\n md: { color, offsetX: 0, offsetY: 3, radius: 8, elevation: 4 },\n lg: { color, offsetX: 0, offsetY: 10, radius: 24, elevation: 12 },\n };\n}\n","// Full Theme type + runtime appearance application.\n// design-system.md § Theme token shape, § Behavior (appearance application)\n\nimport {\n baseFontSize,\n baseLineHeight,\n borderRadius,\n borderWidth,\n buildShadows,\n DEFAULT_MONO_FONT,\n DEFAULT_UI_FONT,\n fontWeight,\n iconSize,\n opacity,\n spacing,\n type FontSizeKey,\n type ShadowSet,\n} from \"./tokens.js\";\nimport { type ThemeColors } from \"./colors.js\";\nimport { type ThemeName, THEME_VARIANTS } from \"./variants.js\";\n\nexport type Theme = {\n name: ThemeName;\n colorScheme: \"light\" | \"dark\";\n colors: ThemeColors;\n spacing: typeof spacing;\n // fontSize is Record<FontSizeKey, number> (not readonly narrowed literals) so appearance\n // patch can replace values without a type error.\n fontSize: Record<FontSizeKey, number>;\n fontFamily: {\n // Both are plain strings so custom fonts can be spliced in at runtime.\n ui: string;\n mono: string;\n };\n lineHeight: Record<\"diff\", number>;\n iconSize: typeof iconSize;\n fontWeight: typeof fontWeight;\n borderRadius: typeof borderRadius;\n borderWidth: typeof borderWidth;\n opacity: typeof opacity;\n shadow: ShadowSet;\n swatch: string;\n};\n\nfunction buildTheme(name: ThemeName): Theme {\n const variant = THEME_VARIANTS[name];\n return {\n name,\n colorScheme: variant.colorScheme,\n colors: variant.colors,\n spacing,\n fontSize: { ...baseFontSize },\n fontFamily: {\n ui: DEFAULT_UI_FONT,\n mono: DEFAULT_MONO_FONT,\n },\n lineHeight: { ...baseLineHeight },\n iconSize,\n fontWeight,\n borderRadius,\n borderWidth,\n opacity,\n shadow: buildShadows(variant.colorScheme),\n swatch: variant.swatch,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Appearance settings + runtime patch\n// ---------------------------------------------------------------------------\n\nexport type AppearanceSettings = {\n /** One of the six named theme variants. */\n themeName?: ThemeName;\n /** Custom UI font family string; empty/undefined → platform default. */\n uiFont?: string;\n /** Custom mono font family string; empty/undefined → platform default. */\n monoFont?: string;\n /**\n * Desired base font size (px). Every fontSize rung is scaled relative to `baseFontSize.base`,\n * so passing that value through is a no-op. Clamped 10–24.\n */\n fontSize?: number;\n};\n\n// Derived, never a literal: a hardcoded copy silently miscalibrates the whole setting the moment\n// tokens.ts's `base` rung is retuned (it has been, twice).\nconst FONT_SIZE_BASE = baseFontSize.base;\nconst FONT_SIZE_MIN = 10;\nconst FONT_SIZE_MAX = 24;\n\n/**\n * Apply appearance settings to a base theme and return a new patched theme.\n * The base theme object is never mutated.\n * Code/mono surfaces keep the mono font because they reference `fontFamily.mono`\n * independently — a custom `uiFont` only patches `fontFamily.ui`.\n */\nexport function applyAppearance(base: Theme, settings: AppearanceSettings): Theme {\n const themeName: ThemeName = settings.themeName ?? base.name;\n const baseTheme = buildTheme(themeName);\n\n const uiFont =\n settings.uiFont && settings.uiFont.trim() !== \"\" ? settings.uiFont : DEFAULT_UI_FONT;\n const monoFont =\n settings.monoFont && settings.monoFont.trim() !== \"\" ? settings.monoFont : DEFAULT_MONO_FONT;\n\n const fontSize = { ...baseTheme.fontSize };\n if (settings.fontSize != null) {\n const clamped = Math.max(FONT_SIZE_MIN, Math.min(FONT_SIZE_MAX, settings.fontSize));\n const scale = clamped / FONT_SIZE_BASE;\n // Every rung scales, including `code` — it keeps its own base (which is not the prose\n // `base`) so code surfaces don't grow disproportionately, but it still follows the setting.\n // Keys come from the table itself; a new rung in tokens.ts is picked up with no edit here.\n for (const k of Object.keys(baseFontSize) as FontSizeKey[]) {\n fontSize[k] = Math.round(baseFontSize[k] * scale);\n }\n }\n\n return {\n ...baseTheme,\n fontSize,\n fontFamily: { ui: uiFont, mono: monoFont },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Theme registry / accessor\n// ---------------------------------------------------------------------------\n\n/** Build a fresh Theme for the given name with default appearance settings. */\nexport function getTheme(name: ThemeName): Theme {\n return buildTheme(name);\n}\n\nexport const DEFAULT_THEME_NAME: ThemeName = \"dark\";\nexport { type ThemeName };\n","/**\n * Theme → CSS custom properties bridge.\n * Flattens a Theme object into `--pi-*` CSS variables on :root.\n * design-system.md § Purpose (render-stack decision)\n */\n\nimport { type Theme } from \"./theme.js\";\nimport { type ThemeColors, type SyntaxColors, type TerminalTheme } from \"./colors.js\";\n\n// ---------------------------------------------------------------------------\n// Token → variable-name map\n// ---------------------------------------------------------------------------\n\n// Root `html` font-size is left at the browser/OS default (see global.css) — 1rem tracks the\n// user's own zoom / accessibility text-size setting. `REM_BASE_PX` is only the conversion\n// constant for our px-authored design scale (tokens.ts `baseFontSize`), not a CSS override.\nconst REM_BASE_PX = 16;\n\nfunction pxToRem(px: number): string {\n const rem = px / REM_BASE_PX;\n // Trim trailing zeros (e.g. 0.7500 -> 0.75) without reintroducing exponent notation.\n return `${Number(rem.toFixed(4))}rem`;\n}\n\n/**\n * Given a Theme, produces a flat Record<`--pi-${path}`, value> map\n * suitable for setting on `document.documentElement.style`.\n */\nexport function flattenThemeToVars(theme: Theme): Record<string, string> {\n const vars: Record<string, string> = {};\n\n // Colors (flat semantic tokens)\n flattenColors(theme.colors, vars);\n\n // Spacing\n for (const [key, val] of Object.entries(theme.spacing)) {\n vars[`--pi-spacing-${key}`] = `${val}px`;\n }\n\n // Font size — rem, not px, so it scales with the user's browser/OS text-size\n // preference instead of rendering visually smaller/larger across screens.\n for (const [key, val] of Object.entries(theme.fontSize)) {\n vars[`--pi-font-size-${key}`] = pxToRem(val);\n }\n\n // Font family\n vars[\"--pi-font-ui\"] = theme.fontFamily.ui;\n vars[\"--pi-font-mono\"] = theme.fontFamily.mono;\n\n // Line height\n vars[\"--pi-line-height-diff\"] = `${theme.lineHeight.diff}px`;\n\n // Icon size\n for (const [key, val] of Object.entries(theme.iconSize)) {\n vars[`--pi-icon-size-${key}`] = `${val}px`;\n }\n\n // Font weight\n for (const [key, val] of Object.entries(theme.fontWeight)) {\n vars[`--pi-font-weight-${key}`] = String(val);\n }\n\n // Border radius\n for (const [key, val] of Object.entries(theme.borderRadius)) {\n vars[`--pi-radius-${key}`] = val === 9999 ? \"9999px\" : `${val}px`;\n }\n\n // Border width\n for (const [key, val] of Object.entries(theme.borderWidth)) {\n vars[`--pi-border-width-${key}`] = `${val}px`;\n }\n\n // Opacity\n for (const [key, val] of Object.entries(theme.opacity)) {\n vars[`--pi-opacity-${key}`] = String(val);\n }\n\n // Shadow (convert { offsetX, offsetY, radius, color } → CSS box-shadow string)\n for (const [key, val] of Object.entries(theme.shadow)) {\n if (typeof val === \"string\") {\n vars[`--pi-shadow-${key}`] = val;\n } else if (val && typeof val === \"object\") {\n const s = val as { offsetX: number; offsetY: number; radius: number; color: string };\n vars[`--pi-shadow-${key}`] = `${s.offsetX}px ${s.offsetY}px ${s.radius}px ${s.color}`;\n }\n }\n\n // Color scheme marker (for prefers-color-scheme media query override)\n vars[\"--pi-color-scheme\"] = theme.colorScheme;\n\n return vars;\n}\n\nfunction flattenColors(colors: ThemeColors, vars: Record<string, string>): void {\n // Top-level semantic color tokens\n const { palette: _palette, syntax: _syntax, terminal: _terminal, ...semanticColors } = colors;\n\n // Emit color tokens VERBATIM (camelCase), e.g. `--pi-color-surfaceSidebar`,\n // `--pi-color-foregroundMuted`, `--pi-color-statusDanger`. This matches how\n // every component references them; kebab-casing here silently broke all\n // multi-word tokens (they fell back to defaults).\n for (const [key, val] of Object.entries(semanticColors)) {\n if (typeof val === \"string\") {\n vars[`--pi-color-${key}`] = val;\n }\n }\n\n // Syntax colors → --syntax-*\n if (_syntax) {\n flattenSyntax(_syntax, vars);\n }\n\n // Terminal colors are not CSS variables (consumed by xterm config directly);\n // but we expose a few key ones for CSS terminal-wrapper styling.\n if (_terminal) {\n vars[\"--pi-terminal-bg\"] = _terminal.background;\n vars[\"--pi-terminal-fg\"] = _terminal.foreground;\n }\n}\n\nfunction flattenSyntax(syntax: SyntaxColors, vars: Record<string, string>): void {\n for (const [key, val] of Object.entries(syntax)) {\n if (typeof val === \"string\") {\n vars[`--syntax-${camelToKebab(key)}`] = val;\n }\n }\n}\n\n/** Convert camelCase to kebab-case. */\nfunction camelToKebab(str: string): string {\n return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);\n}\n\n// ---------------------------------------------------------------------------\n// DOM application\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a flattened variable map to the document root.\n * Idempotent — re-applying the same map is a no-op (DOM diffing).\n */\nexport function applyVarsToRoot(vars: Record<string, string>): void {\n if (typeof document === \"undefined\") return;\n const style = document.documentElement.style;\n for (const [prop, val] of Object.entries(vars)) {\n style.setProperty(prop, val);\n }\n // Set color-scheme for native form control theming\n const scheme = vars[\"--pi-color-scheme\"];\n if (scheme) {\n document.documentElement.style.setProperty(\"color-scheme\", scheme);\n }\n}\n\n/**\n * Full pipeline: flatten a Theme and apply to the DOM.\n */\nexport function applyThemeToDOM(theme: Theme): void {\n const vars = flattenThemeToVars(theme);\n applyVarsToRoot(vars);\n}\n","// Brand config schema + resolver.\n// features/white-label-branding.md § Public Contract, § Resolution\n\nimport { z } from \"zod\";\nimport { contrastForeground, isHexColor, lighten } from \"@pi-studio-ui/theme/color-utils.js\";\n\n// ---------------------------------------------------------------------------\n// Schema\n// ---------------------------------------------------------------------------\n\nconst AssetRefSchema = z.string().min(1, \"Asset path must not be empty\");\n\nconst BrandColorsSchema = z.object({\n accent: z.string().refine(isHexColor, { message: \"accent must be a valid #rrggbb hex color\" }),\n accentBright: z\n .string()\n .refine(isHexColor, { message: \"accentBright must be a valid #rrggbb hex color\" })\n .optional(),\n accentForeground: z\n .string()\n .refine(isHexColor, { message: \"accentForeground must be a valid #rrggbb hex color\" })\n .optional(),\n swatch: z\n .string()\n .refine(isHexColor, { message: \"swatch must be a valid #rrggbb hex color\" })\n .optional(),\n});\n\nconst BrandAssetsSchema = z.object({\n logoLight: AssetRefSchema,\n logoDark: AssetRefSchema,\n logoMark: AssetRefSchema.optional(),\n icon: AssetRefSchema,\n splash: AssetRefSchema.optional(),\n});\n\nconst BrandLinksSchema = z\n .object({\n docs: z.string().url().optional(),\n support: z.string().url().optional(),\n issues: z.string().url().optional(),\n website: z.string().url().optional(),\n })\n .optional();\n\nconst BrandLegalSchema = z\n .object({\n copyright: z.string().optional(),\n companyName: z.string().optional(),\n })\n .optional();\n\nexport const BrandConfigSchema = z.object({\n productName: z.string().min(1, \"productName is required\"),\n shortName: z.string().optional(),\n tagline: z.string().optional(),\n colors: BrandColorsSchema,\n assets: BrandAssetsSchema,\n links: BrandLinksSchema,\n legal: BrandLegalSchema,\n});\n\nexport type BrandConfig = z.infer<typeof BrandConfigSchema>;\n\n// ---------------------------------------------------------------------------\n// Derived brand colors (fill in optional accent fields if omitted)\n// ---------------------------------------------------------------------------\n\nexport type ResolvedBrandColors = Required<\n Pick<BrandConfig[\"colors\"], \"accent\" | \"accentBright\" | \"accentForeground\" | \"swatch\">\n>;\n\nexport function resolveAccentColors(colors: BrandConfig[\"colors\"]): ResolvedBrandColors {\n const accent = colors.accent;\n const accentBright = colors.accentBright ?? lighten(accent, 0.35);\n const accentForeground = colors.accentForeground ?? contrastForeground(accent);\n const swatch = colors.swatch ?? accent;\n return { accent, accentBright, accentForeground, swatch };\n}\n\n// ---------------------------------------------------------------------------\n// Default Pi-Studio brand\n// ---------------------------------------------------------------------------\n\nexport const DEFAULT_BRAND: BrandConfig = {\n productName: \"Pi-Studio\",\n shortName: \"Pi-Studio\",\n tagline: \"Your local-first AI coding agent\",\n colors: {\n accent: \"#2e5cb8\",\n accentBright: \"#a2b4d7\",\n accentForeground: \"#ffffff\",\n swatch: \"#3b62b0\",\n },\n assets: {\n logoLight: \"brand/logo-light.svg\",\n logoDark: \"brand/logo-dark.svg\",\n logoMark: \"brand/logo-mark.svg\",\n icon: \"brand/icon.png\",\n splash: \"brand/splash.png\",\n },\n links: {\n docs: \"https://pi-studio.dev/docs\",\n issues: \"https://github.com/av-pi-studio/pi-studio/issues\",\n website: \"https://pi-studio.dev\",\n },\n legal: {\n companyName: \"Pi-Studio Contributors\",\n },\n};\n\n// ---------------------------------------------------------------------------\n// Resolver\n// ---------------------------------------------------------------------------\n\nexport type BrandResolution =\n | { ok: true; brand: BrandConfig; source: \"default\" | \"env\" }\n | { ok: false; errors: string[] };\n\n/**\n * Resolve the active brand config.\n *\n * At build time: reads PI_STUDIO_BRAND_DIR from the environment, loads and\n * validates <dir>/brand.config.json, fails fast with field-level errors.\n * When unset: returns the default Pi-Studio brand.\n *\n * In Node unit tests `rawConfig` is injected directly to skip file I/O.\n */\nexport function resolveBrandConfig(rawConfig?: unknown): BrandResolution {\n if (rawConfig === undefined) {\n // No injection → use built-in default.\n return { ok: true, brand: DEFAULT_BRAND, source: \"default\" };\n }\n\n const result = BrandConfigSchema.safeParse(rawConfig);\n if (!result.success) {\n const errors = result.error.errors.map((e) => `${e.path.join(\".\")}: ${e.message}`);\n return { ok: false, errors };\n }\n\n // Fill optional shortName default.\n const brand = result.data;\n if (!brand.shortName) brand.shortName = brand.productName;\n\n return { ok: true, brand, source: \"env\" };\n}\n\n// ---------------------------------------------------------------------------\n// Runtime accessor (frozen after first resolution)\n// ---------------------------------------------------------------------------\n\nlet _resolved: BrandConfig | null = null;\n\nexport function getActiveBrand(): BrandConfig {\n if (!_resolved) _resolved = DEFAULT_BRAND;\n return _resolved;\n}\n\n/** Inject a validated brand (used in tests and by the build-time bootstrap). */\nexport function _setActiveBrand(brand: BrandConfig): void {\n _resolved = brand;\n}\n\nexport function _resetActiveBrand(): void {\n _resolved = null;\n}\n","// Brand accent injection into theme variants.\n// design-system.md § Theme variants (Brand injection)\n// features/white-label-branding.md § Behavior & Algorithms\n\nimport { type ResolvedBrandColors, resolveAccentColors, type BrandConfig } from \"./config.js\";\nimport { type Theme } from \"@pi-studio-ui/theme/theme.js\";\nimport { type ThemeName, THEME_NAMES } from \"@pi-studio-ui/theme/variants.js\";\nimport { getTheme } from \"@pi-studio-ui/theme/theme.js\";\n\n/**\n * Build a branded copy of a theme by replacing only the accent family tokens\n * and swatch. All other tokens (surfaces, semantic status, syntax, terminal)\n * remain unchanged — only the accent family is brand-overridable in this version.\n */\nexport function injectBrandAccent(theme: Theme, resolved: ResolvedBrandColors): Theme {\n return {\n ...theme,\n colors: {\n ...theme.colors,\n accent: resolved.accent,\n accentBright: resolved.accentBright,\n accentForeground: resolved.accentForeground,\n // Legacy aliases that mirror accent.\n primary: resolved.accent,\n ring: resolved.accent,\n },\n swatch: resolved.swatch,\n };\n}\n\n/**\n * Build all six branded themes from a brand config.\n * Returns a map from theme name → patched Theme.\n */\nexport function buildBrandedThemes(brand: BrandConfig): Readonly<Record<ThemeName, Theme>> {\n const resolved = resolveAccentColors(brand.colors);\n const result = {} as Record<ThemeName, Theme>;\n for (const name of THEME_NAMES) {\n result[name] = injectBrandAccent(getTheme(name), resolved);\n }\n return result;\n}\n","/**\n * Appearance store: persisted theme + appearance settings, system dark-mode following.\n * design-system.md § Theme variants, § Behavior (Appearance)\n */\n\nimport { type ThemeName, THEME_NAMES } from \"./variants.js\";\nimport { type AppearanceSettings, getTheme, applyAppearance, DEFAULT_THEME_NAME } from \"./theme.js\";\nimport { type Theme } from \"./theme.js\";\nimport { applyThemeToDOM } from \"./css-bridge.js\";\nimport { injectBrandAccent } from \"@pi-studio-ui/brand/theme-injection.js\";\nimport { resolveAccentColors, type BrandConfig } from \"@pi-studio-ui/brand/config.js\";\n\n// ---------------------------------------------------------------------------\n// KeyValueStore interface (localStorage on web, Electron bridge on desktop)\n// ---------------------------------------------------------------------------\n\nexport interface KeyValueStore {\n get(key: string): string | null;\n set(key: string, value: string): void;\n}\n\nconst STORAGE_KEY = \"pi-studio-appearance\";\n\n// ---------------------------------------------------------------------------\n// Appearance state\n// ---------------------------------------------------------------------------\n\nexport type AppearanceMode = ThemeName | \"system\";\n\nexport interface AppearanceState {\n mode: AppearanceMode;\n settings: AppearanceSettings;\n resolvedTheme: Theme;\n}\n\n// ---------------------------------------------------------------------------\n// Appearance controller\n// ---------------------------------------------------------------------------\n\nexport interface AppearanceController {\n getState(): AppearanceState;\n setMode(mode: AppearanceMode): void;\n updateSettings(patch: Partial<AppearanceSettings>): void;\n /** Call to apply current theme to DOM (call on init + changes). */\n apply(): void;\n /** Start system-follow listener; returns cleanup. */\n listen(): () => void;\n}\n\nexport function createAppearanceController(\n store: KeyValueStore,\n brandConfig?: BrandConfig,\n): AppearanceController {\n let state: AppearanceState = loadInitial(store);\n let mediaCleanup: (() => void) | null = null;\n\n function resolveThemeName(mode: AppearanceMode): ThemeName {\n if (mode === \"system\") {\n const prefersDark =\n typeof window !== \"undefined\" && window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n return prefersDark ? \"dark\" : \"light\";\n }\n return mode;\n }\n\n function buildResolved(mode: AppearanceMode, settings: AppearanceSettings): Theme {\n const themeName = resolveThemeName(mode);\n const base = getTheme(themeName);\n let theme = applyAppearance(base, { ...settings, themeName });\n if (brandConfig?.colors) {\n const resolved = resolveAccentColors(brandConfig.colors);\n theme = injectBrandAccent(theme, resolved);\n }\n return theme;\n }\n\n function persist(): void {\n const serialized = JSON.stringify({ mode: state.mode, settings: state.settings });\n store.set(STORAGE_KEY, serialized);\n }\n\n function loadInitial(store: KeyValueStore): AppearanceState {\n const raw = store.get(STORAGE_KEY);\n let mode: AppearanceMode = DEFAULT_THEME_NAME;\n let settings: AppearanceSettings = {};\n if (raw) {\n try {\n const parsed = JSON.parse(raw);\n if (parsed.mode && (THEME_NAMES.includes(parsed.mode) || parsed.mode === \"system\")) {\n mode = parsed.mode;\n }\n if (parsed.settings && typeof parsed.settings === \"object\") {\n settings = parsed.settings;\n }\n } catch {\n // ignore corrupt data\n }\n }\n const resolvedTheme = buildResolved(mode, settings);\n return { mode, settings, resolvedTheme };\n }\n\n function update(): void {\n state = { ...state, resolvedTheme: buildResolved(state.mode, state.settings) };\n persist();\n }\n\n const controller: AppearanceController = {\n getState() {\n return state;\n },\n setMode(mode) {\n state = { ...state, mode };\n update();\n controller.apply();\n },\n updateSettings(patch) {\n state = { ...state, settings: { ...state.settings, ...patch } };\n update();\n controller.apply();\n },\n apply() {\n applyThemeToDOM(state.resolvedTheme);\n },\n listen() {\n if (typeof window === \"undefined\") return () => {};\n const mq = window.matchMedia(\"(prefers-color-scheme: dark)\");\n const handler = () => {\n if (state.mode === \"system\") {\n update();\n controller.apply();\n }\n };\n mq.addEventListener(\"change\", handler);\n mediaCleanup = () => mq.removeEventListener(\"change\", handler);\n return mediaCleanup;\n },\n };\n\n return controller;\n}\n","/**\n * ThemeBoundary — applies theme CSS variables before first paint.\n * Wraps the app so no flash of wrong theme occurs.\n */\n\nimport { useEffect, useRef, type ReactNode } from \"react\";\nimport {\n createAppearanceController,\n type AppearanceController,\n type KeyValueStore,\n} from \"./appearance-store.js\";\nimport { type BrandConfig } from \"@pi-studio-ui/brand/config.js\";\n\nexport interface ThemeBoundaryProps {\n store: KeyValueStore;\n brandConfig?: BrandConfig;\n children: ReactNode;\n}\n\n/**\n * Synchronously applies theme on first render, then listens for system changes.\n */\nexport function ThemeBoundary({ store, brandConfig, children }: ThemeBoundaryProps) {\n const controllerRef = useRef<AppearanceController | null>(null);\n\n if (!controllerRef.current) {\n controllerRef.current = createAppearanceController(store, brandConfig);\n // Apply synchronously before first paint\n controllerRef.current.apply();\n }\n\n useEffect(() => {\n const cleanup = controllerRef.current!.listen();\n return cleanup;\n }, []);\n\n return <>{children}</>;\n}\n","import { type KeyValueStore } from \"@pi-studio-ui/theme/appearance-store.js\";\n\n// Web KeyValueStore backed by localStorage. In Electron a bridge-backed store\n// can be substituted at the same DI boundary (ThemeBoundary `store` prop).\nexport const localKvStore: KeyValueStore = {\n get(key) {\n try {\n return globalThis.localStorage?.getItem(key) ?? null;\n } catch {\n return null;\n }\n },\n set(key, value) {\n try {\n globalThis.localStorage?.setItem(key, value);\n } catch {\n // Ignore quota / unavailable storage.\n }\n },\n};\n","import { type ReactNode } from \"react\";\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { queryClient } from \"@pi-studio-ui/lib/connection/query-client.js\";\nimport { ThemeBoundary } from \"@pi-studio-ui/theme/ThemeBoundary.js\";\nimport { localKvStore } from \"./kv-store.js\";\n\n// Top-level provider composition. ThemeBoundary applies `--pi-*` CSS variables\n// before first paint; TanStack Query owns server-cache state. Router and\n// connection context are added as their phases land.\nexport function AppProviders({ children }: { children: ReactNode }) {\n return (\n <ThemeBoundary store={localKvStore}>\n <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n </ThemeBoundary>\n );\n}\n","// Button primitive — variant/size/state contracts.\n// ui-components.md § Pressables\n\nexport type ButtonVariant = \"default\" | \"secondary\" | \"outline\" | \"ghost\" | \"destructive\";\nexport type ButtonSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n\n// Min-height per size (px) per ui-components.md: \"min-heights from 28\".\nexport const BUTTON_MIN_HEIGHT: Record<ButtonSize, number> = {\n xs: 28,\n sm: 32,\n md: 36,\n lg: 40,\n};\n\n// Icon size per button size (maps to iconSize token keys).\nexport const BUTTON_ICON_SIZE: Record<ButtonSize, number> = {\n xs: 12,\n sm: 14,\n md: 16,\n lg: 20,\n};\n\n// Horizontal padding per size.\nexport const BUTTON_PADDING_H: Record<ButtonSize, number> = {\n xs: 8,\n sm: 10,\n md: 12,\n lg: 16,\n};\n\n// Font size per size — CSS var references rather than literals, so buttons ride theme/tokens.ts's\n// scale (and the Appearance font-size setting) with nothing to keep in sync here. `lg` maps to the\n// document base; there is no 15px rung and nothing renders a `lg` button today.\nexport const BUTTON_FONT_SIZE: Record<ButtonSize, string> = {\n xs: \"var(--pi-font-size-xs)\",\n sm: \"var(--pi-font-size-sm)\",\n md: \"var(--pi-font-size-md)\",\n lg: \"var(--pi-font-size-base)\",\n};\n\nexport type ButtonStateStyle = {\n opacity: number;\n /** Token key for background color. null = no bg (transparent). */\n bgToken: string | null;\n};\n\n/**\n * Resolve the opacity and background token for a button given its state.\n * - pressed → 0.85 opacity\n * - disabled or loading → 0.5 opacity\n * - ghost hover → special token swap (handled by caller reading `isGhostHover`)\n */\nexport function resolveButtonState(opts: {\n variant: ButtonVariant;\n pressed: boolean;\n disabled: boolean;\n loading: boolean;\n}): { opacity: number; isGhostHover: false } {\n const { pressed, disabled, loading } = opts;\n if (disabled || loading) return { opacity: 0.5, isGhostHover: false };\n if (pressed) return { opacity: 0.85, isGhostHover: false };\n return { opacity: 1, isGhostHover: false };\n}\n\n/**\n * Returns the correct text/icon color token key for each variant.\n * This is what callers pass to the theme-prop binder for icon color.\n */\nexport function buttonIconColorToken(variant: ButtonVariant): string {\n switch (variant) {\n case \"default\":\n return \"accentForeground\";\n case \"secondary\":\n return \"foreground\";\n case \"outline\":\n return \"accent\";\n case \"ghost\":\n return \"foregroundMuted\"; // swaps to \"foreground\" on hover\n case \"destructive\":\n return \"destructiveForeground\";\n }\n}\n\n/** Returns \"foreground\" when ghost is hovered, else the default ghost color. */\nexport function ghostHoverIconToken(isHovered: boolean): string {\n return isHovered ? \"foreground\" : \"foregroundMuted\";\n}\n","/**\n * Button — pressable with variant/size/state, styled from --pi-* CSS variables.\n * ui-components.md § Pressables\n */\n\nimport { type ReactNode, type ButtonHTMLAttributes } from \"react\";\nimport { clsx } from \"clsx\";\nimport { Loader2 } from \"lucide-react\";\nimport styles from \"./Button.module.css\";\nimport {\n type ButtonVariant,\n type ButtonSize,\n BUTTON_MIN_HEIGHT,\n BUTTON_PADDING_H,\n BUTTON_FONT_SIZE,\n BUTTON_ICON_SIZE,\n resolveButtonState,\n} from \"@pi-studio-ui/ui/button.js\";\n\nexport interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"children\"> {\n variant?: ButtonVariant;\n size?: ButtonSize;\n loading?: boolean;\n leftIcon?: ReactNode;\n trailing?: ReactNode;\n iconOnly?: boolean;\n children?: ReactNode;\n}\n\nexport function Button({\n variant = \"default\",\n size = \"md\",\n loading = false,\n disabled = false,\n leftIcon,\n trailing,\n iconOnly = false,\n children,\n style,\n className,\n ...rest\n}: ButtonProps) {\n const { opacity } = resolveButtonState({\n variant,\n pressed: false,\n disabled: disabled ?? false,\n loading,\n });\n const isDisabled = disabled || loading;\n\n const iconPx = BUTTON_ICON_SIZE[size];\n\n return (\n <button\n className={clsx(styles.btn, styles[variant], iconOnly && styles.iconOnly, className)}\n disabled={isDisabled}\n aria-busy={loading || undefined}\n style={{\n minHeight: BUTTON_MIN_HEIGHT[size],\n paddingLeft: iconOnly ? undefined : BUTTON_PADDING_H[size],\n paddingRight: iconOnly ? undefined : BUTTON_PADDING_H[size],\n fontSize: BUTTON_FONT_SIZE[size],\n opacity,\n ...(iconOnly ? { width: BUTTON_MIN_HEIGHT[size] } : {}),\n ...style,\n }}\n {...rest}\n >\n {loading ? (\n <Loader2 className={styles.loadingSpinner} size={iconPx} aria-hidden />\n ) : leftIcon ? (\n <span aria-hidden style={{ display: \"flex\", alignItems: \"center\", flexShrink: 0 }}>\n {leftIcon}\n </span>\n ) : null}\n {children}\n {trailing && !loading && (\n <span aria-hidden style={{ display: \"flex\", alignItems: \"center\", flexShrink: 0 }}>\n {trailing}\n </span>\n )}\n </button>\n );\n}\n","/**\n * TextInput / TextArea — form input controls.\n * ui-components.md § Inputs & form controls (AdaptiveTextInput)\n */\n\nimport { forwardRef, type InputHTMLAttributes, type TextareaHTMLAttributes } from \"react\";\nimport { clsx } from \"clsx\";\nimport styles from \"./TextInput.module.css\";\n\n// ---- TextInput ----\n\nexport interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> {\n className?: string;\n}\n\nexport const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function TextInput(\n { className, ...rest },\n ref,\n) {\n return <input ref={ref} className={clsx(styles.input, className)} {...rest} />;\n});\n\n// ---- TextArea ----\n\nexport interface TextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {\n className?: string;\n}\n\nexport const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function TextArea(\n { className, ...rest },\n ref,\n) {\n return (\n <textarea ref={ref} className={clsx(styles.input, styles.textarea, className)} {...rest} />\n );\n});\n","import { z } from \"zod\";\n\n/**\n * Shared validation conventions for Pi-Studio.\n *\n * Every wire frame and every on-disk JSON document is parsed through Zod before it is trusted\n * (MAIN-SCOPE.md §9). These helpers are the backbone of \"validate at every boundary\" and are\n * reused by the protocol message schemas and the file-based persistence stores.\n *\n * ## Append-only compatibility rules (always)\n *\n * Schemas are **append-only**. A 6-month-old client must still parse new-daemon messages and\n * vice-versa. Concretely:\n *\n * - **New fields are optional with a default or transform.** Use {@link optionalWithDefault} so a\n * missing field deserializes to a known value instead of `undefined`.\n * - **Never flip `optional` → `required`.** Once a field is optional it stays optional.\n * - **Never remove a field.** Stop *sending* it if you must, but keep *reading* it.\n * - **Never narrow a type** (e.g. `string` → enum, `nullable` → non-null). New enum values are\n * gated at serialization behind capability flags so old clients only ever receive values they\n * advertised support for.\n *\n * ## COMPAT(name) convention\n *\n * Back-compat shims are tagged so a single grep (`COMPAT(`) lists all pending cleanup work. Each\n * tag records the version the shim was added in and the date it may be removed. Use {@link COMPAT}\n * at the call site of a shim, e.g.:\n *\n * ```ts\n * // COMPAT(legacy-provider-key): added 1.2.0, remove by 2026-12-31\n * const normalized = COMPAT({ name: \"legacy-provider-key\", addedIn: \"1.2.0\", removeBy: \"2026-12-31\" });\n * ```\n */\n\n/** Metadata describing a back-compat shim. Grep `COMPAT(` to enumerate cleanup work. */\nexport interface CompatTag {\n /** Stable identifier for the shim (kebab-case). */\n readonly name: string;\n /** Version the shim was introduced in (semver). */\n readonly addedIn: string;\n /** ISO date (YYYY-MM-DD) after which the shim may be removed. */\n readonly removeBy: string;\n}\n\n/**\n * Marks a back-compat shim. Returns the tag unchanged so it can be used inline; its real value is\n * the grep-able `COMPAT(` token and the structured metadata it carries.\n */\nexport function COMPAT(tag: CompatTag): CompatTag {\n return tag;\n}\n\n/**\n * Append-only field helper: declares a schema field that is optional on the wire/disk but always\n * resolves to a concrete default once parsed. This is the sanctioned way to add new fields without\n * breaking older producers that omit them.\n */\nexport function optionalWithDefault<S extends z.ZodTypeAny>(\n schema: S,\n defaultValue: z.infer<S>,\n): z.ZodDefault<z.ZodOptional<S>> {\n return schema.optional().default(defaultValue);\n}\n\n/**\n * Parse `value` with `schema`, returning the parsed value on success or `defaults` on any failure.\n * Used by the file-based stores so corrupt/partial JSON falls back to defaults instead of crashing\n * the daemon (architecture/persistence.md — \"Corrupt/partial JSON → fall back to defaults\").\n */\nexport function safeParseOrDefault<S extends z.ZodTypeAny>(\n schema: S,\n value: unknown,\n defaults: z.infer<S>,\n): z.infer<S> {\n const result = schema.safeParse(value);\n return result.success ? (result.data as z.infer<S>) : defaults;\n}\n\n// ---------------------------------------------------------------------------\n// Shared primitive schemas\n// ---------------------------------------------------------------------------\n\n/**\n * ISO-8601 timestamp string (UTC `Z` or explicit offset), e.g. `2026-06-11T15:00:00.000Z`.\n * Timeline row timestamps are canonical, daemon-owned strings (MAIN-SCOPE.md §9).\n */\nexport const isoTimestampSchema = z.string().datetime({ offset: true });\n\n/** RFC-4122 UUID string (the primary key shape for agents, chat rooms, messages, etc.). */\nexport const uuidSchema = z.string().uuid();\n\n/**\n * base64url character set with no padding (`A–Z a–z 0–9 - _`). The encoding used for stable ids\n * like the daemon `server-id` (`srv_<base64url>`).\n */\nexport const base64UrlSchema = z.string().regex(/^[A-Za-z0-9_-]+$/, \"expected base64url\");\n\n/** Returns true if `value` is a non-empty base64url string. */\nexport function isBase64Url(value: string): boolean {\n return base64UrlSchema.safeParse(value).success;\n}\n\n/**\n * Schema for a prefixed base64url id of the form `${prefix}_<base64url>` (e.g. `srv_AbC-123`).\n */\nexport function prefixedIdSchema(prefix: string): z.ZodString {\n const escaped = prefix.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return z\n .string()\n .regex(new RegExp(`^${escaped}_[A-Za-z0-9_-]+$`), `expected ${prefix}_<base64url> id`);\n}\n","import { z } from \"zod\";\n\nimport { isoTimestampSchema } from \"./validation.js\";\n\n/**\n * WebSocket wire schemas (architecture/websocket-protocol.md).\n *\n * The protocol mixes JSON text frames (these schemas) with a small binary framing for\n * terminal/file streams (see `binary-frames/`). All schemas are append-only: new fields are\n * optional with defaults, types are never narrowed, fields are never removed.\n *\n * Built up across sprint-002:\n * - task-001: top-level envelopes + handshake.\n * - task-003: the session message family union (agent / timeline / permission core).\n */\n\n// ===========================================================================\n// Shared wire primitives\n// ===========================================================================\n\n/**\n * A wire timestamp. Accepts either an epoch-millis number or an ISO-8601 string so the type is\n * never narrowed across versions (append-only rule).\n */\nexport const wireTimestampSchema = z.union([z.number(), isoTimestampSchema]);\nexport type WireTimestamp = z.infer<typeof wireTimestampSchema>;\n\n// ===========================================================================\n// Handshake\n// ===========================================================================\n\n/** Client kinds that may connect. Desktop connects as `browser` (TODO(verify) MAIN-SCOPE). */\nexport const clientTypeSchema = z.enum([\"mobile\", \"browser\", \"cli\", \"mcp\"]);\nexport type ClientType = z.infer<typeof clientTypeSchema>;\n\n/**\n * Client → Server handshake. Must be the first frame on a connection. `capabilities` advertises\n * the `CLIENT_CAPS.*` flags the client supports (a string→boolean map; see client-capabilities.ts).\n */\nexport const helloSchema = z.object({\n type: z.literal(\"hello\"),\n clientId: z.string(),\n clientType: clientTypeSchema,\n protocolVersion: z.number(),\n appVersion: z.string().optional(),\n capabilities: z.record(z.string(), z.boolean()).optional(),\n});\nexport type Hello = z.infer<typeof helloSchema>;\n\n/**\n * The `server_info` payload carried by the `status` envelope. There is no dedicated welcome\n * message — the server emits this after accepting the hello, then begins streaming.\n */\nexport const serverInfoPayloadSchema = z.object({\n status: z.literal(\"server_info\"),\n serverId: z.string(),\n hostname: z.string().optional(),\n version: z.string().optional(),\n capabilities: z.record(z.string(), z.unknown()),\n features: z.record(z.string(), z.unknown()),\n});\nexport type ServerInfoPayload = z.infer<typeof serverInfoPayloadSchema>;\n\n/** Server → Client status envelope wrapping `server_info`. */\nexport const statusSchema = z.object({\n type: z.literal(\"status\"),\n payload: serverInfoPayloadSchema,\n});\nexport type Status = z.infer<typeof statusSchema>;\n\n// ===========================================================================\n// Liveness (JSON ping/pong, NOT RFC6455 ping — browsers/RN cannot access protocol ping)\n// ===========================================================================\n\nexport const pingSchema = z.object({\n type: z.literal(\"ping\"),\n requestId: z.string(),\n clientSentAt: wireTimestampSchema.optional(),\n});\nexport type Ping = z.infer<typeof pingSchema>;\n\nexport const pongSchema = z.object({\n type: z.literal(\"pong\"),\n requestId: z.string(),\n clientSentAt: wireTimestampSchema.optional(),\n serverReceivedAt: wireTimestampSchema,\n serverSentAt: wireTimestampSchema,\n});\nexport type Pong = z.infer<typeof pongSchema>;\n\n// ===========================================================================\n// RPC naming convention\n// ===========================================================================\n\nexport type RpcDirection = \"request\" | \"response\";\n\n/**\n * Builds a dotted RPC name: `domain.provider.operation.direction`\n * (e.g. `checkout.github.set_auto_merge.request`). Segments read left→right:\n * domain → provider/subsystem → operation (a verb) → direction. New RPCs use this form; legacy\n * flat names (e.g. `checkout_pr_merge_request`) remain *accepted* but must not be generated.\n */\nexport function rpcName(\n domain: string,\n providerOrSubsystem: string,\n operation: string,\n direction: RpcDirection,\n): string {\n return `${domain}.${providerOrSubsystem}.${operation}.${direction}`;\n}\n\n// ===========================================================================\n// Agent session config (create_agent_request)\n// ===========================================================================\n\n/** Status the daemon reports for an agent (architecture/persistence.md — `lastStatus`). */\nexport const agentStatusEnum = z.enum([\"initializing\", \"idle\", \"running\", \"error\", \"closed\"]);\nexport type AgentStatus = z.infer<typeof agentStatusEnum>;\n\nexport const imageAttachmentSchema = z\n .object({ mimeType: z.string().optional(), data: z.string().optional() })\n .passthrough();\nexport type ImageAttachment = z.infer<typeof imageAttachmentSchema>;\n\nexport const agentAttachmentsSchema = z\n .object({\n prs: z.array(z.unknown()).optional(),\n issues: z.array(z.unknown()).optional(),\n })\n .passthrough();\n\n/**\n * `AgentSessionConfig` — provider/mode/model and run options for an agent. `provider`/`modeId`/\n * `model`/`thinkingOptionId` are dynamic strings (discovered at runtime per agent-providers.md),\n * not fixed enums, so they are not narrowed here.\n */\nexport const agentSessionConfigSchema = z.object({\n provider: z.string(),\n cwd: z.string(),\n modeId: z.string().optional(),\n model: z.string().optional(),\n /** The model's own underlying LLM provider (e.g. `\"anthropic\"`), pinned alongside `model` when\n * a deferred draft materializes (either the resolved default or an explicit `/model` pick) —\n * required to replay it via `setProviderModel` on first spawn, since neither `createSession`\n * nor `resumeSession` consult this field (Pi resolves its own default at spawn). */\n modelProvider: z.string().optional(),\n thinkingOptionId: z.string().optional(),\n featureValues: z.record(z.string(), z.unknown()).optional(),\n title: z.string().nullable().optional(),\n approvalPolicy: z.string().optional(),\n sandboxMode: z.string().optional(),\n networkAccess: z.boolean().optional(),\n webSearch: z.boolean().optional(),\n extra: z.object({ pi: z.unknown().optional() }).passthrough().optional(),\n systemPrompt: z.string().optional(),\n mcpServers: z.record(z.string(), z.unknown()).optional(),\n});\nexport type AgentSessionConfig = z.infer<typeof agentSessionConfigSchema>;\n\nexport const createAgentRequestSchema = z.object({\n type: z.literal(\"create_agent_request\"),\n requestId: z.string(),\n config: agentSessionConfigSchema,\n env: z.record(z.string(), z.string()).optional(),\n workspaceId: z.string().optional(),\n worktreeName: z.string().optional(),\n initialPrompt: z.string().optional(),\n clientMessageId: z.string().optional(),\n outputSchema: z.record(z.string(), z.unknown()).optional(),\n images: z.array(imageAttachmentSchema).optional(),\n attachments: agentAttachmentsSchema.optional(),\n git: z.unknown().optional(),\n worktree: z.unknown().optional(),\n autoArchive: z.boolean().optional(),\n labels: z.record(z.string(), z.string()).default({}),\n});\nexport type CreateAgentRequest = z.infer<typeof createAgentRequestSchema>;\n\nexport const createAgentResponseSchema = z.object({\n type: z.literal(\"create_agent_response\"),\n requestId: z.string(),\n payload: z.object({ agentId: z.string() }).passthrough(),\n});\nexport type CreateAgentResponse = z.infer<typeof createAgentResponseSchema>;\n\n// ===========================================================================\n// Agent lifecycle messages\n// ===========================================================================\n\nexport const agentUpdateSchema = z.object({\n type: z.literal(\"agent_update\"),\n agentId: z.string(),\n status: agentStatusEnum.optional(),\n title: z.string().nullable().optional(),\n labels: z.record(z.string(), z.string()).optional(),\n});\n\nexport const agentStatusMessageSchema = z.object({\n type: z.literal(\"agent_status\"),\n agentId: z.string(),\n status: agentStatusEnum,\n});\n\nexport const agentListSchema = z.object({\n type: z.literal(\"agent_list\"),\n agents: z.array(z.object({ id: z.string() }).passthrough()),\n});\n\nexport const agentDeletedSchema = z.object({\n type: z.literal(\"agent_deleted\"),\n agentId: z.string(),\n});\n\nexport const agentArchivedSchema = z.object({\n type: z.literal(\"agent_archived\"),\n agentId: z.string(),\n archivedAt: isoTimestampSchema.optional(),\n});\n\n// ===========================================================================\n// Tool-call details + stream events\n// ===========================================================================\n\n/** `ToolCallDetail` — normalized across providers, discriminated on `kind`. */\nexport const toolCallDetailSchema = z.discriminatedUnion(\"kind\", [\n z.object({\n kind: z.literal(\"shell\"),\n command: z.string().optional(),\n output: z.string().optional(),\n }),\n z.object({ kind: z.literal(\"read\"), path: z.string().optional(), output: z.string().optional() }),\n z.object({\n kind: z.literal(\"edit\"),\n path: z.string().optional(),\n diff: z.string().optional(),\n output: z.string().optional(),\n }),\n z.object({\n kind: z.literal(\"write\"),\n path: z.string().optional(),\n output: z.string().optional(),\n }),\n z.object({\n kind: z.literal(\"search\"),\n query: z.string().optional(),\n output: z.string().optional(),\n }),\n z.object({ kind: z.literal(\"fetch\"), url: z.string().optional(), output: z.string().optional() }),\n z.object({\n kind: z.literal(\"task\"),\n description: z.string().optional(),\n output: z.string().optional(),\n }),\n]);\nexport type ToolCallDetail = z.infer<typeof toolCallDetailSchema>;\n\n/** `AgentStreamEvent` — discriminated on `kind`. */\nexport const agentStreamEventSchema = z.discriminatedUnion(\"kind\", [\n z.object({\n kind: z.literal(\"user_message\"),\n messageId: z.string().optional(),\n text: z.string().optional(),\n images: z.array(imageAttachmentSchema).optional(),\n }),\n z.object({\n kind: z.literal(\"assistant_message\"),\n messageId: z.string().optional(),\n text: z.string().optional(),\n /**\n * Set on the marker event that closes an assistant text block (Pi's `text_end` delta). The\n * text is complete and will not grow, so a renderer can switch from its cheap streaming tier\n * to full markdown immediately instead of waiting for `turn_completed`, which is one\n * `agent_end` — potentially minutes of tool execution — away. Carries no `text` of its own\n * when emitted as a standalone marker; hydrated history sets it alongside the block's full\n * text. Older clients see an empty `assistant_message` and no-op.\n */\n final: z.boolean().optional(),\n }),\n z.object({\n kind: z.literal(\"reasoning\"),\n text: z.string().optional(),\n /** Closes a thinking block (Pi's `thinking_end` delta) — see `assistant_message.final`. */\n final: z.boolean().optional(),\n }),\n z.object({\n kind: z.literal(\"tool_call\"),\n callId: z.string().optional(),\n tool: toolCallDetailSchema,\n status: z.string().optional(),\n }),\n z.object({ kind: z.literal(\"turn_started\"), turnId: z.string().optional() }),\n z.object({ kind: z.literal(\"turn_completed\"), turnId: z.string().optional() }),\n z.object({\n kind: z.literal(\"turn_failed\"),\n turnId: z.string().optional(),\n error: z.string().optional(),\n }),\n z.object({ kind: z.literal(\"turn_canceled\"), turnId: z.string().optional() }),\n z.object({ kind: z.literal(\"error\"), message: z.string().optional() }),\n z.object({\n kind: z.literal(\"queue_update\"),\n steering: z.array(z.string()).optional(),\n followUp: z.array(z.string()).optional(),\n }),\n]);\nexport type AgentStreamEvent = z.infer<typeof agentStreamEventSchema>;\n\nexport const agentStreamSchema = z.object({\n type: z.literal(\"agent_stream\"),\n agentId: z.string(),\n seq: z.number().optional(),\n timestamp: wireTimestampSchema.optional(),\n event: agentStreamEventSchema,\n});\n\n// ===========================================================================\n// Timeline fetch (authoritative paged history)\n// ===========================================================================\n\nexport const timelineDirectionEnum = z.enum([\"before\", \"after\"]);\nexport type TimelineDirection = z.infer<typeof timelineDirectionEnum>;\n\nexport const sourceSeqRangeSchema = z.object({ start: z.number(), end: z.number() });\nexport type SourceSeqRange = z.infer<typeof sourceSeqRangeSchema>;\n\nexport const fetchAgentTimelineRequestSchema = z.object({\n type: z.literal(\"fetch_agent_timeline_request\"),\n requestId: z.string(),\n agentId: z.string(),\n cursor: z.string().nullable().optional(),\n direction: timelineDirectionEnum,\n limit: z.number().optional(),\n});\nexport type FetchAgentTimelineRequest = z.infer<typeof fetchAgentTimelineRequestSchema>;\n\nexport const fetchAgentTimelineResponseSchema = z.object({\n type: z.literal(\"fetch_agent_timeline_response\"),\n requestId: z.string(),\n agentId: z.string().optional(),\n items: z.array(z.unknown()),\n seqStart: z.number(),\n seqEnd: z.number(),\n sourceSeqRanges: z.array(sourceSeqRangeSchema),\n collapsed: z.boolean(),\n hasNewer: z.boolean(),\n startCursor: z.string().nullable().optional(),\n endCursor: z.string().nullable().optional(),\n});\nexport type FetchAgentTimelineResponse = z.infer<typeof fetchAgentTimelineResponseSchema>;\n\n// ===========================================================================\n// Tool-call permissions\n// ===========================================================================\n\nexport const agentPermissionRequestSchema = z.object({\n type: z.literal(\"agent_permission_request\"),\n requestId: z.string(),\n agentId: z.string(),\n toolName: z.string().optional(),\n tool: toolCallDetailSchema.optional(),\n action: z.unknown().optional(),\n responses: z.array(z.string()).optional(),\n});\n\nexport const agentPermissionResolvedSchema = z.object({\n type: z.literal(\"agent_permission_resolved\"),\n requestId: z.string(),\n agentId: z.string(),\n decision: z.string().optional(),\n});\n\n/** respond-to-permission RPC (client → daemon), dotted name `agent.permission.respond.request`. */\nexport const respondToPermissionRequestSchema = z.object({\n type: z.literal(\"agent.permission.respond.request\"),\n requestId: z.string(),\n permissionRequestId: z.string(),\n response: z.unknown(),\n});\n\nexport const respondToPermissionResponseSchema = z.object({\n type: z.literal(\"agent.permission.respond.response\"),\n requestId: z.string(),\n payload: z.object({ resolved: z.boolean() }).passthrough(),\n});\n\n/** Legacy flat name for the same operation — still accepted (parsed), never generated. */\nexport const legacyRespondToPermissionSchema = z.object({\n type: z.literal(\"respond_to_permission\"),\n requestId: z.string(),\n permissionRequestId: z.string(),\n response: z.unknown(),\n});\n\n// ===========================================================================\n// ===========================================================================\n// Rewind RPC (features/rewind.md) — additive, new in sprint-015\n// ===========================================================================\n\nexport const rewindModeSchema = z.enum([\"conversation\", \"files\", \"both\"]);\nexport type RewindMode = z.infer<typeof rewindModeSchema>;\n\nexport const agentRewindRequestSchema = z\n .object({\n type: z.literal(\"agent.rewind.request\"),\n requestId: z.string(),\n agentId: z.string(),\n messageId: z.string(),\n mode: rewindModeSchema,\n })\n .passthrough();\nexport type AgentRewindRequest = z.infer<typeof agentRewindRequestSchema>;\n\nexport const agentRewindResponseSchema = z\n .object({\n type: z.literal(\"agent.rewind.response\"),\n requestId: z.string(),\n payload: z\n .object({\n agentId: z.string(),\n messageId: z.string(),\n mode: rewindModeSchema,\n truncatedAt: z.string().optional(),\n })\n .passthrough(),\n })\n .passthrough();\nexport type AgentRewindResponse = z.infer<typeof agentRewindResponseSchema>;\n\n// ===========================================================================\n// Slash-command operations (sprint-037): Pi built-ins with an RPC equivalent —\n// /session, /compact, /new, /resume, /fork, /clone, /name, /export, /model, /copy.\n// Pi's own RPC docs are explicit that built-in TUI commands without one of these RPC\n// equivalents (e.g. /settings, /hotkeys) are never expanded by the `prompt` command and are\n// intentionally NOT represented on the wire.\n// ===========================================================================\n\nexport const agentTokenUsageSchema = z\n .object({\n input: z.number().optional(),\n output: z.number().optional(),\n cacheRead: z.number().optional(),\n cacheWrite: z.number().optional(),\n total: z.number().optional(),\n })\n .passthrough();\nexport type AgentTokenUsage = z.infer<typeof agentTokenUsageSchema>;\n\nexport const agentContextUsageSchema = z\n .object({\n tokens: z.number().nullable().optional(),\n contextWindow: z.number().optional(),\n percent: z.number().nullable().optional(),\n })\n .passthrough();\nexport type AgentContextUsage = z.infer<typeof agentContextUsageSchema>;\n\n/** `/session` — mirrors Pi RPC `get_session_stats`. */\nexport const agentSessionStatsRequestSchema = z\n .object({\n type: z.literal(\"agent_session_stats_request\"),\n requestId: z.string(),\n agentId: z.string(),\n })\n .passthrough();\nexport type AgentSessionStatsRequest = z.infer<typeof agentSessionStatsRequestSchema>;\n\nexport const agentSessionStatsResponseSchema = z\n .object({\n type: z.literal(\"agent_session_stats_response\"),\n requestId: z.string(),\n payload: z\n .object({\n sessionId: z.string().optional(),\n sessionFile: z.string().optional(),\n userMessages: z.number().optional(),\n assistantMessages: z.number().optional(),\n toolCalls: z.number().optional(),\n toolResults: z.number().optional(),\n totalMessages: z.number().optional(),\n tokens: agentTokenUsageSchema.optional(),\n cost: z.number().optional(),\n contextUsage: agentContextUsageSchema.optional(),\n /** Current model id (sprint-042: poll-authoritative source for the web-client status\n * bar's model segment; back-filled server-side when the provider's own stats omit it). */\n model: z.string().optional(),\n })\n .passthrough(),\n })\n .passthrough();\nexport type AgentSessionStatsResponse = z.infer<typeof agentSessionStatsResponseSchema>;\n\n/** `/compact` — mirrors Pi RPC `compact`. */\nexport const agentCompactRequestSchema = z\n .object({\n type: z.literal(\"agent_compact_request\"),\n requestId: z.string(),\n agentId: z.string(),\n customInstructions: z.string().optional(),\n })\n .passthrough();\nexport type AgentCompactRequest = z.infer<typeof agentCompactRequestSchema>;\n\nexport const agentCompactResponseSchema = z\n .object({\n type: z.literal(\"agent_compact_response\"),\n requestId: z.string(),\n payload: z\n .object({\n summary: z.string().optional(),\n firstKeptEntryId: z.string().optional(),\n tokensBefore: z.number().optional(),\n details: z.unknown().optional(),\n })\n .passthrough(),\n })\n .passthrough();\nexport type AgentCompactResponse = z.infer<typeof agentCompactResponseSchema>;\n\n/** `/new` — mirrors Pi RPC `new_session`. */\nexport const agentNewSessionRequestSchema = z\n .object({\n type: z.literal(\"agent_new_session_request\"),\n requestId: z.string(),\n agentId: z.string(),\n })\n .passthrough();\nexport type AgentNewSessionRequest = z.infer<typeof agentNewSessionRequestSchema>;\n\nexport const agentNewSessionResponseSchema = z\n .object({\n type: z.literal(\"agent_new_session_response\"),\n requestId: z.string(),\n payload: z.object({ cancelled: z.boolean() }).passthrough(),\n })\n .passthrough();\nexport type AgentNewSessionResponse = z.infer<typeof agentNewSessionResponseSchema>;\n\n/** `/resume` — mirrors Pi RPC `switch_session`. */\nexport const agentSwitchSessionRequestSchema = z\n .object({\n type: z.literal(\"agent_switch_session_request\"),\n requestId: z.string(),\n agentId: z.string(),\n sessionPath: z.string(),\n })\n .passthrough();\nexport type AgentSwitchSessionRequest = z.infer<typeof agentSwitchSessionRequestSchema>;\n\nexport const agentSwitchSessionResponseSchema = z\n .object({\n type: z.literal(\"agent_switch_session_response\"),\n requestId: z.string(),\n payload: z.object({ cancelled: z.boolean() }).passthrough(),\n })\n .passthrough();\nexport type AgentSwitchSessionResponse = z.infer<typeof agentSwitchSessionResponseSchema>;\n\n/** `/fork` — mirrors Pi RPC `fork`. */\nexport const agentForkRequestSchema = z\n .object({\n type: z.literal(\"agent_fork_request\"),\n requestId: z.string(),\n agentId: z.string(),\n entryId: z.string(),\n })\n .passthrough();\nexport type AgentForkRequest = z.infer<typeof agentForkRequestSchema>;\n\nexport const agentForkResponseSchema = z\n .object({\n type: z.literal(\"agent_fork_response\"),\n requestId: z.string(),\n payload: z.object({ text: z.string(), cancelled: z.boolean() }).passthrough(),\n })\n .passthrough();\nexport type AgentForkResponse = z.infer<typeof agentForkResponseSchema>;\n\n/** Fork picker — mirrors Pi RPC `get_fork_messages`. */\nexport const agentForkMessagesRequestSchema = z\n .object({\n type: z.literal(\"agent_fork_messages_request\"),\n requestId: z.string(),\n agentId: z.string(),\n })\n .passthrough();\nexport type AgentForkMessagesRequest = z.infer<typeof agentForkMessagesRequestSchema>;\n\nexport const agentForkMessagesResponseSchema = z\n .object({\n type: z.literal(\"agent_fork_messages_response\"),\n requestId: z.string(),\n payload: z\n .object({\n messages: z.array(z.object({ entryId: z.string(), text: z.string() }).passthrough()),\n })\n .passthrough(),\n })\n .passthrough();\nexport type AgentForkMessagesResponse = z.infer<typeof agentForkMessagesResponseSchema>;\n\n/** `/clone` — mirrors Pi RPC `clone`. */\nexport const agentCloneRequestSchema = z\n .object({\n type: z.literal(\"agent_clone_request\"),\n requestId: z.string(),\n agentId: z.string(),\n })\n .passthrough();\nexport type AgentCloneRequest = z.infer<typeof agentCloneRequestSchema>;\n\nexport const agentCloneResponseSchema = z\n .object({\n type: z.literal(\"agent_clone_response\"),\n requestId: z.string(),\n payload: z.object({ cancelled: z.boolean() }).passthrough(),\n })\n .passthrough();\nexport type AgentCloneResponse = z.infer<typeof agentCloneResponseSchema>;\n\n/** `/name` — mirrors Pi RPC `set_session_name`. */\nexport const agentSetSessionNameRequestSchema = z\n .object({\n type: z.literal(\"agent_set_session_name_request\"),\n requestId: z.string(),\n agentId: z.string(),\n name: z.string(),\n })\n .passthrough();\nexport type AgentSetSessionNameRequest = z.infer<typeof agentSetSessionNameRequestSchema>;\n\nexport const agentSetSessionNameResponseSchema = z\n .object({\n type: z.literal(\"agent_set_session_name_response\"),\n requestId: z.string(),\n payload: z.object({}).passthrough().optional(),\n })\n .passthrough();\nexport type AgentSetSessionNameResponse = z.infer<typeof agentSetSessionNameResponseSchema>;\n\n/** `/export` — mirrors Pi RPC `export_html`. */\nexport const agentExportHtmlRequestSchema = z\n .object({\n type: z.literal(\"agent_export_html_request\"),\n requestId: z.string(),\n agentId: z.string(),\n outputPath: z.string().optional(),\n })\n .passthrough();\nexport type AgentExportHtmlRequest = z.infer<typeof agentExportHtmlRequestSchema>;\n\nexport const agentExportHtmlResponseSchema = z\n .object({\n type: z.literal(\"agent_export_html_response\"),\n requestId: z.string(),\n payload: z.object({ path: z.string() }).passthrough(),\n })\n .passthrough();\nexport type AgentExportHtmlResponse = z.infer<typeof agentExportHtmlResponseSchema>;\n\n/** `/model` (set) — mirrors Pi RPC `set_model`. Model payload shape varies by provider. */\nexport const agentSetModelRequestSchema = z\n .object({\n type: z.literal(\"agent_set_model_request\"),\n requestId: z.string(),\n agentId: z.string(),\n provider: z.string(),\n modelId: z.string(),\n })\n .passthrough();\nexport type AgentSetModelRequest = z.infer<typeof agentSetModelRequestSchema>;\n\nexport const agentSetModelResponseSchema = z\n .object({\n type: z.literal(\"agent_set_model_response\"),\n requestId: z.string(),\n payload: z.object({}).passthrough(),\n })\n .passthrough();\nexport type AgentSetModelResponse = z.infer<typeof agentSetModelResponseSchema>;\n\n/** `/model` (cycle) — mirrors Pi RPC `cycle_model`. */\nexport const agentCycleModelRequestSchema = z\n .object({\n type: z.literal(\"agent_cycle_model_request\"),\n requestId: z.string(),\n agentId: z.string(),\n })\n .passthrough();\nexport type AgentCycleModelRequest = z.infer<typeof agentCycleModelRequestSchema>;\n\nexport const agentCycleModelResponseSchema = z\n .object({\n type: z.literal(\"agent_cycle_model_response\"),\n requestId: z.string(),\n payload: z\n .object({\n model: z.unknown().nullable().optional(),\n thinkingLevel: z.string().optional(),\n isScoped: z.boolean().optional(),\n })\n .passthrough(),\n })\n .passthrough();\nexport type AgentCycleModelResponse = z.infer<typeof agentCycleModelResponseSchema>;\n\n/** `/copy` — mirrors Pi RPC `get_last_assistant_text`. */\nexport const agentLastAssistantTextRequestSchema = z\n .object({\n type: z.literal(\"agent_last_assistant_text_request\"),\n requestId: z.string(),\n agentId: z.string(),\n })\n .passthrough();\nexport type AgentLastAssistantTextRequest = z.infer<typeof agentLastAssistantTextRequestSchema>;\n\nexport const agentLastAssistantTextResponseSchema = z\n .object({\n type: z.literal(\"agent_last_assistant_text_response\"),\n requestId: z.string(),\n payload: z.object({ text: z.string().nullable() }).passthrough(),\n })\n .passthrough();\nexport type AgentLastAssistantTextResponse = z.infer<typeof agentLastAssistantTextResponseSchema>;\n\n// ===========================================================================\n// Steering (steer_agent / follow_up_agent) — inject a message into a LIVE turn\n// (Pi RPC `steer` / `follow_up`, docs/rpc.md). Fire-and-forget like interrupt_agent:\n// does not start a new turn or change agent status. Delivery is confirmed by a\n// `queue_update` stream event, not by the response payload.\n// ===========================================================================\n\nexport const steerAgentRequestSchema = z\n .object({\n type: z.literal(\"steer_agent_request\"),\n requestId: z.string(),\n agentId: z.string(),\n message: z.string(),\n images: z.array(imageAttachmentSchema).optional(),\n clientMessageId: z.string().optional(),\n })\n .passthrough();\nexport type SteerAgentRequest = z.infer<typeof steerAgentRequestSchema>;\n\nexport const steerAgentResponseSchema = z\n .object({\n type: z.literal(\"steer_agent_response\"),\n requestId: z.string(),\n agentId: z.string(),\n ok: z.boolean(),\n })\n .passthrough();\nexport type SteerAgentResponse = z.infer<typeof steerAgentResponseSchema>;\n\nexport const followUpAgentRequestSchema = z\n .object({\n type: z.literal(\"follow_up_agent_request\"),\n requestId: z.string(),\n agentId: z.string(),\n message: z.string(),\n images: z.array(imageAttachmentSchema).optional(),\n clientMessageId: z.string().optional(),\n })\n .passthrough();\nexport type FollowUpAgentRequest = z.infer<typeof followUpAgentRequestSchema>;\n\nexport const followUpAgentResponseSchema = z\n .object({\n type: z.literal(\"follow_up_agent_response\"),\n requestId: z.string(),\n agentId: z.string(),\n ok: z.boolean(),\n })\n .passthrough();\nexport type FollowUpAgentResponse = z.infer<typeof followUpAgentResponseSchema>;\n\n// ===========================================================================\n// Command discovery (agent_list_commands) — surfaces Pi's `get_commands`: user/project-authored\n// extension commands (`pi.registerCommand()`), prompt templates (`.pi/agent/prompts/*.md`), and\n// skills (`.pi/agent/skills/<name>/SKILL.md`). Disjoint from the built-in slash-command RPCs above\n// (Pi's own structured commands are never returned by `get_commands`). Read-only, session-scoped.\n// ===========================================================================\n\nexport const agentCommandDescriptorSchema = z\n .object({\n name: z.string(),\n id: z.string().optional(),\n description: z.string().optional(),\n source: z.enum([\"extension\", \"prompt\", \"skill\"]).optional(),\n scope: z.enum([\"user\", \"project\", \"temporary\"]).optional(),\n path: z.string().optional(),\n })\n .passthrough();\nexport type AgentCommandDescriptor = z.infer<typeof agentCommandDescriptorSchema>;\n\n/** Surfaces Pi's `get_commands` RPC: extension/prompt/skill discovery for a live session. */\nexport const agentListCommandsRequestSchema = z\n .object({\n type: z.literal(\"agent_list_commands_request\"),\n requestId: z.string(),\n agentId: z.string(),\n })\n .passthrough();\nexport type AgentListCommandsRequest = z.infer<typeof agentListCommandsRequestSchema>;\n\nexport const agentListCommandsResponseSchema = z\n .object({\n type: z.literal(\"agent_list_commands_response\"),\n requestId: z.string(),\n payload: z\n .object({\n commands: z.array(agentCommandDescriptorSchema),\n })\n .passthrough(),\n })\n .passthrough();\nexport type AgentListCommandsResponse = z.infer<typeof agentListCommandsResponseSchema>;\n\n// ===========================================================================\n// Extension packs (sprint-057, sprint-056 preinstalled-extensions.md § RPC surface) — read curated-\n// pack state and change the selection over the wire. No push/broadcast type: sync is\n// request-triggered and the response carries the result (the `checkout_status_update` per-session\n// `send()` family is the precedent to copy if live progress is ever wanted, not a new mechanism).\n// ===========================================================================\n\n/**\n * Documents today's values for `EntryInfo.status`. Structurally mirrors\n * `packages/server/src/extensions/sync-planner.ts`'s `EntryStatus` — duplicated deliberately\n * (protocol keeps zero workspace imports, root invariant 2); task-003 maps between them. The wire\n * field itself is `z.string()`, never a narrowed enum: an older client must still parse a status\n * value a later daemon introduces (append-only rule).\n */\nexport type EntryStatus =\n | \"installed\"\n | \"pending\"\n | \"failed\"\n | \"user_removed\"\n | \"user_modified\"\n | \"deprecated\";\n\n/**\n * Documents today's values for `SyncReport.outcome` / `lastSync.outcome`. The wire field is\n * `z.string()` for the same forward-compat reason as {@link EntryStatus} — the server already\n * commits to this for the persisted form (`ExtensionsDescribe.lastSync.outcome`,\n * `packages/server/src/extensions/extensions-service.ts`), since a persisted future outcome must\n * round-trip through an older daemon.\n */\nexport type SyncOutcome = \"ok\" | \"noop\" | \"partial\" | \"failed\" | \"skipped\";\n\nexport const extensionEntryInfoSchema = z\n .object({\n source: z.string(),\n identity: z.string(),\n addedIn: z.string(),\n deprecated: z.boolean().optional(),\n /** {@link EntryStatus} today; plain string on the wire (see above). */\n status: z.string(),\n lastError: z\n .object({\n at: z.string(),\n attempts: z.number(),\n /** `ExtensionFailureReason` today (`sync-executor.ts`); plain string, same reason. */\n reason: z.string(),\n message: z.string(),\n })\n .passthrough()\n .optional(),\n })\n .passthrough();\nexport type ExtensionEntryInfo = z.infer<typeof extensionEntryInfoSchema>;\n\nexport const extensionPackInfoSchema = z\n .object({\n id: z.string(),\n title: z.string(),\n description: z.string(),\n packages: z.array(extensionEntryInfoSchema),\n })\n .passthrough();\nexport type ExtensionPackInfo = z.infer<typeof extensionPackInfoSchema>;\n\n/** One failed install within a triggered sync run. */\nexport const extensionSyncFailureSchema = z\n .object({\n identity: z.string(),\n source: z.string(),\n pack: z.string(),\n /** `ExtensionFailureReason` today; plain string, same forward-compat reason as above. */\n reason: z.string(),\n message: z.string(),\n })\n .passthrough();\n\n/** The full result of a triggered sync run — returned only as an RPC response field, never\n * persisted as-is (see {@link extensionsLastSyncSummarySchema}). */\nexport const extensionSyncReportSchema = z\n .object({\n at: z.string(),\n /** {@link SyncOutcome} today; plain string on the wire. */\n outcome: z.string(),\n installed: z.array(z.string()),\n failures: z.array(extensionSyncFailureSchema),\n })\n .passthrough();\nexport type ExtensionSyncReport = z.infer<typeof extensionSyncReportSchema>;\n\n/**\n * `lastSync` is a **summary**, not a report: only `{ at, outcome }` is ever persisted\n * (`extensions-state.json`), so promising a full {@link ExtensionSyncReport} here would be a lie\n * after any daemon restart. No `installed`/`failures` fields — do not widen this to the report shape.\n */\nexport const extensionsLastSyncSummarySchema = z\n .object({\n at: z.string(),\n /** {@link SyncOutcome} today; plain string on the wire. */\n outcome: z.string(),\n })\n .passthrough();\n\nexport const extensionPacksListRequestSchema = z\n .object({\n type: z.literal(\"extension_packs_list_request\"),\n requestId: z.string(),\n })\n .passthrough();\nexport type ExtensionPacksListRequest = z.infer<typeof extensionPacksListRequestSchema>;\n\nexport const extensionPacksListResponseSchema = z\n .object({\n type: z.literal(\"extension_packs_list_response\"),\n requestId: z.string(),\n autoSync: z.boolean(),\n selected: z.array(z.string()),\n packs: z.array(extensionPackInfoSchema),\n lastSync: extensionsLastSyncSummarySchema.optional(),\n })\n .passthrough();\nexport type ExtensionPacksListResponse = z.infer<typeof extensionPacksListResponseSchema>;\n\n/**\n * `packs` is optional; its **absence** is the manual-sync trigger. Present ⇒ change the selection\n * and sync. Absent ⇒ change nothing, run an ungated manual sync — this is what carries\n * `pi-studio extensions sync` (task-005), which must keep working even with `autoSync: false`.\n */\nexport const extensionPacksSetRequestSchema = z\n .object({\n type: z.literal(\"extension_packs_set_request\"),\n requestId: z.string(),\n packs: z.array(z.string()).optional(),\n })\n .passthrough();\nexport type ExtensionPacksSetRequest = z.infer<typeof extensionPacksSetRequestSchema>;\n\n/**\n * `ok`/`error` are domain fields, not `rpc_error` — a handler cannot express a domain failure\n * (e.g. an unknown pack slug) through `rpc_error`, which carries only transport-level codes\n * (mirrors `file_watch_subscribe_response`'s `ok: false, error: \"…\"` idiom). `report` is optional\n * for the same reason: a rejected request ran no sync.\n */\nexport const extensionPacksSetResponseSchema = z\n .object({\n type: z.literal(\"extension_packs_set_response\"),\n requestId: z.string(),\n autoSync: z.boolean(),\n selected: z.array(z.string()),\n packs: z.array(extensionPackInfoSchema),\n lastSync: extensionsLastSyncSummarySchema.optional(),\n ok: z.boolean(),\n error: z.string().optional(),\n report: extensionSyncReportSchema.optional(),\n })\n .passthrough();\nexport type ExtensionPacksSetResponse = z.infer<typeof extensionPacksSetResponseSchema>;\n\n// ===========================================================================\n// RPC error\n// ===========================================================================\n\nexport const rpcErrorSchema = z.object({\n type: z.literal(\"rpc_error\"),\n requestId: z.string(),\n code: z.string().optional(),\n message: z.string().optional(),\n error: z.string().optional(),\n});\nexport type RpcError = z.infer<typeof rpcErrorSchema>;\n\n// ===========================================================================\n// Session message union (agent / timeline / permission core)\n// ===========================================================================\n\n/**\n * The discriminated union of session messages defined so far. Later feature sprints extend this\n * union with workspace/git/terminal/chat/schedule/loop families.\n */\nexport const sessionMessageSchema = z.discriminatedUnion(\"type\", [\n createAgentRequestSchema,\n createAgentResponseSchema,\n agentUpdateSchema,\n agentStatusMessageSchema,\n agentListSchema,\n agentDeletedSchema,\n agentArchivedSchema,\n agentStreamSchema,\n fetchAgentTimelineRequestSchema,\n fetchAgentTimelineResponseSchema,\n agentPermissionRequestSchema,\n agentPermissionResolvedSchema,\n respondToPermissionRequestSchema,\n respondToPermissionResponseSchema,\n legacyRespondToPermissionSchema,\n agentRewindRequestSchema,\n agentRewindResponseSchema,\n agentSessionStatsRequestSchema,\n agentSessionStatsResponseSchema,\n agentCompactRequestSchema,\n agentCompactResponseSchema,\n agentNewSessionRequestSchema,\n agentNewSessionResponseSchema,\n agentSwitchSessionRequestSchema,\n agentSwitchSessionResponseSchema,\n agentForkRequestSchema,\n agentForkResponseSchema,\n agentForkMessagesRequestSchema,\n agentForkMessagesResponseSchema,\n agentCloneRequestSchema,\n agentCloneResponseSchema,\n agentSetSessionNameRequestSchema,\n agentSetSessionNameResponseSchema,\n agentExportHtmlRequestSchema,\n agentExportHtmlResponseSchema,\n agentSetModelRequestSchema,\n agentSetModelResponseSchema,\n agentCycleModelRequestSchema,\n agentCycleModelResponseSchema,\n agentLastAssistantTextRequestSchema,\n agentLastAssistantTextResponseSchema,\n steerAgentRequestSchema,\n steerAgentResponseSchema,\n followUpAgentRequestSchema,\n followUpAgentResponseSchema,\n agentListCommandsRequestSchema,\n agentListCommandsResponseSchema,\n extensionPacksListRequestSchema,\n extensionPacksListResponseSchema,\n extensionPacksSetRequestSchema,\n extensionPacksSetResponseSchema,\n rpcErrorSchema,\n]);\nexport type SessionMessage = z.infer<typeof sessionMessageSchema>;\n\n/**\n * Structural fallback for any session message (carries a `type` discriminant). Unknown/future\n * session types still parse here (append-only / \"ignore unknown type per handler policy\"), so the\n * envelope accepts them and handlers validate per-family with the specific schema above.\n */\nexport const sessionMessageBaseSchema = z.object({ type: z.string() }).passthrough();\nexport type SessionMessageBase = z.infer<typeof sessionMessageBaseSchema>;\n\n// ===========================================================================\n// Session envelope + top-level union\n// ===========================================================================\n\n/**\n * Envelope wrapping the rich session message union. Known message types validate strictly against\n * `sessionMessageSchema`; unknown/future types fall back to the structural base so an older client\n * still parses newer messages.\n */\nexport const sessionEnvelopeSchema = z.object({\n type: z.literal(\"session\"),\n message: z.union([sessionMessageSchema, sessionMessageBaseSchema]),\n});\nexport type SessionEnvelope = z.infer<typeof sessionEnvelopeSchema>;\n\n/** Discriminated union of all top-level envelopes, keyed by `type`. */\nexport const topLevelEnvelopeSchema = z.discriminatedUnion(\"type\", [\n helloSchema,\n statusSchema,\n pingSchema,\n pongSchema,\n sessionEnvelopeSchema,\n]);\nexport type TopLevelEnvelope = z.infer<typeof topLevelEnvelopeSchema>;\n","import { COMPAT, type CompatTag } from \"./validation.js\";\n\n/**\n * Capability flags — the per-feature compatibility layer (MAIN-SCOPE §9,\n * architecture/websocket-protocol.md § Capability flags / Compatibility rules).\n *\n * Two directions:\n * - `CLIENT_CAPS` (client → daemon, advertised in `hello.capabilities`): the daemon stores these\n * per connection so the wire boundary can ask one question, `supports(caps, flag)`. New wire\n * enum values are gated through this so an old client never receives a value it didn't advertise.\n * - `SERVER_FEATURES` (daemon → client, in `server_info.features.*`): new *features* advertise a\n * flag; clients either run the feature or show \"update the host.\" No degraded fallback path.\n */\n\n// ---------------------------------------------------------------------------\n// CLIENT_CAPS (client → daemon, hello.capabilities)\n// ---------------------------------------------------------------------------\n\nexport const CLIENT_CAPS = {\n /** Client renders custom per-mode icons. */\n custom_mode_icons: \"custom_mode_icons\",\n /** Client understands the merged reasoning stream enum value. */\n reasoning_merge_enum: \"reasoning_merge_enum\",\n /** Client can consume a reflowable terminal snapshot payload. */\n terminal_reflowable_snapshot: \"terminal_reflowable_snapshot\",\n /** Client renders markdown images (`![alt](path)`) whose target is a local filesystem path. */\n inline_image_markdown: \"inline_image_markdown\",\n /** Client renders markdown links (`[label](path)`) whose target is a local filesystem path as an open-file action. */\n file_link_markdown: \"file_link_markdown\",\n} as const;\n\nexport type ClientCapabilityKey = keyof typeof CLIENT_CAPS;\nexport type ClientCapability = (typeof CLIENT_CAPS)[ClientCapabilityKey];\n\n// ---------------------------------------------------------------------------\n// SERVER_FEATURES (daemon → client, server_info.features.*)\n// ---------------------------------------------------------------------------\n\nexport const SERVER_FEATURES = {\n providersSnapshot: \"providersSnapshot\",\n checkoutGithubSetAutoMerge: \"checkoutGithubSetAutoMerge\",\n daemonStatusRpc: \"daemonStatusRpc\",\n \"terminal-restore-modes\": \"terminal-restore-modes\",\n rewind: \"rewind\",\n checkoutRefresh: \"checkoutRefresh\",\n extensionPacks: \"extensionPacks\",\n} as const;\n\nexport type ServerFeatureKey = keyof typeof SERVER_FEATURES;\nexport type ServerFeature = (typeof SERVER_FEATURES)[ServerFeatureKey];\n\n/**\n * COMPAT annotations for each server feature flag (version added + removal date). The removal dates\n * are placeholders to be confirmed against the live `ServerInfoStatusPayloadSchema`. Grep `COMPAT(`\n * to enumerate cleanup work.\n */\nexport const SERVER_FEATURE_COMPAT: Record<ServerFeatureKey, CompatTag> = {\n // COMPAT(providersSnapshot): added 0.0.0, remove by TBD\n providersSnapshot: COMPAT({ name: \"providersSnapshot\", addedIn: \"0.0.0\", removeBy: \"TBD\" }),\n // COMPAT(checkoutGithubSetAutoMerge): added 0.0.0, remove by TBD\n checkoutGithubSetAutoMerge: COMPAT({\n name: \"checkoutGithubSetAutoMerge\",\n addedIn: \"0.0.0\",\n removeBy: \"TBD\",\n }),\n // COMPAT(daemonStatusRpc): added 0.0.0, remove by TBD\n daemonStatusRpc: COMPAT({ name: \"daemonStatusRpc\", addedIn: \"0.0.0\", removeBy: \"TBD\" }),\n // COMPAT(terminal-restore-modes): added 0.0.0, remove by TBD\n \"terminal-restore-modes\": COMPAT({\n name: \"terminal-restore-modes\",\n addedIn: \"0.0.0\",\n removeBy: \"TBD\",\n }),\n // COMPAT(rewind): added 0.0.0, remove by TBD\n rewind: COMPAT({ name: \"rewind\", addedIn: \"0.0.0\", removeBy: \"TBD\" }),\n // COMPAT(checkoutRefresh): added 0.0.0, remove by TBD\n checkoutRefresh: COMPAT({ name: \"checkoutRefresh\", addedIn: \"0.0.0\", removeBy: \"TBD\" }),\n // COMPAT(extensionPacks): added 0.0.0, remove by TBD\n extensionPacks: COMPAT({ name: \"extensionPacks\", addedIn: \"0.0.0\", removeBy: \"TBD\" }),\n};\n\n// ---------------------------------------------------------------------------\n// Gating helper (models daemon-side `session.supports(...)`)\n// ---------------------------------------------------------------------------\n\n/** The forms a stored capability set can take on the daemon session. */\nexport type CapabilitySet = Record<string, boolean> | readonly string[] | Set<string> | undefined;\n\n/**\n * Returns true only if `flag` is advertised by `caps`. Used at serialization time to gate new wire\n * enum values: `if (session.supports(CLIENT_CAPS.reasoning_merge_enum)) emit(newValue) else emit(old)`.\n */\nexport function supports(caps: CapabilitySet, flag: string): boolean {\n if (!caps) return false;\n if (caps instanceof Set) return caps.has(flag);\n if (Array.isArray(caps)) return caps.includes(flag);\n return (caps as Record<string, boolean>)[flag] === true;\n}\n","import { z } from \"zod\";\n\n/**\n * Terminal stream binary frame codec (architecture/websocket-protocol.md § Binary frames,\n * features/terminals.md § Binary stream protocol).\n *\n * Frame layout: `[1-byte opcode][1-byte slot][payload]`.\n * - `slot` demuxes multiple terminals on one socket (0–255).\n * - Output/Input/Snapshot/Restore payloads are raw bytes (pass-through).\n * - Resize payload is UTF-8 JSON `{ rows, cols }`.\n *\n * Uses `Uint8Array` (not Node `Buffer`) so the codec runs in browser/RN as well as Node.\n */\n\n/** Terminal frame opcodes. `Restore` value is TODO(verify) against the live codec. */\nexport const TerminalOpcode = {\n Output: 0x01,\n Input: 0x02,\n Resize: 0x03,\n Snapshot: 0x04,\n Restore: 0x05,\n} as const;\n\nexport type TerminalOpcodeName = keyof typeof TerminalOpcode;\nexport type TerminalOpcodeValue = (typeof TerminalOpcode)[TerminalOpcodeName];\n\nconst OPCODE_BY_VALUE: Record<number, TerminalOpcodeName> = Object.fromEntries(\n Object.entries(TerminalOpcode).map(([name, value]) => [value, name as TerminalOpcodeName]),\n);\n\n/** Resize payload schema (`{ rows, cols }`, non-negative integers). */\nexport const terminalResizeSchema = z.object({\n rows: z.number().int().nonnegative(),\n cols: z.number().int().nonnegative(),\n});\nexport type TerminalResize = z.infer<typeof terminalResizeSchema>;\n\nexport type TerminalFrame =\n | { opcode: \"Output\"; slot: number; data: Uint8Array }\n | { opcode: \"Input\"; slot: number; data: Uint8Array }\n | { opcode: \"Resize\"; slot: number; rows: number; cols: number }\n | { opcode: \"Snapshot\"; slot: number; data: Uint8Array }\n | { opcode: \"Restore\"; slot: number; data: Uint8Array };\n\n/** Thrown when a frame cannot be decoded (too short, unknown opcode, bad payload). */\nexport class TerminalFrameError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"TerminalFrameError\";\n }\n}\n\nconst textEncoder = new TextEncoder();\nconst textDecoder = new TextDecoder();\n\nfunction assertSlot(slot: number): void {\n if (!Number.isInteger(slot) || slot < 0 || slot > 0xff) {\n throw new TerminalFrameError(`slot must be an integer 0–255, got ${slot}`);\n }\n}\n\nfunction frameWithPayload(opcodeValue: number, slot: number, payload: Uint8Array): Uint8Array {\n const out = new Uint8Array(2 + payload.length);\n out[0] = opcodeValue;\n out[1] = slot;\n out.set(payload, 2);\n return out;\n}\n\n/** Encode a typed terminal frame to bytes. */\nexport function encodeTerminalFrame(frame: TerminalFrame): Uint8Array {\n assertSlot(frame.slot);\n switch (frame.opcode) {\n case \"Output\":\n return frameWithPayload(TerminalOpcode.Output, frame.slot, frame.data);\n case \"Input\":\n return frameWithPayload(TerminalOpcode.Input, frame.slot, frame.data);\n case \"Snapshot\":\n return frameWithPayload(TerminalOpcode.Snapshot, frame.slot, frame.data);\n case \"Restore\":\n return frameWithPayload(TerminalOpcode.Restore, frame.slot, frame.data);\n case \"Resize\": {\n const json = JSON.stringify(\n terminalResizeSchema.parse({ rows: frame.rows, cols: frame.cols }),\n );\n return frameWithPayload(TerminalOpcode.Resize, frame.slot, textEncoder.encode(json));\n }\n }\n}\n\n/** Decode bytes into a typed terminal frame. Throws `TerminalFrameError` on invalid input. */\nexport function decodeTerminalFrame(bytes: Uint8Array): TerminalFrame {\n if (bytes.length < 2) {\n throw new TerminalFrameError(`frame too short: ${bytes.length} bytes (need ≥ 2)`);\n }\n const opcodeValue = bytes[0] as number;\n const slot = bytes[1] as number;\n const opcode = OPCODE_BY_VALUE[opcodeValue];\n if (!opcode) {\n throw new TerminalFrameError(`unknown terminal opcode 0x${opcodeValue.toString(16)}`);\n }\n const payload = bytes.subarray(2);\n\n if (opcode === \"Resize\") {\n let parsed: unknown;\n try {\n parsed = JSON.parse(textDecoder.decode(payload));\n } catch {\n throw new TerminalFrameError(\"resize payload is not valid JSON\");\n }\n const result = terminalResizeSchema.safeParse(parsed);\n if (!result.success) {\n throw new TerminalFrameError(\"resize payload is not { rows, cols }\");\n }\n return { opcode: \"Resize\", slot, rows: result.data.rows, cols: result.data.cols };\n }\n\n // Output/Input/Snapshot/Restore: raw bytes pass through (copied so callers own the buffer).\n return { opcode, slot, data: payload.slice() };\n}\n\n/** Returns the decoded frame, or `null` instead of throwing (safe handling of unknown opcodes). */\nexport function tryDecodeTerminalFrame(bytes: Uint8Array): TerminalFrame | null {\n try {\n return decodeTerminalFrame(bytes);\n } catch {\n return null;\n }\n}\n","import { z } from \"zod\";\n\n/**\n * File-transfer binary frame codec (architecture/websocket-protocol.md § Binary frames,\n * features/file-explorer-transfer.md § Binary transfer frames). Separate from the terminal codec\n * but lives in the same module family.\n *\n * Frame layout: `[1-byte opcode][1-byte stream][payload]`.\n * - `stream` multiplexes concurrent transfers on one socket (0–255).\n * - A transfer is `Begin` (metadata header) → N × `Chunk` (raw bytes) → `End` (completion marker),\n * or `Error` to abort.\n *\n * NOTE: exact opcode values / chunk sizing / completion marker are TODO(verify) against the live\n * codec; this is a faithful chunked-with-completion-marker reconstruction from the scope.\n */\n\nexport const FileTransferOpcode = {\n Begin: 0x10,\n Chunk: 0x11,\n End: 0x12,\n Error: 0x13,\n} as const;\n\nexport type FileTransferOpcodeName = keyof typeof FileTransferOpcode;\n\nconst OPCODE_BY_VALUE: Record<number, FileTransferOpcodeName> = Object.fromEntries(\n Object.entries(FileTransferOpcode).map(([name, value]) => [\n value,\n name as FileTransferOpcodeName,\n ]),\n);\n\n/** Metadata header for a transfer (carried by the `Begin` frame as UTF-8 JSON). */\nexport const fileTransferBeginSchema = z.object({\n transferId: z.string(),\n fileName: z.string().optional(),\n mimeType: z.string().optional(),\n totalBytes: z.number().int().nonnegative().optional(),\n});\nexport type FileTransferBegin = z.infer<typeof fileTransferBeginSchema>;\n\nexport type FileTransferFrame =\n | { opcode: \"Begin\"; stream: number; meta: FileTransferBegin }\n | { opcode: \"Chunk\"; stream: number; data: Uint8Array }\n | { opcode: \"End\"; stream: number; ok: boolean }\n | { opcode: \"Error\"; stream: number; message: string };\n\nexport class FileTransferFrameError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"FileTransferFrameError\";\n }\n}\n\nconst textEncoder = new TextEncoder();\nconst textDecoder = new TextDecoder();\n\nfunction assertStream(stream: number): void {\n if (!Number.isInteger(stream) || stream < 0 || stream > 0xff) {\n throw new FileTransferFrameError(`stream must be an integer 0–255, got ${stream}`);\n }\n}\n\nfunction frame(opcodeValue: number, stream: number, payload: Uint8Array): Uint8Array {\n const out = new Uint8Array(2 + payload.length);\n out[0] = opcodeValue;\n out[1] = stream;\n out.set(payload, 2);\n return out;\n}\n\nexport function encodeFileTransferFrame(f: FileTransferFrame): Uint8Array {\n assertStream(f.stream);\n switch (f.opcode) {\n case \"Begin\":\n return frame(\n FileTransferOpcode.Begin,\n f.stream,\n textEncoder.encode(JSON.stringify(fileTransferBeginSchema.parse(f.meta))),\n );\n case \"Chunk\":\n return frame(FileTransferOpcode.Chunk, f.stream, f.data);\n case \"End\":\n // Completion marker; payload encodes the ok/abort flag as a single byte.\n return frame(FileTransferOpcode.End, f.stream, new Uint8Array([f.ok ? 1 : 0]));\n case \"Error\":\n return frame(FileTransferOpcode.Error, f.stream, textEncoder.encode(f.message));\n }\n}\n\nexport function decodeFileTransferFrame(bytes: Uint8Array): FileTransferFrame {\n if (bytes.length < 2) {\n throw new FileTransferFrameError(`frame too short: ${bytes.length} bytes (need ≥ 2)`);\n }\n const opcodeValue = bytes[0] as number;\n const stream = bytes[1] as number;\n const opcode = OPCODE_BY_VALUE[opcodeValue];\n if (!opcode) {\n throw new FileTransferFrameError(`unknown file-transfer opcode 0x${opcodeValue.toString(16)}`);\n }\n const payload = bytes.subarray(2);\n\n switch (opcode) {\n case \"Begin\": {\n let parsed: unknown;\n try {\n parsed = JSON.parse(textDecoder.decode(payload));\n } catch {\n throw new FileTransferFrameError(\"begin payload is not valid JSON\");\n }\n const result = fileTransferBeginSchema.safeParse(parsed);\n if (!result.success) {\n throw new FileTransferFrameError(\"begin payload is not a valid transfer header\");\n }\n return { opcode: \"Begin\", stream, meta: result.data };\n }\n case \"Chunk\":\n return { opcode: \"Chunk\", stream, data: payload.slice() };\n case \"End\":\n return { opcode: \"End\", stream, ok: (payload[0] ?? 1) !== 0 };\n case \"Error\":\n return { opcode: \"Error\", stream, message: textDecoder.decode(payload) };\n }\n}\n\nexport function tryDecodeFileTransferFrame(bytes: Uint8Array): FileTransferFrame | null {\n try {\n return decodeFileTransferFrame(bytes);\n } catch {\n return null;\n }\n}\n","/**\n * Slot/stream id allocation for the binary frame protocols (architecture/websocket-protocol.md\n * § Binary frames).\n *\n * Both binary framings — terminal (`[opcode][slot]`) and file transfer (`[opcode][stream]`) —\n * spend exactly ONE byte on the multiplexing id, so the id space is 0–255 and is a **reusable\n * pool, not a monotonic counter**. Every allocator that hands out these ids must therefore recycle\n * ids of finished transfers/terminals; a bare `next++` silently produces 256 after 255 hand-outs\n * and every subsequent `encode*Frame` call throws, killing the feature until the process restarts.\n *\n * Allocation walks forward from a caller-held cursor rather than always returning the lowest free\n * id: a just-released id is then the LAST one reused, which keeps a stale peer-side subscription\n * (a client that never unsubscribed from a closed terminal) from being re-pointed at a brand new\n * transfer the moment the old one ends.\n */\n\n/** Size of the one-byte slot/stream id space shared by both binary frame protocols. */\nexport const SLOT_SPACE = 256;\n\n/**\n * Lowest free id at or after `cursor` (wrapping), or `null` when all {@link SLOT_SPACE} ids are\n * live. `inUse` is anything keyed by slot id — a `Map` or `Set` of live transfers satisfies it.\n */\nexport function nextFreeSlot(inUse: { has(id: number): boolean }, cursor = 0): number | null {\n const start = ((cursor % SLOT_SPACE) + SLOT_SPACE) % SLOT_SPACE;\n for (let i = 0; i < SLOT_SPACE; i++) {\n const slot = (start + i) % SLOT_SPACE;\n if (!inUse.has(slot)) return slot;\n }\n return null;\n}\n","import { z } from \"zod\";\n\n/**\n * Provider manifest types (features/agent-providers.md § Registration surface, § Capability flags).\n *\n * The manifest is UI scaffolding only — actual models/modes/features are discovered at runtime via\n * the provider RPC. These types express provider definitions, mode metadata (icon + colorTier), and\n * the `AgentCapabilityFlags` shape.\n */\n\n/** Provider ids must match `/^[a-z][a-z0-9-]*$/` (agent-providers.md). */\nexport const providerIdSchema = z.string().regex(/^[a-z][a-z0-9-]*$/, \"invalid provider id\");\n\n/** Mode danger/intent tier driving UI color. */\nexport const colorTierSchema = z.enum([\"safe\", \"moderate\", \"dangerous\", \"planning\"]);\nexport type ColorTier = z.infer<typeof colorTierSchema>;\n\n/** Per-provider capability flags. */\nexport const agentCapabilityFlagsSchema = z.object({\n supportsStreaming: z.boolean(),\n supportsSessionPersistence: z.boolean(),\n supportsDynamicModes: z.boolean(),\n supportsMcpServers: z.boolean(),\n supportsReasoningStream: z.boolean(),\n supportsToolInvocations: z.boolean(),\n // Rewind capability flags (features/rewind.md) — additive, optional, old clients ignore.\n supportsRewindConversation: z.boolean().optional(),\n supportsRewindFiles: z.boolean().optional(),\n supportsRewindBoth: z.boolean().optional(),\n // Steering capability (features/agent-sessions.md § Steering) — additive, optional. When true,\n // the provider accepts `steer`/`follow_up` messages injected into a live turn.\n supportsSteering: z.boolean().optional(),\n}).passthrough();\nexport type AgentCapabilityFlags = z.infer<typeof agentCapabilityFlagsSchema>;\n\n/** UI metadata for a provider mode (plan/default/full-access/...). */\nexport const providerModeSchema = z.object({\n id: z.string(),\n label: z.string().optional(),\n description: z.string().optional(),\n icon: z.string().optional(),\n colorTier: colorTierSchema,\n});\nexport type ProviderMode = z.infer<typeof providerModeSchema>;\n\n/** A provider manifest definition. `extends` marks a custom Pi-compatible profile. */\nexport const providerDefinitionSchema = z.object({\n id: providerIdSchema,\n label: z.string(),\n description: z.string().optional(),\n extends: z.string().optional(),\n modes: z.array(providerModeSchema),\n capabilities: agentCapabilityFlagsSchema,\n});\nexport type ProviderDefinition = z.infer<typeof providerDefinitionSchema>;\n","/**\n * Pluggable transport abstraction — direct WebSocket and (future) relay E2EE share this API.\n * See architecture/client-app-runtime.md § Layered client library.\n */\n\nexport type TransportMessageHandler = (data: string | ArrayBuffer | Blob) => void;\nexport type TransportCloseHandler = (code: number, reason: string) => void;\nexport type TransportErrorHandler = (error: unknown) => void;\n\nexport interface Transport {\n /** Initiate the connection. Resolves when the socket is open (raw, pre-handshake). */\n connect(url: string): Promise<void>;\n /** Send a text frame. */\n sendText(data: string): void;\n /** Send a binary frame. */\n sendBinary(data: Uint8Array): void;\n /** Close the transport cleanly. */\n close(code?: number, reason?: string): void;\n /** Whether the transport is currently open and writeable. */\n readonly isOpen: boolean;\n\n onMessage: TransportMessageHandler | null;\n onClose: TransportCloseHandler | null;\n onError: TransportErrorHandler | null;\n}\n\n// ─── Browser / Node WebSocket transport ────────────────────────────────────────\n\nexport type AnyWebSocket = {\n readyState: number;\n send(data: string | ArrayBuffer | Uint8Array): void;\n close(code?: number, reason?: string): void;\n onopen: ((ev: unknown) => void) | null;\n onclose: ((ev: { code: number; reason: string }) => void) | null;\n onerror: ((ev: unknown) => void) | null;\n onmessage: ((ev: { data: unknown }) => void) | null;\n /** Browsers/RN default this to `\"blob\"`; Node's `ws` ignores it (always delivers `Buffer`).\n * `connect()` forces `\"arraybuffer\"` where the property exists — see its comment for why. */\n binaryType?: string;\n};\n\nexport type WsFactory = (url: string, protocols?: string[]) => AnyWebSocket;\n\n// Node `ws` compatibility: ws closes have reason as Buffer, not string.\nexport function reasonString(reason: unknown): string {\n if (typeof reason === \"string\") return reason;\n if (reason instanceof Uint8Array || Buffer.isBuffer(reason)) return reason.toString(\"utf8\");\n return String(reason ?? \"\");\n}\n\n/**\n * Create a direct WebSocket transport using the supplied factory.\n * Default factory uses the global `WebSocket` (browser/RN/Node 22+) or throws.\n */\nexport function createWebSocketTransport(factory?: WsFactory): Transport {\n let ws: AnyWebSocket | null = null;\n const self: Transport = {\n onMessage: null,\n onClose: null,\n onError: null,\n get isOpen(): boolean {\n return ws !== null && ws.readyState === 1; // OPEN\n },\n connect(url: string): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const create = factory ?? (() => new WebSocket(url) as unknown as AnyWebSocket);\n try {\n ws = factory ? create(url) : (new WebSocket(url) as unknown as AnyWebSocket);\n // Force synchronous `ArrayBuffer` delivery. Browsers/RN default `binaryType` to\n // `\"blob\"`, which forces `DaemonClient.handleIncoming` onto an async\n // `Blob.arrayBuffer()` decode per message — and that decode's resolution order across\n // independent Blobs is NOT guaranteed to match wire order. For a multi-frame binary\n // transfer (file-transfer chunks, terminal frames) that race can let a later frame's\n // decode finish before an earlier one's, reordering what `FileTransferClient.dispatch()`\n // sees and silently dropping chunks once `End` deletes the stream's pending state before\n // a straggling `Chunk` arrives (real bug: truncated downloaded images, bottom rows\n // missing). `ws` (Node) always delivers `Buffer`/`ArrayBuffer` synchronously regardless\n // of this property; setting it there is a harmless no-op.\n ws.binaryType = \"arraybuffer\";\n } catch (err) {\n reject(err);\n return;\n }\n ws.onopen = () => resolve();\n ws.onerror = (ev) => {\n if (ws?.readyState !== 1) {\n reject(ev);\n }\n self.onError?.(ev);\n };\n ws.onclose = (ev) => {\n self.onClose?.(ev.code, reasonString(ev.reason));\n };\n ws.onmessage = (ev) => {\n const { data } = ev;\n if (data instanceof ArrayBuffer || data instanceof Uint8Array) {\n self.onMessage?.(data as ArrayBuffer);\n } else if (typeof data === \"string\") {\n self.onMessage?.(data);\n } else if (typeof Blob !== \"undefined\" && data instanceof Blob) {\n // Unreachable once `binaryType = \"arraybuffer\"` takes effect (set above) — kept as a\n // defensive fallback for a WebSocket-like object that ignores the property, so binary\n // frames still decode (via `DaemonClient.handleIncoming`'s async `Blob.arrayBuffer()`\n // path) instead of being silently dropped, rather than as the expected delivery mode.\n self.onMessage?.(data);\n } else if (Buffer.isBuffer(data)) {\n // Node `ws` may deliver Buffer\n self.onMessage?.(\n data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer,\n );\n }\n };\n });\n },\n sendText(data: string): void {\n ws?.send(data);\n },\n sendBinary(data: Uint8Array): void {\n ws?.send(data);\n },\n close(code = 1000, reason = \"\"): void {\n ws?.close(code, reason);\n ws = null;\n },\n };\n return self;\n}\n","/**\n * Pure-JS base64 codec (no Node `Buffer`) so the relay's crypto framing runs identically in the\n * daemon (Node) and future browser/RN clients — see architecture/relay-e2ee.md § Behavior\n * (per-message wire format uses `base64([nonce] ++ ciphertext)`).\n */\nconst CHARS = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n/** Encode raw bytes as standard (padded) base64. */\nexport function encodeBase64(bytes) {\n let out = \"\";\n for (let i = 0; i < bytes.length; i += 3) {\n const b0 = bytes[i];\n const hasB1 = i + 1 < bytes.length;\n const hasB2 = i + 2 < bytes.length;\n const b1 = hasB1 ? bytes[i + 1] : 0;\n const b2 = hasB2 ? bytes[i + 2] : 0;\n out += CHARS[b0 >> 2];\n out += CHARS[((b0 & 0x03) << 4) | (b1 >> 4)];\n out += hasB1 ? CHARS[((b1 & 0x0f) << 2) | (b2 >> 6)] : \"=\";\n out += hasB2 ? CHARS[b2 & 0x3f] : \"=\";\n }\n return out;\n}\n/** Decode standard base64 (padded or not) into raw bytes. Throws on invalid characters. */\nexport function decodeBase64(input) {\n const clean = input.replace(/=+$/, \"\");\n const bytes = [];\n let buffer = 0;\n let bits = 0;\n for (const ch of clean) {\n const val = CHARS.indexOf(ch);\n if (val === -1)\n throw new Error(`invalid base64 character: ${JSON.stringify(ch)}`);\n buffer = (buffer << 6) | val;\n bits += 6;\n if (bits >= 8) {\n bits -= 8;\n bytes.push((buffer >> bits) & 0xff);\n }\n }\n return new Uint8Array(bytes);\n}\n//# sourceMappingURL=base64.js.map","/**\n * Relay encrypted channel primitives (architecture/relay-e2ee.md § Channel API, § Handshake\n * frames, § Behavior). `createClientChannel` and `createDaemonChannel` expose an IDENTICAL API so\n * the daemon and client share the same E2EE code path over a relay-provided transport.\n *\n * Handshake:\n * client → daemon: e2ee_hello { ephemeralPublicKey } (client's FRESH ephemeral Curve25519 key)\n * daemon → client: e2ee_ready (handshake acknowledgement)\n * both: sharedKey = ECDH(localSecret, remotePublic) (Curve25519 box.before)\n *\n * Per-message wire format (after handshake):\n * frame = base64( [24-byte random nonce] ++ box(plaintext, nonce, sharedKey) ) // XSalsa20-Poly1305\n * receive → split nonce/ciphertext → box_open → reject (drop, never throw to the app) on auth failure.\n *\n * The daemon secret never leaves the daemon; the client keypair is ephemeral per connection, so\n * there is no persistent client-side secret to steal. Session keys are fresh per session, so\n * ciphertext cannot replay across sessions — but replay protection WITHIN a live session is not\n * implemented (random nonces, no counter) per architecture/relay-e2ee.md § Behavior TODO(verify).\n */\nimport nacl from \"tweetnacl\";\nimport { decodeBase64, encodeBase64 } from \"./base64.js\";\nfunction parseFrame(data) {\n try {\n const parsed = JSON.parse(data);\n if (!parsed || typeof parsed.type !== \"string\")\n return null;\n return parsed;\n }\n catch {\n return null;\n }\n}\nconst NONCE_LENGTH = 24; // XSalsa20-Poly1305 nonce size (nacl.box.nonceLength)\n/** Encrypt raw `message` bytes under `sharedKey`, producing the wire frame `base64(nonce ++ ciphertext)`. */\nfunction sealBytes(message, sharedKey) {\n const nonce = nacl.randomBytes(NONCE_LENGTH);\n const ciphertext = nacl.box.after(message, nonce, sharedKey);\n const combined = new Uint8Array(nonce.length + ciphertext.length);\n combined.set(nonce, 0);\n combined.set(ciphertext, nonce.length);\n return encodeBase64(combined);\n}\n/** Decrypt a `base64(nonce ++ ciphertext)` wire frame under `sharedKey` to raw bytes, or `null` on auth failure. */\nfunction openBytes(frame, sharedKey) {\n let combined;\n try {\n combined = decodeBase64(frame);\n }\n catch {\n return null;\n }\n if (combined.length < NONCE_LENGTH)\n return null;\n const nonce = combined.slice(0, NONCE_LENGTH);\n const ciphertext = combined.slice(NONCE_LENGTH);\n return nacl.box.open.after(ciphertext, nonce, sharedKey);\n}\n/** Encrypt `plaintext` under `sharedKey`, producing the wire frame `base64(nonce ++ ciphertext)`. */\nfunction seal(plaintext, sharedKey) {\n return sealBytes(new TextEncoder().encode(plaintext), sharedKey);\n}\n/** Decrypt a `base64(nonce ++ ciphertext)` wire frame under `sharedKey`, or return `null` on auth failure. */\nfunction open(frame, sharedKey) {\n const opened = openBytes(frame, sharedKey);\n return opened ? new TextDecoder().decode(opened) : null;\n}\n/** Shared app-frame plumbing once both sides agree on `sharedKey` — identical for client/daemon. */\nfunction wireAppMessages(role, attachment, transport, events, getSharedKey, isReady) {\n let closed = false;\n transport.onClose((reason) => {\n closed = true;\n events.onClose?.(reason);\n });\n return {\n handleAppFrame(frame) {\n if (closed)\n return;\n const sharedKey = getSharedKey();\n if (!isReady() || !sharedKey)\n return; // refuse ANY app message before handshake completes\n const plaintext = open(frame.frame, sharedKey);\n if (plaintext === null) {\n events.onAuthError?.(new Error(`relay ${role} channel: message failed authentication`));\n return;\n }\n events.onMessage?.(plaintext);\n },\n handleBinaryFrame(frame) {\n if (closed)\n return;\n const sharedKey = getSharedKey();\n if (!isReady() || !sharedKey)\n return; // refuse ANY app message before handshake completes\n const bytes = openBytes(frame.frame, sharedKey);\n if (bytes === null) {\n events.onAuthError?.(new Error(`relay ${role} channel: binary message failed authentication`));\n return;\n }\n events.onBinaryMessage?.(bytes);\n },\n send(plaintext) {\n if (closed)\n throw new Error(`relay ${role} channel: cannot send after close()`);\n const sharedKey = getSharedKey();\n if (!isReady() || !sharedKey) {\n throw new Error(`relay ${role} channel: cannot send before the E2EE handshake completes`);\n }\n transport.send(JSON.stringify({ type: \"e2ee_app\", frame: seal(plaintext, sharedKey) }));\n },\n sendBinary(bytes) {\n if (closed)\n throw new Error(`relay ${role} channel: cannot send after close()`);\n const sharedKey = getSharedKey();\n if (!isReady() || !sharedKey) {\n throw new Error(`relay ${role} channel: cannot send before the E2EE handshake completes`);\n }\n transport.send(JSON.stringify({ type: \"e2ee_bin\", frame: sealBytes(bytes, sharedKey) }));\n },\n close() {\n if (closed)\n return;\n closed = true;\n transport.close();\n },\n };\n}\n/**\n * Daemon side of an encrypted relay channel. The daemon waits for the client's `e2ee_hello`,\n * derives the shared key from its own persistent secret key + the client's fresh ephemeral public\n * key, replies with `e2ee_ready`, and only then becomes `ready` (refusing all app traffic before\n * that — architecture/relay-e2ee.md § Error Handling).\n */\nexport function createDaemonChannel(opts) {\n const { transport, attachment, daemonKeypair } = opts;\n const events = opts.events ?? {};\n let sharedKey = null;\n let ready = false;\n const plumbing = wireAppMessages(\"daemon\", attachment, transport, events, () => sharedKey, () => ready);\n transport.onMessage((data) => {\n const frame = parseFrame(data);\n if (!frame)\n return; // ignore anything the relay/peer sends that isn't a recognized frame\n if (frame.type === \"e2ee_hello\") {\n // Re-run the handshake on EVERY e2ee_hello rather than ignoring it once `ready`. The relay\n // multiplexes one deterministic session id across however many client sockets attach over\n // the daemon's lifetime (browser reload, second tab, reconnect) — each is a legitimate NEW\n // peer presenting a fresh ephemeral key, not a replay of the first one. Re-deriving simply\n // re-arms the channel for whichever peer is currently attached (\"last hello wins\"); a truly\n // identical replayed frame just re-derives the same shared key, so this is idempotent for\n // an actual replay and correct for a new peer.\n let remotePublicKey;\n try {\n remotePublicKey = decodeBase64(frame.ephemeralPublicKey);\n }\n catch {\n return;\n }\n sharedKey = nacl.box.before(remotePublicKey, daemonKeypair.secretKey);\n ready = true;\n transport.send(JSON.stringify({ type: \"e2ee_ready\" }));\n events.onReady?.();\n return;\n }\n if (frame.type === \"e2ee_app\") {\n plumbing.handleAppFrame(frame);\n return;\n }\n if (frame.type === \"e2ee_bin\") {\n plumbing.handleBinaryFrame(frame);\n }\n // `e2ee_ready` is never sent to a daemon channel; ignore if it arrives.\n });\n return {\n role: \"daemon\",\n attachment,\n get ready() {\n return ready;\n },\n send: plumbing.send,\n sendBinary: plumbing.sendBinary,\n close: plumbing.close,\n };\n}\n/**\n * Client side of an encrypted relay channel. The client already knows the daemon's persistent\n * public key (from the pairing offer), so it generates a FRESH ephemeral keypair, derives the\n * shared key immediately, and sends `e2ee_hello`. It becomes `ready` only after the daemon\n * confirms with `e2ee_ready` — mirroring the daemon's \"no app traffic before handshake\" gating on\n * the client side too, for a genuinely symmetric API.\n */\nexport function createClientChannel(opts) {\n const { transport, attachment, daemonPublicKey } = opts;\n const events = opts.events ?? {};\n const ephemeral = nacl.box.keyPair();\n const sharedKey = nacl.box.before(daemonPublicKey, ephemeral.secretKey);\n let ready = false;\n const plumbing = wireAppMessages(\"client\", attachment, transport, events, () => sharedKey, () => ready);\n transport.onMessage((data) => {\n const frame = parseFrame(data);\n if (!frame)\n return;\n if (frame.type === \"e2ee_ready\") {\n if (ready)\n return;\n ready = true;\n events.onReady?.();\n return;\n }\n if (frame.type === \"e2ee_app\") {\n plumbing.handleAppFrame(frame);\n return;\n }\n if (frame.type === \"e2ee_bin\") {\n plumbing.handleBinaryFrame(frame);\n }\n // `e2ee_hello` is never sent to a client channel; ignore if it arrives.\n });\n transport.send(JSON.stringify({\n type: \"e2ee_hello\",\n ephemeralPublicKey: encodeBase64(ephemeral.publicKey),\n }));\n return {\n role: \"client\",\n attachment,\n get ready() {\n return ready;\n },\n send: plumbing.send,\n sendBinary: plumbing.sendBinary,\n close: plumbing.close,\n };\n}\n//# sourceMappingURL=channel.js.map","/**\n * Deterministic relay session id derivation (architecture/relay-e2ee.md § Pairing).\n *\n * The relay's rendezvous `sessionId` (the `{ type: \"relay_register\", sessionId }` frame) must be\n * something both sides can compute independently from data the pairing link already carries — the\n * daemon's persistent public key — so a pairing link keeps working across relay reconnects\n * (each of which previously minted a fresh random session id, silently invalidating any link\n * printed before the next drop).\n *\n * This derivation adds no new secret: the pairing link already carries the daemon's public key\n * and is documented as \"the trust anchor, treat it like a password\" (relay-e2ee.md § Pairing). A\n * session id computed from that same public value reveals nothing an attacker didn't already have\n * once they held the link. What it changes is durability — the daemon and any pairing-link holder\n * now agree on the same session id forever (until the keypair itself is regenerated), instead of a\n * fresh id being renegotiated implicitly on every reconnect.\n */\nimport nacl from \"tweetnacl\";\nconst SESSION_ID_BYTES = 16; // 32 hex chars — plenty of entropy for a rendezvous label, not a secret.\n/** Derive the relay session id for a daemon identified by `publicKey` (its persistent Curve25519 public key). */\nexport function deriveRelaySessionId(publicKey) {\n const digest = nacl.hash(publicKey); // SHA-512, 64 bytes\n let hex = \"\";\n for (let i = 0; i < SESSION_ID_BYTES; i++) {\n hex += digest[i].toString(16).padStart(2, \"0\");\n }\n return hex;\n}\n//# sourceMappingURL=session-id.js.map","/**\n * Client-side relay E2EE transport (architecture/relay-e2ee.md § Pairing, § Behavior;\n * architecture/client-app-runtime.md § Layered client library — relay transport).\n *\n * `createRelayTransport(...)` implements the same {@link Transport} interface as\n * `createWebSocketTransport` (`./transport.js`), so `DaemonClient` can use either interchangeably —\n * \"relay rides the same API\" (see `index.ts`). Internally it connects to the relay endpoint,\n * registers under `sessionId` (matching the daemon's outbound registration in\n * `packages/server/src/daemon/relay-transport.ts`), generates a FRESH ephemeral Curve25519 keypair,\n * completes the `e2ee_hello`/`e2ee_ready` handshake via `createClientChannel`\n * (`@av-pi-studio/relay`), and only then reports the transport as open — no app RPC (including\n * `hello`) can cross the wire before the handshake finishes.\n *\n * `sendBinary()` works too (terminal I/O, file-transfer chunks) — carried as the channel's\n * `e2ee_bin` sibling wire frame to `e2ee_app` (`@av-pi-studio/relay`'s `channel.ts`). It's still a\n * JSON text frame under the hood (base64-wrapped ciphertext), never a raw binary WS frame, so the\n * relay bridge itself needs no changes — only the channel layer and this transport's `onmessage`\n * dispatch (`onBinaryMessage` → `self.onMessage` with an `ArrayBuffer`, mirroring\n * `createWebSocketTransport`'s binary delivery contract) know binary exists.\n *\n * TODO(verify): the relay's registration frame format matches\n * `packages/server/src/daemon/relay-transport.ts`'s choice (`{ type: \"relay_register\", sessionId\n * }`) — the real protocol is unresolved upstream (architecture/relay-e2ee.md § TODO(verify)).\n */\nimport { createClientChannel, deriveRelaySessionId, type EncryptedChannel } from \"@av-pi-studio/relay\";\n\nimport type { Transport } from \"./transport.js\";\nimport { reasonString, type AnyWebSocket, type WsFactory } from \"./transport.js\";\n\nexport interface RelayTransportOptions {\n /**\n * The relay rendezvous session id. Optional — defaults to\n * `deriveRelaySessionId(daemonPublicKey)`, the SAME deterministic id the daemon's own outbound\n * dial registers under (`packages/server/src/daemon/relay-transport.ts`), so a pairing offer's\n * public key alone is enough to find the daemon on the relay without a separately-transmitted\n * session id.\n */\n sessionId?: string;\n /** The daemon's persistent Curve25519 public key, from the pairing offer — never a fresh key. */\n daemonPublicKey: Uint8Array;\n /** Inject a WebSocket factory (tests). Defaults to the global `WebSocket`. */\n factory?: WsFactory;\n}\n\n/** Build the relay's own `ws://`/`wss://` dial URL from a pairing offer's `relay` info. */\nexport function relayDialUrl(relay: { endpoint: string; useTls: boolean }): string {\n const scheme = relay.useTls ? \"wss\" : \"ws\";\n // Accept `http(s)://`/`ws(s)://`-prefixed endpoints (someone pasted a URL, not a bare host)\n // and normalize to the scheme `useTls` actually calls for — never concatenate two schemes.\n const host = relay.endpoint.replace(/^(?:https?|wss?):\\/\\//, \"\").replace(/\\/+$/, \"\");\n return `${scheme}://${host}`;\n}\n\n/**\n * Create a `Transport` that connects to a relay endpoint and carries app traffic E2EE. `connect(url)`\n * dials `url` (the relay's own WebSocket address, NOT the daemon's — see {@link relayDialUrl}),\n * registers `sessionId` (or its derived default), and resolves only once the daemon's `e2ee_ready`\n * arrives — mirroring `createWebSocketTransport`'s contract of resolving once genuinely usable.\n */\nexport function createRelayTransport(opts: RelayTransportOptions): Transport {\n let ws: AnyWebSocket | null = null;\n let channel: EncryptedChannel | null = null;\n const sessionId = opts.sessionId ?? deriveRelaySessionId(opts.daemonPublicKey);\n\n const self: Transport = {\n onMessage: null,\n onClose: null,\n onError: null,\n get isOpen(): boolean {\n return ws !== null && ws.readyState === 1 && channel !== null && channel.ready;\n },\n connect(url: string): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const create = opts.factory ?? (() => new WebSocket(url) as unknown as AnyWebSocket);\n try {\n ws = opts.factory ? create(url) : (new WebSocket(url) as unknown as AnyWebSocket);\n } catch (err) {\n reject(err);\n return;\n }\n const socket = ws;\n\n const messageHandlers: Array<(data: string) => void> = [];\n const closeHandlers: Array<(reason?: string) => void> = [];\n\n socket.onopen = () => {\n socket.send(JSON.stringify({ type: \"relay_register\", sessionId }));\n channel = createClientChannel({\n transport: {\n send: (data) => socket.send(data),\n onMessage: (h) => messageHandlers.push(h),\n onClose: (h) => closeHandlers.push(h),\n close: () => socket.close(),\n },\n attachment: { sessionId },\n daemonPublicKey: opts.daemonPublicKey,\n events: {\n onReady: () => resolve(),\n onMessage: (plaintext) => self.onMessage?.(plaintext),\n onBinaryMessage: (bytes) =>\n self.onMessage?.(bytes.slice().buffer),\n onAuthError: (err) => self.onError?.(err),\n },\n });\n };\n socket.onerror = (ev) => {\n if (socket.readyState !== 1) reject(ev);\n self.onError?.(ev);\n };\n socket.onclose = (ev) => {\n for (const h of closeHandlers) h(reasonString(ev.reason));\n self.onClose?.(ev.code, reasonString(ev.reason));\n };\n socket.onmessage = (ev) => {\n const { data } = ev;\n if (typeof data === \"string\") {\n for (const h of messageHandlers) h(data);\n }\n // Binary frames never arrive as raw WS binary here — the relay channel's `e2ee_bin`\n // wire frame is itself a JSON text frame (base64-wrapped ciphertext), so all decrypted\n // binary payloads surface via `onBinaryMessage` above, not this raw-binary branch.\n };\n });\n },\n sendText(data: string): void {\n if (!channel || !channel.ready) {\n throw new Error(\"relay transport: cannot send before the E2EE handshake completes\");\n }\n channel.send(data);\n },\n sendBinary(data: Uint8Array): void {\n if (!channel || !channel.ready) {\n throw new Error(\"relay transport: cannot send before the E2EE handshake completes\");\n }\n channel.sendBinary(data);\n },\n close(code = 1000, reason = \"\"): void {\n channel?.close();\n channel = null;\n ws?.close(code, reason);\n ws = null;\n },\n };\n return self;\n}\n","/**\n * Client-side pairing helpers (architecture/relay-e2ee.md § Pairing). Parses the pairing URL\n * produced by `packages/cli/src/pairing.ts#buildPairingUrl` — the daemon's persistent Curve25519\n * public key rides in the URL **fragment** (`#offer=<base64>&host=<...>` or, for a relay-routed\n * daemon, `#offer=<base64>&relay=<endpoint>&relayTls=<0|1>`), which a browser/RN client never\n * sends to the web server hosting the pairing page (`app.pi-studio.sh`). Treat the decoded key\n * like a password (trust anchor) — see relay-e2ee.md § Pairing.\n */\nimport { decodeBase64 } from \"@av-pi-studio/relay\";\n\nexport interface PairingOffer {\n /** The daemon's persistent Curve25519 public key, base64-encoded exactly as carried in `offer=`. */\n publicKeyB64: string;\n /** The same key decoded to raw bytes, ready for `createClientChannel({ daemonPublicKey })`. */\n publicKey: Uint8Array;\n /** Optional direct host hint (`host=`), if the pairing link included one. Mutually exclusive with `relay`. */\n host?: string;\n /** Relay endpoint (`relay=`) + TLS flag (`relayTls=`), if the pairing link routes through a relay. */\n relay?: { endpoint: string; useTls: boolean };\n}\n\n/**\n * Parse a pairing URL's fragment for the `offer` (daemon public key) and either a direct `host`\n * hint or `relay`/`relayTls` relay-routing info. Accepts either a full URL\n * (`https://app.pi-studio.sh/#offer=...`) or a bare fragment (`#offer=...` / `offer=...`). Returns\n * `null` if no `offer` parameter is present — the caller must treat that as \"not a valid pairing\n * link\", never fall back to an unauthenticated connection.\n */\nexport function parsePairingUrl(input: string): PairingOffer | null {\n const hashIndex = input.indexOf(\"#\");\n const fragment = hashIndex === -1 ? input : input.slice(hashIndex + 1);\n const params = new URLSearchParams(fragment);\n const publicKeyB64 = params.get(\"offer\");\n if (!publicKeyB64) return null;\n\n let publicKey: Uint8Array;\n try {\n publicKey = decodeBase64(publicKeyB64);\n } catch {\n return null;\n }\n\n const relayEndpoint = params.get(\"relay\");\n if (relayEndpoint) {\n const relay = { endpoint: relayEndpoint, useTls: params.get(\"relayTls\") === \"1\" };\n return { publicKeyB64, publicKey, relay };\n }\n\n const host = params.get(\"host\") ?? undefined;\n return { publicKeyB64, publicKey, ...(host ? { host } : {}) };\n}\n","import {\n decodeTerminalFrame,\n tryDecodeFileTransferFrame,\n FileTransferOpcode,\n type ClientType,\n type ServerInfoPayload,\n type SessionMessage,\n type TerminalFrame,\n type FileTransferFrame,\n} from \"@av-pi-studio/protocol\";\n\nimport type { Transport } from \"./transport.js\";\nimport { createWebSocketTransport } from \"./transport.js\";\n\n/**\n * Low-level `DaemonClient` WebSocket driver: connect, hello handshake, framing, RPC correlation,\n * ping/pong liveness, and a pluggable transport abstraction.\n *\n * See architecture/client-app-runtime.md § Connection and architecture/websocket-protocol.md\n * § Connection & handshake, § Top-level envelopes.\n *\n * Out of scope here: the `Pi-StudioClient` facade (task-002), relay transport (sprint-013), and the\n * terminal-stream router (task-003) — though this driver exposes the hooks the router rides on.\n */\n\n/** Connection lifecycle states. */\nexport type ConnectionState = \"idle\" | \"connecting\" | \"open\" | \"closing\" | \"closed\";\n\n/** The default wire protocol version this client speaks. */\nexport const PROTOCOL_VERSION = 1;\n\n/** Portable random id — `crypto.randomUUID` where available (browser secure context, RN Hermes\n * 0.74+, Node 16+), else a timestamp+random fallback for insecure/older contexts. */\nexport function randomId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `id-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nexport interface DaemonClientOptions {\n url: string;\n clientId: string;\n clientType: ClientType;\n protocolVersion?: number;\n appVersion?: string;\n capabilities?: Record<string, boolean>;\n /** Inject a transport (tests/relay). Defaults to a direct WebSocket transport. */\n transport?: Transport;\n /** Default RPC timeout (ms). RPC timeouts are operation errors, NOT socket deaths. */\n rpcTimeoutMs?: number;\n /** Optional clock for deterministic tests. */\n now?: () => number;\n}\n\n/** A pending RPC awaiting its correlated response. */\ninterface PendingRpc {\n resolve: (payload: unknown) => void;\n reject: (error: Error) => void;\n timer: ReturnType<typeof setTimeout> | null;\n}\n\n/** A failed RPC surfaced from a correlated `rpc_error`. */\nexport class RpcError extends Error {\n constructor(\n message: string,\n readonly requestId: string,\n readonly code?: string,\n ) {\n super(message);\n this.name = \"RpcError\";\n }\n}\n\n/** A timed-out RPC. Distinguished from a dead socket per the protocol contract. */\nexport class RpcTimeoutError extends Error {\n constructor(readonly requestId: string) {\n super(`RPC ${requestId} timed out`);\n this.name = \"RpcTimeoutError\";\n }\n}\n\nexport type SessionMessageHandler = (message: SessionMessage) => void;\nexport type TerminalFrameHandler = (frame: TerminalFrame) => void;\nexport type FileTransferFrameHandler = (frame: FileTransferFrame) => void;\nexport type StateChangeHandler = (state: ConnectionState) => void;\n\nexport class DaemonClient {\n private readonly transport: Transport;\n private readonly rpcTimeoutMs: number;\n private readonly now: () => number;\n\n private _state: ConnectionState = \"idle\";\n private _serverId: string | null = null;\n private _features: Record<string, unknown> = {};\n private _serverCapabilities: Record<string, unknown> = {};\n\n private readonly pending = new Map<string, PendingRpc>();\n private readonly sessionHandlers = new Set<SessionMessageHandler>();\n private readonly terminalHandlers = new Set<TerminalFrameHandler>();\n private readonly fileTransferHandlers = new Set<FileTransferFrameHandler>();\n private readonly stateHandlers = new Set<StateChangeHandler>();\n\n private helloResolve: ((info: ServerInfoPayload) => void) | null = null;\n private helloReject: ((error: Error) => void) | null = null;\n\n constructor(private readonly options: DaemonClientOptions) {\n this.transport = options.transport ?? createWebSocketTransport();\n this.rpcTimeoutMs = options.rpcTimeoutMs ?? 30_000;\n this.now = options.now ?? (() => Date.now());\n\n this.transport.onMessage = (data) => this.handleIncoming(data);\n this.transport.onClose = (code, reason) => this.handleClose(code, reason);\n this.transport.onError = () => {\n /* surfaced via close / rpc errors; do not treat as fatal on its own */\n };\n }\n\n // ─── Public accessors ─────────────────────────────────────────────────────\n\n get state(): ConnectionState {\n return this._state;\n }\n get serverId(): string | null {\n return this._serverId;\n }\n get features(): Record<string, unknown> {\n return this._features;\n }\n get serverCapabilities(): Record<string, unknown> {\n return this._serverCapabilities;\n }\n /** True iff the daemon advertised the given `server_info.features.*` flag truthy. */\n hasFeature(flag: string): boolean {\n return Boolean(this._features[flag]);\n }\n\n // ─── Lifecycle ────────────────────────────────────────────────────────────\n\n /**\n * Open the socket, send `hello`, and await `status`/`server_info`. Resolves with the\n * recorded server info (serverId + features).\n */\n async connect(): Promise<ServerInfoPayload> {\n this.setState(\"connecting\");\n await this.transport.connect(this.options.url);\n\n const handshake = new Promise<ServerInfoPayload>((resolve, reject) => {\n this.helloResolve = resolve;\n this.helloReject = reject;\n });\n\n this.transport.sendText(\n JSON.stringify({\n type: \"hello\",\n clientId: this.options.clientId,\n clientType: this.options.clientType,\n protocolVersion: this.options.protocolVersion ?? PROTOCOL_VERSION,\n appVersion: this.options.appVersion,\n capabilities: this.options.capabilities,\n }),\n );\n\n const info = await handshake;\n this._serverId = info.serverId;\n this._features = info.features;\n this._serverCapabilities = info.capabilities;\n this.setState(\"open\");\n return info;\n }\n\n /** Close the socket cleanly. */\n close(code = 1000, reason = \"\"): void {\n this.setState(\"closing\");\n this.transport.close(code, reason);\n }\n\n // ─── RPC / send ───────────────────────────────────────────────────────────\n\n /**\n * Send a correlated session RPC. Resolves with the response `payload` when a session message\n * carrying the same `requestId` arrives, rejects with `RpcError` on a correlated `rpc_error`,\n * and rejects with `RpcTimeoutError` (operation error only, socket stays alive) on timeout.\n */\n request<T = unknown>(\n type: string,\n params: Record<string, unknown> = {},\n timeoutMs?: number,\n ): Promise<T> {\n const requestId = (params.requestId as string | undefined) ?? randomId();\n return new Promise<T>((resolve, reject) => {\n const timeout = timeoutMs ?? this.rpcTimeoutMs;\n const timer =\n timeout > 0\n ? setTimeout(() => {\n this.pending.delete(requestId);\n reject(new RpcTimeoutError(requestId));\n }, timeout)\n : null;\n this.pending.set(requestId, {\n resolve: (payload) => resolve(payload as T),\n reject,\n timer,\n });\n this.sendSession({ type, ...params, requestId } as unknown as SessionMessage);\n });\n }\n\n /** Send a fire-and-forget session message (wrapped in a `session` envelope). */\n sendSession(message: SessionMessage): void {\n this.transport.sendText(JSON.stringify({ type: \"session\", message }));\n }\n\n /** Send a raw binary frame (terminal / file-transfer). */\n sendBinary(data: Uint8Array): void {\n this.transport.sendBinary(data);\n }\n\n /**\n * Send a JSON `ping` and await the correlated `pong`. Liveness rides on the data path because\n * browser/RN WebSocket APIs do not expose RFC6455 ping.\n */\n ping(timeoutMs = 10_000): Promise<void> {\n const requestId = randomId();\n return new Promise<void>((resolve, reject) => {\n const timer =\n timeoutMs > 0\n ? setTimeout(() => {\n this.pending.delete(requestId);\n reject(new RpcTimeoutError(requestId));\n }, timeoutMs)\n : null;\n this.pending.set(requestId, {\n resolve: () => resolve(),\n reject,\n timer,\n });\n this.transport.sendText(\n JSON.stringify({ type: \"ping\", requestId, clientSentAt: this.now() }),\n );\n });\n }\n\n // ─── Subscriptions ──────────────────────────────────────────────────────────\n\n /** Subscribe to all inbound session messages (e.g. `agent_stream`, `agent_update`). */\n onSessionMessage(handler: SessionMessageHandler): () => void {\n this.sessionHandlers.add(handler);\n return () => this.sessionHandlers.delete(handler);\n }\n\n /** Subscribe to decoded inbound terminal binary frames. */\n onTerminalFrame(handler: TerminalFrameHandler): () => void {\n this.terminalHandlers.add(handler);\n return () => this.terminalHandlers.delete(handler);\n }\n\n /** Subscribe to decoded inbound file-transfer binary frames (download/upload chunks). */\n onFileTransferFrame(handler: FileTransferFrameHandler): () => void {\n this.fileTransferHandlers.add(handler);\n return () => this.fileTransferHandlers.delete(handler);\n }\n\n /** Subscribe to connection-state transitions. */\n onStateChange(handler: StateChangeHandler): () => void {\n this.stateHandlers.add(handler);\n return () => this.stateHandlers.delete(handler);\n }\n\n // ─── Internals ──────────────────────────────────────────────────────────────\n\n private setState(next: ConnectionState): void {\n if (this._state === next) return;\n this._state = next;\n for (const handler of this.stateHandlers) handler(next);\n }\n\n private handleClose(_code: number, reason: string): void {\n // Reject the in-flight handshake if the socket closed before server_info.\n if (this.helloReject) {\n this.helloReject(new Error(`socket closed during handshake: ${reason}`));\n this.helloResolve = null;\n this.helloReject = null;\n }\n // Reject all pending RPCs — the socket is gone (distinct from an RPC timeout).\n for (const [, pending] of this.pending) {\n if (pending.timer) clearTimeout(pending.timer);\n pending.reject(new Error(`socket closed: ${reason}`));\n }\n this.pending.clear();\n this.setState(\"closed\");\n }\n\n private handleIncoming(data: string | ArrayBuffer | Blob): void {\n if (typeof data === \"string\") {\n this.handleTextFrame(data);\n return;\n }\n if (data instanceof ArrayBuffer) {\n this.handleBinaryFrame(new Uint8Array(data));\n return;\n }\n // Blob (browser) — read asynchronously.\n if (typeof (data as Blob).arrayBuffer === \"function\") {\n void (data as Blob).arrayBuffer().then((buf) => this.handleBinaryFrame(new Uint8Array(buf)));\n }\n }\n\n /**\n * Route an inbound binary frame by its opcode byte: file-transfer opcodes (`0x10`–`0x13`) go to\n * `fileTransferHandlers`, everything else decodes as a terminal frame. Two independent binary\n * protocols share one opcode byte's numeric range without colliding — see\n * `@av-pi-studio/protocol`'s `TerminalOpcode` (`0x01`–`0x05`) vs `FileTransferOpcode`\n * (`0x10`–`0x13`).\n */\n private handleBinaryFrame(bytes: Uint8Array): void {\n const opcodeByte = bytes[0];\n const isFileTransfer =\n opcodeByte !== undefined &&\n Object.values(FileTransferOpcode).includes(opcodeByte as never);\n if (isFileTransfer) {\n const frame = tryDecodeFileTransferFrame(bytes);\n if (!frame) return; // malformed frame — drop rather than throw\n for (const handler of this.fileTransferHandlers) handler(frame);\n return;\n }\n const frame = decodeTerminalFrame(bytes);\n for (const handler of this.terminalHandlers) handler(frame);\n }\n\n private handleTextFrame(raw: string): void {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return; // ignore malformed frames\n }\n const envelope = parsed as Record<string, unknown>;\n switch (envelope.type) {\n case \"status\":\n this.handleStatus(envelope);\n return;\n case \"pong\":\n this.resolvePending(envelope.requestId as string, undefined);\n return;\n case \"ping\":\n // Some daemons send keepalive pings; reply with a pong.\n this.transport.sendText(\n JSON.stringify({\n type: \"pong\",\n requestId: envelope.requestId,\n serverReceivedAt: this.now(),\n serverSentAt: this.now(),\n }),\n );\n return;\n case \"session\":\n this.handleSession(envelope.message as SessionMessage);\n return;\n default:\n return; // unknown top-level envelope — ignore\n }\n }\n\n private handleStatus(envelope: Record<string, unknown>): void {\n const payload = envelope.payload as ServerInfoPayload | undefined;\n if (payload?.status === \"server_info\" && this.helloResolve) {\n this.helloResolve(payload);\n this.helloResolve = null;\n this.helloReject = null;\n }\n }\n\n private handleSession(message: SessionMessage | undefined): void {\n if (!message || typeof message !== \"object\") return;\n const msg = message as unknown as Record<string, unknown>;\n\n // Correlated rpc_error → reject the matching pending RPC.\n if (msg.type === \"rpc_error\" && typeof msg.requestId === \"string\") {\n const pending = this.pending.get(msg.requestId);\n if (pending) {\n if (pending.timer) clearTimeout(pending.timer);\n this.pending.delete(msg.requestId);\n pending.reject(\n new RpcError(\n (msg.message as string) ?? (msg.error as string) ?? \"RPC failed\",\n msg.requestId,\n msg.code as string | undefined,\n ),\n );\n }\n return;\n }\n\n // Correlated response → resolve the matching pending RPC (and still fan out).\n if (typeof msg.requestId === \"string\" && this.pending.has(msg.requestId)) {\n const payload = \"payload\" in msg ? msg.payload : msg;\n this.resolvePending(msg.requestId, payload);\n }\n\n // Fan out to subscribers (streams, updates, uncorrelated broadcasts).\n for (const handler of this.sessionHandlers) handler(message);\n }\n\n private resolvePending(requestId: string, payload: unknown): void {\n const pending = this.pending.get(requestId);\n if (!pending) return;\n if (pending.timer) clearTimeout(pending.timer);\n this.pending.delete(requestId);\n pending.resolve(payload);\n }\n}\n","import type {\n AgentCloneResponse,\n AgentCompactResponse,\n AgentCycleModelResponse,\n AgentExportHtmlResponse,\n AgentForkMessagesResponse,\n AgentForkResponse,\n AgentLastAssistantTextResponse,\n AgentListCommandsResponse,\n AgentNewSessionResponse,\n AgentSessionStatsResponse,\n AgentSetModelResponse,\n AgentSetSessionNameResponse,\n AgentStreamEvent,\n AgentSwitchSessionResponse,\n CreateAgentRequest,\n ExtensionPacksListResponse,\n ExtensionPacksSetResponse,\n FetchAgentTimelineResponse,\n SessionMessage,\n TimelineDirection,\n} from \"@av-pi-studio/protocol\";\nimport type { DaemonClient } from \"./daemon-client.js\";\n\n/**\n * High-level `PiStudioClient` SDK facade over the low-level `DaemonClient` driver.\n *\n * Exposes workspace/agent/provider handles plus update handler registration, mirroring the\n * scope's `Pi-StudioClient` surface (architecture/client-app-runtime.md § Layered client library /\n * Facade and features/agent-sessions.md § Public Contract). Method names follow the server RPC\n * type names (`create_agent_request`, `send_agent_prompt`, `interrupt_agent`, `update_agent`,\n * `resume_agent`, `import_agent_session`, `fetch_agent_timeline_request`, and the sprint-037\n * slash-command RPCs: `agent_session_stats_request`, `agent_compact_request`,\n * `agent_new_session_request`, `agent_switch_session_request`, `agent_fork_request`,\n * `agent_fork_messages_request`, `agent_clone_request`, `agent_set_session_name_request`,\n * `agent_export_html_request`, `agent_set_model_request`, `agent_cycle_model_request`,\n * `agent_last_assistant_text_request`), plus command discovery `agent_list_commands_request`.\n *\n * Out of scope: app runtime controller (sprint-012), terminal router (task-003).\n */\n\n// ─── Update handler types ─────────────────────────────────────────────────────\n\nexport type PiStudioAgentUpdateHandler = (update: AgentUpdateMessage) => void;\nexport type PiStudioWorkspaceUpdateHandler = (update: WorkspaceUpdateMessage) => void;\nexport type PiStudioAgentStreamHandler = (event: AgentStreamEvent) => void;\n\n/** Shape of an inbound `agent_update` session message (append-only; passthrough). */\nexport interface AgentUpdateMessage {\n type: \"agent_update\";\n agentId: string;\n status?: string;\n title?: string;\n labels?: Record<string, string>;\n [key: string]: unknown;\n}\n\n/** Shape of an inbound `workspace_update` session message. */\nexport interface WorkspaceUpdateMessage {\n type: \"workspace_update\";\n workspaceId: string;\n [key: string]: unknown;\n}\n\n// ─── Agent / timeline / workspace / provider handle interfaces ──────────────────\n\nexport interface PiStudioAgentTimelineHandle {\n /** Fetch a bounded page of authoritative timeline history. */\n fetch(opts?: {\n cursor?: string;\n direction?: TimelineDirection;\n limit?: number;\n }): Promise<FetchAgentTimelineResponse>;\n /** Subscribe to live `agent_stream` events for this agent. Returns an unsubscribe fn. */\n subscribe(handler: PiStudioAgentStreamHandler): () => void;\n}\n\nexport interface PiStudioAgentActions {\n readonly agentId: string;\n readonly timeline: PiStudioAgentTimelineHandle;\n /** Send a follow-up prompt to the running/idle agent. */\n send(prompt: string, opts?: { clientMessageId?: string; images?: unknown[] }): Promise<unknown>;\n /** Alias for `send` reflecting the scope's \"run a turn\" verb. */\n run(prompt: string, opts?: { clientMessageId?: string; images?: unknown[] }): Promise<unknown>;\n /** Interrupt the current turn. */\n interrupt(): Promise<unknown>;\n /** Steer the LIVE turn — inject a message delivered after the current assistant turn's tool\n * calls, before the next LLM call. Provider must support steering (Pi does). */\n steer(message: string, opts?: { clientMessageId?: string; images?: unknown[] }): Promise<unknown>;\n /** Queue a follow-up message delivered only after the agent fully stops. */\n followUp(\n message: string,\n opts?: { clientMessageId?: string; images?: unknown[] },\n ): Promise<unknown>;\n /** Update model/mode/thinking/features/title/labels without recreating the session. */\n update(patch: {\n modeId?: string;\n model?: string;\n thinkingOptionId?: string;\n featureValues?: Record<string, unknown>;\n title?: string;\n labels?: Record<string, string>;\n }): Promise<unknown>;\n /** Resume a closed session via its persistence handle. */\n resume(): Promise<unknown>;\n /** Archive (soft-delete) the agent — closes the runtime, keeps the record for resume. */\n archive(): Promise<unknown>;\n /** Permanently delete the agent's persisted record (hard delete — no trace, cannot resume). */\n delete(): Promise<unknown>;\n /** Subscribe to `agent_update` events scoped to this agent. */\n onUpdate(handler: PiStudioAgentUpdateHandler): () => void;\n\n // Slash-command operations (sprint-037): Pi built-ins with a real Pi RPC equivalent. Each\n // resolves to the RPC's `payload` (the daemon unwraps `{type, requestId, payload}` to just\n // `payload` for a correlated response — see DaemonClient.request's resolvePending contract).\n /** `/session` — read-only stats (tokens, cost, context-window usage). */\n sessionStats(): Promise<AgentSessionStatsResponse[\"payload\"]>;\n /** `/compact` — manually compact conversation context. */\n compact(customInstructions?: string): Promise<AgentCompactResponse[\"payload\"]>;\n /** `/new` — start a fresh session in place. */\n newSession(): Promise<AgentNewSessionResponse[\"payload\"]>;\n /** `/resume` — load a different session file in place. */\n switchSession(sessionPath: string): Promise<AgentSwitchSessionResponse[\"payload\"]>;\n /** `/fork` — create a new branch from a previous user message. */\n fork(entryId: string): Promise<AgentForkResponse[\"payload\"]>;\n /** Fork picker — user messages available to fork from. */\n forkMessages(): Promise<AgentForkMessagesResponse[\"payload\"]>;\n /** `/clone` — duplicate the active branch into a new session at the current position. */\n clone(): Promise<AgentCloneResponse[\"payload\"]>;\n /** `/name` — set the session display name. */\n setSessionName(name: string): Promise<AgentSetSessionNameResponse[\"payload\"]>;\n /** `/export` — export the session to an HTML file. */\n exportHtml(outputPath?: string): Promise<AgentExportHtmlResponse[\"payload\"]>;\n /** `/model` (set) — switch to a specific provider model. */\n setModel(provider: string, modelId: string): Promise<AgentSetModelResponse[\"payload\"]>;\n /** `/model` (cycle) — cycle to the next available model. */\n cycleModel(): Promise<AgentCycleModelResponse[\"payload\"]>;\n /** `/copy` — the text content of the last assistant message. */\n lastAssistantText(): Promise<AgentLastAssistantTextResponse[\"payload\"]>;\n\n // Command discovery (sprint-040): user/project-authored commands — extension commands,\n // prompt templates, and skills — surfaced from Pi's `get_commands`. Disjoint from the\n // built-in slash commands above.\n /** List the session's discoverable commands (extensions, prompt templates, skills). */\n listCommands(): Promise<AgentListCommandsResponse[\"payload\"]>;\n}\n\nexport interface PiStudioWorkspaceActions {\n readonly workspaceId: string;\n /** Subscribe to `workspace_update` events scoped to this workspace. */\n onUpdate(handler: PiStudioWorkspaceUpdateHandler): () => void;\n}\n\nexport interface ProviderModel {\n id: string;\n label?: string;\n description?: string;\n /**\n * The model's own underlying LLM provider (e.g. `\"anthropic\"`) — REQUIRED by\n * `AgentHandle.setModel(provider, modelId)`'s `provider` argument. Distinct from the\n * `ListProviderModelsResponse.provider` field above, which is the pi-studio `AgentClient` id\n * (`\"pi\"`/`\"mock\"`) used only to pick which client answered this list.\n */\n provider?: string;\n}\n\nexport interface ListProviderModelsResponse {\n type: \"list_provider_models_response\";\n requestId: string;\n provider: string;\n models: ProviderModel[];\n}\n\nexport interface ResolveDefaultModelResponse {\n type: \"resolve_default_model_response\";\n requestId: string;\n provider: string;\n /** The model a brand-new session with no override would run on — settings' configured default,\n * else the provider's built-in default — or `undefined` if the provider can't resolve one\n * without spawning a session. Display-only: never itself sent back to the daemon as a pick. */\n model?: string;\n /** The resolved model's own underlying LLM provider (e.g. `\"anthropic\"`) — same distinction as\n * `ProviderModel.provider`, required to pin it via `config.modelProvider` if the draft\n * materializes without the user ever changing it. */\n modelProvider?: string;\n}\n\nexport interface PiStudioProviderActions {\n /** List available providers. */\n listProviders(): Promise<unknown>;\n /** List models for a provider. */\n listModels(provider: string): Promise<ListProviderModelsResponse>;\n /** List modes for a provider. */\n listModes(provider: string): Promise<unknown>;\n /** Resolve the model a brand-new session would run on with no override — backs the\n * web-client's \"preselect the default model on a new chat\" before anything is spawned. */\n resolveDefaultModel(provider: string, cwd?: string): Promise<ResolveDefaultModelResponse>;\n /** Trigger an explicit provider snapshot refresh (no hidden revalidation). */\n refreshSnapshot(): Promise<unknown>;\n}\n\n// ─── Facade implementation ──────────────────────────────────────────────────────\n\nexport class PiStudioClient {\n constructor(private readonly daemon: DaemonClient) {}\n\n get connection(): DaemonClient {\n return this.daemon;\n }\n\n /**\n * Create a new agent session. Returns the response payload (includes `agentId`). When\n * `initialPrompt` is set, the daemon runs the first turn and streams events to subscribers.\n */\n async createAgent(\n req: Omit<CreateAgentRequest, \"type\" | \"requestId\"> & { requestId?: string },\n ): Promise<{ agentId: string; [key: string]: unknown }> {\n const payload = await this.daemon.request<{ agentId: string }>(\"create_agent_request\", {\n ...req,\n });\n return payload as { agentId: string };\n }\n\n /** Get an agent handle for an existing agentId. */\n agent(agentId: string): PiStudioAgentActions {\n return new AgentHandle(this.daemon, agentId);\n }\n\n /** Get a workspace handle for an existing workspaceId. */\n workspace(workspaceId: string): PiStudioWorkspaceActions {\n return new WorkspaceHandle(this.daemon, workspaceId);\n }\n\n /** Provider actions (list providers/models/modes, refresh snapshot). */\n get providers(): PiStudioProviderActions {\n return new ProviderHandle(this.daemon);\n }\n\n listExtensionPacks(): Promise<ExtensionPacksListResponse> {\n return this.daemon.request<ExtensionPacksListResponse>(\"extension_packs_list_request\", {});\n }\n\n /**\n * Change the selection and sync. Resolves only after the daemon's sync completes — pass a\n * generous opts.timeoutMs for a first-run install, which can exceed the client's default\n * rpcTimeoutMs.\n */\n setExtensionPacks(\n packs: string[],\n opts?: { timeoutMs?: number },\n ): Promise<ExtensionPacksSetResponse> {\n return this.daemon.request<ExtensionPacksSetResponse>(\n \"extension_packs_set_request\",\n { packs },\n opts?.timeoutMs,\n );\n }\n\n /**\n * Sync now without changing the selection (sends the request with no `packs` key at all — the\n * ungated manual-sync path).\n */\n syncExtensionPacks(opts?: { timeoutMs?: number }): Promise<ExtensionPacksSetResponse> {\n return this.daemon.request<ExtensionPacksSetResponse>(\n \"extension_packs_set_request\",\n {},\n opts?.timeoutMs,\n );\n }\n\n /** Subscribe to ALL `agent_update` events (any agent). */\n onAgentUpdate(handler: PiStudioAgentUpdateHandler): () => void {\n return this.daemon.onSessionMessage((msg) => {\n if ((msg as { type?: string }).type === \"agent_update\") {\n handler(msg as unknown as AgentUpdateMessage);\n }\n });\n }\n\n /** Subscribe to ALL `workspace_update` events (any workspace). */\n onWorkspaceUpdate(handler: PiStudioWorkspaceUpdateHandler): () => void {\n return this.daemon.onSessionMessage((msg) => {\n if ((msg as { type?: string }).type === \"workspace_update\") {\n handler(msg as unknown as WorkspaceUpdateMessage);\n }\n });\n }\n}\n\n// ─── Handle classes ──────────────────────────────────────────────────────────────\n\nclass TimelineHandle implements PiStudioAgentTimelineHandle {\n constructor(\n private readonly daemon: DaemonClient,\n private readonly agentId: string,\n ) {}\n\n fetch(\n opts: { cursor?: string; direction?: TimelineDirection; limit?: number } = {},\n ): Promise<FetchAgentTimelineResponse> {\n return this.daemon.request<FetchAgentTimelineResponse>(\"fetch_agent_timeline_request\", {\n agentId: this.agentId,\n cursor: opts.cursor,\n direction: opts.direction,\n limit: opts.limit,\n });\n }\n\n subscribe(handler: PiStudioAgentStreamHandler): () => void {\n return this.daemon.onSessionMessage((msg) => {\n const m = msg as unknown as { type?: string; agentId?: string; event?: AgentStreamEvent };\n if (m.type === \"agent_stream\" && m.agentId === this.agentId && m.event) {\n handler(m.event);\n }\n });\n }\n}\n\nclass AgentHandle implements PiStudioAgentActions {\n readonly timeline: PiStudioAgentTimelineHandle;\n\n constructor(\n private readonly daemon: DaemonClient,\n readonly agentId: string,\n ) {\n this.timeline = new TimelineHandle(daemon, agentId);\n }\n\n send(\n prompt: string,\n opts: { clientMessageId?: string; images?: unknown[] } = {},\n ): Promise<unknown> {\n return this.daemon.request(\"send_agent_prompt\", {\n agentId: this.agentId,\n prompt,\n clientMessageId: opts.clientMessageId,\n images: opts.images,\n });\n }\n\n run(prompt: string, opts?: { clientMessageId?: string; images?: unknown[] }): Promise<unknown> {\n return this.send(prompt, opts);\n }\n\n interrupt(): Promise<unknown> {\n return this.daemon.request(\"interrupt_agent\", { agentId: this.agentId });\n }\n\n steer(\n message: string,\n opts: { clientMessageId?: string; images?: unknown[] } = {},\n ): Promise<unknown> {\n return this.daemon.request(\"steer_agent_request\", {\n agentId: this.agentId,\n message,\n clientMessageId: opts.clientMessageId,\n images: opts.images,\n });\n }\n\n followUp(\n message: string,\n opts: { clientMessageId?: string; images?: unknown[] } = {},\n ): Promise<unknown> {\n return this.daemon.request(\"follow_up_agent_request\", {\n agentId: this.agentId,\n message,\n clientMessageId: opts.clientMessageId,\n images: opts.images,\n });\n }\n\n update(patch: {\n modeId?: string;\n model?: string;\n thinkingOptionId?: string;\n featureValues?: Record<string, unknown>;\n title?: string;\n labels?: Record<string, string>;\n }): Promise<unknown> {\n return this.daemon.request(\"update_agent\", { agentId: this.agentId, ...patch });\n }\n\n resume(): Promise<unknown> {\n return this.daemon.request(\"resume_agent\", { agentId: this.agentId });\n }\n\n archive(): Promise<unknown> {\n return this.daemon.request(\"archive_agent\", { agentId: this.agentId });\n }\n\n delete(): Promise<unknown> {\n return this.daemon.request(\"delete_agent\", { agentId: this.agentId });\n }\n\n onUpdate(handler: PiStudioAgentUpdateHandler): () => void {\n return this.daemon.onSessionMessage((msg) => {\n const m = msg as unknown as AgentUpdateMessage;\n if (m.type === \"agent_update\" && m.agentId === this.agentId) handler(m);\n });\n }\n\n // Slash-command operations (sprint-037).\n\n sessionStats(): Promise<AgentSessionStatsResponse[\"payload\"]> {\n return this.daemon.request(\"agent_session_stats_request\", { agentId: this.agentId });\n }\n\n compact(customInstructions?: string): Promise<AgentCompactResponse[\"payload\"]> {\n return this.daemon.request(\"agent_compact_request\", {\n agentId: this.agentId,\n customInstructions,\n });\n }\n\n newSession(): Promise<AgentNewSessionResponse[\"payload\"]> {\n return this.daemon.request(\"agent_new_session_request\", { agentId: this.agentId });\n }\n\n switchSession(sessionPath: string): Promise<AgentSwitchSessionResponse[\"payload\"]> {\n return this.daemon.request(\"agent_switch_session_request\", {\n agentId: this.agentId,\n sessionPath,\n });\n }\n\n fork(entryId: string): Promise<AgentForkResponse[\"payload\"]> {\n return this.daemon.request(\"agent_fork_request\", { agentId: this.agentId, entryId });\n }\n\n forkMessages(): Promise<AgentForkMessagesResponse[\"payload\"]> {\n return this.daemon.request(\"agent_fork_messages_request\", { agentId: this.agentId });\n }\n\n clone(): Promise<AgentCloneResponse[\"payload\"]> {\n return this.daemon.request(\"agent_clone_request\", { agentId: this.agentId });\n }\n\n setSessionName(name: string): Promise<AgentSetSessionNameResponse[\"payload\"]> {\n return this.daemon.request(\"agent_set_session_name_request\", {\n agentId: this.agentId,\n name,\n });\n }\n\n exportHtml(outputPath?: string): Promise<AgentExportHtmlResponse[\"payload\"]> {\n return this.daemon.request(\"agent_export_html_request\", {\n agentId: this.agentId,\n outputPath,\n });\n }\n\n setModel(provider: string, modelId: string): Promise<AgentSetModelResponse[\"payload\"]> {\n return this.daemon.request(\"agent_set_model_request\", {\n agentId: this.agentId,\n provider,\n modelId,\n });\n }\n\n cycleModel(): Promise<AgentCycleModelResponse[\"payload\"]> {\n return this.daemon.request(\"agent_cycle_model_request\", { agentId: this.agentId });\n }\n\n lastAssistantText(): Promise<AgentLastAssistantTextResponse[\"payload\"]> {\n return this.daemon.request(\"agent_last_assistant_text_request\", { agentId: this.agentId });\n }\n\n /** Command discovery (sprint-040) — Pi `get_commands` via the daemon. */\n listCommands(): Promise<AgentListCommandsResponse[\"payload\"]> {\n return this.daemon.request(\"agent_list_commands_request\", { agentId: this.agentId });\n }\n}\n\nclass WorkspaceHandle implements PiStudioWorkspaceActions {\n constructor(\n private readonly daemon: DaemonClient,\n readonly workspaceId: string,\n ) {}\n\n onUpdate(handler: PiStudioWorkspaceUpdateHandler): () => void {\n return this.daemon.onSessionMessage((msg) => {\n const m = msg as unknown as WorkspaceUpdateMessage;\n if (m.type === \"workspace_update\" && m.workspaceId === this.workspaceId) handler(m);\n });\n }\n}\n\nclass ProviderHandle implements PiStudioProviderActions {\n constructor(private readonly daemon: DaemonClient) {}\n\n listProviders(): Promise<unknown> {\n return this.daemon.request(\"list_providers\", {});\n }\n listModels(provider: string): Promise<ListProviderModelsResponse> {\n return this.daemon.request<ListProviderModelsResponse>(\"list_provider_models\", { provider });\n }\n listModes(provider: string): Promise<unknown> {\n return this.daemon.request(\"list_provider_modes\", { provider });\n }\n resolveDefaultModel(provider: string, cwd?: string): Promise<ResolveDefaultModelResponse> {\n return this.daemon.request<ResolveDefaultModelResponse>(\"resolve_default_model\", {\n provider,\n ...(cwd ? { cwd } : {}),\n });\n }\n refreshSnapshot(): Promise<unknown> {\n // Dotted-namespace RPC per the protocol convention.\n return this.daemon.request(\"providers.snapshot.refresh.request\", {});\n }\n}\n\n/** Convenience re-export of the import operation (not agent-scoped). */\nexport function importAgentSession(\n daemon: DaemonClient,\n args: { provider: string; cwd: string; providerHandleId: string },\n): Promise<unknown> {\n return daemon.request(\"import_agent_session\", args);\n}\n\nexport type { SessionMessage };\n","import {\n encodeFileTransferFrame,\n type FileTransferFrame,\n nextFreeSlot,\n SLOT_SPACE,\n} from \"@av-pi-studio/protocol\";\n\nimport { randomId, type DaemonClient } from \"./daemon-client.js\";\n\n/**\n * Client-side demux + assembly for binary file downloads plus chunked uploads\n * (features/file-explorer-transfer.md § Binary transfer frames). Mirrors `TerminalStreamRouter`'s\n * inbound-frame-routing shape, but a download is a one-shot request/response rather than a\n * persistent per-slot subscription: request a token, request the download, await the assembled\n * bytes. An upload is the inverse: request an upload stream, then push `Begin → Chunk* → End`\n * binary frames the server writes to the target path.\n */\n\n/** Matches the server's default chunk size (`FileTransferService` DEFAULT_CHUNK_BYTES). */\nconst UPLOAD_CHUNK_BYTES = 32 * 1024;\n\n/** The daemon's `startDownload` rejection when a token is unknown, spent, or past its TTL. */\nconst EXPIRED_TOKEN = \"invalid_or_expired_token\";\n\nexport interface DownloadedFile {\n bytes: Uint8Array;\n fileName?: string;\n mimeType?: string;\n}\n\ninterface PendingDownload {\n chunks: Uint8Array[];\n meta: { fileName?: string; mimeType?: string };\n resolve: (file: DownloadedFile) => void;\n reject: (error: Error) => void;\n}\n\ninterface TokenResponse {\n ok: boolean;\n token?: string;\n error?: string;\n}\n\ninterface DownloadRequestResponse {\n ok: boolean;\n error?: string;\n}\n\ninterface UploadRequestResponse {\n ok: boolean;\n stream?: number;\n error?: string;\n}\n\nfunction concat(chunks: Uint8Array[]): Uint8Array {\n const total = chunks.reduce((sum, c) => sum + c.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.length;\n }\n return out;\n}\n\nexport class FileTransferClient {\n private readonly pending = new Map<number, PendingDownload>();\n /** Rotating hand-out point in the one-byte stream id space — ids are a pool, not a counter. */\n private streamCursor = 1;\n private detach: (() => void) | null = null;\n\n constructor(private readonly daemon: DaemonClient) {}\n\n /** Begin routing inbound file-transfer frames. Idempotent. */\n start(): void {\n if (this.detach) return;\n this.detach = this.daemon.onFileTransferFrame((frame) => this.dispatch(frame));\n }\n\n /** Stop routing inbound frames. Any downloads still pending are rejected. */\n stop(): void {\n this.detach?.();\n this.detach = null;\n for (const [stream, pending] of this.pending) {\n pending.reject(new Error(\"file transfer router stopped\"));\n this.pending.delete(stream);\n }\n }\n\n /**\n * Download the file at `path`: requests a single-use token, then requests the chunked\n * transfer, assembling `Begin → Chunk* → End` frames into one buffer.\n *\n * Retries exactly once on an expired token. The daemon's TTL runs from the moment it ISSUES a\n * token, but the token only becomes usable once its response reaches us — queued behind every\n * `Chunk` frame already in flight on this one socket. A large transfer ahead of it can hand us\n * a token that is dead on arrival (relay makes this routine: base64-inflated frames, extra\n * hop). By the time we see the rejection that backlog has drained, so a second attempt lands on\n * a quiet socket; looping further would only spin on a genuinely broken link.\n */\n async download(path: string): Promise<DownloadedFile> {\n try {\n return await this.attemptDownload(path);\n } catch (error) {\n if (!(error instanceof Error) || error.message !== EXPIRED_TOKEN) throw error;\n return await this.attemptDownload(path);\n }\n }\n\n private async attemptDownload(path: string): Promise<DownloadedFile> {\n const tokenResponse = await this.daemon.request<TokenResponse>(\"file_download_token_request\", {\n path,\n });\n if (!tokenResponse.ok || !tokenResponse.token) {\n throw new Error(tokenResponse.error ?? \"failed to issue download token\");\n }\n\n // Stream ids live in a single frame-header byte, so they are recycled as transfers finish;\n // an ever-incrementing counter would emit 256 and be rejected by the codec from then on.\n const stream = nextFreeSlot(this.pending, this.streamCursor);\n if (stream === null) throw new Error(\"no free download stream\");\n this.streamCursor = (stream + 1) % SLOT_SPACE;\n\n const result = new Promise<DownloadedFile>((resolve, reject) => {\n this.pending.set(stream, { chunks: [], meta: {}, resolve, reject });\n });\n\n let requestResponse: DownloadRequestResponse;\n try {\n requestResponse = await this.daemon.request<DownloadRequestResponse>(\n \"file_download_request\",\n { token: tokenResponse.token, stream },\n );\n } catch (error) {\n // A timed-out or socket-killed request never streams `End`, so release the id here or it\n // stays claimed for the life of the connection and eats the pool.\n this.pending.delete(stream);\n throw error;\n }\n if (!requestResponse.ok) {\n this.pending.delete(stream);\n throw new Error(requestResponse.error ?? \"download request failed\");\n }\n\n return result;\n }\n\n /**\n * Upload `bytes` to `path` on the daemon: requests an upload stream, then pushes\n * `Begin → Chunk* → End` binary frames the server writes to disk (creating parent dirs,\n * overwriting any existing file). The server keys the transfer by `transferId`, which the\n * `Begin` frame must echo. Resolves once every frame is flushed to the socket.\n */\n async upload(path: string, bytes: Uint8Array): Promise<void> {\n const transferId = randomId();\n const response = await this.daemon.request<UploadRequestResponse>(\"file_upload_request\", {\n path,\n transferId,\n });\n if (!response.ok || response.stream === undefined) {\n throw new Error(response.error ?? \"upload request failed\");\n }\n const stream = response.stream;\n\n this.daemon.sendBinary(\n encodeFileTransferFrame({ opcode: \"Begin\", stream, meta: { transferId } }),\n );\n for (let offset = 0; offset < bytes.length; offset += UPLOAD_CHUNK_BYTES) {\n const data = bytes.subarray(offset, offset + UPLOAD_CHUNK_BYTES);\n this.daemon.sendBinary(encodeFileTransferFrame({ opcode: \"Chunk\", stream, data }));\n }\n this.daemon.sendBinary(encodeFileTransferFrame({ opcode: \"End\", stream, ok: true }));\n }\n\n private dispatch(frame: FileTransferFrame): void {\n const pending = this.pending.get(frame.stream);\n if (!pending) return; // not a download stream we're tracking (e.g. an upload) — ignore\n switch (frame.opcode) {\n case \"Begin\":\n pending.meta.fileName = frame.meta.fileName;\n pending.meta.mimeType = frame.meta.mimeType;\n return;\n case \"Chunk\":\n pending.chunks.push(frame.data);\n return;\n case \"End\":\n this.pending.delete(frame.stream);\n if (frame.ok) {\n pending.resolve({ bytes: concat(pending.chunks), ...pending.meta });\n } else {\n pending.reject(new Error(\"download failed\"));\n }\n return;\n case \"Error\":\n this.pending.delete(frame.stream);\n pending.reject(new Error(frame.message));\n return;\n }\n }\n}\n","import type { DaemonClient } from \"./daemon-client.js\";\n\n/**\n * Reconnection + capability-rehydrate driver (architecture/client-app-runtime.md § Connection,\n * § Error Handling; architecture/websocket-protocol.md § Behavior / on reconnect).\n *\n * On socket drop, backoff-reconnect, re-`hello` (the `DaemonClient.connect()` resends the same\n * capabilities), and rehydrate the recorded `serverId`/`features`. Timeline resume planning lives\n * in sprint-012; this driver exposes the hooks (`onReconnected`) the planner rides on.\n *\n * Reconnection backoff parameters are TODO(verify) — defaults below are conservative.\n */\n\nexport interface ReconnectionOptions {\n /** Initial backoff delay (ms). Default 500. */\n initialDelayMs?: number;\n /** Maximum backoff delay (ms). Default 30_000. */\n maxDelayMs?: number;\n /** Exponential growth factor. Default 2. */\n factor?: number;\n /** Random jitter ratio [0..1] applied to each delay. Default 0.2. */\n jitter?: number;\n /** Max reconnect attempts before giving up (Infinity = never). Default Infinity. */\n maxAttempts?: number;\n /** Injected timer scheduler (tests). Defaults to setTimeout. */\n setTimer?: (cb: () => void, ms: number) => unknown;\n clearTimer?: (handle: unknown) => void;\n /** Injected RNG in [0,1) (tests). Defaults to Math.random. */\n random?: () => number;\n}\n\nexport type ReconnectedHandler = (info: { attempt: number; serverId: string | null }) => void;\nexport type ReconnectFailedHandler = (error: unknown, attempt: number) => void;\n\nexport class ReconnectionManager {\n private readonly initialDelayMs: number;\n private readonly maxDelayMs: number;\n private readonly factor: number;\n private readonly jitter: number;\n private readonly maxAttempts: number;\n private readonly setTimer: (cb: () => void, ms: number) => unknown;\n private readonly clearTimer: (handle: unknown) => void;\n private readonly random: () => number;\n\n private attempt = 0;\n private timer: unknown = null;\n private active = false;\n private reconnecting = false;\n private detachState: (() => void) | null = null;\n\n private readonly reconnectedHandlers = new Set<ReconnectedHandler>();\n private readonly failedHandlers = new Set<ReconnectFailedHandler>();\n\n constructor(\n private readonly daemon: DaemonClient,\n options: ReconnectionOptions = {},\n ) {\n this.initialDelayMs = options.initialDelayMs ?? 500;\n this.maxDelayMs = options.maxDelayMs ?? 30_000;\n this.factor = options.factor ?? 2;\n this.jitter = options.jitter ?? 0.2;\n this.maxAttempts = options.maxAttempts ?? Number.POSITIVE_INFINITY;\n this.setTimer = options.setTimer ?? ((cb, ms) => setTimeout(cb, ms));\n this.clearTimer =\n options.clearTimer ?? ((h) => clearTimeout(h as ReturnType<typeof setTimeout>));\n this.random = options.random ?? Math.random;\n }\n\n /** Compute the backoff delay for a given attempt number (1-based), with jitter. */\n delayForAttempt(attempt: number): number {\n const base = Math.min(this.maxDelayMs, this.initialDelayMs * this.factor ** (attempt - 1));\n const jitterAmount = base * this.jitter * (this.random() * 2 - 1);\n return Math.max(0, Math.round(base + jitterAmount));\n }\n\n /** Subscribe to successful reconnections (after re-handshake + rehydrate). */\n onReconnected(handler: ReconnectedHandler): () => void {\n this.reconnectedHandlers.add(handler);\n return () => this.reconnectedHandlers.delete(handler);\n }\n\n /** Subscribe to reconnect attempt failures. */\n onReconnectFailed(handler: ReconnectFailedHandler): () => void {\n this.failedHandlers.add(handler);\n return () => this.failedHandlers.delete(handler);\n }\n\n /**\n * Start watching the daemon connection. On a `closed` transition, schedule a backoff reconnect.\n */\n start(): void {\n if (this.active) return;\n this.active = true;\n this.detachState = this.daemon.onStateChange((state) => {\n if (state === \"closed\" && this.active) this.scheduleReconnect();\n if (state === \"open\") this.attempt = 0; // reset backoff on a healthy connection\n });\n }\n\n /** Stop watching and cancel any pending reconnect. */\n stop(): void {\n this.active = false;\n if (this.timer) this.clearTimer(this.timer);\n this.timer = null;\n this.detachState?.();\n this.detachState = null;\n }\n\n /**\n * Cancel any pending backoff timer and attempt a reconnect immediately, resetting the backoff\n * ladder. No-op unless the manager is active and the daemon is `closed`; no-op while an attempt\n * is already in flight.\n *\n * `attempt: 0` is the forced-reconnect signal: a successful forced reconnect notifies\n * `onReconnected` with `{ attempt: 0, serverId }`, distinguishing it from ladder attempts\n * (always ≥ 1). A forced attempt that fails still falls back to `scheduleReconnect()`'s normal\n * failure path, which restarts the ladder at rung 1 — the correct aggression level for a user\n * who just triggered this via a fresh external signal (tab visible, network online).\n */\n reconnectNow(): void {\n if (!this.active) return;\n if (this.reconnecting) return;\n if (this.daemon.state !== \"closed\") return;\n if (this.timer) {\n this.clearTimer(this.timer);\n this.timer = null;\n }\n this.attempt = 0;\n void this.tryReconnect();\n }\n\n /** Number of reconnect attempts made since the last healthy open. */\n get attemptCount(): number {\n return this.attempt;\n }\n\n private scheduleReconnect(): void {\n if (!this.active) return;\n if (this.timer) return; // already armed — see reconnectNow()'s docstring for why this must be unique\n if (this.attempt >= this.maxAttempts) return;\n this.attempt += 1;\n const delay = this.delayForAttempt(this.attempt);\n this.timer = this.setTimer(() => {\n this.timer = null;\n if (this.reconnecting) return;\n void this.tryReconnect();\n }, delay);\n }\n\n private async tryReconnect(): Promise<void> {\n if (!this.active) return;\n if (this.reconnecting) return;\n this.reconnecting = true;\n const attempt = this.attempt;\n try {\n // connect() re-sends hello with the same capabilities → daemon rehydrates them, and the\n // client re-records serverId + features from the fresh server_info.\n await this.daemon.connect();\n for (const handler of this.reconnectedHandlers) {\n handler({ attempt, serverId: this.daemon.serverId });\n }\n } catch (error) {\n for (const handler of this.failedHandlers) handler(error, attempt);\n this.scheduleReconnect(); // back off and retry\n } finally {\n this.reconnecting = false;\n }\n }\n}\n","/**\n * Normalize a user-typed daemon address into the `ws://` / `wss://` URL that\n * `DaemonClient` (via the browser `WebSocket`) actually opens.\n *\n * The daemon is a single HTTP server that upgrades to WebSocket on the same port, so users may\n * type whichever scheme they're familiar with:\n * - `http://host[:port]` → `ws://host[:port]`\n * - `https://host[:port]` → `wss://host[:port]`\n * - `ws://` / `wss://` → used as-is\n * - bare `host[:port]` → `ws://host[:port]`\n *\n * `new WebSocket(\"http://…\")` throws in browsers, so this MUST run before the URL reaches the\n * transport. Trailing slashes are trimmed; the input is otherwise left intact (path, query).\n */\nexport function normalizeDaemonUrl(input: string): string {\n const raw = input.trim();\n if (raw === \"\") return raw;\n\n const schemeMatch = raw.match(/^([a-z][a-z0-9+.-]*):\\/\\//i);\n if (!schemeMatch) {\n // Bare host or host:port.\n return `ws://${raw.replace(/\\/+$/, \"\")}`;\n }\n\n const scheme = (schemeMatch[1] as string).toLowerCase();\n const rest = raw.slice(schemeMatch[0].length).replace(/\\/+$/, \"\");\n\n switch (scheme) {\n case \"http\":\n return `ws://${rest}`;\n case \"https\":\n return `wss://${rest}`;\n case \"ws\":\n case \"wss\":\n return `${scheme}://${rest}`;\n default:\n // Unknown scheme — leave it to the transport to reject with a clear error.\n return raw;\n }\n}\n","/**\n * Pure decision logic for `connect()`: given whatever the user typed or pasted into the host\n * field, decide whether this is a direct daemon address or a pairing link (optionally\n * relay-routed) — see `pi-studio daemon pair` / architecture/relay-e2ee.md § Pairing.\n *\n * Kept separate from `connection-store.ts` (which has the side-effecting `DaemonClient`/transport\n * construction) so the routing decision itself is unit-testable without a live socket.\n */\nimport { parsePairingUrl, relayDialUrl } from \"@av-pi-studio/client\";\nimport { normalizeDaemonUrl } from \"./normalize-url.js\";\n\nexport type ConnectTarget =\n | { mode: \"direct\"; url: string }\n | { mode: \"relay\"; url: string; daemonPublicKey: Uint8Array };\n\n/**\n * Resolve what to connect to and how. `input` may be a plain daemon address (`ws://`, `http://`,\n * bare `host:port`) or a full pairing link. A pairing link with a `relay` offer routes to\n * `{ mode: \"relay\", ... }` (the relay's own dial URL, not the daemon's); a pairing link with a\n * `host` hint routes to `{ mode: \"direct\", url: <that host> }`; anything else is treated as a\n * plain daemon address, normalized via {@link normalizeDaemonUrl}.\n */\nexport function resolveConnectTarget(input: string): ConnectTarget {\n const offer = parsePairingUrl(input);\n if (offer?.relay) {\n return { mode: \"relay\", url: relayDialUrl(offer.relay), daemonPublicKey: offer.publicKey };\n }\n if (offer?.host) {\n return { mode: \"direct\", url: normalizeDaemonUrl(offer.host) };\n }\n return { mode: \"direct\", url: normalizeDaemonUrl(input) };\n}\n","/**\n * `setTimeout`/`clearTimeout` equivalents backed by a dedicated Web Worker\n * (swe/features/connection-resilience.md § Purpose item 1).\n *\n * Dedicated-worker timers are exempt from a hidden tab's page-visibility throttling (Chrome\n * intensive throttling clamps chained main-thread `setTimeout` chains to ~1 fire/minute after\n * ~5 minutes hidden — see the spec for the user-facing symptom this fixes). The worker is built\n * from an inline `Blob` URL: no separate asset file, no bundler/Vite worker-plugin config, nothing\n * for the build to resolve.\n *\n * Falls back to plain `setTimeout`/`clearTimeout` if `Worker`/`Blob`/`URL.createObjectURL` is\n * unavailable or Worker construction throws (e.g. a CSP without `worker-src blob:` — see\n * `docker/web-client.nginx.conf.template`, which today sets no CSP at all, so this is a\n * forward-looking safety net rather than an active constraint). The fallback decision is latched\n * on first use: a throwing Worker is never retried on a later `setTimer` call. Behavior in the\n * fallback path is identical to not having this module at all — the feature degrades, never\n * regresses.\n */\n\ninterface TimerBackend {\n setTimer: (cb: () => void, ms: number) => unknown;\n clearTimer: (handle: unknown) => void;\n}\n\nconst WORKER_SOURCE = `\n const pending = new Set();\n self.onmessage = (e) => {\n const msg = e.data;\n if (msg.cancel) {\n pending.delete(msg.id);\n return;\n }\n pending.add(msg.id);\n setTimeout(() => {\n if (!pending.has(msg.id)) return; // cancelled before it fired\n pending.delete(msg.id);\n self.postMessage({ id: msg.id });\n }, msg.ms);\n };\n`;\n\nfunction createFallbackBackend(): TimerBackend {\n return {\n setTimer: (cb, ms) => setTimeout(cb, ms),\n clearTimer: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),\n };\n}\n\nfunction createWorkerBackend(): TimerBackend {\n const blob = new Blob([WORKER_SOURCE], { type: \"application/javascript\" });\n const url = URL.createObjectURL(blob);\n const worker = new Worker(url);\n URL.revokeObjectURL(url); // the Worker already holds its own reference to the blob's bytes\n\n const callbacks = new Map<number, () => void>();\n let nextId = 1;\n\n worker.addEventListener(\"message\", (e: MessageEvent<{ id: number }>) => {\n const cb = callbacks.get(e.data.id);\n callbacks.delete(e.data.id);\n cb?.();\n });\n\n return {\n setTimer: (cb, ms) => {\n const id = nextId++;\n callbacks.set(id, cb);\n worker.postMessage({ id, ms });\n return id;\n },\n clearTimer: (handle) => {\n const id = handle as number;\n if (!callbacks.delete(id)) return; // already fired or never armed — nothing to cancel\n worker.postMessage({ id, cancel: true });\n },\n };\n}\n\nlet backend: TimerBackend | null = null;\n\nfunction getBackend(): TimerBackend {\n if (backend) return backend;\n try {\n backend = createWorkerBackend();\n } catch {\n // Worker construction threw (e.g. CSP without `worker-src blob:`, or no Worker/Blob support\n // at all) — latch the plain-timer fallback permanently rather than retrying per call.\n backend = createFallbackBackend();\n }\n return backend;\n}\n\n/**\n * Returns `setTimer`/`clearTimer` backed by a lazily-created, module-level shared Worker (created\n * on first `setTimer`, not at import time — importing this module has no side effects).\n */\nexport function createWorkerTimers(): TimerBackend {\n return {\n setTimer: (cb, ms) => getBackend().setTimer(cb, ms),\n clearTimer: (handle) => getBackend().clearTimer(handle),\n };\n}\n","/**\n * Ref-counted, LRU-bounded object-URL cache for inline chat images (features/inline-image-\n * rendering.md § Inline image fetch + cache). Framework-free and module-scoped so it survives\n * virtualized row unmount/remount: `useInlineImage` (`hooks/use-inline-image.ts`) is the only\n * consumer, but this file has no React import so it is testable without one.\n *\n * Deliberately NOT `useFileDownload`'s retention policy (`staleTime: Infinity, gcTime: 0`,\n * revoke-on-unmount) — that model is correct for a file tab (one exclusive viewer at a time) and\n * wrong here: the timeline virtualizes rows, so an image scrolled out of view and back would\n * revoke its URL and re-download on every scroll past. This cache instead revokes an object URL\n * only on LRU eviction (bounded at `MAX_INLINE_IMAGE_ENTRIES`) or on `clearInlineImageCache()`\n * (connection teardown) — never on a mount's own unmount. Do not \"fix\" this into a `useQuery`;\n * TanStack Query's cache has no way to express \"revoke only on LRU eviction, not on unmount\".\n */\n\nexport interface InlineImageEntry {\n objectUrl: string;\n mimeType?: string;\n}\n\n/** Bounded LRU size, exported so tests can assert against it directly. */\nexport const MAX_INLINE_IMAGE_ENTRIES = 32;\n\ninterface CacheRecord {\n refs: number;\n /** Set once the download settles successfully; `null` while in flight or after a failure. */\n entry: InlineImageEntry | null;\n /** The in-flight fetch, shared by every concurrent acquirer of the same path. */\n pending: Promise<InlineImageEntry> | null;\n}\n\n/** Keyed by absolute resolved path. Map insertion order doubles as LRU order — every acquire\n * re-inserts its key to mark it most-recently-used. */\nconst cache = new Map<string, CacheRecord>();\n\n/** Evicts the least-recently-used entry with `refs === 0`, if any, until the cache is back at or\n * under the bound. An entry still referenced by a mounted consumer is never evicted — the cache\n * is allowed to exceed the bound rather than revoke a URL something is still displaying. */\nfunction evictOverBound(): void {\n while (cache.size > MAX_INLINE_IMAGE_ENTRIES) {\n let evictedPath: string | null = null;\n for (const [path, record] of cache) {\n if (record.refs === 0 && record.entry) {\n URL.revokeObjectURL(record.entry.objectUrl);\n evictedPath = path;\n break;\n }\n }\n if (evictedPath === null) return; // every entry is live (or still downloading) — allow overflow\n cache.delete(evictedPath);\n }\n}\n\n/**\n * Acquire (fetching if needed) and increment the ref count. Concurrent callers of the same path\n * share one in-flight fetch and both resolve to the same object URL. `download` is injected\n * (never imported) so this module is testable with no daemon/client.\n */\nexport function acquireInlineImage(\n path: string,\n download: (path: string) => Promise<{ bytes: Uint8Array; mimeType?: string }>,\n): Promise<InlineImageEntry> {\n const existing = cache.get(path);\n if (existing) {\n existing.refs += 1;\n cache.delete(path);\n cache.set(path, existing); // touch: mark most-recently-used\n return existing.entry ? Promise.resolve(existing.entry) : existing.pending!;\n }\n\n const record: CacheRecord = { refs: 1, entry: null, pending: null };\n cache.set(path, record);\n evictOverBound();\n\n record.pending = download(path)\n .then((file) => {\n // `Blob`'s DOM typings require an `ArrayBuffer`-backed view; the assembled `Uint8Array`'s\n // `ArrayBufferLike` backing doesn't satisfy that on its own (mirrors `use-file-download.ts`).\n const blob = new Blob([Uint8Array.from(file.bytes)], {\n type: file.mimeType || \"application/octet-stream\",\n });\n const result: InlineImageEntry = {\n objectUrl: URL.createObjectURL(blob),\n mimeType: file.mimeType,\n };\n if (cache.get(path) === record) {\n record.entry = result;\n record.pending = null;\n } else {\n // Evicted, cleared, or replaced while this fetch was in flight — nothing owns this URL\n // anymore, so revoke it immediately instead of leaking it.\n URL.revokeObjectURL(result.objectUrl);\n }\n return result;\n })\n .catch((error: unknown) => {\n if (cache.get(path) === record) cache.delete(path); // let the next mount retry\n throw error;\n });\n\n return record.pending;\n}\n\n/** Decrement the ref count. Never revokes — retention past zero refs is the LRU's job. */\nexport function releaseInlineImage(path: string): void {\n const existing = cache.get(path);\n if (!existing) return;\n existing.refs = Math.max(0, existing.refs - 1);\n}\n\n/** Revoke every object URL and clear the map (connection teardown / tests). */\nexport function clearInlineImageCache(): void {\n for (const record of cache.values()) {\n if (record.entry) URL.revokeObjectURL(record.entry.objectUrl);\n }\n cache.clear();\n}\n","/**\n * Connection store — Zustand wrapper around `DaemonClient` + `PiStudioClient`\n * (@av-pi-studio/client). Replaces the POC's raw `WebSocket`/`doConnect()`/`rpc()` globals\n * (POC_TO_APP_PLAN_UI.md §4.1). No raw WebSockets outside this module.\n */\n\nimport { create } from \"zustand\";\nimport {\n DaemonClient,\n PiStudioClient,\n ReconnectionManager,\n createRelayTransport,\n createWebSocketTransport,\n type ConnectionState,\n type Transport,\n} from \"@av-pi-studio/client\";\nimport type { ServerInfoPayload } from \"@av-pi-studio/protocol\";\nimport { CLIENT_CAPS } from \"@av-pi-studio/protocol\";\nimport { resolveConnectTarget } from \"./resolve-connect-target.js\";\nimport { createWorkerTimers } from \"./worker-timers.js\";\nimport { clearInlineImageCache } from \"../inline-image-cache.js\";\n\nexport interface ConnectOptions {\n /**\n * Daemon address, OR a full pairing link (`https://app.pi-studio.sh/#offer=...`) copied from\n * `pi-studio daemon pair`. A pairing link is detected via {@link parsePairingUrl} and switches\n * this connection to the relay transport automatically — see {@link connectViaPairingOffer}.\n * Otherwise accepts `ws://`/`wss://`, `http://`/`https://` (mapped to `ws`/`wss`), or a bare\n * `host[:port]` (assumed `ws://`), normalized via {@link normalizeDaemonUrl}.\n */\n url: string;\n /** Bearer password, sent via the `pi-studio.bearer.<pw>` WS subprotocol. Ignored for a pairing-link (relay) connection — the pairing link's public key is itself the credential. */\n password?: string;\n /** Stable per-tab client id; generated once if omitted. */\n clientId?: string;\n}\n\ninterface ConnectionStoreState {\n status: ConnectionState;\n serverInfo: ServerInfoPayload | null;\n error: string | null;\n /** Non-null once `connect()` has ever succeeded — the live SDK handles. */\n daemon: DaemonClient | null;\n client: PiStudioClient | null;\n reconnection: ReconnectionManager | null;\n\n connect(opts: ConnectOptions): Promise<void>;\n disconnect(): void;\n}\n\nfunction generateClientId(): string {\n return \"web-\" + Math.random().toString(36).slice(2, 10);\n}\n\nexport const useConnectionStore = create<ConnectionStoreState>()((set, get) => ({\n status: \"idle\",\n serverInfo: null,\n error: null,\n daemon: null,\n client: null,\n reconnection: null,\n\n async connect(opts) {\n // Tear down any previous connection before opening a new one.\n get().disconnect();\n\n // A pasted pairing link (`pi-studio daemon pair`'s QR/link) takes over connection setup\n // entirely: it carries the daemon's public key plus either a relay endpoint (branch to the\n // E2EE relay transport, ignoring `opts.password` — the pairing key IS the credential) or a\n // direct host hint (connect there exactly like a typed `url`, no password support in that\n // form since a pairing link is generated once and doesn't carry one).\n const target = resolveConnectTarget(opts.url);\n\n let transport: Transport | undefined;\n if (target.mode === \"relay\") {\n transport = createRelayTransport({ daemonPublicKey: target.daemonPublicKey });\n } else if (opts.password) {\n transport = createWebSocketTransport(\n (u) => new WebSocket(u, [`pi-studio.bearer.${opts.password}`]) as unknown as never,\n );\n }\n\n const daemon = new DaemonClient({\n url: target.url,\n clientId: opts.clientId ?? generateClientId(),\n clientType: \"browser\",\n capabilities: {\n [CLIENT_CAPS.inline_image_markdown]: true,\n [CLIENT_CAPS.file_link_markdown]: true,\n },\n transport,\n // The PiStudioClient facade has no per-call timeout override for\n // createAgent/send/interrupt, and agent turns can run far longer than a\n // typical RPC (POC used a 30 min turn timeout). One rpcTimeoutMs applies\n // to every request on this connection, so size it for the longest op.\n rpcTimeoutMs: 30 * 60 * 1000,\n });\n const client = new PiStudioClient(daemon);\n // Worker-backed timers keep backoff scheduling accurate in a hidden/throttled tab (sprint-050\n // connection-resilience) — falls back to plain setTimeout when Worker construction is\n // unavailable, identical to pre-sprint-050 behavior.\n const { setTimer, clearTimer } = createWorkerTimers();\n const reconnection = new ReconnectionManager(daemon, { setTimer, clearTimer });\n\n daemon.onStateChange((state) => set({ status: state }));\n reconnection.onReconnected(() => set({ error: null }));\n reconnection.onReconnectFailed((error) => {\n set({ error: error instanceof Error ? error.message : String(error) });\n });\n\n set({ daemon, client, reconnection, error: null, status: \"connecting\" });\n\n try {\n const info = await daemon.connect();\n set({ serverInfo: info });\n reconnection.start();\n } catch (error) {\n set({ error: error instanceof Error ? error.message : String(error) });\n throw error;\n }\n },\n\n disconnect() {\n const { daemon, reconnection } = get();\n reconnection?.stop();\n daemon?.close();\n // A reconnect must not leak object URLs pointing at a now-dead transfer instance\n // (`transferFor` is per-daemon-instance; this cache is global — see its module header).\n clearInlineImageCache();\n set({\n status: \"idle\",\n serverInfo: null,\n daemon: null,\n client: null,\n reconnection: null,\n });\n },\n}));\n\n/** Convenience selector: the live `PiStudioClient`, or `null` when disconnected. */\nexport function useClient(): PiStudioClient | null {\n return useConnectionStore((s) => s.client);\n}\n\n/** Convenience selector: current connection lifecycle state. */\nexport function useConnectionStatus(): ConnectionState {\n return useConnectionStore((s) => s.status);\n}\n","/**\n * UI store — ephemeral, session-independent UI state that doesn't belong to any single agent\n * session: the toolbar's connection fields, overlay visibility (workspace-open dialog, session\n * context menu), and `cwd` — the default cwd for a brand-new bare session and the initial path\n * of `OpenWorkspaceDialog` when no workspace is in view. NOT the \"current browsing\n * scope\" — that's `tab-store.ts`'s `activeWorkspaceCwd`, driven by whichever tab is in view\n * (§4.7 follow-up: workspace-scoped tabs). Sidebar visibility/width (collapsed flags, resizable\n * widths dragged via `ResizeHandle`) also live here. Mirrors POC globals `$(\"host\")`,\n * `$(\"password\")`, `$(\"chat-cwd\")`, `cwdPickerPath`, `menuTargetSessionId`.\n */\n\nimport { create } from \"zustand\";\n\nexport const MIN_SIDEBAR_WIDTH = 100;\nexport const MAX_SIDEBAR_WIDTH = 480;\nconst DEFAULT_LEFT_SIDEBAR_WIDTH = 220;\nconst DEFAULT_RIGHT_SIDEBAR_WIDTH = 280;\n\nfunction clampSidebarWidth(width: number): number {\n return Math.min(MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, width));\n}\n\nexport interface UiStoreState {\n host: string;\n password: string;\n cwd: string;\n\n cwdPickerOpen: boolean;\n sessionMenu: { sessionId: string; x: number; y: number } | null;\n /** Per-workspace \"⋮\" menu (sidebar workspace header — New conversation / Delete workspace). */\n workspaceMenu: { cwd: string; x: number; y: number } | null;\n /** `background: true` is the empty-space variant (right-click below the last row, or a\n * selected directory's \"New File\"/\"New Folder\") — `path` is the target directory, not a\n * specific row; the menu renders New File/New Folder/Copy Path only, no Open/Download/Delete. */\n fileMenu: {\n path: string;\n isDirectory: boolean;\n x: number;\n y: number;\n background?: boolean;\n } | null;\n rightSidebarTab: \"files\" | \"changes\";\n /** Workspace cwds collapsed in the sidebar tree (§4.3 workspace grouping); expanded by default. */\n collapsedWorkspaces: Set<string>;\n /** Left (sessions tree) / right (Files·Changes) sidebar visibility; both expanded by default. */\n leftSidebarCollapsed: boolean;\n rightSidebarCollapsed: boolean;\n /** User-resizable sidebar widths in px, draggable via `ResizeHandle` — clamped to [MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH]. */\n leftSidebarWidth: number;\n rightSidebarWidth: number;\n\n setHost(host: string): void;\n setPassword(password: string): void;\n setCwd(cwd: string): void;\n openCwdPicker(): void;\n closeCwdPicker(): void;\n openSessionMenu(sessionId: string, x: number, y: number): void;\n closeSessionMenu(): void;\n openWorkspaceMenu(cwd: string, x: number, y: number): void;\n closeWorkspaceMenu(): void;\n openFileMenu(\n path: string,\n isDirectory: boolean,\n x: number,\n y: number,\n background?: boolean,\n ): void;\n closeFileMenu(): void;\n setRightSidebarTab(tab: \"files\" | \"changes\"): void;\n toggleWorkspaceCollapsed(cwd: string): void;\n setCollapsedWorkspaces(cwds: Set<string>): void;\n toggleLeftSidebar(): void;\n toggleRightSidebar(): void;\n setLeftSidebarWidth(width: number): void;\n setRightSidebarWidth(width: number): void;\n /** Delta-based resize, reading current width from store state — avoids stale-closure drift\n * across the rapid-fire `pointermove` deltas `ResizeHandle` reports. */\n resizeLeftSidebar(deltaX: number): void;\n resizeRightSidebar(deltaX: number): void;\n}\n\nexport const useUiStore = create<UiStoreState>()((set) => ({\n host: \"ws://127.0.0.1:6767\",\n password: \"\",\n cwd: \"~\",\n\n cwdPickerOpen: false,\n sessionMenu: null,\n workspaceMenu: null,\n fileMenu: null,\n rightSidebarTab: \"files\",\n collapsedWorkspaces: new Set(),\n leftSidebarCollapsed: false,\n rightSidebarCollapsed: false,\n leftSidebarWidth: DEFAULT_LEFT_SIDEBAR_WIDTH,\n rightSidebarWidth: DEFAULT_RIGHT_SIDEBAR_WIDTH,\n\n setHost: (host) => set({ host }),\n setPassword: (password) => set({ password }),\n setCwd: (cwd) => set({ cwd }),\n openCwdPicker: () => set({ cwdPickerOpen: true }),\n closeCwdPicker: () => set({ cwdPickerOpen: false }),\n openSessionMenu: (sessionId, x, y) => set({ sessionMenu: { sessionId, x, y } }),\n closeSessionMenu: () => set({ sessionMenu: null }),\n openWorkspaceMenu: (cwd, x, y) => set({ workspaceMenu: { cwd, x, y } }),\n closeWorkspaceMenu: () => set({ workspaceMenu: null }),\n openFileMenu: (path, isDirectory, x, y, background) =>\n set({ fileMenu: { path, isDirectory, x, y, background } }),\n closeFileMenu: () => set({ fileMenu: null }),\n setRightSidebarTab: (tab) => set({ rightSidebarTab: tab }),\n toggleWorkspaceCollapsed: (cwd) =>\n set((s) => {\n const next = new Set(s.collapsedWorkspaces);\n if (next.has(cwd)) next.delete(cwd);\n else next.add(cwd);\n return { collapsedWorkspaces: next };\n }),\n setCollapsedWorkspaces: (cwds) => set({ collapsedWorkspaces: cwds }),\n toggleLeftSidebar: () => set((s) => ({ leftSidebarCollapsed: !s.leftSidebarCollapsed })),\n toggleRightSidebar: () => set((s) => ({ rightSidebarCollapsed: !s.rightSidebarCollapsed })),\n setLeftSidebarWidth: (width) => set({ leftSidebarWidth: clampSidebarWidth(width) }),\n setRightSidebarWidth: (width) => set({ rightSidebarWidth: clampSidebarWidth(width) }),\n resizeLeftSidebar: (deltaX) =>\n set((s) => ({ leftSidebarWidth: clampSidebarWidth(s.leftSidebarWidth + deltaX) })),\n resizeRightSidebar: (deltaX) =>\n set((s) => ({ rightSidebarWidth: clampSidebarWidth(s.rightSidebarWidth + deltaX) })),\n}));\n","/**\n * Connection status dot + label (POC `#dot`/`#status`, POC_TO_APP_PLAN_UI.md §4.1).\n */\n\nimport { clsx } from \"clsx\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport styles from \"./ConnectionStatus.module.css\";\n\nconst LABEL: Record<string, string> = {\n idle: \"disconnected\",\n connecting: \"connecting…\",\n open: \"connected\",\n closing: \"disconnecting…\",\n closed: \"disconnected\",\n};\n\nexport function ConnectionStatus() {\n const status = useConnectionStore((s) => s.status);\n const error = useConnectionStore((s) => s.error);\n\n return (\n <span className={styles.wrap} title={error ?? undefined}>\n <span className={clsx(styles.dot, styles[status])} />\n {LABEL[status] ?? status}\n </span>\n );\n}\n","/**\n * Toolbar — brand, connection status, host/password inputs, connect/disconnect (POC toolbar,\n * POC_TO_APP_PLAN_UI.md §4.1). Both sidebar-visibility toggles sit together at the far right\n * (`.sidebarToggles`, `margin-left: auto`). Provider is always \"pi\" (see Composer.tsx) — this\n * project has no other provider to select. \"Open Workspace\" lives in the sidebar header\n * (SessionList.tsx), not here.\n *\n * The host field ALSO accepts a full pairing link (`pi-studio daemon pair`'s QR/link,\n * architecture/relay-e2ee.md § Pairing) pasted verbatim — `connect()` detects it via\n * `parsePairingUrl` and routes through the relay transport automatically; the password field is\n * ignored in that case (the pairing link's key is itself the credential).\n */\n\nimport { PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen } from \"lucide-react\";\nimport { Button } from \"@pi-studio-ui/components/primitives/Button.js\";\nimport { TextInput } from \"@pi-studio-ui/components/primitives/TextInput.js\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useUiStore } from \"@pi-studio-ui/stores/ui-store.js\";\nimport { ConnectionStatus } from \"./ConnectionStatus.js\";\nimport styles from \"./Toolbar.module.css\";\n\nexport function Toolbar() {\n const status = useConnectionStore((s) => s.status);\n const connect = useConnectionStore((s) => s.connect);\n const disconnect = useConnectionStore((s) => s.disconnect);\n\n const host = useUiStore((s) => s.host);\n const setHost = useUiStore((s) => s.setHost);\n const password = useUiStore((s) => s.password);\n const setPassword = useUiStore((s) => s.setPassword);\n const leftSidebarCollapsed = useUiStore((s) => s.leftSidebarCollapsed);\n const toggleLeftSidebar = useUiStore((s) => s.toggleLeftSidebar);\n const rightSidebarCollapsed = useUiStore((s) => s.rightSidebarCollapsed);\n const toggleRightSidebar = useUiStore((s) => s.toggleRightSidebar);\n\n const connected = status === \"open\" || status === \"connecting\";\n\n return (\n <div className={styles.toolbar}>\n <span className={styles.brand}>Pi-Studio</span>\n <span className={styles.version}>v{__APP_VERSION__}</span>\n <ConnectionStatus />\n <div className={styles.sep} />\n <TextInput\n className={styles.host}\n value={host}\n onChange={(e) => setHost(e.target.value)}\n placeholder=\"host:port, ws://…/http://…, or a pasted pairing link\"\n disabled={connected}\n />\n <TextInput\n className={styles.password}\n type=\"password\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n placeholder=\"password\"\n disabled={connected}\n />\n {status === \"open\" || status === \"connecting\" || status === \"closing\" ? (\n <Button size=\"sm\" onClick={() => disconnect()}>\n Disconnect\n </Button>\n ) : (\n <Button\n size=\"sm\"\n variant=\"default\"\n onClick={() => void connect({ url: host, password: password || undefined })}\n >\n Connect\n </Button>\n )}\n <div className={styles.sidebarToggles}>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n iconOnly\n title={leftSidebarCollapsed ? \"Show sessions sidebar\" : \"Hide sessions sidebar\"}\n onClick={() => toggleLeftSidebar()}\n >\n {leftSidebarCollapsed ? <PanelLeftOpen size={16} /> : <PanelLeftClose size={16} />}\n </Button>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n iconOnly\n title={\n rightSidebarCollapsed ? \"Show files/changes sidebar\" : \"Hide files/changes sidebar\"\n }\n onClick={() => toggleRightSidebar()}\n >\n {rightSidebarCollapsed ? <PanelRightOpen size={16} /> : <PanelRightClose size={16} />}\n </Button>\n </div>\n </div>\n );\n}\n","/**\n * TimelineRow — the render model streamed/replayed chat rows collapse into.\n * Discriminated union mirrors the POC's per-kind DOM append logic\n * (`handleAgentStream`, POC_TO_APP_PLAN_UI.md §4.3) without the DOM.\n */\n\nimport type { ToolCallDetail } from \"@av-pi-studio/protocol\";\n\nexport type ToolCallStatus = \"running\" | \"completed\" | \"error\";\n\nexport interface UserRow {\n kind: \"user\";\n id: string;\n text: string;\n images?: Array<{ mimeType?: string; data?: string }>;\n /**\n * Client-generated id passed as `clientMessageId` on `createAgent`/`send` (features/\n * agent-sessions.md — the daemon echoes it back verbatim as the canonical `user_message`\n * event's `messageId` since the live pi provider never mints its own). Lets the optimistic\n * row inserted on Send reconcile against the server's broadcast instead of duplicating it.\n * Undefined for rows sourced from session-restore hydration/replay, which had no local\n * optimistic counterpart to reconcile against.\n */\n clientMessageId?: string;\n /**\n * True from the moment `Composer` inserts this row locally until the server's `user_message`\n * broadcast for the same `clientMessageId` confirms it (see `reducer.ts`'s `onUserMessage`).\n * Hydrated/replayed rows are never pending — they are already server-confirmed history.\n */\n pending?: boolean;\n /**\n * True when the RPC that was supposed to confirm this row (`createAgent`/`send`) rejected\n * before any `user_message` broadcast arrived — e.g. a dropped connection. Never confirmed,\n * never retried automatically; `pending` is cleared alongside it (see `reducer.ts`'s\n * `markUserMessageFailed`) so the row stops showing a spinner and shows a failure indicator\n * instead.\n */\n failed?: boolean;\n /**\n * True from the moment a steered message is optimistically inserted (`Composer`'s Steer path,\n * `clientMessageId`/`queued: true` on `addOptimisticUserMessage`) until a `queue_update` stream\n * event no longer lists this row's `text` in its `steering[]` array — i.e. Pi has handed the\n * steering message to the LLM. Best-effort text correlation (`queue_update` carries strings, not\n * ids — see `reducer.ts`'s `onQueueUpdate`). Never set on a normal (non-steered) send.\n */\n queued?: boolean;\n}\n\nexport interface AssistantRow {\n kind: \"assistant\";\n id: string;\n text: string;\n /**\n * True while more `assistant_message` deltas may still land in *this* row. Cleared as soon as\n * the block closes (`assistant_message.final`, a following tool call, or a turn boundary) —\n * that is the flag `AssistantRow` reads to switch from plain text to rendered markdown, so it\n * must never outlive the block it describes.\n */\n streaming: boolean;\n}\n\nexport interface ReasoningRow {\n kind: \"reasoning\";\n id: string;\n text: string;\n /** Same contract as `AssistantRow.streaming`. */\n streaming: boolean;\n}\n\nexport interface ToolRow {\n kind: \"tool\";\n id: string;\n /** Stable key used to upsert/dedupe: `callId` when present, else `${tool.kind}:${detail}`. */\n callId: string;\n tool: ToolCallDetail;\n status: ToolCallStatus;\n}\n\nexport interface ErrorRow {\n kind: \"error\";\n id: string;\n text: string;\n}\n\nexport interface SystemRow {\n kind: \"system\";\n id: string;\n text: string;\n}\n\nexport type TimelineRow = UserRow | AssistantRow | ReasoningRow | ToolRow | ErrorRow | SystemRow;\n\nexport interface TimelineState {\n rows: TimelineRow[];\n /** Index into `rows` of the in-progress assistant row, if any (fast tail lookup). */\n streamingAssistantIndex: number | null;\n /** Index into `rows` of the in-progress reasoning row, if any. */\n streamingReasoningIndex: number | null;\n /** callId → row index, for O(1) tool-call upsert. */\n toolIndexByCallId: Record<string, number>;\n}\n\nexport const EMPTY_TIMELINE: TimelineState = {\n rows: [],\n streamingAssistantIndex: null,\n streamingReasoningIndex: null,\n toolIndexByCallId: {},\n};\n\nlet rowSeq = 0;\nexport function nextRowId(): string {\n rowSeq += 1;\n return `row-${rowSeq}`;\n}\n\n/** Detail text extracted from a tool-call for display (POC `t.command || t.path || …`). */\nexport function toolDetailText(tool: ToolCallDetail): string {\n switch (tool.kind) {\n case \"shell\":\n return tool.command ?? \"\";\n case \"read\":\n case \"write\":\n return tool.path ?? \"\";\n case \"edit\":\n return tool.path ?? \"\";\n case \"search\":\n return tool.query ?? \"\";\n case \"fetch\":\n return tool.url ?? \"\";\n case \"task\":\n return tool.description ?? \"\";\n default:\n return \"\";\n }\n}\n\n/** Stable dedupe key for a tool call (POC `e.callId || (t.kind + \":\" + detail)`). */\nexport function toolCallKey(callId: string | undefined, tool: ToolCallDetail): string {\n return callId ?? `${tool.kind}:${toolDetailText(tool)}`;\n}\n","/**\n * Pure `(TimelineState, AgentStreamEvent) → TimelineState` reducer. Replaces the POC's\n * imperative `handleAgentStream` DOM switch (POC_TO_APP_PLAN_UI.md §4.3). No DOM, no globals —\n * fully unit-testable.\n */\n\nimport type { AgentStreamEvent, ToolCallDetail } from \"@av-pi-studio/protocol\";\nimport {\n EMPTY_TIMELINE,\n nextRowId,\n toolCallKey,\n type TimelineRow,\n type TimelineState,\n} from \"./row-model.js\";\n\n/**\n * Flip a still-streaming assistant/reasoning row to its finalized form, in place on an already\n * copied `rows` array.\n *\n * This is the switch that swaps `AssistantRow`'s cheap plain-text streaming tier for the full\n * `<Markdown>` render, so it must fire the moment a row can no longer grow. Clearing the\n * streaming *index* without clearing the row's `streaming` flag (what this reducer used to do on\n * `tool_call`) strands the row in plain text forever — `turn_completed` only ever finalizes the\n * index it still holds, which is `null` by then.\n */\nfunction finalizeRow(rows: TimelineRow[], index: number | null): void {\n if (index === null) return;\n const row = rows[index];\n if (!row) return;\n if (row.kind !== \"assistant\" && row.kind !== \"reasoning\") return;\n if (!row.streaming) return;\n rows[index] = { ...row, streaming: false };\n}\n\n/** Close both streaming rows — the stream has moved on to something that is neither. */\nfunction finalizeStreamingRows(state: TimelineState): TimelineState {\n if (state.streamingAssistantIndex === null && state.streamingReasoningIndex === null) {\n return state;\n }\n const rows = state.rows.slice();\n finalizeRow(rows, state.streamingAssistantIndex);\n finalizeRow(rows, state.streamingReasoningIndex);\n return { ...state, rows, streamingAssistantIndex: null, streamingReasoningIndex: null };\n}\n\n/**\n * Reset streaming refs for a new turn (POC: `turn_started` clears `currentBubble`/\n * `currentReasoning`/`toolEls`).\n *\n * `toolIndexByCallId` intentionally persists across turns — the POC only cleared `st.toolEls` on\n * `turn_started`, but tool rows themselves remain in the DOM; dedupe-by-callId is scoped to the\n * whole session, not the turn, and callIds are unique per invocation anyway.\n */\nfunction onTurnStarted(state: TimelineState): TimelineState {\n return finalizeStreamingRows(state);\n}\n\n/**\n * `final` marks a block-close marker (Pi's `text_end`, mapped in the daemon's `event-mapper.ts`)\n * or a whole hydrated block — either way the text will not grow, so the row finalizes now rather\n * than at `turn_completed`, which is a whole tool loop away.\n */\nfunction onAssistantMessage(state: TimelineState, text: string, final: boolean): TimelineState {\n const rows = state.rows.slice();\n finalizeRow(rows, state.streamingReasoningIndex); // POC: assistant_message clears currentReasoning\n let index = state.streamingAssistantIndex;\n if (index === null || rows[index]?.kind !== \"assistant\") {\n // A textless `final` marker with no open row has nothing to close — never open an empty row.\n if (!text)\n return { ...state, rows, streamingAssistantIndex: null, streamingReasoningIndex: null };\n const row: TimelineRow = { kind: \"assistant\", id: nextRowId(), text: \"\", streaming: true };\n rows.push(row);\n index = rows.length - 1;\n }\n const row = rows[index];\n if (!row || row.kind !== \"assistant\") return state; // unreachable given the guard above\n rows[index] = { ...row, text: row.text + text, streaming: !final };\n return {\n ...state,\n rows,\n streamingAssistantIndex: final ? null : index,\n streamingReasoningIndex: null,\n };\n}\n\nfunction onReasoning(state: TimelineState, text: string, final: boolean): TimelineState {\n const rows = state.rows.slice();\n finalizeRow(rows, state.streamingAssistantIndex); // POC: reasoning clears currentBubble\n let index = state.streamingReasoningIndex;\n if (index === null || rows[index]?.kind !== \"reasoning\") {\n if (!text)\n return { ...state, rows, streamingAssistantIndex: null, streamingReasoningIndex: null };\n const row: TimelineRow = { kind: \"reasoning\", id: nextRowId(), text: \"\", streaming: true };\n rows.push(row);\n index = rows.length - 1;\n }\n const row = rows[index];\n if (!row || row.kind !== \"reasoning\") return state;\n rows[index] = { ...row, text: row.text + text, streaming: !final };\n return {\n ...state,\n rows,\n streamingReasoningIndex: final ? null : index,\n streamingAssistantIndex: null,\n };\n}\n\n/**\n * Merge tool detail across upserts for the same callId. Pi splits detail across events: the\n * command/path arrive on `tool_execution_start` (→ status \"running\") while the edit diff and the\n * command's output arrive only on `tool_execution_end` (→ \"completed\"/\"error\"), which carries no\n * args. Overwriting wholesale would blank the command/path when the end event lands, so we keep\n * each existing non-empty field unless the incoming event provides a value for it. Same `kind`\n * always (both events describe the same tool), so the union stays well-formed.\n */\nfunction mergeTool(prev: ToolCallDetail, next: ToolCallDetail): ToolCallDetail {\n if (prev.kind !== next.kind) return next;\n const merged: Record<string, unknown> = { ...prev };\n for (const [key, value] of Object.entries(next)) {\n if (value !== undefined && value !== \"\") merged[key] = value;\n }\n return merged as ToolCallDetail;\n}\n\nfunction onToolCall(\n state: TimelineState,\n event: Extract<AgentStreamEvent, { kind: \"tool_call\" }>,\n): TimelineState {\n const key = toolCallKey(event.callId, event.tool);\n const status =\n event.status === \"completed\" || event.status === \"error\" ? event.status : \"running\";\n const existingIndex = state.toolIndexByCallId[key];\n const rows = state.rows.slice();\n // A tool call means the prose that preceded it is done — finalize it now instead of leaving it\n // in the plain-text streaming tier until the whole turn ends.\n finalizeRow(rows, state.streamingAssistantIndex);\n finalizeRow(rows, state.streamingReasoningIndex);\n\n if (existingIndex !== undefined && rows[existingIndex]?.kind === \"tool\") {\n const row = rows[existingIndex];\n if (row.kind !== \"tool\") return state;\n rows[existingIndex] = { ...row, tool: mergeTool(row.tool, event.tool), status };\n return {\n ...state,\n rows,\n streamingAssistantIndex: null,\n streamingReasoningIndex: null,\n };\n }\n\n const row: TimelineRow = { kind: \"tool\", id: nextRowId(), callId: key, tool: event.tool, status };\n rows.push(row);\n return {\n ...state,\n rows,\n toolIndexByCallId: { ...state.toolIndexByCallId, [key]: rows.length - 1 },\n streamingAssistantIndex: null,\n streamingReasoningIndex: null,\n };\n}\n\nfunction onTurnCompleted(state: TimelineState): TimelineState {\n return finalizeStreamingRows(state);\n}\n\nfunction onTurnFailed(state: TimelineState, error: string | undefined): TimelineState {\n const finalized = onTurnCompleted(state);\n const row: TimelineRow = { kind: \"error\", id: nextRowId(), text: error || \"turn failed\" };\n return { ...finalized, rows: [...finalized.rows, row] };\n}\n\nfunction onTurnCanceled(state: TimelineState): TimelineState {\n const finalized = onTurnCompleted(state);\n const row: TimelineRow = { kind: \"system\", id: nextRowId(), text: \"(canceled)\" };\n return { ...finalized, rows: [...finalized.rows, row] };\n}\n\nfunction onError(state: TimelineState, message: string | undefined): TimelineState {\n const row: TimelineRow = { kind: \"error\", id: nextRowId(), text: message || \"error\" };\n return { ...state, rows: [...state.rows, row] };\n}\n\nfunction onUserMessage(\n state: TimelineState,\n text: string,\n images?: Array<{ mimeType?: string; data?: string }>,\n messageId?: string,\n): TimelineState {\n // Reconcile against the optimistic row `Composer` inserted synchronously on Send (same\n // `clientMessageId`, echoed back verbatim by the daemon as this event's `messageId` — see\n // `packages/server/src/agent/agent-service.ts` `runTurn`): confirm it in place instead of\n // appending a duplicate. No match (session-restore replay, or a `messageId` the daemon minted\n // itself) → append fresh, already-confirmed.\n if (messageId !== undefined) {\n const idx = state.rows.findIndex(\n (r) => r.kind === \"user\" && r.pending && r.clientMessageId === messageId,\n );\n if (idx !== -1) {\n const rows = state.rows.slice();\n const prev = rows[idx];\n if (prev?.kind === \"user\") {\n rows[idx] = { ...prev, text, images, pending: false };\n return { ...state, rows };\n }\n }\n }\n const row: TimelineRow = {\n kind: \"user\",\n id: nextRowId(),\n text,\n images,\n clientMessageId: messageId,\n };\n return { ...state, rows: [...state.rows, row] };\n}\n\n/**\n * `queue_update` carries the pending-steering queue as raw strings, not ids (Pi's `steer`/\n * `follow_up` RPC has no message-id concept — see `provider-contract.ts`). Clear the `queued` flag\n * on any row whose text no longer appears in `steering[]`: Pi has handed it to the LLM. Rows are\n * matched by exact text, best-effort — sufficient for a UX badge, not a precise queue model.\n */\nfunction onQueueUpdate(state: TimelineState, steering: readonly string[]): TimelineState {\n let changed = false;\n const rows = state.rows.map((row) => {\n if (row.kind !== \"user\" || !row.queued) return row;\n if (steering.includes(row.text)) return row;\n changed = true;\n return { ...row, queued: false };\n });\n return changed ? { ...state, rows } : state;\n}\n\n/** Apply one `AgentStreamEvent` to timeline state. Pure — no mutation of the input. */\nexport function applyStreamEvent(state: TimelineState, event: AgentStreamEvent): TimelineState {\n switch (event.kind) {\n case \"turn_started\":\n return onTurnStarted(state);\n case \"user_message\":\n return onUserMessage(state, event.text ?? \"\", event.images, event.messageId);\n case \"assistant_message\":\n return onAssistantMessage(state, event.text ?? \"\", event.final ?? false);\n case \"reasoning\":\n return onReasoning(state, event.text ?? \"\", event.final ?? false);\n case \"tool_call\":\n return onToolCall(state, event);\n case \"turn_completed\":\n return onTurnCompleted(state);\n case \"turn_failed\":\n return onTurnFailed(state, event.error);\n case \"turn_canceled\":\n return onTurnCanceled(state);\n case \"error\":\n return onError(state, event.message);\n case \"queue_update\":\n return onQueueUpdate(state, event.steering ?? []);\n default:\n return state; // unknown/future kind — ignore gracefully (append-only protocol)\n }\n}\n\n/** Replay a whole ordered event list (session restore / timeline hydration) into fresh state. */\nexport function replayEvents(events: readonly AgentStreamEvent[]): TimelineState {\n return events.reduce(applyStreamEvent, EMPTY_TIMELINE);\n}\n\n/**\n * Insert the local optimistic echo `Composer` renders synchronously on Send/Steer, before the\n * daemon's `create_agent_request`/`send_agent_prompt`/`steer_agent_request` round trip resolves.\n * Keyed by the same `clientMessageId` sent on the RPC — the eventual `user_message` broadcast\n * (`onUserMessage` above) reconciles this row in place by matching `pending` + `clientMessageId`,\n * so the row never duplicates once the server confirms it. `queued: true` marks a steered message\n * (cleared later by `onQueueUpdate` above); a normal send never sets it.\n */\nexport function addOptimisticUserMessage(\n state: TimelineState,\n clientMessageId: string,\n text: string,\n images?: Array<{ mimeType?: string; data?: string }>,\n queued?: boolean,\n): TimelineState {\n const row: TimelineRow = {\n kind: \"user\",\n id: nextRowId(),\n text,\n images,\n clientMessageId,\n pending: true,\n queued,\n };\n return { ...state, rows: [...state.rows, row] };\n}\n\n/**\n * Mark a still-pending optimistic row as failed — the RPC that was meant to confirm it\n * (`createAgent`/`send`) rejected before any `user_message` broadcast arrived. No-op if the row\n * was already reconciled (server confirmed it before the RPC promise settled — the RPC contract\n * is \"resolves after the whole turn\", so a `user_message` broadcast can arrive, then the promise\n * can still reject later from a failure elsewhere in that same turn; the row is already correct\n * by then and must not be clobbered back into a failed state).\n */\nexport function markUserMessageFailed(\n state: TimelineState,\n clientMessageId: string,\n): TimelineState {\n const idx = state.rows.findIndex(\n (r) => r.kind === \"user\" && r.pending && r.clientMessageId === clientMessageId,\n );\n if (idx === -1) return state;\n const rows = state.rows.slice();\n const prev = rows[idx];\n if (prev?.kind !== \"user\") return state;\n rows[idx] = { ...prev, pending: false, failed: true };\n return { ...state, rows };\n}\n\nexport { EMPTY_TIMELINE };\nexport type { TimelineState, TimelineRow };\n","/**\n * Session store — replaces the POC's `sessions[]`/`activeSessionId` globals\n * (POC_TO_APP_PLAN_UI.md §4.3). Kills the POC's `expectingAgent` race: a session only subscribes\n * to `agent(id).timeline.subscribe` once `agentId` is bound (from the `createAgent` response),\n * so no event can arrive before the session that owns it is known — no guessing required.\n */\n\nimport { create } from \"zustand\";\nimport type { AgentStatus } from \"@av-pi-studio/protocol\";\nimport {\n EMPTY_TIMELINE,\n applyStreamEvent as applyStreamEventToTimeline,\n addOptimisticUserMessage as addOptimisticUserMessageToTimeline,\n markUserMessageFailed as markUserMessageFailedInTimeline,\n} from \"@pi-studio-ui/timeline/reducer.js\";\nimport type { TimelineState } from \"@pi-studio-ui/timeline/reducer.js\";\nimport type { AgentStreamEvent } from \"@av-pi-studio/protocol\";\n\nexport interface SessionEntry {\n id: string;\n agentId: string | null;\n title: string;\n status: AgentStatus | \"idle\";\n cwd: string;\n timeline: TimelineState;\n /** User-turn count, for the sidebar meta line (POC: `messages.filter(role==='user').length`). */\n userMessageCount: number;\n /** Current model id, shown by the workspace status bar's model segment (sprint-042). Seeded\n * from the preselected default (`materialize.ts` `resolveDefaultModel`) or create-agent config\n * / a restored `list_agents` entry, updated live by an `agent_update({model})` broadcast, and\n * reconciled by the periodic session-stats poll. */\n model?: string;\n /** The current model's own underlying LLM provider (e.g. `\"anthropic\"`) — travels alongside\n * `model` so a materialized draft can pin `config.modelProvider` for first-spawn replay\n * (`agent-service.ts` `spawnOrResumeSession`) without a second round trip to re-derive it. */\n modelProvider?: string;\n}\n\ninterface SessionStoreState {\n sessions: Record<string, SessionEntry>;\n order: string[];\n activeSessionId: string | null;\n\n createSession(cwd: string): string;\n bindAgent(sessionId: string, agentId: string): void;\n setStatus(sessionId: string, status: AgentStatus | \"idle\"): void;\n setStatusByAgentId(agentId: string, status: AgentStatus | \"idle\"): void;\n setModel(sessionId: string, model: string | undefined, modelProvider?: string): void;\n setModelByAgentId(agentId: string, model: string | undefined, modelProvider?: string): void;\n setTitle(sessionId: string, title: string): void;\n setCwd(sessionId: string, cwd: string): void;\n applyStreamEvent(sessionId: string, event: AgentStreamEvent): void;\n addOptimisticUserMessage(\n sessionId: string,\n clientMessageId: string,\n text: string,\n images?: Array<{ mimeType?: string; data?: string }>,\n queued?: boolean,\n ): void;\n markUserMessageFailed(sessionId: string, clientMessageId: string): void;\n activate(sessionId: string): void;\n remove(sessionId: string): void;\n /** Register a restored session (session restore on connect, §4.3 \"Session restore\"). */\n hydrate(entry: SessionEntry): void;\n findByAgentId(agentId: string): SessionEntry | undefined;\n}\n\nlet sessionSeq = 0;\nfunction newSessionId(): string {\n sessionSeq += 1;\n return `s-${Date.now().toString(36)}-${sessionSeq}`;\n}\n\nexport const useSessionStore = create<SessionStoreState>()((set, get) => ({\n sessions: {},\n order: [],\n activeSessionId: null,\n\n createSession(cwd) {\n const id = newSessionId();\n const entry: SessionEntry = {\n id,\n agentId: null,\n title: \"New chat\",\n status: \"idle\",\n cwd,\n timeline: EMPTY_TIMELINE,\n userMessageCount: 0,\n };\n set((s) => ({\n sessions: { ...s.sessions, [id]: entry },\n order: [id, ...s.order],\n activeSessionId: id,\n }));\n return id;\n },\n\n hydrate(entry) {\n set((s) => {\n if (s.sessions[entry.id]) return s; // already known — don't clobber live state\n return {\n sessions: { ...s.sessions, [entry.id]: entry },\n order: [...s.order, entry.id],\n };\n });\n },\n\n bindAgent(sessionId, agentId) {\n set((s) => {\n const entry = s.sessions[sessionId];\n if (!entry) return s;\n return { sessions: { ...s.sessions, [sessionId]: { ...entry, agentId } } };\n });\n },\n\n setStatus(sessionId, status) {\n set((s) => {\n const entry = s.sessions[sessionId];\n if (!entry) return s;\n return { sessions: { ...s.sessions, [sessionId]: { ...entry, status } } };\n });\n },\n\n setStatusByAgentId(agentId, status) {\n const entry = get().findByAgentId(agentId);\n if (entry) get().setStatus(entry.id, status);\n },\n\n setModel(sessionId, model, modelProvider) {\n set((s) => {\n const entry = s.sessions[sessionId];\n if (!entry) return s;\n return { sessions: { ...s.sessions, [sessionId]: { ...entry, model, modelProvider } } };\n });\n },\n\n setModelByAgentId(agentId, model, modelProvider) {\n const entry = get().findByAgentId(agentId);\n if (entry) get().setModel(entry.id, model, modelProvider);\n },\n\n setTitle(sessionId, title) {\n set((s) => {\n const entry = s.sessions[sessionId];\n if (!entry) return s;\n return { sessions: { ...s.sessions, [sessionId]: { ...entry, title } } };\n });\n },\n\n setCwd(sessionId, cwd) {\n set((s) => {\n const entry = s.sessions[sessionId];\n if (!entry) return s;\n return { sessions: { ...s.sessions, [sessionId]: { ...entry, cwd } } };\n });\n },\n\n applyStreamEvent(sessionId, event) {\n set((s) => {\n const entry = s.sessions[sessionId];\n if (!entry) return s;\n const timeline = applyStreamEventToTimeline(entry.timeline, event);\n let title = entry.title;\n if (event.kind === \"turn_completed\" && title === \"New chat\") {\n const lastAssistant = timeline.rows\n .slice()\n .reverse()\n .find((r) => r.kind === \"assistant\");\n if (lastAssistant && lastAssistant.kind === \"assistant\" && lastAssistant.text) {\n title = lastAssistant.text.slice(0, 40) + (lastAssistant.text.length > 40 ? \"…\" : \"\");\n }\n }\n return { sessions: { ...s.sessions, [sessionId]: { ...entry, timeline, title } } };\n });\n },\n\n addOptimisticUserMessage(sessionId, clientMessageId, text, images, queued) {\n set((s) => {\n const entry = s.sessions[sessionId];\n if (!entry) return s;\n const timeline = addOptimisticUserMessageToTimeline(\n entry.timeline,\n clientMessageId,\n text,\n images,\n queued,\n );\n return {\n sessions: {\n ...s.sessions,\n [sessionId]: { ...entry, timeline, userMessageCount: entry.userMessageCount + 1 },\n },\n };\n });\n },\n\n markUserMessageFailed(sessionId, clientMessageId) {\n set((s) => {\n const entry = s.sessions[sessionId];\n if (!entry) return s;\n const timeline = markUserMessageFailedInTimeline(entry.timeline, clientMessageId);\n return { sessions: { ...s.sessions, [sessionId]: { ...entry, timeline } } };\n });\n },\n\n activate(sessionId) {\n set({ activeSessionId: sessionId });\n },\n\n remove(sessionId) {\n set((s) => {\n const { [sessionId]: _removed, ...rest } = s.sessions;\n const order = s.order.filter((id) => id !== sessionId);\n const activeSessionId =\n s.activeSessionId === sessionId ? (order[0] ?? null) : s.activeSessionId;\n return { sessions: rest, order, activeSessionId };\n });\n },\n\n findByAgentId(agentId) {\n return Object.values(get().sessions).find((s) => s.agentId === agentId);\n },\n}));\n","/**\n * Deferred-draft materialization + preselected default model — the client-side half of the\n * server's deferred-spawn draft creation (`agent-service.ts` `handleCreate`/\n * `spawnOrResumeSession`). Opening a \"New chat\" tab creates only a client-only `SessionEntry`\n * (`session-store.ts` `createSession`, `agentId: null`) — nothing is persisted or spawned\n * server-side until `tab-store.ts` `openNewChat` eagerly calls `ensureMaterialized` the moment the\n * tab is created, which commits it into a real, restorable `AgentRecord` without spawning a `pi`\n * process (that stays deferred to the first send). `ensureMaterialized` also resolves the default\n * model itself when the entry doesn't have one yet, so callers never need a separate display-only\n * lookup before materializing.\n */\n\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\nimport { useSessionStore } from \"./session-store.js\";\n\nexport interface DefaultModel {\n model?: string;\n modelProvider?: string;\n}\n\n/**\n * Cached per connection (`WeakMap` keyed by the live `PiStudioClient` instance — a reconnect gets\n * a new client and therefore a fresh cache, exactly matching the daemon-side cache's \"per\n * process\" scope). The first call after a connection opens pays one transient `--no-session`\n * spawn (`resolve_default_model`, `providers/pi/agent.ts`); every subsequent \"New chat\" on the\n * same connection is instant. A failed lookup (e.g. the provider can't resolve one) is cached too\n * — an empty result just means the composer shows its placeholder, so retrying on every keystroke\n * would be pure waste.\n */\nconst defaultModelCache = new WeakMap<PiStudioClient, Promise<DefaultModel>>();\n\n/** Resolve the model a brand-new chat would run on with no override — settings' configured\n * default, else the provider's built-in default — WITHOUT spawning anything. Called by\n * `ensureMaterialized` itself when an entry has no model yet; exported separately only because\n * cache scope (per `PiStudioClient` instance) is useful to assert on directly in tests. */\nexport function resolveDefaultModel(\n client: PiStudioClient,\n provider = \"pi\",\n): Promise<DefaultModel> {\n const cached = defaultModelCache.get(client);\n if (cached) return cached;\n const promise = client.providers\n .resolveDefaultModel(provider)\n .then((res) => ({ model: res.model, modelProvider: res.modelProvider }))\n .catch(() => ({}) as DefaultModel);\n defaultModelCache.set(client, promise);\n return promise;\n}\n\n/** In-flight materializations, keyed by `sessionId` — serializes concurrent callers (eager-open\n * racing an explicit model pick racing Send) onto the same `createAgent` call instead of each\n * firing its own and racing to `bindAgent`. */\nconst materializing = new Map<string, Promise<string>>();\n\n/**\n * Ensure `sessionId` has a bound `agentId`, materializing a deferred draft if it doesn't yet.\n * Materializing means `createAgent` with NO `initialPrompt` — the server persists the record but\n * does not spawn a provider process (`agent-service.ts` `handleCreate`'s deferred-draft branch) —\n * carrying whatever model is CURRENTLY on the session entry (the preselected default from\n * `resolveDefaultModel`, or an explicit `/model` pick; either way, `config.model`/\n * `config.modelProvider` is what the server replays on first spawn, unconditionally, since\n * neither `createSession` nor `resumeSession` consult it themselves).\n *\n * Idempotent: returns the already-bound `agentId` immediately, or the same in-flight promise if\n * a materialize is already running for this session.\n */\nexport function ensureMaterialized(client: PiStudioClient, sessionId: string): Promise<string> {\n const entry = useSessionStore.getState().sessions[sessionId];\n if (!entry) return Promise.reject(new Error(`unknown session: ${sessionId}`));\n if (entry.agentId) return Promise.resolve(entry.agentId);\n\n const inFlight = materializing.get(sessionId);\n if (inFlight) return inFlight;\n\n const promise = (async () => {\n // Seed the default model before building `config` — `spawnOrResumeSession` reads\n // `record.config.model` at spawn time, so this must land in the SAME `createAgent` call\n // rather than a follow-up `setModel` (see module header). Skipped once the entry already has\n // a model, whether from an earlier preselect or an explicit pick — and re-checked against the\n // CURRENT entry (not the stale `entry` closure) after the lookup resolves, so an explicit\n // pick that lands while the lookup is in flight always wins over the resolved default.\n if (!entry.model) {\n const resolved = await resolveDefaultModel(client);\n const current = useSessionStore.getState().sessions[sessionId];\n if (resolved.model && current && !current.model) {\n useSessionStore.getState().setModel(sessionId, resolved.model, resolved.modelProvider);\n }\n }\n // Re-read: an explicit pick or a `remove` may have landed while the lookup above was in\n // flight (a no-op re-read when no await happened above, since nothing else runs between two\n // synchronous statements).\n const fresh = useSessionStore.getState().sessions[sessionId];\n if (!fresh) throw new Error(`unknown session: ${sessionId}`);\n\n const config: { provider: string; cwd: string; model?: string; modelProvider?: string } = {\n provider: \"pi\",\n cwd: fresh.cwd || \"~\",\n };\n if (fresh.model) config.model = fresh.model;\n if (fresh.modelProvider) config.modelProvider = fresh.modelProvider;\n const result = await client.createAgent({ config, labels: {} });\n useSessionStore.getState().bindAgent(sessionId, result.agentId);\n return result.agentId;\n })();\n\n materializing.set(sessionId, promise);\n void promise.finally(() => materializing.delete(sessionId));\n return promise;\n}\n\n/** Discard a chat whose conversation never started: closing its tab throws it away instead of\n * leaving an empty persisted record behind forever. A record that's never closed persists\n * indefinitely by design (`use-session-restore.ts` hydrates every known agent unconditionally,\n * including an untouched draft) — this is the ONLY path that removes one short of an explicit\n * delete. A chat with any timeline row is kept (closing the tab keeps the session, as before).\n * Returns whether the session was removed, purely for tests — callers (`tab-store.ts`\n * `closeTab`) treat this as fire-and-forget. */\nexport async function discardIfEmpty(\n client: PiStudioClient | null,\n sessionId: string,\n): Promise<boolean> {\n const entry = useSessionStore.getState().sessions[sessionId];\n if (!entry) return false;\n if (entry.userMessageCount > 0 || entry.timeline.rows.length > 0) return false;\n\n // An eager materialize (`tab-store.ts` `openNewChat`) may still be in flight — await it so the\n // `agentId` it is about to bind isn't leaked (created, then never deleted because `entry`\n // was read before the bind landed).\n await materializing.get(sessionId)?.catch(() => {});\n const fresh = useSessionStore.getState().sessions[sessionId];\n if (!fresh) return false;\n\n if (fresh.agentId && client) {\n await client\n .agent(fresh.agentId)\n .delete()\n .catch(() => {\n // Best-effort hard delete: the local entry goes away regardless (mirrors\n // `SessionContextMenu.remove`'s convention), even if the RPC itself failed.\n });\n }\n useSessionStore.getState().remove(sessionId);\n return true;\n}\n","/**\n * Pane tree — the pure split/collapse algebra behind workspace split panes.\n *\n * No React, no store, no persistence: a workspace's pane arrangement is a tree of leaves and\n * splits, and every operation on it is a total function from tree to tree. Keeping it here means\n * the two rules that carry real bug risk are exhaustively testable without a DOM:\n *\n * 1. **Depth is checked per branch.** Inserting a sibling into an existing same-direction run\n * never deepens the tree, so at `MAX_PANE_DEPTH` a split *along* the run's direction is still\n * legal — only a perpendicular (nesting) split is refused.\n * 2. **Collapse keeps runs flat.** A split left with one child is replaced by that child, and if\n * that child is a same-direction split its children are spliced into the parent — otherwise\n * removing a pane silently produces same-direction nesting, wasting depth against the cap and\n * changing divider behaviour.\n *\n * swe/features/workspace-split-panes.md § Pane tree, § Splitting,\n * § Removing a pane / collapsing\n */\n\nexport interface LeafPane {\n kind: \"leaf\";\n id: string;\n}\n\nexport interface SplitNode {\n kind: \"split\";\n direction: SplitDirection;\n /** Always >= 2 entries; parallel to `sizes`. */\n children: PaneNode[];\n /** Parallel to `children`; each > 0, summing to 1. */\n sizes: number[];\n}\n\nexport type SplitDirection = \"row\" | \"column\";\nexport type PaneNode = LeafPane | SplitNode;\nexport type SplitRegion = \"left\" | \"right\" | \"top\" | \"bottom\";\n\n/** Deepest tree a split may produce. A lone leaf is depth 1. Matches `workspace-ui.md`. */\nexport const MAX_PANE_DEPTH = 4;\n\n/** Region → split axis. Shared by `canSplit`, `splitPane`, and the drop-region gesture layer. */\nexport function directionOfRegion(region: SplitRegion): SplitDirection {\n return region === \"left\" || region === \"right\" ? \"row\" : \"column\";\n}\n\nexport function leafIds(tree: PaneNode): string[] {\n if (tree.kind === \"leaf\") return [tree.id];\n return tree.children.flatMap(leafIds);\n}\n\nfunction hasLeaf(tree: PaneNode, paneId: string): boolean {\n if (tree.kind === \"leaf\") return tree.id === paneId;\n return tree.children.some((child) => hasLeaf(child, paneId));\n}\n\n/** 1-based: a lone leaf is depth 1, a leaf under three nested splits is depth 4. `depth` is the\n * recursion accumulator — callers pass only the tree and the pane id. */\nexport function paneDepth(node: PaneNode, paneId: string, depth = 1): number | null {\n if (node.kind === \"leaf\") return node.id === paneId ? depth : null;\n for (const child of node.children) {\n const found = paneDepth(child, paneId, depth + 1);\n if (found !== null) return found;\n }\n return null;\n}\n\n/** The split that directly holds `paneId` as a leaf child, or `null` for the root leaf. */\nfunction parentSplitOf(tree: PaneNode, paneId: string): SplitNode | null {\n if (tree.kind === \"leaf\") return null;\n if (directChildIndex(tree, paneId) !== -1) return tree;\n for (const child of tree.children) {\n const found = parentSplitOf(child, paneId);\n if (found) return found;\n }\n return null;\n}\n\nfunction directChildIndex(split: SplitNode, paneId: string): number {\n return split.children.findIndex((child) => child.kind === \"leaf\" && child.id === paneId);\n}\n\n/**\n * Whether splitting `targetPaneId` towards `region` is legal.\n *\n * Evaluated per branch, *after* resolving whether the split reuses a same-direction parent: a\n * sibling insert leaves depth untouched and is always legal, so only a nesting split can hit the\n * cap. A single `depth + 1 > MAX` check would wrongly refuse both.\n */\nexport function canSplit(tree: PaneNode, targetPaneId: string, region: SplitRegion): boolean {\n const depth = paneDepth(tree, targetPaneId);\n if (depth === null) return false;\n const parent = parentSplitOf(tree, targetPaneId);\n if (parent && parent.direction === directionOfRegion(region)) return true;\n return depth + 1 <= MAX_PANE_DEPTH;\n}\n\n/**\n * Split `targetPaneId`, placing `newPaneId` on the side `region` names.\n *\n * Returns the tree **unchanged** when the split is illegal (`canSplit` false, unknown target, or a\n * `newPaneId` already in the tree). Refusal is a backstop for programmatic callers and corrupted\n * state, not an error: the drag gesture degrades an illegal edge region to a `center` move instead\n * of ever calling this and getting nothing.\n */\nexport function splitPane(\n tree: PaneNode,\n targetPaneId: string,\n region: SplitRegion,\n newPaneId: string,\n): PaneNode {\n if (!canSplit(tree, targetPaneId, region)) return tree;\n if (hasLeaf(tree, newPaneId)) return tree;\n\n const direction = directionOfRegion(region);\n // `left`/`top` place the new pane before the target; `right`/`bottom` after it.\n const before = region === \"left\" || region === \"top\";\n const added: LeafPane = { kind: \"leaf\", id: newPaneId };\n\n const nest = (target: LeafPane): SplitNode => ({\n kind: \"split\",\n direction,\n children: before ? [added, target] : [target, added],\n sizes: [0.5, 0.5],\n });\n\n const transform = (node: PaneNode): PaneNode => {\n if (node.kind === \"leaf\") return node.id === targetPaneId ? nest(node) : node;\n const idx = directChildIndex(node, targetPaneId);\n // Reuse an existing same-direction run instead of nesting deeper: repeated splits in one\n // direction stay one flat run, which keeps sibling resizing intuitive and spends no depth.\n if (idx !== -1 && node.direction === direction) {\n const half = node.sizes[idx]! / 2;\n const at = before ? idx : idx + 1;\n const children = node.children.slice();\n const sizes = node.sizes.slice();\n sizes[idx] = half;\n children.splice(at, 0, added);\n sizes.splice(at, 0, half);\n return { kind: \"split\", direction: node.direction, children, sizes };\n }\n return {\n kind: \"split\",\n direction: node.direction,\n children: node.children.map(transform),\n sizes: node.sizes.slice(),\n };\n };\n\n return transform(tree);\n}\n\n/**\n * Remove a leaf, redistributing its size proportionally across its remaining siblings and\n * collapsing upward. Never removes the last leaf — a workspace always has at least one pane, and\n * the single-leaf tree is the terminal state.\n */\nexport function removePane(tree: PaneNode, paneId: string): PaneNode {\n if (!hasLeaf(tree, paneId)) return tree;\n if (tree.kind === \"leaf\") return tree; // the only leaf\n\n const drop = (node: PaneNode): PaneNode => {\n if (node.kind === \"leaf\") return node;\n const idx = directChildIndex(node, paneId);\n if (idx === -1) {\n return {\n kind: \"split\",\n direction: node.direction,\n children: node.children.map(drop),\n sizes: node.sizes.slice(),\n };\n }\n const kept = node.sizes.filter((_, i) => i !== idx);\n const keptTotal = kept.reduce((sum, size) => sum + size, 0);\n const slotTotal = keptTotal + node.sizes[idx]!;\n return {\n kind: \"split\",\n direction: node.direction,\n children: node.children.filter((_, i) => i !== idx),\n // Proportional: each survivor keeps its share of the siblings' combined size, rescaled to\n // fill the whole slot the removed pane was part of.\n sizes: kept.map((size) => (size / keptTotal) * slotTotal),\n };\n };\n\n return normalizePaneTree(drop(tree));\n}\n\n/**\n * Collapse single-child splits and splice same-direction runs flat, bottom-up.\n *\n * Without the splice, removing `B` from `row[A, column[B, row[C, D]]]` would promote `row[C, D]`\n * into the outer row as a nested same-direction split; this restores `row[A, C, D]`.\n */\nexport function normalizePaneTree(node: PaneNode): PaneNode {\n if (node.kind === \"leaf\") return node;\n\n const normalized = node.children.map(normalizePaneTree);\n // A split left with one child is replaced by that child, which inherits the parent's slot.\n if (normalized.length === 1) return normalized[0]!;\n\n const children: PaneNode[] = [];\n const sizes: number[] = [];\n normalized.forEach((child, i) => {\n const slot = node.sizes[i]!;\n if (child.kind === \"split\" && child.direction === node.direction) {\n child.children.forEach((grandChild, j) => {\n children.push(grandChild);\n sizes.push(slot * child.sizes[j]!);\n });\n } else {\n children.push(child);\n sizes.push(slot);\n }\n });\n\n if (children.length === 1) return children[0]!;\n return { kind: \"split\", direction: node.direction, children, sizes };\n}\n\n// ─── Geometry ──────────────────────────────────────────────────────────────────────────────\n\n/**\n * Smallest fraction a divider drag may leave a pane at.\n *\n * A **resize** constraint, not a global invariant: `splitPane` halves its target unconditionally,\n * so repeated splits can legitimately *birth* a pane below this. The clamp only guarantees a drag\n * never reduces a neighbour below the minimum — an already-sub-minimum neighbour can still grow.\n */\nexport const MIN_PANE_FRACTION = 0.1;\n\nexport interface Rect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * Fractional rectangles (0..1) keyed by pane id; callers multiply by their measured content box.\n *\n * Kept fractional on purpose: the flat panel host re-derives pixel boxes from one `ResizeObserver`\n * measurement, so a container resize never has to walk the tree again.\n */\nexport function paneRects(tree: PaneNode): Map<string, Rect> {\n const rects = new Map<string, Rect>();\n const walk = (node: PaneNode, rect: Rect): void => {\n if (node.kind === \"leaf\") {\n rects.set(node.id, rect);\n return;\n }\n childRects(node, rect).forEach((childRect, i) => walk(node.children[i]!, childRect));\n };\n walk(tree, { x: 0, y: 0, width: 1, height: 1 });\n return rects;\n}\n\n/** A split's own rect divided along `direction` in proportion to `sizes`. */\nfunction childRects(split: SplitNode, rect: Rect): Rect[] {\n const out: Rect[] = [];\n let offset = 0;\n for (const size of split.sizes) {\n out.push(\n split.direction === \"row\"\n ? {\n x: rect.x + offset * rect.width,\n y: rect.y,\n width: size * rect.width,\n height: rect.height,\n }\n : {\n x: rect.x,\n y: rect.y + offset * rect.height,\n width: rect.width,\n height: size * rect.height,\n },\n );\n offset += size;\n }\n return out;\n}\n\n/** Child indices from the root down to a split; the root split's path is `[]`. */\nexport type SplitPath = readonly number[];\n\nexport interface Divider {\n splitPath: SplitPath;\n /** Boundary between `children[boundaryIndex]` and `children[boundaryIndex + 1]`. */\n boundaryIndex: number;\n direction: SplitDirection;\n /** Zero-thickness line spanning the split's cross-axis extent; the UI gives it a hit area. */\n rect: Rect;\n /**\n * The split's OWN rect. `sizes` are fractions of this, not of the whole host, so this is the\n * denominator a drag needs: `deltaFraction = deltaPx / (splitRect extent along `direction` × host\n * extent)`. Using the host extent instead would under-move every nested split's divider.\n */\n splitRect: Rect;\n}\n\n/** A split with N children owns N−1 interior boundaries. */\nexport function dividers(tree: PaneNode): Divider[] {\n const out: Divider[] = [];\n const walk = (node: PaneNode, rect: Rect, path: number[]): void => {\n if (node.kind === \"leaf\") return;\n const rects = childRects(node, rect);\n for (let i = 0; i < node.children.length - 1; i += 1) {\n const after = rects[i]!;\n out.push({\n splitPath: path.slice(),\n boundaryIndex: i,\n direction: node.direction,\n rect:\n node.direction === \"row\"\n ? { x: after.x + after.width, y: rect.y, width: 0, height: rect.height }\n : { x: rect.x, y: after.y + after.height, width: rect.width, height: 0 },\n splitRect: rect,\n });\n }\n rects.forEach((childRect, i) => walk(node.children[i]!, childRect, [...path, i]));\n };\n walk(tree, { x: 0, y: 0, width: 1, height: 1 }, []);\n return out;\n}\n\nfunction nodeAtPath(tree: PaneNode, path: SplitPath): PaneNode | null {\n let node: PaneNode | undefined = tree;\n for (const index of path) {\n if (!node || node.kind !== \"split\") return null;\n node = node.children[index];\n }\n return node ?? null;\n}\n\n/**\n * Shift `deltaFraction` from `children[boundaryIndex + 1]` to `children[boundaryIndex]`, clamped so\n * neither neighbour is reduced below `MIN_PANE_FRACTION`. Only those two siblings change.\n *\n * `deltaFraction` is the pointer delta along the split axis divided by the content box's extent on\n * that axis — the pixel conversion belongs to the UI, not here.\n */\nexport function resizeAtDivider(\n tree: PaneNode,\n splitPath: SplitPath,\n boundaryIndex: number,\n deltaFraction: number,\n): PaneNode {\n const split = nodeAtPath(tree, splitPath);\n if (!split || split.kind !== \"split\") return tree;\n if (boundaryIndex < 0 || boundaryIndex > split.children.length - 2) return tree;\n if (!Number.isFinite(deltaFraction) || deltaFraction === 0) return tree;\n\n const before = split.sizes[boundaryIndex]!;\n const after = split.sizes[boundaryIndex + 1]!;\n // Each neighbour's floor is its own size when already under the minimum, so a sub-minimum pane\n // can be grown but never shrunk further — the \"born below the minimum\" case falls out of this.\n const giveable = after - Math.min(after, MIN_PANE_FRACTION);\n const takeable = before - Math.min(before, MIN_PANE_FRACTION);\n const delta = Math.max(-takeable, Math.min(giveable, deltaFraction));\n if (delta === 0) return tree;\n\n const resized = (node: SplitNode): SplitNode => {\n const sizes = node.sizes.slice();\n sizes[boundaryIndex] = before + delta;\n sizes[boundaryIndex + 1] = after - delta;\n return { kind: \"split\", direction: node.direction, children: node.children, sizes };\n };\n\n const rebuild = (node: PaneNode, depth: number): PaneNode => {\n if (node.kind !== \"split\") return node;\n if (depth === splitPath.length) return resized(node);\n const index = splitPath[depth]!;\n const children = node.children.slice();\n children[index] = rebuild(node.children[index]!, depth + 1);\n return { kind: \"split\", direction: node.direction, children, sizes: node.sizes.slice() };\n };\n\n return rebuild(tree, 0);\n}\n\n/**\n * The tree **as rendered**: leaves absent from `occupied` are dropped and collapsed exactly as\n * `removePane` would, without touching the stored tree.\n *\n * During restore a persisted pane may hold no tab yet; it must render as absent (its fraction\n * redistributed) while staying in the stored tree, because its tab may simply not have arrived from\n * the daemon yet. Pruning for real is tied to the hydration settle point, not to geometry.\n */\nexport function effectiveTree(tree: PaneNode, occupied: ReadonlySet<string>): PaneNode {\n const vacant = leafIds(tree).filter((id) => !occupied.has(id));\n if (vacant.length === 0) return tree;\n // `removePane` refuses the last leaf, so an entirely unoccupied tree lands on the single-leaf\n // terminal state rather than an empty tree.\n return vacant.reduce<PaneNode>((node, id) => removePane(node, id), tree);\n}\n\n// ─── Parsing untrusted persisted trees ─────────────────────────────────────────────────────\n\n/** Bumped only on a breaking layout-record shape change; a mismatch discards the whole record. */\nexport const PANE_LAYOUT_VERSION = 1;\n\n/**\n * Parse untrusted persisted JSON into a valid tree, or `null` when it is structurally damaged.\n * Never throws — callers treat `null` as \"start this workspace from a single pane\".\n *\n * The line between repair and rejection is deliberate. Sizes that merely drift from summing to 1\n * are **renormalized**: float error accumulates across save/load cycles and losing a layout to\n * rounding would be a bug. Structural damage — length mismatch, duplicate pane ids, non-finite or\n * non-positive sizes, a split with fewer than two children — is **rejected**, because a malformed\n * tree would otherwise crash geometry or render one pane twice.\n */\nexport function parsePaneTree(input: unknown): PaneNode | null {\n return parseNode(input, new Set());\n}\n\nfunction parseNode(input: unknown, seenIds: Set<string>): PaneNode | null {\n if (typeof input !== \"object\" || input === null) return null;\n const node = input as Partial<Record<keyof SplitNode | \"id\", unknown>>;\n\n if (node.kind === \"leaf\") {\n const { id } = node;\n if (typeof id !== \"string\" || id.trim() === \"\") return null;\n if (seenIds.has(id)) return null; // a repeated id would render the same pane twice\n seenIds.add(id);\n return { kind: \"leaf\", id };\n }\n\n if (node.kind !== \"split\") return null;\n if (node.direction !== \"row\" && node.direction !== \"column\") return null;\n if (!Array.isArray(node.children) || !Array.isArray(node.sizes)) return null;\n if (node.children.length < 2 || node.sizes.length !== node.children.length) return null;\n\n let total = 0;\n for (const size of node.sizes) {\n if (typeof size !== \"number\" || !Number.isFinite(size) || size <= 0) return null;\n total += size;\n }\n\n const children: PaneNode[] = [];\n for (const child of node.children) {\n const parsed = parseNode(child, seenIds);\n if (!parsed) return null; // damage propagates up: a partially valid tree is still discarded\n children.push(parsed);\n }\n\n return {\n kind: \"split\",\n direction: node.direction,\n children,\n // Renormalize unconditionally — no epsilon threshold — so \"sizes sum to 1\" is true by\n // construction for every tree that enters the store.\n sizes: (node.sizes as number[]).map((size) => size / total),\n };\n}\n","/**\n * Layout store — pane structure per workspace: the pane tree, which pane holds each tab, each\n * pane's active tab, and which pane is focused.\n *\n * **Division of labour with `tab-store`.** The spec requires one owner for the tab↔pane invariant,\n * never two mutable directions that can drift:\n *\n * - `tab-store` remains the single owner of tab identity and *global* tab order. A pane's tab order\n * is **derived** — the global order filtered by pane membership — so reordering within a pane\n * stays `tab-store.reorder` and there is no second order to fall out of sync.\n * - this store owns pane structure: `tree`, `placement` (tab id → pane id), `activeByPane`, and\n * `focusedPaneId`, keyed by normalized workspace cwd.\n * - the workspace-active tab is **derived** (`activeTabOf` = `activeByPane[focusedPaneId]`), never\n * stored — a stored duplicate can drift and point at an invisible tab.\n *\n * Because per-pane order lives in `tab-store`, the operations that must pick a fallback active tab\n * accept an optional `order` argument: the workspace's tab ids in global order. Callers that have it\n * (sprint-049's `tab-store` wiring) pass it so the fallback matches the strip the user sees; without\n * it the store falls back to `placement` insertion order, which is deterministic but not necessarily\n * the visible order.\n *\n * **Restore is claim-driven.** Tabs are not persisted — they are rebuilt from daemon state on\n * connect by two independent one-shot hooks, in no guaranteed order. So `installPersistedLayouts`\n * installs geometry immediately and holds the persisted `placement`/`activeByPane` as *pending\n * claims*, which `claimPaneFor` consumes as each tab arrives.\n *\n * The settle point (`markHydrationSource`) does more than cleanup, though: while it is still\n * pending, `claimPaneFor` refuses to let an UNCLAIMED arrival (an orphaned terminal the daemon\n * still has, a chat nothing persisted claimed) steal an already-occupied pane's active slot —\n * only a claim, or a genuinely empty pane, may take one. Without that guard, two independent\n * restore hooks racing in undefined order meant a terminal-restore arrival could silently replace\n * a chat a claim had *just* placed a moment earlier in the same pass — restore noise overwriting a\n * deliberate arrangement. After the settle point this restriction lifts: a live \"+\"/Ctrl+T open\n * into the focused pane taking it over immediately is the correct, expected behaviour.\n *\n * swe/features/workspace-split-panes.md § Tab ↔ pane assignment, § Splitting,\n * § Programmatic splits, § Moving a tab between panes, § Removing a pane / collapsing,\n * § Restoring a persisted layout\n */\n\nimport { create } from \"zustand\";\nimport {\n canSplit,\n leafIds,\n removePane,\n resizeAtDivider,\n splitPane,\n type PaneNode,\n type SplitPath,\n type SplitRegion,\n} from \"@pi-studio-ui/features/workspace/pane-tree.js\";\nimport type { ValidatedWorkspaceLayout } from \"@pi-studio-ui/lib/pane-layout-persistence.js\";\n\nexport interface WorkspacePaneLayout {\n tree: PaneNode;\n /** tab id → pane id. The single owner of the tab↔pane invariant. */\n placement: Record<string, string>;\n /** pane id → that pane's active tab id. Every value is placed in its own pane. */\n activeByPane: Record<string, string>;\n /** Always a leaf id present in `tree`. */\n focusedPaneId: string;\n\n // ── Restore state: session-local, never persisted ──\n /** Unconsumed persisted claims: tab identity → pane id. Emptied at the settle point. */\n pendingPlacement: Record<string, string>;\n /** Which identity each pane wants as its active tab. Held until the settle point. */\n pendingActive: Record<string, string>;\n /** Panes whose active tab the user chose directly — a claim must never displace it. */\n userActedPanes: ReadonlySet<string>;\n}\n\n/** The connect-time inventories whose arrival together means initial hydration is complete. */\nexport type HydrationSource = \"sessions\" | \"terminals\";\nconst HYDRATION_SOURCES: readonly HydrationSource[] = [\"sessions\", \"terminals\"];\n\nexport interface LayoutStoreState {\n /** Keyed by normalized workspace cwd (`workspace-grouping.ts#normalizeCwd`). */\n layouts: Record<string, WorkspacePaneLayout>;\n /** Which restore sources have reported. Hydration is complete once every one is `true`. */\n hydrationSources: Readonly<Record<HydrationSource, boolean>>;\n /** Whether a restore cycle is in flight — `true` from `installPersistedLayouts` until hydration\n * settles. Distinct from `!every(hydrationSources)`, which is also true before any restore cycle\n * has ever started (e.g. every unit test that never calls `installPersistedLayouts`); this field\n * exists so `claimPaneFor`'s restore-time guard only restricts unclaimed arrivals during an actual\n * restore, never ordinary live use. */\n restoring: boolean;\n /**\n * The workspace that was in view when the record was written, captured at boot and held until the\n * settle point — `null` when nothing was persisted.\n *\n * Captured rather than re-read on demand: `writePaneLayout` persists `activeWorkspaceCwd` from the\n * *current* tab-store value and writes fire throughout the restore window, so a mid-restore write\n * can clobber the stored target before a later reader gets to it. Consumers that must agree on\n * which workspace restore is heading for (`use-session-restore.ts`'s sidebar/explorer seeding, and\n * `restore-active-workspace.ts`'s view switch) read this one captured value instead of racing\n * localStorage.\n */\n pendingActiveWorkspace: string | null;\n\n /** Create the single-leaf default on first touch; idempotent. */\n ensureWorkspace(cwd: string): void;\n /**\n * Place a tab: the explicit pane when it exists, else the focused pane, else the first leaf.\n * Makes it that pane's active tab and focuses the pane unless `background`.\n */\n assignTab(\n cwd: string,\n tabId: string,\n paneId?: string,\n background?: boolean,\n order?: readonly string[],\n ): void;\n /**\n * Move a tab into another pane: activate it there, focus that pane, fall the source pane's active\n * tab back to its nearest remaining sibling, and remove the source pane if it is now empty.\n * A move into the pane the tab already occupies is a no-op — cross-pane reorder is `tab-store`'s.\n */\n moveTab(cwd: string, tabId: string, targetPaneId: string, order?: readonly string[]): void;\n /**\n * Split `targetPaneId` and move `tabId` into the new pane. No-op when the split is illegal or the\n * tab is the target pane's only tab — the new pane would immediately collapse, so nothing should\n * visibly happen.\n */\n splitWithTab(\n cwd: string,\n tabId: string,\n targetPaneId: string,\n region: SplitRegion,\n order?: readonly string[],\n ): void;\n /**\n * Split only, returning the new (focused, empty) pane id, or `null` when refused. The caller opens\n * a tab into it immediately — an empty pane cannot exist at rest.\n */\n splitEmpty(cwd: string, targetPaneId: string, region: SplitRegion): string | null;\n /** Drop a closed tab; an emptied pane collapses, except the last one or one still under claim. */\n removeTab(cwd: string, tabId: string, order?: readonly string[]): void;\n /** Set a pane's active tab *without* focusing it — restore needs to do exactly that. */\n setActiveTab(cwd: string, paneId: string, tabId: string): void;\n focusPane(cwd: string, paneId: string): void;\n resizeDivider(\n cwd: string,\n splitPath: SplitPath,\n boundaryIndex: number,\n deltaFraction: number,\n ): void;\n\n /** Install persisted geometry up front and hold its placement as pending claims, plus the\n * workspace that was in view, as `pendingActiveWorkspace`. */\n installPersistedLayouts(\n loaded: ReadonlyMap<string, ValidatedWorkspaceLayout>,\n activeWorkspaceCwd?: string | null,\n ): void;\n /**\n * Resolve the pane an arriving tab belongs in, consuming a matching claim. The single entry point\n * for every tab open, restored or user-initiated.\n */\n claimPaneFor(cwd: string, tabId: string, identity: string | null, paneId?: string): void;\n /**\n * Report that one restore source has finished. Once every source has, unconsumed claims are\n * discarded and unclaimed panes are pruned from the stored tree.\n */\n markHydrationSource(source: HydrationSource): void;\n\n paneOfTab(cwd: string, tabId: string): string | null;\n /** The workspace's active tab, derived from the focused pane. */\n activeTabOf(cwd: string): string | null;\n}\n\n// ─── Layout helpers (pure) ─────────────────────────────────────────────────────────────────\n\n/** Unique per tree, and never derived from a tab id — a pane outlives the tabs that pass through. */\nfunction mintPaneId(): string {\n return `pane-${crypto.randomUUID()}`;\n}\n\nexport function createPaneLayout(): WorkspacePaneLayout {\n const id = mintPaneId();\n return {\n tree: { kind: \"leaf\", id },\n placement: {},\n activeByPane: {},\n focusedPaneId: id,\n pendingPlacement: {},\n pendingActive: {},\n userActedPanes: new Set(),\n };\n}\n\n/** A pane's tabs in the caller's order (global tab order filtered by pane membership). */\nfunction tabsInPane(\n layout: WorkspacePaneLayout,\n paneId: string,\n order?: readonly string[],\n): string[] {\n const ids = order ?? Object.keys(layout.placement);\n return ids.filter((id) => layout.placement[id] === paneId);\n}\n\n/** The tab that should take over as active, mirroring `tab-store.close`'s nearest-by-index rule. */\nfunction nearestSibling(\n layout: WorkspacePaneLayout,\n paneId: string,\n leavingTabId: string,\n order?: readonly string[],\n): string | undefined {\n const tabs = tabsInPane(layout, paneId, order);\n const index = tabs.indexOf(leavingTabId);\n const remaining = tabs.filter((id) => id !== leavingTabId);\n if (remaining.length === 0) return undefined;\n return remaining[Math.min(Math.max(index, 0), remaining.length - 1)];\n}\n\n/**\n * Whether some persisted claim still points at this pane.\n *\n * Self-disabling: the settle point empties both pending maps, so after initial hydration this is\n * always `false` and imposes no cost on ordinary closes.\n */\nfunction hasPendingClaim(layout: WorkspacePaneLayout, paneId: string): boolean {\n if (layout.pendingActive[paneId] !== undefined) return true;\n return Object.values(layout.pendingPlacement).includes(paneId);\n}\n\n/**\n * Remove an **empty** pane and keep the invariants: its `activeByPane` entry goes with it, and focus\n * lands on a survivor. Only ever called for a pane with no tabs, so no `placement` entry can be left\n * pointing at a pane that is gone.\n *\n * Refuses while a claim still names the pane: restore is asynchronous, and closing some other tab\n * early must not destroy the pane of a terminal whose daemon listing has not arrived yet.\n */\nfunction withPaneRemoved(layout: WorkspacePaneLayout, paneId: string): WorkspacePaneLayout {\n if (hasPendingClaim(layout, paneId)) return layout;\n const tree = removePane(layout.tree, paneId);\n if (tree === layout.tree) return layout; // the last pane is kept and renders the empty state\n const activeByPane = { ...layout.activeByPane };\n delete activeByPane[paneId];\n const leaves = leafIds(tree);\n return {\n ...layout,\n tree,\n activeByPane,\n focusedPaneId: leaves.includes(layout.focusedPaneId) ? layout.focusedPaneId : leaves[0]!,\n };\n}\n\n/**\n * Reassign `tabId` to `pane`, cleaning up whatever pane it came from. The single write path for\n * `assignTab`, `moveTab`, and `claimPaneFor`, which is what makes \"every `activeByPane` value is\n * placed in its own pane\" hold by construction rather than by later repair.\n *\n * `activate` and `focus` are separate because a claim-driven arrival may legitimately become its\n * pane's active tab without moving focus — the persisted `activePaneId` already decided that.\n */\nfunction placeTab(\n layout: WorkspacePaneLayout,\n tabId: string,\n pane: string,\n options: { activate: boolean; focus: boolean; order?: readonly string[] },\n): WorkspacePaneLayout {\n const source = layout.placement[tabId];\n const placement = { ...layout.placement, [tabId]: pane };\n const activeByPane = { ...layout.activeByPane };\n if (options.activate) activeByPane[pane] = tabId;\n\n if (source !== undefined && source !== pane && activeByPane[source] === tabId) {\n const next = nearestSibling(layout, source, tabId, options.order);\n if (next === undefined) delete activeByPane[source];\n else activeByPane[source] = next;\n }\n\n const moved: WorkspacePaneLayout = {\n ...layout,\n placement,\n activeByPane,\n focusedPaneId: options.focus ? pane : layout.focusedPaneId,\n };\n const sourceIsEmpty =\n source !== undefined && source !== pane && !Object.values(placement).includes(source);\n return sourceIsEmpty ? withPaneRemoved(moved, source) : moved;\n}\n\n/** Mark a pane's active tab as user-chosen, so no later claim displaces it. */\nfunction withUserAction(layout: WorkspacePaneLayout, paneId: string): WorkspacePaneLayout {\n if (layout.userActedPanes.has(paneId)) return layout;\n return { ...layout, userActedPanes: new Set(layout.userActedPanes).add(paneId) };\n}\n\n/**\n * Initial hydration is complete: drop every unconsumed claim, prune the panes no tab ever claimed,\n * and give any pane holding tabs but no active tab one — its wanted identity never arrived.\n */\nfunction settleWorkspace(layout: WorkspacePaneLayout): WorkspacePaneLayout {\n // Claims go first, which is what lets `withPaneRemoved` stop refusing below.\n let settled: WorkspacePaneLayout = { ...layout, pendingPlacement: {}, pendingActive: {} };\n\n const occupied = new Set(Object.values(settled.placement));\n for (const paneId of leafIds(settled.tree)) {\n if (!occupied.has(paneId)) settled = withPaneRemoved(settled, paneId);\n }\n\n const activeByPane = { ...settled.activeByPane };\n let repaired = false;\n for (const paneId of leafIds(settled.tree)) {\n if (activeByPane[paneId] !== undefined) continue;\n const first = Object.keys(settled.placement).find((id) => settled.placement[id] === paneId);\n if (first !== undefined) {\n activeByPane[paneId] = first;\n repaired = true;\n }\n }\n return repaired ? { ...settled, activeByPane } : settled;\n}\n\n/**\n * Whether `tabId` is its own pane's active tab — the per-pane visibility test panels apply, as\n * opposed to `tab-store.activeTabId`, which names one tab per *workspace*. Pure, so it is testable\n * without a DOM; `tab-store.useIsTabVisible` is the subscribing wrapper.\n */\nexport function isPaneActiveTab(layout: WorkspacePaneLayout | undefined, tabId: string): boolean {\n if (layout === undefined) return false;\n const pane = layout.placement[tabId];\n return pane !== undefined && layout.activeByPane[pane] === tabId;\n}\n\n// ─── Store ─────────────────────────────────────────────────────────────────────────────────\n\nexport const useLayoutStore = create<LayoutStoreState>()((set, get) => {\n /**\n * Apply a pure layout transform, materializing the workspace's default layout on first touch.\n * `fn` returning `null` means \"no change\".\n */\n const update = (\n cwd: string,\n fn: (layout: WorkspacePaneLayout) => WorkspacePaneLayout | null,\n ): void => {\n set((s) => {\n const current = s.layouts[cwd] ?? createPaneLayout();\n const next = fn(current) ?? current;\n if (next === s.layouts[cwd]) return s;\n return { layouts: { ...s.layouts, [cwd]: next } };\n });\n };\n\n return {\n layouts: {},\n hydrationSources: { sessions: false, terminals: false },\n restoring: false,\n pendingActiveWorkspace: null,\n\n ensureWorkspace(cwd) {\n set((s) => (s.layouts[cwd] ? s : { layouts: { ...s.layouts, [cwd]: createPaneLayout() } }));\n },\n\n assignTab(cwd, tabId, paneId, background, order) {\n update(cwd, (layout) => {\n const leaves = leafIds(layout.tree);\n const pane =\n paneId !== undefined && leaves.includes(paneId)\n ? paneId\n : leaves.includes(layout.focusedPaneId)\n ? layout.focusedPaneId\n : leaves[0]!;\n const activate = background !== true;\n const placed = placeTab(layout, tabId, pane, { activate, focus: activate, order });\n return activate ? withUserAction(placed, pane) : placed;\n });\n },\n\n moveTab(cwd, tabId, targetPaneId, order) {\n update(cwd, (layout) => {\n if (layout.placement[tabId] === undefined) return null;\n if (layout.placement[tabId] === targetPaneId) return null;\n if (!leafIds(layout.tree).includes(targetPaneId)) return null;\n const placed = placeTab(layout, tabId, targetPaneId, {\n activate: true,\n focus: true,\n order,\n });\n return withUserAction(placed, targetPaneId);\n });\n },\n\n splitWithTab(cwd, tabId, targetPaneId, region, order) {\n update(cwd, (layout) => {\n if (!leafIds(layout.tree).includes(targetPaneId)) return null;\n if (!canSplit(layout.tree, targetPaneId, region)) return null;\n // The dragged tab being the target pane's only tab means the new pane would take it and the\n // source would collapse right back — a no-op, and it must not flash.\n const targetTabs = tabsInPane(layout, targetPaneId, order);\n if (targetTabs.length === 1 && targetTabs[0] === tabId) return null;\n\n const newPaneId = mintPaneId();\n const tree = splitPane(layout.tree, targetPaneId, region, newPaneId);\n if (tree === layout.tree) return null;\n const placed = placeTab({ ...layout, tree }, tabId, newPaneId, {\n activate: true,\n focus: true,\n order,\n });\n return withUserAction(placed, newPaneId);\n });\n },\n\n splitEmpty(cwd, targetPaneId, region) {\n let created: string | null = null;\n update(cwd, (layout) => {\n if (!leafIds(layout.tree).includes(targetPaneId)) return null;\n if (!canSplit(layout.tree, targetPaneId, region)) return null;\n const newPaneId = mintPaneId();\n const tree = splitPane(layout.tree, targetPaneId, region, newPaneId);\n if (tree === layout.tree) return null;\n created = newPaneId;\n return { ...layout, tree, focusedPaneId: newPaneId };\n });\n return created;\n },\n\n removeTab(cwd, tabId, order) {\n update(cwd, (layout) => {\n const pane = layout.placement[tabId];\n if (pane === undefined) return null;\n\n const placement = { ...layout.placement };\n delete placement[tabId];\n const activeByPane = { ...layout.activeByPane };\n if (activeByPane[pane] === tabId) {\n const next = nearestSibling(layout, pane, tabId, order);\n if (next === undefined) delete activeByPane[pane];\n else activeByPane[pane] = next;\n }\n\n const closed: WorkspacePaneLayout = { ...layout, placement, activeByPane };\n return Object.values(placement).includes(pane) ? closed : withPaneRemoved(closed, pane);\n });\n },\n\n setActiveTab(cwd, paneId, tabId) {\n update(cwd, (layout) => {\n if (layout.placement[tabId] !== paneId) return null;\n if (layout.activeByPane[paneId] === tabId) return withUserAction(layout, paneId);\n return withUserAction(\n { ...layout, activeByPane: { ...layout.activeByPane, [paneId]: tabId } },\n paneId,\n );\n });\n },\n\n focusPane(cwd, paneId) {\n update(cwd, (layout) => {\n if (layout.focusedPaneId === paneId) return null;\n if (!leafIds(layout.tree).includes(paneId)) return null;\n return { ...layout, focusedPaneId: paneId };\n });\n },\n\n resizeDivider(cwd, splitPath, boundaryIndex, deltaFraction) {\n update(cwd, (layout) => {\n const tree = resizeAtDivider(layout.tree, splitPath, boundaryIndex, deltaFraction);\n return tree === layout.tree ? null : { ...layout, tree };\n });\n },\n\n installPersistedLayouts(loaded, activeWorkspaceCwd = null) {\n set((s) => {\n const layouts = { ...s.layouts };\n for (const [cwd, entry] of loaded) {\n const leaves = leafIds(entry.tree);\n layouts[cwd] = {\n tree: entry.tree,\n placement: {},\n activeByPane: {},\n focusedPaneId:\n entry.activePaneId !== null && leaves.includes(entry.activePaneId)\n ? entry.activePaneId\n : leaves[0]!,\n pendingPlacement: { ...entry.placement },\n pendingActive: { ...entry.activeByPane },\n userActedPanes: new Set(),\n };\n }\n // A fresh install starts a fresh hydration cycle.\n return {\n layouts,\n hydrationSources: { sessions: false, terminals: false },\n restoring: true,\n // Only a workspace whose entry survived validation is worth heading for.\n pendingActiveWorkspace:\n activeWorkspaceCwd !== null && layouts[activeWorkspaceCwd] !== undefined\n ? activeWorkspaceCwd\n : null,\n };\n });\n },\n\n claimPaneFor(cwd, tabId, identity, paneId) {\n update(cwd, (layout) => {\n const leaves = leafIds(layout.tree);\n\n let pane: string | undefined;\n let fromClaim = false;\n if (paneId !== undefined && leaves.includes(paneId)) {\n pane = paneId;\n } else if (identity !== null) {\n const claimed = layout.pendingPlacement[identity];\n if (claimed !== undefined && leaves.includes(claimed)) {\n pane = claimed;\n fromClaim = true;\n }\n }\n pane ??= leaves.includes(layout.focusedPaneId) ? layout.focusedPaneId : leaves[0]!;\n\n let base = layout;\n if (fromClaim && identity !== null) {\n const pendingPlacement = { ...layout.pendingPlacement };\n delete pendingPlacement[identity]; // the claim is spent either way\n base = { ...layout, pendingPlacement };\n }\n\n // Never steal what the user chose, and never displace the identity this pane is waiting for.\n // A pane with no active tab yet always takes the arrival, so it is never left blank — when\n // the wanted identity turns up later it takes over, making the outcome order-independent.\n let activate = true;\n if (fromClaim) {\n const wanted = base.pendingActive[pane];\n if (base.userActedPanes.has(pane)) activate = false;\n else if (wanted !== undefined && wanted !== identity) {\n activate = base.activeByPane[pane] === undefined;\n }\n } else if (get().restoring) {\n // An UNCLAIMED arrival while hydration is still running is restore noise, not a user\n // action — an orphaned terminal the daemon still has, or a chat with no persisted claim\n // for it. It may still open (so nothing leaks silently), but it must never steal an\n // already-occupied pane's active slot out from under a claim that placed something there\n // moments earlier in this same restore pass; it only takes over a genuinely empty pane.\n // Once hydration completes this no longer applies — a live \"+\"/Ctrl+T open into the\n // focused pane taking it over immediately is exactly the correct, expected behaviour.\n activate = base.activeByPane[pane] === undefined;\n }\n\n const placed = placeTab(base, tabId, pane, {\n activate,\n // A claim-driven arrival never moves focus: the persisted `activePaneId` already chose it.\n focus: !fromClaim && activate,\n });\n return fromClaim ? placed : withUserAction(placed, pane);\n });\n },\n\n markHydrationSource(source) {\n set((s) => {\n if (s.hydrationSources[source]) return s;\n const hydrationSources = { ...s.hydrationSources, [source]: true };\n if (!HYDRATION_SOURCES.every((required) => hydrationSources[required])) {\n return { hydrationSources };\n }\n const layouts: Record<string, WorkspacePaneLayout> = {};\n for (const [cwd, layout] of Object.entries(s.layouts)) {\n layouts[cwd] = settleWorkspace(layout);\n }\n return { hydrationSources, layouts, restoring: false, pendingActiveWorkspace: null };\n });\n },\n\n paneOfTab(cwd, tabId) {\n return get().layouts[cwd]?.placement[tabId] ?? null;\n },\n\n activeTabOf(cwd) {\n const layout = get().layouts[cwd];\n if (!layout) return null;\n return layout.activeByPane[layout.focusedPaneId] ?? null;\n },\n };\n});\n","/**\n * Tab store — replaces the POC's `tabs[]`/`activeTabId` globals + `addTab`/`closeTab`/\n * `activateTab` (POC_TO_APP_PLAN_UI.md §4.2). `TabPanelHost` keeps inactive panels mounted but\n * hidden, so this store only tracks identity/order/label — not panel content.\n *\n * Tabs are scoped by workspace (§4.3/§4.7 follow-up: workspace-scoped tabs): every `Tab` carries\n * the (already `normalizeCwd`-normalized) workspace cwd it belongs to, and `activeWorkspaceCwd`\n * names which workspace's tabs `TabStrip`/`TabPanelHost` currently render. All tabs stay mounted\n * regardless of workspace — same \"hidden but alive\" model that already preserves terminal/scroll\n * state across in-workspace tab switches — only the visible SUBSET changes on a workspace switch.\n *\n * **`activeTabId` is derived, not owned.** Since sprint-049 every lifecycle mutation here goes\n * through `layout-store`, which owns pane structure: which pane holds each tab, each pane's active\n * tab, and which pane is focused. The workspace-active tab is that focused pane's active tab, and\n * this store's `activeTabId` field is a cached projection of it written by `syncActiveFromLayout`\n * alone — the dozens of `activeTabId` consumers keep working, without a second mutable truth that\n * can drift. A per-workspace \"last active tab\" map used to live here; the layout store's focused\n * pane + per-pane actives are the same information, now persisted across reloads, so it is gone.\n *\n * With splits, \"active\" and \"visible\" part company: a pane's active tab is visible even when\n * another pane is focused. Panels therefore ask `useIsTabVisible`, not `=== activeTabId`.\n */\n\nimport { create } from \"zustand\";\nimport { useSessionStore } from \"@pi-studio-ui/stores/session-store.js\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { ensureMaterialized, discardIfEmpty } from \"@pi-studio-ui/stores/materialize.js\";\nimport { isPaneActiveTab, useLayoutStore } from \"@pi-studio-ui/stores/layout-store.js\";\n\nexport type TabKind = \"chat\" | \"file\" | \"diff\" | \"terminal\" | \"molecule\";\n\nexport interface FileTabData {\n path: string;\n}\n\nexport interface DiffTabData {\n path: string;\n staged: boolean;\n}\n\nexport interface ChatTabData {\n sessionId: string;\n}\n\nexport interface TerminalTabData {\n slot: number | null;\n /** Cwd captured at shortcut-fire time — the terminal launches here regardless of which\n * session/workspace becomes active before its `create_terminal_request` resolves. */\n cwd: string;\n}\n\n/** `path: null` is the empty (\"+\"-menu) molecule tab — molviewer's own drag-drop empty state. */\nexport interface MoleculeTabData {\n path: string | null;\n}\n\nexport type TabData = ChatTabData | FileTabData | DiffTabData | TerminalTabData | MoleculeTabData;\n\nexport interface Tab {\n id: string;\n kind: TabKind;\n label: string;\n closable: boolean;\n data: TabData;\n /** Normalized workspace cwd this tab belongs to (see `workspace-grouping.ts#normalizeCwd`) —\n * fixed at creation time, never re-derived live from whatever session happens to be active. */\n workspaceCwd: string;\n}\n\ninterface TabStoreState {\n tabs: Tab[];\n /** The focused pane's active tab, projected from `layout-store` — see the module header. */\n activeTabId: string | null;\n /** Which workspace's tabs are currently visible in the strip/panel host. */\n activeWorkspaceCwd: string | null;\n\n /** Open a tab, or activate it if a tab with the same `id` already exists (POC `addTab`).\n * Always brings the tab's workspace into view. `targetPaneId` places it in a specific pane\n * (a pane's own \"+\" menu, or the pane a Split right/down just created); the default is the\n * focused pane. */\n open(tab: Tab, targetPaneId?: string): void;\n close(id: string): void;\n /** Activate a tab by id, bringing its workspace into view too. */\n activate(id: string): void;\n /** Switch the visible workspace, restoring its focused pane's active tab (or none). */\n switchWorkspace(cwd: string): void;\n reorder(fromId: string, toId: string): void;\n updateLabel(id: string, label: string): void;\n updateData(id: string, data: Partial<TabData>): void;\n /** Close every file/diff/molecule tab whose path is `prefix` or nested under it (e.g. a\n * directory delete closing every open tab underneath — `FileContextMenu.tsx`'s `remove()`).\n * No-op for chat/terminal tabs, which have no filesystem path. */\n closeByPathPrefix(prefix: string): void;\n}\n\nfunction tabsInWorkspace(tabs: Tab[], cwd: string): Tab[] {\n return tabs.filter((t) => t.workspaceCwd === cwd);\n}\n\n/** The strip order the layout store's nearest-sibling fallbacks need: global order, one workspace. */\nfunction workspaceOrder(tabs: Tab[], cwd: string): string[] {\n return tabsInWorkspace(tabs, cwd).map((t) => t.id);\n}\n\n/**\n * The single writer of `activeTabId`: recompute it from the layout store's focused pane and bring\n * `cwd` into view. Returns the tab it now names, for the sidebar sync.\n */\nfunction syncActiveFromLayout(cwd: string | null): Tab | undefined {\n const activeTabId = cwd === null ? null : useLayoutStore.getState().activeTabOf(cwd);\n useTabStore.setState({ activeTabId, activeWorkspaceCwd: cwd });\n if (activeTabId === null) return undefined;\n return useTabStore.getState().tabs.find((t) => t.id === activeTabId);\n}\n\n/** Keep the sidebar's `session-store.activeSessionId` in lockstep with whichever chat tab is\n * now active, so switching tabs in the center pane highlights the matching sidebar row exactly\n * as if it had been clicked there directly. No-op for non-chat tabs (file/diff/terminal) — the\n * sidebar simply keeps showing whichever chat was last active. */\nfunction syncActiveSession(tab: Tab | undefined): void {\n if (tab?.kind !== \"chat\") return;\n useSessionStore.getState().activate((tab.data as ChatTabData).sessionId);\n}\n\nexport const useTabStore = create<TabStoreState>()((set, get) => ({\n tabs: [],\n activeTabId: null,\n activeWorkspaceCwd: null,\n\n open(tab, targetPaneId) {\n const existing = get().tabs.find((t) => t.id === tab.id);\n if (existing) {\n get().activate(existing.id);\n return;\n }\n set((s) => ({ tabs: [...s.tabs, tab] }));\n const layout = useLayoutStore.getState();\n layout.ensureWorkspace(tab.workspaceCwd);\n // Every arrival — user-opened or restore-driven — resolves its pane here: the explicit target\n // when given, else a persisted claim naming it, else the focused pane. A claimed arrival may\n // land in a non-focused pane, which is why the sidebar syncs to whatever is active *afterwards*\n // rather than to `tab` itself.\n layout.claimPaneFor(tab.workspaceCwd, tab.id, tabIdentity(tab), targetPaneId);\n syncActiveSession(syncActiveFromLayout(tab.workspaceCwd));\n },\n\n close(id) {\n const closed = get().tabs.find((t) => t.id === id);\n if (!closed) return;\n // Captured before the removal: the layout store's fallback needs the leaving tab's own position\n // among its pane's tabs to pick the nearest survivor.\n const order = workspaceOrder(get().tabs, closed.workspaceCwd);\n const before = get().activeTabId;\n\n set((s) => ({ tabs: s.tabs.filter((t) => t.id !== id) }));\n useLayoutStore.getState().removeTab(closed.workspaceCwd, id, order);\n\n // The visible workspace never changes on a close, so the derivation stays on it — closing a tab\n // must not jump the workspace out from under the user.\n const next = syncActiveFromLayout(get().activeWorkspaceCwd);\n if (get().activeTabId !== before) syncActiveSession(next);\n },\n\n activate(id) {\n const tab = get().tabs.find((t) => t.id === id);\n if (!tab) return;\n const layout = useLayoutStore.getState();\n layout.ensureWorkspace(tab.workspaceCwd);\n const pane = layout.paneOfTab(tab.workspaceCwd, id);\n if (pane === null) {\n // No placement yet (a tab opened before its workspace had a layout) — resolve one now.\n layout.claimPaneFor(tab.workspaceCwd, id, tabIdentity(tab));\n } else {\n layout.focusPane(tab.workspaceCwd, pane);\n layout.setActiveTab(tab.workspaceCwd, pane, id);\n }\n syncActiveSession(syncActiveFromLayout(tab.workspaceCwd));\n },\n\n switchWorkspace(cwd) {\n useLayoutStore.getState().ensureWorkspace(cwd);\n // The workspace's focused pane and that pane's active tab ARE its remembered tab — persisted by\n // `pane-layout-persistence`, which is why no separate last-active map lives here any more.\n syncActiveSession(syncActiveFromLayout(cwd));\n },\n\n reorder(fromId, toId) {\n set((s) => {\n const fromIdx = s.tabs.findIndex((t) => t.id === fromId);\n const toIdx = s.tabs.findIndex((t) => t.id === toId);\n if (fromIdx === -1 || toIdx === -1 || fromIdx === toIdx) return s;\n const tabs = s.tabs.slice();\n const [moved] = tabs.splice(fromIdx, 1);\n if (!moved) return s;\n tabs.splice(toIdx, 0, moved);\n return { tabs };\n });\n },\n\n updateLabel(id, label) {\n set((s) => ({ tabs: s.tabs.map((t) => (t.id === id ? { ...t, label } : t)) }));\n },\n\n updateData(id, data) {\n set((s) => ({\n tabs: s.tabs.map((t) => (t.id === id ? { ...t, data: { ...t.data, ...data } } : t)),\n }));\n },\n\n closeByPathPrefix(prefix) {\n const matches = get().tabs.filter((t) => {\n if (t.kind !== \"file\" && t.kind !== \"diff\" && t.kind !== \"molecule\") return false;\n const path = (t.data as FileTabData | DiffTabData | MoleculeTabData).path;\n return path === prefix || (path?.startsWith(`${prefix}/`) ?? false);\n });\n for (const t of matches) get().close(t.id);\n },\n}));\n\n// ─── Identity & visibility ─────────────────────────────────────────────────────────────────\n\n/**\n * A tab's stable cross-session identity, or `null` when it has none yet. Identity — not tab id — is\n * what `pane-layout-persistence` keys a pane assignment on, because ids are not stable across a\n * reconnect: `openNewTerminal` mints `term-new-<n>` while `use-terminal-restore` reopens the same\n * terminal as `term-<slot>`. Keys are kind-prefixed, so a `file` and a `molecule` tab on the same\n * absolute path cannot collide. Identity-less tabs (a terminal awaiting its daemon slot, an empty\n * molecule tab) are simply omitted from the record — there is nothing to restore them against.\n *\n * Lives here rather than in `pane-layout-persistence` because `tab-store` must call it on every\n * open, and that module already depends on this one.\n */\nexport function tabIdentity(tab: Tab): string | null {\n switch (tab.kind) {\n case \"chat\": {\n // NOT the session id: that is client-local and regenerated on every load (`s-<seq>` when\n // created here, `s-<agentId prefix>` when restored — `use-session-restore.ts`), so a chat's\n // pane would never be recognised after a reload. The daemon-side agent id is the stable name;\n // a draft whose `createAgent` has not landed yet has no identity to persist (a window of one\n // RPC — `materialize.ts` binds it right after the tab appears).\n const { sessionId } = tab.data as ChatTabData;\n const agentId = useSessionStore.getState().sessions[sessionId]?.agentId;\n return agentId ? `agent:${agentId}` : null;\n }\n case \"file\":\n return `file:${(tab.data as FileTabData).path}`;\n case \"diff\": {\n const { path, staged } = tab.data as DiffTabData;\n return `diff:${staged ? \"staged\" : \"worktree\"}:${path}`;\n }\n case \"terminal\": {\n const { slot } = tab.data as TerminalTabData;\n return slot === null ? null : `terminal:${slot}`;\n }\n case \"molecule\": {\n const { path } = tab.data as MoleculeTabData;\n return path === null ? null : `molecule:${path}`;\n }\n }\n}\n\n/**\n * Whether a tab is on screen: its workspace is in view **and** it is its own pane's active tab.\n *\n * Not `=== activeTabId`. With splits every pane shows a tab simultaneously, so a pane's active tab\n * is visible even while another pane is focused — panels that measure themselves or refit a terminal\n * grid must react to this, not to workspace-wide activeness.\n */\nexport function useIsTabVisible(tabId: string): boolean {\n // Both selectors return primitives on purpose: a panel must not re-render every time some\n // unrelated tab's label changes or another pane is rearranged.\n const cwd = useTabStore((s) =>\n s.tabs.find((t) => t.id === tabId)?.workspaceCwd === s.activeWorkspaceCwd\n ? s.activeWorkspaceCwd\n : null,\n );\n return useLayoutStore((s) => (cwd === null ? false : isPaneActiveTab(s.layouts[cwd], tabId)));\n}\n\n// ─── Tab id / open helpers (POC id conventions: \"chat-<id>\", \"file-<path>\", …) ────────────\n\nexport const tabIds = {\n chat: (sessionId: string) => `chat-${sessionId}`,\n file: (path: string) => `file-${path}`,\n diff: (path: string, staged: boolean) => `diff-${path}${staged ? \"-staged\" : \"\"}`,\n terminal: (slotOrToken: number | string) => `term-${slotOrToken}`,\n molecule: (key: string | number) => `mol-${key}`,\n};\n\nlet terminalCount = 0;\n\n/** Open a brand-new terminal tab against a workspace cwd (no slot yet — `TerminalPanel` creates\n * one on mount). Shared by the `Ctrl/Cmd+T` shortcut (`use-shortcuts.ts`) and the TabStrip's \"+\"\n * button so both paths mint tab ids/labels identically.\n *\n * `targetPaneId` places the tab in a named pane; omitted (the shortcut's case) it lands in the\n * focused pane. Same contract on `openNewMolecule`/`openNewChat` below. */\nexport function openNewTerminal(workspaceCwd: string, targetPaneId?: string): void {\n terminalCount += 1;\n useTabStore.getState().open(\n {\n id: tabIds.terminal(`new-${terminalCount}`),\n kind: \"terminal\",\n label: `Terminal ${terminalCount}`,\n closable: true,\n data: { slot: null, cwd: workspaceCwd },\n workspaceCwd,\n },\n targetPaneId,\n );\n}\n\nlet moleculeCount = 0;\n\n/** Open a brand-new empty molecule tab (no path — molviewer's own drag-drop empty state). Shared\n * by the \"+\" menu so it mints tab ids/labels identically wherever a future shortcut adds another\n * entry point. */\nexport function openNewMolecule(workspaceCwd: string, targetPaneId?: string): void {\n moleculeCount += 1;\n useTabStore.getState().open(\n {\n id: tabIds.molecule(`new-${moleculeCount}`),\n kind: \"molecule\",\n label: `Molecule ${moleculeCount}`,\n closable: true,\n data: { path: null },\n workspaceCwd,\n },\n targetPaneId,\n );\n}\n\n/** Open a brand-new chat: creates a session against a workspace cwd and opens/focuses its chat\n * tab. Shared by the sidebar's \"+ New conversation\" button, `open-workspace.ts`'s create-new\n * path, and the TabStrip's \"+\" button — so every caller mints identical tab ids/labels (mirrors\n * `openNewTerminal` above). `workspaceCwd` MUST already be normalized by the caller (same\n * contract as `openNewTerminal`).\n *\n * Materializes eagerly: the tab and sidebar row appear synchronously from `createSession`/`open`\n * above, then `ensureMaterialized` (`materialize.ts`) commits a real, persisted `AgentRecord` in\n * the background — no spawn yet, just the record (`agent-service.ts` `handleCreate`'s\n * deferred-draft branch) — so every \"New chat\" has a bound `agentId` before the user does\n * anything. Best-effort: a failure here (or opening while disconnected, guarded by `if (client)`)\n * is retried by `Composer.submit`'s own `ensureMaterialized` call on the first send. */\nexport function openNewChat(workspaceCwd: string, targetPaneId?: string): void {\n const id = useSessionStore.getState().createSession(workspaceCwd);\n useTabStore.getState().open(\n {\n id: tabIds.chat(id),\n kind: \"chat\",\n label: \"New chat\",\n closable: true,\n data: { sessionId: id },\n workspaceCwd,\n },\n targetPaneId,\n );\n\n const client = useConnectionStore.getState().client;\n if (client) {\n void ensureMaterialized(client, id).catch(() => {\n // Best-effort: `Composer.submit`'s own `ensureMaterialized` retries on the first send.\n });\n }\n}\n\n/** Close a tab, discarding a never-used chat's draft record with it (`materialize.ts`\n * `discardIfEmpty`). Every UI close path MUST go through this, not `useTabStore.close` directly.\n */\nexport function closeTab(tabId: string): void {\n const tab = useTabStore.getState().tabs.find((t) => t.id === tabId);\n useTabStore.getState().close(tabId);\n if (tab?.kind !== \"chat\") return;\n const { sessionId } = tab.data as ChatTabData;\n void discardIfEmpty(useConnectionStore.getState().client, sessionId);\n}\n\n/**\n * `activeTabId` is a *projection* of layout state, so it has to follow **every** layout mutation — not\n * only the ones that originate in this store's own methods. Clicking into another pane calls\n * `layout-store.focusPane` directly (`TabStrip`'s and `TabPanelHost`'s `onPointerDown`), which changes\n * which tab is effectively active without touching this store at all; the same is true of any future\n * focus path (drag commit, keyboard pane navigation, a close fallback).\n *\n * Without this, focusing a second conversation left `activeTabId` — and through `syncActiveSession`,\n * `session-store.activeSessionId` — pointing at the previously focused pane's chat, so the status bar\n * kept showing the *old* conversation's model, context, tokens and cost while the user typed into a\n * different one. Making the projection genuinely derived is the only fix that also covers the next\n * caller who focuses a pane without going through here.\n *\n * Cheap enough to sit on every layout write, including a divider drag's per-frame mutations: it reads\n * one derived id and bails unless that id actually changed.\n */\nuseLayoutStore.subscribe(() => {\n const { activeWorkspaceCwd, activeTabId } = useTabStore.getState();\n if (activeWorkspaceCwd === null) return;\n if (useLayoutStore.getState().activeTabOf(activeWorkspaceCwd) === activeTabId) return;\n syncActiveSession(syncActiveFromLayout(activeWorkspaceCwd));\n});\n","/**\n * Panel — full-height flex-column container, the shell every tab/sidebar panel starts from\n * (ChatPanel, FilePanel, MoleculeViewerPanel, RightSidebar, TextViewer, SessionList — all were\n * byte-identical local `.panel`/`.wrap` CSS classes). ui-components.md § Surfaces\n */\n\nimport { type HTMLAttributes } from \"react\";\nimport { clsx } from \"clsx\";\nimport styles from \"./Panel.module.css\";\n\nexport type PanelProps = HTMLAttributes<HTMLDivElement>;\nexport function Panel({ className, ...rest }: PanelProps) {\n return <div className={clsx(styles.panel, className)} {...rest} />;\n}\n","/**\n * EmptyState — centered, muted placeholder text (loading/error/no-results states) — the shared\n * shape behind FileExplorer/ChangesPanel/OpenWorkspaceDialog/SessionList's plain-text variant and\n * FilePanel/MoleculeViewer's `<Spinner/> Loading…` variant (both are a centered flex row; a\n * lone text child renders identically to the plain-text sites' old `text-align: center`).\n * ui-components.md § Feedback\n */\n\nimport { type HTMLAttributes } from \"react\";\nimport { clsx } from \"clsx\";\nimport styles from \"./EmptyState.module.css\";\n\nexport type EmptyStateProps = HTMLAttributes<HTMLDivElement>;\n\nexport function EmptyState({ className, ...rest }: EmptyStateProps) {\n return <div className={clsx(styles.emptyState, className)} {...rest} />;\n}\n","/**\n * Explorer store — the right-sidebar Files tab's tree-expansion state (POC `currentFilePath`\n * global, superseded by a tree view: POC_TO_APP_PLAN_UI.md §4.7 / §3 `stores/explorer-store.ts`).\n * File listing data itself is TanStack Query cache (see `hooks/use-explorer.ts`), NOT store\n * state — this store only tracks *which directories are expanded*, not their contents.\n *\n * `expandedByRoot` remembers each workspace's expanded-path set across workspace-tab switches,\n * keyed by that workspace's resolved root path — restored when `setRoot` is called again with the\n * same root (in-memory only, for the lifetime of the session; not persisted to disk).\n */\n\nimport { create } from \"zustand\";\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\n\ninterface ExplorerStoreState {\n /** The active workspace's resolved cwd — the tree root. Rendered as the tree's own first row,\n * a collapsible directory row with its children at depth 1 (`file-tree.ts`'s `flattenTree`);\n * there is nothing above it to show, since the explorer is workspace-scoped, not a general\n * filesystem browser. */\n rootPath: string;\n /** Absolute paths of directories currently expanded in the tree. Contains `rootPath` unless\n * the user has collapsed the root row itself. */\n expanded: Set<string>;\n /** Session memory of each workspace's `expanded` set, keyed by its resolved root path. Keys\n * are inserted dynamically as workspaces are visited this session, so a `Map` (not `Record`)\n * is the right fit here. */\n expandedByRoot: Map<string, Set<string>>;\n\n /** Switch the tree root — called when the active workspace changes. Saves the outgoing root's\n * `expanded` set into `expandedByRoot` and restores the incoming root's remembered set. A root\n * never visited this session is seeded expanded; a *remembered* set is restored verbatim, so a\n * deliberately collapsed root stays collapsed across workspace switches. */\n setRoot(path: string): void;\n toggle(path: string): void;\n /** In-progress inline \"new file\"/\"new folder\" row in the tree: the directory it will be\n * created in, and which kind. Only one draft at a time. */\n draft: { parentPath: string; kind: \"file\" | \"directory\" } | null;\n /** Begin an inline draft under `parentPath`, expanding it so the draft row is visible. */\n startDraft(parentPath: string, kind: \"file\" | \"directory\"): void;\n cancelDraft(): void;\n /** Absolute path of the row currently being renamed in place, or null. Only one inline editor\n * exists at a time, so this and `draft` are never both set. */\n renaming: string | null;\n /** Begin renaming `path`; discards any in-progress create draft. */\n startRename(path: string): void;\n cancelRename(): void;\n /** Last-clicked row (directory or file) — where \"New File\"/\"New Folder\" (header toolbar and\n * the empty-space context menu) create their new entry: the selected directory itself, or the\n * selected file's parent directory. `null` (nothing selected yet) falls back to `rootPath`. */\n selected: { path: string; isDirectory: boolean } | null;\n setSelected(selected: { path: string; isDirectory: boolean } | null): void;\n /** Rewrite tree state after a move: any expanded path equal to `from` or nested under it\n * becomes the same path under `to`, and `selected` follows the same rewrite. Also expands\n * `toParent` so the moved item is visible where it landed. */\n repathAfterMove(from: string, to: string, toParent: string): void;\n}\n\nexport const useExplorerStore = create<ExplorerStoreState>()((set) => ({\n rootPath: \"\",\n expanded: new Set(),\n expandedByRoot: new Map(),\n draft: null,\n renaming: null,\n selected: null,\n\n setRoot: (path) =>\n set((s) => {\n const expandedByRoot = new Map(s.expandedByRoot);\n if (s.rootPath) expandedByRoot.set(s.rootPath, s.expanded);\n const remembered = expandedByRoot.get(path);\n return {\n rootPath: path,\n expanded: remembered ? new Set(remembered) : new Set([path]),\n expandedByRoot,\n draft: null,\n selected: null,\n renaming: null,\n };\n }),\n\n toggle: (path) =>\n set((s) => {\n const expanded = new Set(s.expanded);\n if (expanded.has(path)) expanded.delete(path);\n else expanded.add(path);\n const expandedByRoot = new Map(s.expandedByRoot);\n expandedByRoot.set(s.rootPath, expanded);\n return { expanded, expandedByRoot };\n }),\n\n startDraft: (parentPath, kind) =>\n set((s) => {\n const expanded = new Set(s.expanded);\n expanded.add(parentPath);\n const expandedByRoot = new Map(s.expandedByRoot);\n if (s.rootPath) expandedByRoot.set(s.rootPath, expanded);\n return { draft: { parentPath, kind }, expanded, expandedByRoot, renaming: null };\n }),\n\n cancelDraft: () => set({ draft: null }),\n\n startRename: (path) => set({ renaming: path, draft: null }),\n\n cancelRename: () => set({ renaming: null }),\n\n setSelected: (selected) => set({ selected }),\n\n repathAfterMove: (from, to, toParent) =>\n set((s) => {\n const rewrite = (p: string) =>\n p === from ? to : p.startsWith(`${from}/`) ? to + p.slice(from.length) : p;\n\n const expanded = new Set([...s.expanded].map(rewrite));\n expanded.add(toParent);\n const expandedByRoot = new Map(s.expandedByRoot);\n if (s.rootPath) expandedByRoot.set(s.rootPath, expanded);\n\n const selected = s.selected\n ? { path: rewrite(s.selected.path), isDirectory: s.selected.isDirectory }\n : null;\n\n return { expanded, expandedByRoot, selected, renaming: null };\n }),\n}));\ninterface ExplorerEntry {\n name?: string;\n kind?: string;\n type?: string;\n}\n\ninterface ExplorerRpcResult {\n entries?: ExplorerEntry[];\n}\n\ninterface ExplorerRpcResponse {\n result?: ExplorerRpcResult;\n entries?: ExplorerEntry[];\n}\n\nlet cachedHomeDir: string | null = null;\ntype HomeDirListener = (homeDir: string) => void;\nconst homeDirListeners = new Set<HomeDirListener>();\n\n/** Subscribe to the resolved home dir becoming available; returns an unsubscribe function. */\nexport function onHomeDirResolved(listener: HomeDirListener): () => void {\n homeDirListeners.add(listener);\n return () => homeDirListeners.delete(listener);\n}\n\n/**\n * Ports the POC's `resolveHome()` (chat.html ~line 1055-1056): probes `file_explorer_request`\n * for `/home`, takes the first directory entry, and caches the result for the process lifetime.\n * A failure (e.g. requested before the connection is fully open) returns `/tmp` for that call but\n * is NOT cached, so a later call — once connected — can still resolve the real home dir.\n */\nexport async function resolveHome(client: PiStudioClient): Promise<string> {\n if (cachedHomeDir) return cachedHomeDir;\n try {\n const response = await client.connection.request<ExplorerRpcResponse>(\"file_explorer_request\", {\n path: \"/home\",\n });\n const entries = response.result?.entries ?? response.entries ?? [];\n const first = entries.find((e) => e.kind === \"directory\" || e.type === \"directory\");\n cachedHomeDir = \"/home/\" + (first?.name || \"user\");\n } catch {\n return \"/tmp\";\n }\n for (const listener of homeDirListeners) listener(cachedHomeDir);\n return cachedHomeDir;\n}\n\n/** Synchronous best-effort peek at the cached home dir (populated once `resolveHome` has run). */\nexport function peekCachedHomeDir(): string | null {\n return cachedHomeDir;\n}\n\n/** Expand a leading `~` (POC's `(path === \"~\" || path.startsWith(\"~/\")) ? …` pattern). */\nexport async function resolveTildePath(client: PiStudioClient, path: string): Promise<string> {\n if (path === \"~\" || path.startsWith(\"~/\")) {\n const home = await resolveHome(client);\n return home + path.slice(1);\n }\n return path;\n}\n","/**\n * Resolves (and reactively exposes) the daemon's home directory, so `~`-relative session `cwd`s\n * can be normalized against their absolute form for workspace grouping (§4.3 sidebar tree).\n * Triggers `resolveHome` once the connection is fully open; re-renders the caller once it resolves.\n */\n\nimport { useEffect, useState } from \"react\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport {\n resolveHome,\n peekCachedHomeDir,\n onHomeDirResolved,\n} from \"@pi-studio-ui/stores/explorer-store.js\";\n\nexport function useHomeDir(): string | null {\n const client = useConnectionStore((s) => s.client);\n const status = useConnectionStore((s) => s.status);\n const [homeDir, setHomeDir] = useState<string | null>(peekCachedHomeDir());\n\n useEffect(() => onHomeDirResolved(setHomeDir), []);\n\n useEffect(() => {\n if (!client || status !== \"open\" || homeDir) return;\n void resolveHome(client);\n }, [client, status, homeDir]);\n\n return homeDir;\n}\n","/**\n * IconButton — compact, chromeless icon affordance for row actions and menu triggers (file-row\n * \"⋮\", session-row \"⋮\", workspace-header chevron/\"⋮\", Dialog's inline close, TabStrip's \"+\").\n * Distinct from `Button`'s `iconOnly` mode: `Button` sizes from 28px up (a labeled-CTA minimum\n * tap target) and hovers to a fixed `surface1` — too large and the wrong hover color for a tiny\n * hover-reveal affordance sitting inside an already-compact row (20px tall, absolutely\n * positioned, opacity-on-row-hover) against a variety of ambient backgrounds\n * (`surfaceSidebar`/`surfaceWorkspace`/page `background`/`surface1`, matching each row's own\n * container — see `--icon-btn-hover-base` below). ui-components.md § Pressables\n */\n\nimport { type ButtonHTMLAttributes, type CSSProperties } from \"react\";\nimport { clsx } from \"clsx\";\nimport styles from \"./IconButton.module.css\";\n\ntype StyleWithVars = CSSProperties & { [customProperty: `--${string}`]: string | undefined };\n\nexport type IconButtonSize = \"xs\" | \"sm\";\n\nexport interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n /** `xs` (20px) — row actions and menu triggers, the common case. `sm` (28px) — a standalone\n * button not embedded in a compact row, e.g. Dialog's inline header close. */\n size?: IconButtonSize;\n /** Overrides the hover background's base color (mixed 85/15 with `foreground`, matching\n * `theme/color-utils.ts`'s app-wide hover-lift recipe) — defaults to `--pi-color-surface1`,\n * the right base for a button sitting directly on a panel surface. A button sitting on a\n * different ambient background (the sidebar, page background, a workspace header) passes the\n * matching token, e.g. `hoverBase=\"var(--pi-color-surfaceSidebar)\"`. */\n hoverBase?: string;\n}\n\nexport function IconButton({\n size = \"xs\",\n hoverBase,\n className,\n style,\n type = \"button\",\n ...rest\n}: IconButtonProps) {\n return (\n <button\n type={type}\n className={clsx(styles.iconBtn, styles[size], className)}\n style={\n hoverBase ? ({ \"--icon-btn-hover-base\": hoverBase, ...style } as StyleWithVars) : style\n }\n {...rest}\n />\n );\n}\n","// AgentStatusDot — state-bucket → color mapping.\n// ui-components.md § Status dots & avatars\n\n// Agent status strings that the daemon can send (from agent-sessions.md).\nexport type AgentStatus =\n | \"idle\"\n | \"running\"\n | \"waiting\"\n | \"finished\"\n | \"error\"\n | \"queued\"\n | \"archived\";\n\nexport type AttentionReason = \"finished\" | \"error\" | \"permission\";\n\nexport type StatusDotInput = {\n status: AgentStatus | undefined | null;\n requiresAttention?: boolean;\n attentionReason?: AttentionReason;\n pendingPermissionCount?: number;\n showInactive?: boolean;\n};\n\nexport type StatusDotColor =\n | \"accent\" // running/active\n | \"statusSuccess\" // finished-ok / success\n | \"statusDanger\" // error / attention\n | \"statusWarning\" // waiting / pending permission\n | \"foregroundMuted\"; // idle / inactive\n\n/**\n * Returns the theme token key for the status dot color, or null if the dot\n * should not be rendered (missing/invalid status and showInactive=false).\n */\nexport function statusDotColor(input: StatusDotInput): StatusDotColor | null {\n const { status, requiresAttention, attentionReason, showInactive } = input;\n\n if (!status) return null;\n\n // Attention overrides the base status color.\n if (requiresAttention) {\n switch (attentionReason) {\n case \"permission\":\n return \"statusWarning\";\n case \"error\":\n return \"statusDanger\";\n case \"finished\":\n default:\n return \"statusSuccess\";\n }\n }\n\n switch (status) {\n case \"running\":\n case \"queued\":\n return \"accent\";\n case \"waiting\":\n return \"statusWarning\";\n case \"finished\":\n return \"statusSuccess\";\n case \"error\":\n return \"statusDanger\";\n case \"idle\":\n case \"archived\":\n return showInactive ? \"foregroundMuted\" : null;\n }\n}\n\n// Status dot dimensions per the spec (8×8 round).\nexport const STATUS_DOT_SIZE = 8;\n","/**\n * AgentStatusDot — 8×8 circle whose color reflects agent status.\n * ui-components.md § Status dots & avatars\n */\n\nimport styles from \"./StatusDot.module.css\";\nimport {\n statusDotColor,\n STATUS_DOT_SIZE,\n type StatusDotInput,\n} from \"@pi-studio-ui/ui/status-dot.js\";\n\nconst COLOR_TOKEN_VAR: Record<string, string> = {\n accent: \"var(--pi-color-accent)\",\n statusSuccess: \"var(--pi-color-statusSuccess)\",\n statusDanger: \"var(--pi-color-statusDanger)\",\n statusWarning: \"var(--pi-color-statusWarning)\",\n foregroundMuted: \"var(--pi-color-foregroundMuted)\",\n};\n\nexport type StatusDotProps = StatusDotInput & {\n className?: string;\n};\n\nexport function StatusDot({ className, ...input }: StatusDotProps) {\n const colorToken = statusDotColor(input);\n if (!colorToken) return null;\n\n // Running (and not overridden by an attention state) gets a spinning ring instead of a flat\n // dot — a static \"blue circle\" reads as just another status color, not as work in progress.\n const spinning = input.status === \"running\" && !input.requiresAttention;\n const color = spinning\n ? \"var(--pi-color-accentBright, #a2b4d7)\" // brighter than plain accent — must read clearly\n : // against the dark surface at the small size this ring renders at.\n (COLOR_TOKEN_VAR[colorToken] ?? \"currentColor\");\n\n return (\n <span\n className={`${spinning ? styles.spinner : styles.dot}${className ? ` ${className}` : \"\"}`}\n style={\n spinning\n ? { borderTopColor: color, borderRightColor: color }\n : { width: STATUS_DOT_SIZE, height: STATUS_DOT_SIZE, backgroundColor: color }\n }\n role=\"presentation\"\n />\n );\n}\n","/**\n * Protocol `AgentStatus` (initializing/idle/running/error/closed) → design-system\n * `StatusDotInput.status` (idle/running/waiting/finished/error/queued/archived). The two enums\n * differ because the design system's dot vocabulary predates this protocol; this is the single\n * translation point.\n */\n\nimport type { AgentStatus as ProtocolAgentStatus } from \"@av-pi-studio/protocol\";\nimport type { AgentStatus as DotAgentStatus } from \"@pi-studio-ui/ui/status-dot.js\";\n\nconst MAP: Record<ProtocolAgentStatus, DotAgentStatus> = {\n initializing: \"queued\",\n idle: \"idle\",\n running: \"running\",\n error: \"error\",\n closed: \"finished\",\n};\n\nexport function toDotStatus(status: ProtocolAgentStatus | \"idle\"): DotAgentStatus {\n return MAP[status] ?? \"idle\";\n}\n","/**\n * One row in the session list (POC `renderSessions` per-item markup, POC_TO_APP_PLAN_UI.md §4.2).\n * Presentational: `SessionList.tsx` owns selection, the context menu, and the drag payload.\n */\n\nimport type { DragEvent } from \"react\";\nimport { MoreVertical } from \"lucide-react\";\nimport { clsx } from \"clsx\";\nimport { IconButton } from \"@pi-studio-ui/components/primitives/IconButton.js\";\nimport { StatusDot } from \"@pi-studio-ui/components/primitives/StatusDot.js\";\nimport type { SessionEntry } from \"@pi-studio-ui/stores/session-store.js\";\nimport { toDotStatus } from \"./status-map.js\";\nimport styles from \"./SessionList.module.css\";\n\nexport interface SessionItemProps {\n session: SessionEntry;\n active: boolean;\n /** False for a row outside the workspace currently in view — see `SessionList`'s\n * `handleDragStart` for why those rows must not be draggable into a pane at all. */\n draggable: boolean;\n onSelect: () => void;\n onOpenMenu: (x: number, y: number) => void;\n onDragStartRow: (e: DragEvent) => void;\n}\n\nexport function SessionItem({\n session,\n active,\n draggable,\n onSelect,\n onOpenMenu,\n onDragStartRow,\n}: SessionItemProps) {\n return (\n <div\n className={clsx(styles.item, active && styles.active)}\n draggable={draggable}\n onDragStart={onDragStartRow}\n onClick={onSelect}\n onContextMenu={(ev) => {\n ev.preventDefault();\n onOpenMenu(ev.clientX, ev.clientY);\n }}\n >\n <div className={styles.title}>\n <StatusDot status={toDotStatus(session.status)} showInactive />\n <span>{session.title}</span>\n </div>\n <div className={styles.meta}>\n {session.cwd || \"~\"} · {session.agentId ? session.agentId.slice(0, 8) : \"new\"} ·{\" \"}\n {session.userMessageCount} msgs\n </div>\n <IconButton\n className={styles.menuBtn}\n hoverBase=\"var(--pi-color-surfaceSidebar)\"\n title=\"Actions\"\n onClick={(ev) => {\n ev.stopPropagation();\n const rect = ev.currentTarget.getBoundingClientRect();\n onOpenMenu(rect.left, rect.bottom);\n }}\n >\n <MoreVertical size={14} />\n </IconButton>\n </div>\n );\n}\n","/**\n * Menu — shared chrome for every Radix `DropdownMenu` popup in the app (file/session/workspace\n * right-click context menus, TabStrip's \"+\" menu, ModelMenu's picker). `DropdownMenu.Root` and\n * `.Trigger` need no shared styling and stay imported directly from `@radix-ui/react-dropdown-menu`\n * at each call site; this only wraps the pieces that were previously four (or more) byte-identical\n * CSS Modules: the invisible cursor-anchored trigger button used by every right-click menu, the\n * popover surface, the row, and the separator.\n * ui-components.md § Menus & popovers\n */\n\nimport { forwardRef, type ComponentPropsWithoutRef, type CSSProperties } from \"react\";\nimport * as DropdownMenu from \"@radix-ui/react-dropdown-menu\";\nimport { clsx } from \"clsx\";\nimport styles from \"./Menu.module.css\";\n\nexport interface MenuCursorTriggerProps {\n /** Viewport coordinates (from the triggering `contextmenu`/click event) to anchor the popup at. */\n x: number;\n y: number;\n}\n\n/** Invisible 1x1 trigger positioned at cursor coordinates — the shared shape behind every\n * right-click context menu: `DropdownMenu.Trigger asChild` wraps this so Radix anchors its\n * popover at the click point instead of at a visible element. */\nexport const MenuCursorTrigger = forwardRef<HTMLButtonElement, MenuCursorTriggerProps>(\n function MenuCursorTrigger({ x, y }, ref) {\n return (\n <button\n ref={ref}\n type=\"button\"\n className={styles.trigger}\n style={{ left: x, top: y }}\n aria-hidden\n tabIndex={-1}\n />\n );\n },\n);\n\nexport interface MenuContentProps extends ComponentPropsWithoutRef<typeof DropdownMenu.Content> {\n /** Popover min-width — right-click menus default to a narrow 140-160px; wider pickers (e.g.\n * ModelMenu) override. */\n minWidth?: number;\n}\n\n/** Popover surface (background/border/radius/shadow) + Radix portal, `align=\"start\"` and a\n * `sideOffset` of 2 to match the tight right-click-menu default; callers with a visible,\n * button-style trigger (TabStrip's \"+\" menu, ModelMenu) pass a larger `sideOffset`. */\nexport function MenuContent({\n className,\n minWidth,\n style,\n sideOffset = 2,\n ...rest\n}: MenuContentProps) {\n const mergedStyle: CSSProperties | undefined =\n minWidth === undefined ? style : { minWidth, ...style };\n return (\n <DropdownMenu.Portal>\n <DropdownMenu.Content\n className={clsx(styles.content, className)}\n align=\"start\"\n sideOffset={sideOffset}\n style={mergedStyle}\n {...rest}\n />\n </DropdownMenu.Portal>\n );\n}\n\nexport interface MenuItemProps extends ComponentPropsWithoutRef<typeof DropdownMenu.Item> {\n /** Destructive action styling (delete/archive/remove — red text). */\n danger?: boolean;\n}\n\nexport function MenuItem({ className, danger, ...rest }: MenuItemProps) {\n return (\n <DropdownMenu.Item\n className={clsx(styles.item, danger && styles.danger, className)}\n {...rest}\n />\n );\n}\n\nexport function MenuSeparator(props: ComponentPropsWithoutRef<typeof DropdownMenu.Separator>) {\n return <DropdownMenu.Separator className={styles.sep} {...props} />;\n}\n","/**\n * Session context menu — Radix DropdownMenu anchored at cursor coordinates, replacing the POC's\n * manually-positioned `#session-menu` (POC_TO_APP_PLAN_UI.md §4.7). Actions: rename\n * (`update_agent` labels), stop (`interrupt_agent`), archive (`archive_agent` — soft delete, keeps\n * the record for resume), delete (`delete_agent` — hard delete, no trace). Both archive and delete\n * are real remote RPCs: local session/tab state is only removed after the daemon confirms, so a\n * failure (disconnected, agent already gone, etc.) surfaces instead of silently leaving the record\n * on disk while the UI pretends it's gone.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport * as DropdownMenu from \"@radix-ui/react-dropdown-menu\";\nimport { Pencil, Square, Archive as ArchiveIcon, Trash2 } from \"lucide-react\";\nimport {\n MenuCursorTrigger,\n MenuContent,\n MenuItem,\n MenuSeparator,\n} from \"@pi-studio-ui/components/primitives/Menu.js\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useSessionStore } from \"@pi-studio-ui/stores/session-store.js\";\nimport { useTabStore, tabIds } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { useUiStore } from \"@pi-studio-ui/stores/ui-store.js\";\n\nexport function SessionContextMenu() {\n const menu = useUiStore((s) => s.sessionMenu);\n const closeSessionMenu = useUiStore((s) => s.closeSessionMenu);\n const client = useConnectionStore((s) => s.client);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const [open, setOpen] = useState(false);\n\n useEffect(() => {\n setOpen(menu !== null);\n }, [menu]);\n\n const session = useSessionStore((s) => (menu ? s.sessions[menu.sessionId] : undefined));\n\n if (!menu) return null;\n const canStop = Boolean(session?.agentId) && session?.status === \"running\";\n\n function handleOpenChange(next: boolean) {\n setOpen(next);\n if (!next) closeSessionMenu();\n }\n\n function rename() {\n if (!session) return;\n const next = window.prompt(\"Rename:\", session.title);\n if (next?.trim()) {\n const title = next.trim();\n useSessionStore.getState().setTitle(session.id, title);\n useTabStore.getState().updateLabel(tabIds.chat(session.id), title);\n if (session.agentId)\n void client\n ?.agent(session.agentId)\n .update({ title })\n .catch(() => {});\n }\n closeSessionMenu();\n }\n\n function stop() {\n if (session?.agentId)\n void client\n ?.agent(session.agentId)\n .interrupt()\n .catch(() => {});\n closeSessionMenu();\n }\n\n /** Remove the session/tab from local state only — called after a confirmed remote success, or\n * directly for a session with no `agentId` (never sent a message, nothing exists server-side). */\n function removeLocal(sessionId: string) {\n useSessionStore.getState().remove(sessionId);\n useTabStore.getState().close(tabIds.chat(sessionId));\n }\n\n async function archive() {\n if (!session) return;\n if (!window.confirm(\"Archive this agent? It can be resumed later.\")) return closeSessionMenu();\n closeSessionMenu();\n if (!session.agentId) {\n removeLocal(session.id);\n return;\n }\n if (!client) {\n window.alert(\"Not connected — cannot archive remotely.\");\n return;\n }\n try {\n await client.agent(session.agentId).archive();\n removeLocal(session.id);\n } catch (err) {\n window.alert(`Failed to archive: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n async function remove() {\n if (!session) return;\n if (!window.confirm(\"Permanently delete this agent? This cannot be undone.\")) {\n return closeSessionMenu();\n }\n closeSessionMenu();\n if (!session.agentId) {\n removeLocal(session.id);\n return;\n }\n if (!client) {\n window.alert(\"Not connected — cannot delete remotely.\");\n return;\n }\n try {\n await client.agent(session.agentId).delete();\n removeLocal(session.id);\n } catch (err) {\n window.alert(`Failed to delete: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n return (\n <DropdownMenu.Root open={open} onOpenChange={handleOpenChange} modal={false}>\n <DropdownMenu.Trigger asChild>\n <MenuCursorTrigger ref={triggerRef} x={menu.x} y={menu.y} />\n </DropdownMenu.Trigger>\n <MenuContent minWidth={160}>\n <MenuItem onSelect={rename}>\n <Pencil size={13} />\n Rename\n </MenuItem>\n <MenuItem disabled={!canStop} onSelect={stop}>\n <Square size={13} />\n Stop agent\n </MenuItem>\n <MenuSeparator />\n <MenuItem danger onSelect={archive}>\n <ArchiveIcon size={13} />\n Archive\n </MenuItem>\n <MenuItem danger onSelect={remove}>\n <Trash2 size={13} />\n Delete permanently\n </MenuItem>\n </MenuContent>\n </DropdownMenu.Root>\n );\n}\n","/**\n * Collapsible workspace header in the sidebar tree — one per distinct session `cwd`\n * (POC_TO_APP_PLAN_UI.md §4.3 workspace grouping). Clicking anywhere on the header (or its\n * chevron) toggles collapse/expand; opening a session is a separate, explicit action (click a\n * session row). New conversation / Delete workspace live behind the \"⋮\" button's\n * `WorkspaceContextMenu` — a single consolidated menu instead of two always-visible icon buttons\n * (file-explorer quick-wins-1).\n */\n\nimport { ChevronDown, ChevronRight, FolderClosed, MoreVertical } from \"lucide-react\";\nimport { clsx } from \"clsx\";\nimport { IconButton } from \"@pi-studio-ui/components/primitives/IconButton.js\";\nimport styles from \"./SessionList.module.css\";\n\nexport interface WorkspaceGroupHeaderProps {\n label: string;\n cwd: string;\n sessionCount: number;\n collapsed: boolean;\n onToggleCollapsed: () => void;\n onOpenMenu: (x: number, y: number) => void;\n}\n\nexport function WorkspaceGroupHeader({\n label,\n cwd,\n sessionCount,\n collapsed,\n onToggleCollapsed,\n onOpenMenu,\n}: WorkspaceGroupHeaderProps) {\n return (\n <div className={styles.workspaceHeader} onClick={onToggleCollapsed} title={cwd}>\n <IconButton\n className={styles.workspaceChevron}\n aria-label={collapsed ? \"Expand workspace\" : \"Collapse workspace\"}\n onClick={(ev) => {\n ev.stopPropagation();\n onToggleCollapsed();\n }}\n >\n {collapsed ? <ChevronRight size={13} /> : <ChevronDown size={13} />}\n </IconButton>\n <FolderClosed size={13} className={clsx(styles.workspaceIcon)} />\n <span className={styles.workspaceLabel}>{label}</span>\n <span className={styles.workspaceCount}>{sessionCount}</span>\n <IconButton\n className={styles.workspaceMenuBtn}\n hoverBase=\"var(--pi-color-surfaceSidebar)\"\n title=\"Workspace actions\"\n onClick={(ev) => {\n ev.stopPropagation();\n const rect = ev.currentTarget.getBoundingClientRect();\n onOpenMenu(rect.left, rect.bottom);\n }}\n >\n <MoreVertical size={13} />\n </IconButton>\n </div>\n );\n}\n","/**\n * Groups sidebar sessions by workspace (cwd) — each workspace is a project folder that can host\n * several chat sessions, nested under a collapsible header (POC_TO_APP_PLAN_UI.md §4.3 follow-up:\n * workspace-tree sidebar).\n */\n\nimport type { SessionEntry } from \"@pi-studio-ui/stores/session-store.js\";\n\nexport interface WorkspaceGroup {\n /** Normalized (tilde-expanded when known) cwd — the group identity and RPC-ready path. */\n cwd: string;\n sessions: SessionEntry[];\n}\n\n/** Expand a leading `~` against `homeDir` when known; otherwise return `path` unchanged. Single\n * source of truth for workspace identity — every consumer that needs to compare/derive a\n * workspace cwd (grouping, tab scoping, the open-workspace resolver) MUST go through this, or a\n * second normalization drifting from this one reintroduces tilde/absolute duplicate workspaces. */\nexport function normalizeCwd(path: string, homeDir: string | null): string {\n if (homeDir && (path === \"~\" || path.startsWith(\"~/\"))) return homeDir + path.slice(1);\n return path;\n}\n\n/**\n * Buckets sessions by `cwd` (tilde-normalized against `homeDir` so `~/proj` and\n * `/home/user/proj` merge into one workspace once the home dir is known), preserving `order`'s\n * recency: a workspace's position is its first appearance in `order`, and sessions within it keep\n * their relative `order` position too — so the most recently created/active session in each\n * workspace sorts first.\n */\nexport function groupSessionsByWorkspace(\n order: string[],\n sessions: Record<string, SessionEntry>,\n homeDir: string | null,\n): WorkspaceGroup[] {\n const groups = new Map<string, SessionEntry[]>();\n for (const id of order) {\n const session = sessions[id];\n if (!session) continue;\n const cwd = normalizeCwd(session.cwd || \"~\", homeDir);\n const bucket = groups.get(cwd);\n if (bucket) bucket.push(session);\n else groups.set(cwd, [session]);\n }\n return Array.from(groups, ([cwd, sessionsInGroup]) => ({ cwd, sessions: sessionsInGroup }));\n}\n\n/** Short display label for a workspace path — the last path segment, or `~` for the home dir. */\nexport function workspaceLabel(cwd: string): string {\n if (!cwd || cwd === \"~\") return \"~\";\n return cwd.split(\"/\").filter(Boolean).pop() || cwd;\n}\n\n/** Every workspace's cwd except `activeCwd` — used to seed the sidebar's collapsed set on a\n * fresh connect (`use-session-restore.ts`) so only the just-restored workspace starts expanded. */\nexport function collapseInactiveWorkspaces(\n groups: WorkspaceGroup[],\n activeCwd: string,\n): Set<string> {\n return new Set(groups.filter((g) => g.cwd !== activeCwd).map((g) => g.cwd));\n}\n","/**\n * Workspace context menu — Radix DropdownMenu anchored at cursor/button coordinates, mirroring\n * `SessionContextMenu.tsx`'s pattern. Opened via `WorkspaceGroupHeader`'s \"⋮\" button (a single\n * consolidated menu, replacing two always-visible icon buttons — file-explorer quick-wins-1).\n * Actions: New conversation, Delete workspace (all conversations — a real remote RPC per session,\n * `client.agent(id).delete()`; local state only drops after each one confirms).\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport * as DropdownMenu from \"@radix-ui/react-dropdown-menu\";\nimport { Plus, Trash2 } from \"lucide-react\";\nimport {\n MenuCursorTrigger,\n MenuContent,\n MenuItem,\n MenuSeparator,\n} from \"@pi-studio-ui/components/primitives/Menu.js\";\nimport { useSessionStore } from \"@pi-studio-ui/stores/session-store.js\";\nimport { useTabStore, tabIds, openNewChat } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useUiStore } from \"@pi-studio-ui/stores/ui-store.js\";\nimport { useHomeDir } from \"@pi-studio-ui/hooks/use-home-dir.js\";\nimport { normalizeCwd, workspaceLabel } from \"./workspace-grouping.js\";\n\nexport function WorkspaceContextMenu() {\n const menu = useUiStore((s) => s.workspaceMenu);\n const closeWorkspaceMenu = useUiStore((s) => s.closeWorkspaceMenu);\n const client = useConnectionStore((s) => s.client);\n const order = useSessionStore((s) => s.order);\n const sessions = useSessionStore((s) => s.sessions);\n const homeDir = useHomeDir();\n const triggerRef = useRef<HTMLButtonElement>(null);\n const [open, setOpen] = useState(false);\n\n useEffect(() => {\n setOpen(menu !== null);\n }, [menu]);\n\n if (!menu) return null;\n const groupSessions = order\n .map((id) => sessions[id])\n .filter((s): s is NonNullable<typeof s> => Boolean(s))\n .filter((s) => normalizeCwd(s.cwd || \"~\", homeDir) === menu.cwd);\n\n function handleOpenChange(next: boolean) {\n setOpen(next);\n if (!next) closeWorkspaceMenu();\n }\n\n function newConversation() {\n if (!menu) return;\n closeWorkspaceMenu();\n openNewChat(menu.cwd);\n }\n\n async function deleteWorkspace() {\n if (!menu) return;\n const count = groupSessions.length;\n const confirmed = window.confirm(\n `Delete all ${count} conversation${count === 1 ? \"\" : \"s\"} in \"${workspaceLabel(menu.cwd)}\"? ` +\n \"This deletes the conversations only — files on disk are untouched. This cannot be undone.\",\n );\n if (!confirmed) return closeWorkspaceMenu();\n closeWorkspaceMenu();\n for (const session of groupSessions) {\n if (session.agentId && client) {\n try {\n await client.agent(session.agentId).delete();\n } catch (err) {\n window.alert(\n `Failed to delete \"${session.title}\": ${err instanceof Error ? err.message : String(err)}`,\n );\n continue;\n }\n }\n useSessionStore.getState().remove(session.id);\n useTabStore.getState().close(tabIds.chat(session.id));\n }\n }\n\n return (\n <DropdownMenu.Root open={open} onOpenChange={handleOpenChange} modal={false}>\n <DropdownMenu.Trigger asChild>\n <MenuCursorTrigger ref={triggerRef} x={menu.x} y={menu.y} />\n </DropdownMenu.Trigger>\n <MenuContent minWidth={160}>\n <MenuItem onSelect={newConversation}>\n <Plus size={13} />\n New conversation\n </MenuItem>\n <MenuSeparator />\n <MenuItem danger onSelect={deleteWorkspace}>\n <Trash2 size={13} />\n Delete workspace (all conversations)\n </MenuItem>\n </MenuContent>\n </DropdownMenu.Root>\n );\n}\n","/**\n * Shared \"open a session as a chat tab\" dispatch — the sibling of `features/files/open-file-tab.ts`,\n * and for the same reason: the tab id, label and normalized `workspaceCwd` must be identical no\n * matter which entry point opens the conversation, because `tabIdentity` keys the persisted pane\n * layout off them. Three call sites had grown their own copy of this object literal (the sidebar row\n * click, the connect-time restore, and now a sidebar-to-pane drag); a fourth would have been a fourth\n * chance to drift.\n *\n * `targetPaneId` places the tab in a named pane, exactly as `openNewChat`/`openFileTab` accept it;\n * omitted, the tab lands in the focused pane.\n */\n\nimport { useTabStore, tabIds } from \"@pi-studio-ui/stores/tab-store.js\";\nimport type { SessionEntry } from \"@pi-studio-ui/stores/session-store.js\";\nimport { normalizeCwd } from \"./workspace-grouping.js\";\n\nexport function openChatTab(\n session: SessionEntry,\n homeDir: string | null,\n targetPaneId?: string,\n): void {\n useTabStore.getState().open(\n {\n id: tabIds.chat(session.id),\n kind: \"chat\",\n label: session.title || \"Chat\",\n closable: true,\n data: { sessionId: session.id },\n workspaceCwd: normalizeCwd(session.cwd || \"~\", homeDir),\n },\n targetPaneId,\n );\n}\n","/**\n * Drop-region resolution for dragging a tab over a pane body — pure, so the geometry is unit-tested\n * without jsdom (this package's framework-free testing convention).\n *\n * The gesture layer's contract is that **the preview is always the outcome**: a region that cannot be\n * split degrades to `center` (a move into the pane) here, once, and both the preview and the drop\n * dispatch read the degraded value. That is why `effectiveDropRegion` exists instead of each caller\n * consulting `canSplit` on its own — two independent checks are two chances to disagree, and the user\n * would see a split preview and get a move.\n *\n * swe/features/workspace-split-panes.md § Drop regions, § Resolving a drop region\n */\n\nimport { canSplit, type PaneNode, type SplitRegion } from \"./pane-tree.js\";\n\n/**\n * Half-extent of the central move-into-pane target, as a fraction of the body. `0.25` makes the\n * central 50% of each axis a move target and the outer band a split target.\n */\nexport const CENTER_BAND = 0.25;\n\nexport type DropRegion = SplitRegion | \"center\";\n\nexport interface DropPoint {\n x: number;\n y: number;\n}\n\nexport interface DropBounds {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\n/**\n * A resolved drop: the pane that receives it, and the region *after* degradation.\n *\n * Shared by both drag systems — dnd-kit tab drags (`use-pane-drag.ts`) and native sidebar drags\n * (`use-external-pane-drop.ts`) — because `TabPanelHost` renders one preview from whichever is live.\n * Two identically-shaped types would typecheck at that union and drift apart later.\n */\nexport interface DropOutcome {\n paneId: string;\n region: DropRegion;\n}\n\n/**\n * Which region of `body` the pointer is in. Offsets are normalized to ±0.5 from the centre, so the\n * decision is independent of the pane's size; an exact axis tie resolves to the horizontal axis.\n *\n * A degenerate body (zero width or height — a pane mid-layout) has no meaningful edges, so it reads\n * as `center`: a move, never a split into nothing.\n */\nexport function resolveDropRegion(pointer: DropPoint, body: DropBounds): DropRegion {\n if (body.width <= 0 || body.height <= 0) return \"center\";\n const dx = (pointer.x - (body.left + body.width / 2)) / body.width;\n const dy = (pointer.y - (body.top + body.height / 2)) / body.height;\n if (Math.max(Math.abs(dx), Math.abs(dy)) < CENTER_BAND) return \"center\";\n if (Math.abs(dx) >= Math.abs(dy)) return dx < 0 ? \"left\" : \"right\";\n return dy < 0 ? \"top\" : \"bottom\";\n}\n\n/**\n * Whether `pointer` falls inside `bounds`, edges included.\n *\n * dnd-kit hit-tests its own measured rects, so internal drags never need this; a native drag has to\n * find its pane itself, from the drop zones' real client rects (`use-external-pane-drop.ts`) — the\n * strip offset lives in CSS as `var(--pane-strip-height)`, so measuring beats recomputing it here.\n */\nexport function containsPoint(bounds: DropBounds, pointer: DropPoint): boolean {\n return (\n pointer.x >= bounds.left &&\n pointer.x <= bounds.left + bounds.width &&\n pointer.y >= bounds.top &&\n pointer.y <= bounds.top + bounds.height\n );\n}\n\n/**\n * The region as it will actually be applied: an edge whose split is illegal (depth cap) degrades to\n * `center`. `center` and an unknown pane pass through unchanged.\n */\nexport function effectiveDropRegion(\n tree: PaneNode,\n paneId: string,\n region: DropRegion,\n): DropRegion {\n if (region === \"center\") return \"center\";\n return canSplit(tree, paneId, region) ? region : \"center\";\n}\n\n/**\n * Whether dropping `tabId` on `region` of `paneId` would change anything.\n *\n * Two no-ops, and neither may flash a preview: a `center` drop into the pane the tab already sits in,\n * and a split whose source is the dragged tab's only tab — the new pane would take it and the old one\n * would collapse straight back.\n */\nexport function isNoOpDrop(\n region: DropRegion,\n paneId: string,\n tabId: string,\n placement: Readonly<Record<string, string>>,\n): boolean {\n const source = placement[tabId];\n if (source !== paneId) return false;\n if (region === \"center\") return true;\n return Object.keys(placement).every((id) => id === tabId || placement[id] !== paneId);\n}\n","/**\n * The sidebar-to-pane half of drag-to-split: dragging a chat out of the session list, or a file out\n * of the Files tree, onto a pane body — opening it there, splitting the pane when dropped on an edge.\n *\n * **Why this is native HTML5 drag-and-drop and not dnd-kit** (`use-pane-drag.ts`, which carries drags\n * that *start* on a tab already in a strip): the Files tree's rows are already native draggables for\n * row-to-row move, and its container is a native drop target for OS files — whose payload\n * (`dataTransfer.files`) exists ONLY in native DnD, so the tree must keep native DnD no matter what.\n * Arming dnd-kit on the same rows would start two drag systems on one gesture. Native costs neither\n * that nor hoisting `DndContext` above the sidebars, and the two systems cannot collide: a native\n * `dragstart` suppresses the pointer events dnd-kit's `PointerSensor` activates on.\n *\n * The transport reuses the discrimination pattern the tree already documents: mid-drag a browser in\n * protected mode exposes `dataTransfer.types` but NOT the values, so the *type name* carries the\n * drag's kind and the payload is only readable at `drop`. One MIME per kind, so `types` alone tells a\n * pane whether to preview — before it can know *what* is being dragged.\n *\n * A file row carries its move MIME **and** its open MIME at once: one gesture, and *where* it lands\n * decides the meaning — another row is a move, a pane body is an open.\n *\n * swe/features/workspace-split-panes.md § Drop regions, § Splitting\n */\n\nimport {\n effectiveDropRegion,\n resolveDropRegion,\n type DropBounds,\n type DropPoint,\n type DropRegion,\n} from \"./pane-dnd.js\";\nimport type { PaneNode } from \"./pane-tree.js\";\n\n/** What an external drag carries: a session id (`chat`) or a file's absolute path (`path`). */\nexport type ExternalDragKind = \"chat\" | \"path\";\n\n/**\n * One MIME per kind, because `types` is all a drop target can read mid-drag.\n *\n * The Files tree's row-move MIME (`application/x-pi-studio-path`) is deliberately NOT reused: a move\n * and an open are different outcomes and a row carries both, so they need distinguishable names.\n */\nexport const EXTERNAL_DRAG_MIME: Readonly<Record<ExternalDragKind, string>> = {\n chat: \"application/x-pi-studio-open-chat\",\n path: \"application/x-pi-studio-open-path\",\n};\n\nconst KINDS: readonly ExternalDragKind[] = [\"chat\", \"path\"];\n\n/**\n * Which of our drags a `DataTransfer` carries, from `types` alone — `null` for anything else (an OS\n * file drag, a text selection, another app's payload), none of which a pane accepts. We never emit a\n * transfer carrying two kinds; the fixed `KINDS` order makes it deterministic anyway.\n */\nexport function externalDragKind(types: readonly string[]): ExternalDragKind | null {\n for (const kind of KINDS) {\n if (types.includes(EXTERNAL_DRAG_MIME[kind])) return kind;\n }\n return null;\n}\n\n/**\n * Drag-start handler for a file-path drag source (a `FileLink`/resolved `InlineImage` in the chat\n * timeline; mirrors the Files tree's own inline `dataTransfer.setData(EXTERNAL_DRAG_MIME.path, …)`\n * in `FileExplorer.tsx`'s `handleDragStartRow`) — writes the identical `path`-kind payload so a drop\n * on a pane needs no new drop-side handling. Returned as a closure so the caller supplies `path`\n * once and the result is a plain `onDragStart` prop; the closure itself has no state, so it is\n * exercised directly in tests with a minimal fake `DataTransfer` rather than a rendered drag.\n */\nexport function pathDragStartHandler(\n path: string,\n): (event: { dataTransfer: Pick<DataTransfer, \"setData\" | \"effectAllowed\"> }) => void {\n return (event) => {\n event.dataTransfer.setData(EXTERNAL_DRAG_MIME.path, path);\n event.dataTransfer.effectAllowed = \"copyMove\";\n };\n}\n\nexport interface ExternalDragPayload {\n kind: ExternalDragKind;\n /** A chat's session id, or a file's absolute path. */\n value: string;\n}\n\n/**\n * Decode the payload at drop time. `read` is `dataTransfer.getData` — injected so this is unit-tested\n * without a `DataTransfer`, and because that method answers `\"\"` for a missing type rather than\n * throwing. A blank value counts as no payload: a drag we recognise but cannot act on.\n */\nexport function readExternalDrag(\n types: readonly string[],\n read: (mime: string) => string,\n): ExternalDragPayload | null {\n const kind = externalDragKind(types);\n if (kind === null) return null;\n const value = read(EXTERNAL_DRAG_MIME[kind]).trim();\n return value === \"\" ? null : { kind, value };\n}\n\n/**\n * The region a drop at `pointer` would actually apply — already degraded by `effectiveDropRegion`, so\n * the preview and the drop read one value and cannot disagree. Same contract `use-pane-drag` holds\n * for internal drags, and the reason both go through `pane-dnd` rather than checking `canSplit` again.\n */\nexport function resolveExternalDropRegion(\n tree: PaneNode,\n paneId: string,\n pointer: DropPoint,\n bounds: DropBounds,\n): DropRegion {\n return effectiveDropRegion(tree, paneId, resolveDropRegion(pointer, bounds));\n}\n","/**\n * Left sidebar session list (POC `#session-list`/`renderSessions`, POC_TO_APP_PLAN_UI.md §4.3).\n * Sessions are grouped into a collapsible tree by workspace (project `cwd`): clicking a workspace\n * header toggles that workspace's collapsed state; clicking a nested session opens that session.\n * New conversation / Delete workspace live behind the header's \"⋮\" button\n * (`WorkspaceContextMenu.tsx`) rather than always-visible icon buttons.\n */\n\nimport type { DragEvent } from \"react\";\nimport { FolderOpen } from \"lucide-react\";\nimport { useSessionStore } from \"@pi-studio-ui/stores/session-store.js\";\nimport { useTabStore } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useUiStore } from \"@pi-studio-ui/stores/ui-store.js\";\nimport { Button } from \"@pi-studio-ui/components/primitives/Button.js\";\nimport { Panel } from \"@pi-studio-ui/components/primitives/Panel.js\";\nimport { EmptyState } from \"@pi-studio-ui/components/primitives/EmptyState.js\";\nimport { useHomeDir } from \"@pi-studio-ui/hooks/use-home-dir.js\";\nimport { SessionItem } from \"./SessionItem.js\";\nimport { SessionContextMenu } from \"./SessionContextMenu.js\";\nimport { WorkspaceGroupHeader } from \"./WorkspaceGroupHeader.js\";\nimport { WorkspaceContextMenu } from \"./WorkspaceContextMenu.js\";\nimport { groupSessionsByWorkspace, workspaceLabel } from \"./workspace-grouping.js\";\nimport { openChatTab } from \"./open-chat-tab.js\";\nimport { EXTERNAL_DRAG_MIME } from \"@pi-studio-ui/features/workspace/external-drag.js\";\nimport styles from \"./SessionList.module.css\";\n\nexport function SessionList() {\n const order = useSessionStore((s) => s.order);\n const sessions = useSessionStore((s) => s.sessions);\n const activeSessionId = useSessionStore((s) => s.activeSessionId);\n const activate = useSessionStore((s) => s.activate);\n const activeWorkspaceCwd = useTabStore((s) => s.activeWorkspaceCwd);\n const status = useConnectionStore((s) => s.status);\n const openSessionMenu = useUiStore((s) => s.openSessionMenu);\n const openWorkspaceMenu = useUiStore((s) => s.openWorkspaceMenu);\n const setCwd = useUiStore((s) => s.setCwd);\n const openCwdPicker = useUiStore((s) => s.openCwdPicker);\n const collapsedWorkspaces = useUiStore((s) => s.collapsedWorkspaces);\n const toggleWorkspaceCollapsed = useUiStore((s) => s.toggleWorkspaceCollapsed);\n const homeDir = useHomeDir();\n\n function handleSelect(sessionId: string) {\n activate(sessionId);\n const session = sessions[sessionId];\n if (session?.cwd) setCwd(session.cwd);\n if (session) openChatTab(session, homeDir);\n }\n\n /**\n * Only a row belonging to the workspace currently in view is draggable into a pane. Panes belong to\n * one workspace's layout, and mid-drag a browser exposes `dataTransfer.types` but not its values —\n * so a pane could not tell a foreign chat from a local one in time to refuse the preview. Withholding\n * the MIME at the source keeps \"the preview is always the outcome\" true: a foreign row simply\n * produces no drop target, instead of previewing a split that would then be rejected.\n */\n function handleDragStart(sessionId: string, groupCwd: string, e: DragEvent) {\n if (groupCwd !== activeWorkspaceCwd) return;\n e.dataTransfer.setData(EXTERNAL_DRAG_MIME.chat, sessionId);\n e.dataTransfer.effectAllowed = \"copy\";\n }\n\n const groups = groupSessionsByWorkspace(order, sessions, homeDir);\n return (\n <Panel>\n <div className={styles.header}>\n <h3>Workspaces</h3>\n <Button\n size=\"xs\"\n variant=\"ghost\"\n iconOnly\n className={styles.openBtn}\n title={status !== \"open\" ? \"Connect to open a workspace\" : \"Open a workspace folder\"}\n disabled={status !== \"open\"}\n onClick={() => openCwdPicker()}\n >\n <FolderOpen size={13} />\n </Button>\n </div>\n <div className={styles.list}>\n {groups.length === 0 && (\n <EmptyState style={{ padding: \"var(--pi-spacing-12) var(--pi-spacing-10)\" }}>\n {status !== \"open\" ? \"Not connected\" : \"No workspaces — open a folder to start\"}\n </EmptyState>\n )}\n {groups.map((group) => {\n const collapsed = collapsedWorkspaces.has(group.cwd);\n return (\n <div key={group.cwd} className={styles.workspaceGroup}>\n <WorkspaceGroupHeader\n label={workspaceLabel(group.cwd)}\n cwd={group.cwd}\n sessionCount={group.sessions.length}\n collapsed={collapsed}\n onToggleCollapsed={() => toggleWorkspaceCollapsed(group.cwd)}\n onOpenMenu={(x, y) => openWorkspaceMenu(group.cwd, x, y)}\n />\n {!collapsed && (\n <div className={styles.workspaceSessions}>\n {group.sessions.map((session) => (\n <SessionItem\n key={session.id}\n session={session}\n active={session.id === activeSessionId}\n draggable={group.cwd === activeWorkspaceCwd}\n onSelect={() => handleSelect(session.id)}\n onOpenMenu={(x, y) => openSessionMenu(session.id, x, y)}\n onDragStartRow={(e) => handleDragStart(session.id, group.cwd, e)}\n />\n ))}\n </div>\n )}\n </div>\n );\n })}\n </div>\n <SessionContextMenu />\n <WorkspaceContextMenu />\n </Panel>\n );\n}\n","/**\n * Spinner — a thin activity-indicator circle.\n * ui-components.md § Feedback\n */\n\nimport styles from \"./Spinner.module.css\";\n\nexport type SpinnerSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\nconst SPINNER_SIZE_PX: Record<SpinnerSize, number> = { xs: 12, sm: 16, md: 20, lg: 24 };\nconst SPINNER_BORDER_PX: Record<SpinnerSize, number> = { xs: 1, sm: 2, md: 2, lg: 3 };\n\nexport interface SpinnerProps {\n size?: SpinnerSize | number;\n /** CSS color string; defaults to currentColor. */\n color?: string;\n className?: string;\n \"aria-label\"?: string;\n}\n\nexport function Spinner({\n size = \"md\",\n color = \"currentColor\",\n className,\n \"aria-label\": ariaLabel = \"Loading\",\n}: SpinnerProps) {\n const px = typeof size === \"number\" ? size : SPINNER_SIZE_PX[size];\n const borderPx = typeof size === \"number\" ? 2 : SPINNER_BORDER_PX[size as SpinnerSize];\n\n return (\n <span\n className={`${styles.spinner}${className ? ` ${className}` : \"\"}`}\n role=\"status\"\n aria-label={ariaLabel}\n style={{\n width: px,\n height: px,\n borderWidth: borderPx,\n color,\n }}\n />\n );\n}\n","/**\n * The single drag gesture that spans every pane: reorder inside a strip, move to another strip, and\n * split by dropping on a pane body's edge.\n *\n * One `DndContext` (owned by `TabPanelHost`) is a hard requirement, not a preference — a gesture\n * cannot begin in one drag system and finish in another, so strips and bodies must be droppables of\n * the same context. This hook owns everything that context needs: sensors, collision ranking, the\n * live preview, and the drop dispatch.\n *\n * Three droppable kinds, ranked in that order by `collisionDetection`:\n *\n * |`data.type`|Registered by|Drop meaning|\n * |---|---|---|\n * |`tab`|`TabStrip`'s `useSortable`|move (if cross-pane) + reorder at that tab's position|\n * |`strip`|`TabStrip`'s `useDroppable`|move into that pane, appended last|\n * |`pane`|`TabPanelHost`'s body zone|edge → split; centre → move into the pane|\n *\n * The preview and the drop read ONE already-degraded region (`pane-dnd.effectiveDropRegion`), so what\n * the user sees is what happens — including when an illegal edge degrades to a whole-pane move.\n *\n * swe/features/workspace-split-panes.md § Drop regions, § Resolving a drop region,\n * § Splitting, § UI Behavior\n */\n\nimport { useCallback, useMemo, useState } from \"react\";\nimport {\n PointerSensor,\n pointerWithin,\n useSensor,\n useSensors,\n type CollisionDetection,\n type DragEndEvent,\n type DragMoveEvent,\n type DragStartEvent,\n type SensorDescriptor,\n type SensorOptions,\n} from \"@dnd-kit/core\";\nimport {\n effectiveDropRegion,\n isNoOpDrop,\n resolveDropRegion,\n type DropOutcome,\n} from \"@pi-studio-ui/features/workspace/pane-dnd.js\";\nimport { useLayoutStore, type WorkspacePaneLayout } from \"@pi-studio-ui/stores/layout-store.js\";\nimport { useTabStore, type Tab } from \"@pi-studio-ui/stores/tab-store.js\";\n\ntype DropTargetKind = \"tab\" | \"strip\" | \"pane\";\n\n/** Ranked most specific first: a tab sits inside a strip, so both contain the pointer. */\nconst TARGET_RANK: Readonly<Record<DropTargetKind, number>> = { tab: 0, strip: 1, pane: 2 };\n\ninterface DropTarget {\n kind: DropTargetKind;\n /** Set by the `strip` and `pane` droppables; a `tab` resolves its pane from `placement`. */\n paneId: string | null;\n}\n\n/**\n * Narrow a droppable's `data.current`. Ours to begin with (set by the strip/body droppables), but\n * dnd-kit types it loosely, so it is checked rather than asserted.\n */\nfunction dropTarget(data: unknown): DropTarget | null {\n if (data === null || typeof data !== \"object\") return null;\n if (!(\"type\" in data)) return null;\n const { type } = data;\n if (type !== \"tab\" && type !== \"strip\" && type !== \"pane\") return null;\n const paneId = \"paneId\" in data && typeof data.paneId === \"string\" ? data.paneId : null;\n return { kind: type, paneId };\n}\n\nexport interface PaneDrag {\n sensors: SensorDescriptor<SensorOptions>[];\n collisionDetection: CollisionDetection;\n onDragStart(event: DragStartEvent): void;\n onDragMove(event: DragMoveEvent): void;\n onDragEnd(event: DragEndEvent): void;\n onDragCancel(): void;\n /** The tab being dragged, for the floating drag chip. */\n draggedTab: Tab | null;\n /** What the drop would do, or `null` when it would do nothing (no target, or a no-op drop). */\n preview: DropOutcome | null;\n}\n\n/** dnd-kit reports the activating event plus the accumulated delta; together they are the pointer. */\nfunction pointerOf(event: DragMoveEvent | DragEndEvent): { x: number; y: number } | null {\n const activator = event.activatorEvent;\n if (!(activator instanceof MouseEvent)) return null; // PointerEvent extends MouseEvent\n return { x: activator.clientX + event.delta.x, y: activator.clientY + event.delta.y };\n}\n\n/** Which pane a non-body target belongs to: the strip's own pane, or the pane holding that tab. */\nfunction paneOfTarget(\n target: DropTarget,\n overId: string,\n layout: WorkspacePaneLayout,\n): string | null {\n return target.kind === \"tab\" ? (layout.placement[overId] ?? null) : target.paneId;\n}\n\nexport function usePaneDrag(cwd: string | null): PaneDrag {\n const [draggedId, setDraggedId] = useState<string | null>(null);\n const [preview, setPreview] = useState<DropOutcome | null>(null);\n const draggedTab = useTabStore((s) => s.tabs.find((t) => t.id === draggedId) ?? null);\n\n // Unchanged activation constraint: a plain click still activates a tab instead of starting a drag.\n const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } }));\n\n const collisionDetection = useMemo<CollisionDetection>(\n () => (args) => {\n const ranked = pointerWithin(args)\n .map((hit) => ({\n hit,\n target: dropTarget(\n args.droppableContainers.find((container) => container.id === hit.id)?.data.current,\n ),\n }))\n .filter(\n (entry): entry is { hit: (typeof entry)[\"hit\"]; target: DropTarget } =>\n entry.target !== null,\n )\n .toSorted((a, b) => TARGET_RANK[a.target.kind] - TARGET_RANK[b.target.kind]);\n // Empty means the pointer is over no target at all — releasing there changes nothing.\n return ranked.length === 0 ? [] : [ranked[0]!.hit];\n },\n [],\n );\n\n /** What a drop at the current pointer would do, or `null` when it would change nothing. */\n const resolveTarget = useCallback(\n (event: DragMoveEvent | DragEndEvent): DropOutcome | null => {\n const { over, active } = event;\n const target = dropTarget(over?.data.current);\n if (!over || target === null || cwd === null) return null;\n const layout = useLayoutStore.getState().layouts[cwd];\n if (layout === undefined) return null;\n const tabId = String(active.id);\n const overId = String(over.id);\n\n if (target.kind !== \"pane\") {\n // Strips and tabs carry no region: both are moves into the pane that owns them.\n const paneId = paneOfTarget(target, overId, layout);\n if (paneId === null || (target.kind === \"tab\" && overId === tabId)) return null;\n // A same-pane tab drop is a pure reorder — a real outcome, not a no-op.\n if (target.kind === \"tab\" && layout.placement[tabId] === paneId) {\n return { paneId, region: \"center\" };\n }\n return isNoOpDrop(\"center\", paneId, tabId, layout.placement)\n ? null\n : { paneId, region: \"center\" };\n }\n\n const pointer = pointerOf(event);\n if (target.paneId === null || pointer === null) return null;\n const region = effectiveDropRegion(\n layout.tree,\n target.paneId,\n resolveDropRegion(pointer, over.rect),\n );\n return isNoOpDrop(region, target.paneId, tabId, layout.placement)\n ? null\n : { paneId: target.paneId, region };\n },\n [cwd],\n );\n\n const onDragStart = useCallback((event: DragStartEvent) => {\n setDraggedId(String(event.active.id));\n }, []);\n\n const onDragMove = useCallback(\n (event: DragMoveEvent) => {\n const next = resolveTarget(event);\n // Only ever set a real change: this runs per pointer frame.\n setPreview((current) =>\n current?.paneId === next?.paneId && current?.region === next?.region ? current : next,\n );\n },\n [resolveTarget],\n );\n\n const onDragCancel = useCallback(() => {\n setDraggedId(null);\n setPreview(null);\n }, []);\n\n const onDragEnd = useCallback(\n (event: DragEndEvent) => {\n setDraggedId(null);\n setPreview(null);\n const { active, over } = event;\n const target = dropTarget(over?.data.current);\n if (!over || target === null || cwd === null) return;\n\n const tabId = String(active.id);\n const layout = useLayoutStore.getState().layouts[cwd];\n if (layout === undefined) return;\n const tabStore = useTabStore.getState();\n const order = tabStore.tabs.filter((t) => t.workspaceCwd === cwd).map((t) => t.id);\n const { moveTab, splitWithTab } = useLayoutStore.getState();\n\n if (target.kind === \"tab\") {\n const overId = String(over.id);\n if (overId === tabId) return;\n const targetPane = layout.placement[overId];\n if (targetPane !== undefined && targetPane !== layout.placement[tabId]) {\n moveTab(cwd, tabId, targetPane, order);\n }\n // Landing at the target tab's position, within or across panes.\n tabStore.reorder(tabId, overId);\n return;\n }\n\n const resolved = resolveTarget(event);\n if (resolved === null) return;\n if (resolved.region !== \"center\") {\n splitWithTab(cwd, tabId, resolved.paneId, resolved.region, order);\n return;\n }\n\n moveTab(cwd, tabId, resolved.paneId, order);\n // \"Appended last\" in the receiving pane. Global order is the only order, so this is a reorder\n // past that pane's current last tab — and only when the tab is not already after it.\n const placement = useLayoutStore.getState().layouts[cwd]?.placement ?? {};\n const last = order.filter((id) => id !== tabId && placement[id] === resolved.paneId).at(-1);\n if (last !== undefined && order.indexOf(tabId) < order.indexOf(last)) {\n tabStore.reorder(tabId, last);\n }\n },\n [cwd, resolveTarget],\n );\n\n return {\n sensors,\n collisionDetection,\n onDragStart,\n onDragMove,\n onDragEnd,\n onDragCancel,\n draggedTab,\n preview,\n };\n}\n","/**\n * Tab kind → panel component map (POC `createTabPanel` switch, POC_TO_APP_PLAN_UI.md §4.2).\n * Panels are lazy-loaded — the terminal (xterm) and diff/highlighter modules only pay their\n * bundle cost once a tab of that kind actually opens (§6 Performance strategy).\n */\n\nimport { lazy, type ComponentType } from \"react\";\nimport type { Tab } from \"@pi-studio-ui/stores/tab-store.js\";\n\nexport interface PanelProps {\n tab: Tab;\n owningPaneId: string | null;\n}\n\nconst ChatPanel = lazy(() =>\n import(\"../chat/ChatPanel.js\").then((m) => ({ default: m.ChatPanel })),\n);\nconst FilePanel = lazy(() =>\n import(\"../files/FilePanel.js\").then((m) => ({ default: m.FilePanel })),\n);\nconst TerminalPanel = lazy(() =>\n import(\"../terminal/TerminalPanel.js\").then((m) => ({ default: m.TerminalPanel })),\n);\nconst MoleculeViewerPanel = lazy(() =>\n import(\"../files/MoleculeViewerPanel.js\").then((m) => ({ default: m.MoleculeViewerPanel })),\n);\n\nexport const PANEL_BY_KIND: Record<Tab[\"kind\"], ComponentType<PanelProps>> = {\n chat: ChatPanel,\n file: FilePanel,\n diff: FilePanel, // FilePanel handles both file/diff view toggle internally (POC §4.5)\n terminal: TerminalPanel,\n molecule: MoleculeViewerPanel,\n};\n","/**\n * Pure render decisions for the panel host: where each tab's panel paints and whether it is on\n * screen. No React, no store subscriptions — extracted so the geometry is unit-testable without\n * jsdom (this package's framework-free testing convention, same shape as `status-bar-format.ts` and\n * `timeline/inline-image-view.ts`).\n *\n * The list `panelBoxes` returns is derived from `tabs` alone in `tabs` order, which is the machinery\n * behind the continuity invariant: a layout mutation changes each entry's `style`/`visible`, never\n * the set or the order of entries, so React reconciles the same DOM nodes in place and no panel is\n * ever remounted or re-parented (`TerminalPanel` kills its PTY on unmount).\n *\n * swe/features/workspace-split-panes.md § Panel continuity invariant, § Geometry\n */\n\nimport type { CSSProperties } from \"react\";\nimport type { WorkspacePaneLayout } from \"@pi-studio-ui/stores/layout-store.js\";\nimport type { Tab } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { dividers, effectiveTree, paneRects, type Divider, type Rect } from \"./pane-tree.js\";\nimport type { DropRegion } from \"./pane-dnd.js\";\n\n/**\n * Height of a pane's tab strip, matching `TabStrip.module.css`'s 33px row (border-box, so the bottom\n * border is inside it). A pane's rect is split into this strip plus the panel body below, which is\n * why panel styles are `calc(<percentage> ± this)`.\n *\n * Deliberately NOT a `--pi-*` name: that prefix is reserved for theme-emitted design tokens and\n * `theme/token-integrity.test.ts` fails any `var(--pi-…)` the theme does not define. This is a\n * component-local layout metric, declared on `TabPanelHost.module.css`'s `.area`.\n */\nconst STRIP_HEIGHT = \"var(--pane-strip-height)\";\n\nexport interface PanelBox {\n tabId: string;\n /** Percentage rect of the tab's pane body; `undefined` means \"keep the CSS full box\". */\n style: CSSProperties | undefined;\n /** Its workspace is in view **and** it is its own pane's active tab — per pane, not per workspace. */\n visible: boolean;\n}\n\nexport interface PaneChrome {\n paneId: string;\n /** The strip row across the top of the pane's rect. */\n stripStyle: CSSProperties;\n /** Whether to show the focused-pane indicator — never with a single pane, where it is noise. */\n focused: boolean;\n}\n\n/**\n * Fractional rects of the panes that actually hold tabs. A pane still waiting for a restored tab to\n * claim it renders as absent — `effectiveTree` redistributes its fraction without touching the\n * stored tree, so no blank gap shows while the daemon's inventories are still arriving.\n */\nexport function occupiedPaneRects(layout: WorkspacePaneLayout | undefined): Map<string, Rect> {\n if (layout === undefined) return new Map();\n return paneRects(effectiveTree(layout.tree, new Set(Object.values(layout.placement))));\n}\n\n/** `0.333333` → `\"33.3333%\"`; trailing zeros trimmed, because this string lands in the DOM. */\nexport function percent(fraction: number): string {\n const fixed = (fraction * 100).toFixed(4);\n return `${fixed.includes(\".\") ? fixed.replace(/\\.?0+$/, \"\") : fixed}%`;\n}\n\n/**\n * A pane's **body** as absolute-position percentages: its rect minus the strip row at its top.\n * `undefined` when the tab has no pane in the workspace in view (another workspace's tab — always\n * hidden, and it keeps the stylesheet's full box rather than collapsing to 0×0, which\n * `Timeline.tsx`'s virtualizer measurement guard relies on).\n */\nexport function paneStyle(rect: Rect | undefined): CSSProperties | undefined {\n if (rect === undefined) return undefined;\n return {\n left: percent(rect.x),\n top: `calc(${percent(rect.y)} + ${STRIP_HEIGHT})`,\n width: percent(rect.width),\n height: `calc(${percent(rect.height)} - ${STRIP_HEIGHT})`,\n };\n}\n\n/**\n * Where a drop would land, as an absolute-position box over the target pane's **body**: the half the\n * new pane would occupy for an edge region, the whole body for `center`. Whatever this returns is\n * what the drop does — `pane-dnd.ts` has already degraded an illegal edge to `center`.\n */\nexport function dropPreviewStyle(rect: Rect, region: DropRegion): CSSProperties {\n const bodyTop = `${percent(rect.y)} + ${STRIP_HEIGHT}`;\n const bodyHeight = `${percent(rect.height)} - ${STRIP_HEIGHT}`;\n const halfHeight = `calc((${bodyHeight}) / 2)`;\n switch (region) {\n case \"left\":\n return {\n left: percent(rect.x),\n top: `calc(${bodyTop})`,\n width: percent(rect.width / 2),\n height: `calc(${bodyHeight})`,\n };\n case \"right\":\n return {\n left: percent(rect.x + rect.width / 2),\n top: `calc(${bodyTop})`,\n width: percent(rect.width / 2),\n height: `calc(${bodyHeight})`,\n };\n case \"top\":\n return {\n left: percent(rect.x),\n top: `calc(${bodyTop})`,\n width: percent(rect.width),\n height: halfHeight,\n };\n case \"bottom\":\n return {\n left: percent(rect.x),\n top: `calc(${bodyTop} + (${bodyHeight}) / 2)`,\n width: percent(rect.width),\n height: halfHeight,\n };\n case \"center\":\n return {\n left: percent(rect.x),\n top: `calc(${bodyTop})`,\n width: percent(rect.width),\n height: `calc(${bodyHeight})`,\n };\n }\n}\n\n/**\n * One entry per **rendered** pane (see `occupiedPaneRects`), in tree order. Panes are chrome, not\n * panels: they may come and go freely, since nothing stateful is mounted inside a strip.\n */\nexport function paneChrome(layout: WorkspacePaneLayout | undefined): PaneChrome[] {\n const rects = occupiedPaneRects(layout);\n return [...rects].map(([paneId, rect]) => ({\n paneId,\n stripStyle: {\n left: percent(rect.x),\n top: percent(rect.y),\n width: percent(rect.width),\n height: STRIP_HEIGHT,\n },\n focused: rects.size > 1 && layout?.focusedPaneId === paneId,\n }));\n}\n\n/**\n * The draggable boundaries of the layout **as rendered** — and none at all while some pane is still\n * unoccupied. `resizeDivider` addresses a split by its path in the *stored* tree, so offering a\n * divider whose path came from a pruned effective tree would resize the wrong split. `effectiveTree`\n * returns the stored tree by identity when nothing is vacant, which makes the guard exact and free.\n */\nexport function paneDividers(layout: WorkspacePaneLayout | undefined): Divider[] {\n if (layout === undefined) return [];\n const rendered = effectiveTree(layout.tree, new Set(Object.values(layout.placement)));\n if (rendered !== layout.tree) return [];\n return dividers(rendered);\n}\n\n/**\n * The pane that owns a tab, from the layout's tab→pane placement — `null` for a tab not yet placed\n * (a transient state during restore; render call sites fall back to the pre-existing \"no target\n * given\" default of whichever pane is globally focused).\n */\nexport function resolveOwningPaneId(\n tabId: string,\n layout: WorkspacePaneLayout | undefined,\n): string | null {\n return layout?.placement[tabId] ?? null;\n}\n\nexport function panelBoxes(\n tabs: readonly Tab[],\n activeWorkspaceCwd: string | null,\n layout: WorkspacePaneLayout | undefined,\n): PanelBox[] {\n const rects = occupiedPaneRects(layout);\n return tabs.map((tab) => {\n const pane = tab.workspaceCwd === activeWorkspaceCwd ? layout?.placement[tab.id] : undefined;\n return {\n tabId: tab.id,\n style: paneStyle(pane === undefined ? undefined : rects.get(pane)),\n visible: pane !== undefined && layout?.activeByPane[pane] === tab.id,\n };\n });\n}\n","/**\n * PaneDividers — the draggable boundaries between panes, rendered as an absolutely-positioned\n * overlay inside `TabPanelHost`'s area.\n *\n * An overlay, not a layout participant: a divider that consumed layout space would shift the panes\n * away from the rects `paneRects` computed for them, and panels (positioned from the same rects)\n * would drift out of alignment with their strips. So each handle is centred *on* the boundary with a\n * few px of hit area either side, and takes no space.\n *\n * Dragging is delta-based, exactly like `ResizeHandle` (sidebar widths): each `pointermove` reports\n * the movement since the previous one and converts it with the split's own extent — `sizes` are\n * fractions of the split, not of the host, so a nested divider's px→fraction denominator is\n * `splitRect`'s extent × the host's (`pane-tree.ts`'s `Divider.splitRect`). Absolute\n * pointer-to-fraction math would need the sizes captured at drag start and drift against the store's\n * clamping; deltas simply cannot.\n *\n * swe/features/workspace-split-panes.md § Resizing\n */\n\nimport { useRef } from \"react\";\nimport { clsx } from \"clsx\";\nimport { useLayoutStore } from \"@pi-studio-ui/stores/layout-store.js\";\nimport type { Divider } from \"./pane-tree.js\";\nimport styles from \"./PaneDividers.module.css\";\n\nexport interface PaneDividersProps {\n cwd: string;\n dividers: readonly Divider[];\n /** The host content box, measured at drag start — the px→fraction denominator. */\n hostRef: React.RefObject<HTMLDivElement | null>;\n}\n\nexport function PaneDividers({ cwd, dividers, hostRef }: PaneDividersProps) {\n const resizeDivider = useLayoutStore((s) => s.resizeDivider);\n const lastRef = useRef(0);\n\n function startDrag(ev: React.PointerEvent<HTMLDivElement>, divider: Divider): void {\n const host = hostRef.current?.getBoundingClientRect();\n if (!host) return;\n ev.preventDefault();\n ev.stopPropagation(); // dragging a boundary is not a click into either pane\n\n const horizontal = divider.direction === \"row\";\n const extentPx = horizontal\n ? divider.splitRect.width * host.width\n : divider.splitRect.height * host.height;\n if (extentPx <= 0) return;\n\n lastRef.current = horizontal ? ev.clientX : ev.clientY;\n document.body.style.cursor = horizontal ? \"col-resize\" : \"row-resize\";\n document.body.style.userSelect = \"none\";\n\n const onMove = (move: PointerEvent): void => {\n const position = horizontal ? move.clientX : move.clientY;\n const deltaPx = position - lastRef.current;\n lastRef.current = position;\n resizeDivider(cwd, divider.splitPath, divider.boundaryIndex, deltaPx / extentPx);\n };\n const onUp = (): void => {\n window.removeEventListener(\"pointermove\", onMove);\n window.removeEventListener(\"pointerup\", onUp);\n document.body.style.cursor = \"\";\n document.body.style.userSelect = \"\";\n };\n window.addEventListener(\"pointermove\", onMove);\n window.addEventListener(\"pointerup\", onUp);\n }\n\n return dividers.map((divider) => {\n const horizontal = divider.direction === \"row\";\n return (\n <div\n key={`${divider.splitPath.join(\".\")}:${divider.boundaryIndex}`}\n className={clsx(styles.divider, horizontal ? styles.vertical : styles.horizontal)}\n style={\n horizontal\n ? {\n left: `${divider.rect.x * 100}%`,\n top: `${divider.rect.y * 100}%`,\n height: `${divider.rect.height * 100}%`,\n }\n : {\n left: `${divider.rect.x * 100}%`,\n top: `${divider.rect.y * 100}%`,\n width: `${divider.rect.width * 100}%`,\n }\n }\n role=\"separator\"\n aria-orientation={horizontal ? \"vertical\" : \"horizontal\"}\n onPointerDown={(ev) => startDrag(ev, divider)}\n />\n );\n });\n}\n","/**\n * File-viewer registry — maps a detected file kind to the viewer component that renders it\n * (POC_TO_APP_PLAN_UI.md §4.5 follow-up: modular file preview). `FilePanel` detects the kind from\n * `path` (extension) and the explorer's binary `mimeHint` (for extension-less/ambiguous files),\n * then looks up the viewer here — adding a new file type is one entry in `EXT_TO_VIEWER`/\n * `MIME_PREFIX_TO_VIEWER` plus one entry in `VIEWER_BY_KIND`, no changes to `FilePanel` itself.\n *\n * Second dispatch path: the file explorer calls `isMoleculeFile` (below) to pick a tab's *kind*\n * (`\"molecule\"` vs `\"file\"`) before any panel mounts — molecule files never go through\n * `detectViewerKind`/`VIEWER_BY_KIND`/`FilePanel` at all (they get their own dedicated panel).\n */\n\nimport { lazy, type ComponentType } from \"react\";\n\nexport type ViewerKind = \"text\" | \"markdown\" | \"image\" | \"video\" | \"binary\";\n\nexport interface ViewerProps {\n /** Explorer-relative or absolute path, as opened from the file tree/diff panel. */\n path: string;\n}\n\nconst TextViewer = lazy(() => import(\"./TextViewer.js\").then((m) => ({ default: m.TextViewer })));\nconst MarkdownFileViewer = lazy(() =>\n import(\"./MarkdownFileViewer.js\").then((m) => ({ default: m.MarkdownFileViewer })),\n);\nconst ImageViewer = lazy(() =>\n import(\"./ImageViewer.js\").then((m) => ({ default: m.ImageViewer })),\n);\nconst VideoViewer = lazy(() =>\n import(\"./VideoViewer.js\").then((m) => ({ default: m.VideoViewer })),\n);\nconst BinaryFallbackViewer = lazy(() =>\n import(\"./BinaryFallbackViewer.js\").then((m) => ({ default: m.BinaryFallbackViewer })),\n);\n\n/** One component per `ViewerKind`. Register a new file type here — nowhere else. */\nexport const VIEWER_BY_KIND: Record<ViewerKind, ComponentType<ViewerProps>> = {\n text: TextViewer,\n markdown: MarkdownFileViewer,\n image: ImageViewer,\n video: VideoViewer,\n binary: BinaryFallbackViewer,\n};\n\nconst EXT_TO_VIEWER: Record<string, ViewerKind> = {\n md: \"markdown\",\n markdown: \"markdown\",\n mdx: \"markdown\",\n\n png: \"image\",\n jpg: \"image\",\n jpeg: \"image\",\n gif: \"image\",\n webp: \"image\",\n svg: \"image\",\n bmp: \"image\",\n ico: \"image\",\n avif: \"image\",\n\n mp4: \"video\",\n webm: \"video\",\n mov: \"video\",\n m4v: \"video\",\n ogv: \"video\",\n\n // No dedicated preview yet — show the binary fallback (name + manual download) instead of\n // dumping raw/garbled bytes into the text viewer.\n pdf: \"binary\",\n zip: \"binary\",\n gz: \"binary\",\n tar: \"binary\",\n exe: \"binary\",\n wasm: \"binary\",\n woff: \"binary\",\n woff2: \"binary\",\n ttf: \"binary\",\n otf: \"binary\",\n};\n\nconst MIME_PREFIX_TO_VIEWER: Array<[prefix: string, kind: ViewerKind]> = [\n [\"image/\", \"image\"],\n [\"video/\", \"video\"],\n];\n\nfunction extOf(path: string): string {\n const name = path.split(\"/\").pop() ?? path;\n const dot = name.lastIndexOf(\".\");\n return dot > 0 ? name.slice(dot + 1).toLowerCase() : \"\";\n}\n\n/** Extensions molviewer's built-in readers handle (its `readers/index.ts` registration list). */\nexport const MOLECULE_EXTENSIONS: Readonly<Record<string, true>> = {\n pdb: true,\n cif: true,\n mmcif: true,\n mol: true,\n mol2: true,\n xyz: true,\n extxyz: true,\n gro: true,\n lammpstrj: true,\n xsf: true,\n};\n\n/** Extension-less VASP structure files, matched by exact (case-insensitive) basename. */\nexport const MOLECULE_FILENAMES: Readonly<Record<string, true>> = { poscar: true, contcar: true };\n\n/**\n * True when `path` is a molecular structure file molviewer can render. Pure and synchronous — no\n * file reads. LAMMPS `data` files are deliberately excluded (no fixed extension; content-sniffing\n * would make this async) and keep opening as plain text.\n */\nexport function isMoleculeFile(path: string): boolean {\n if (MOLECULE_EXTENSIONS[extOf(path)]) return true;\n const name = path.split(\"/\").pop() ?? path;\n return Boolean(MOLECULE_FILENAMES[name.toLowerCase()]);\n}\n\n/**\n * Detects the viewer kind for a file, preferring the extension (fast, always available) and\n * falling back to a server-supplied MIME hint (extension-less files, e.g. binary blobs the\n * explorer sniffed) — `isBinary` further distinguishes an unrecognized binary from unrecognized\n * text, so unknown-but-textual files (`.env`, `.gitignore`) still fall through to `TextViewer`.\n */\nexport function detectViewerKind(\n path: string,\n opts: { mimeHint?: string; isBinary?: boolean } = {},\n): ViewerKind {\n const byExt = EXT_TO_VIEWER[extOf(path)];\n if (byExt) return byExt;\n\n const mime = opts.mimeHint ?? \"\";\n for (const [prefix, kind] of MIME_PREFIX_TO_VIEWER) {\n if (mime.startsWith(prefix)) return kind;\n }\n\n if (opts.isBinary) return \"binary\";\n return \"text\";\n}\n","/**\n * Shared \"open a path as a tab\" dispatch — used by the Files tree's row click (`FileExplorer.tsx`),\n * the file context menu's \"Open\" action (`FileContextMenu.tsx`), and a Files-tree-to-pane drag\n * (`use-external-pane-drop.ts`) so they all mint identical tab ids/labels and agree on the\n * molecule-vs-plain-file kind (`viewer-registry.ts#isMoleculeFile`). Either side of that dispatch\n * can also be forced directly — `openMoleculeTab`/`openTextTab` below — for the file context\n * menu's \"Open in MolViewer\" / \"Open as Text\" actions.\n *\n * `targetPaneId` places the tab in a named pane — the pane a drop landed on, or the one a split just\n * created; omitted, it lands in the focused pane. Same contract as `openNewTerminal`/`openNewChat`.\n */\n\nimport { useTabStore, tabIds } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { isMoleculeFile } from \"./viewer-registry.js\";\n\nexport function openFileTab(path: string, workspaceCwd: string, targetPaneId?: string): void {\n if (isMoleculeFile(path)) openMoleculeTab(path, workspaceCwd, targetPaneId);\n else openTextTab(path, workspaceCwd, targetPaneId);\n}\n\n/**\n * Open a path as a plain `kind: \"file\"` tab (`FilePanel` + the viewer registry), skipping the\n * molecule dispatch above. Used by `openFileTab` for every non-molecule path, and directly by the\n * context menu's \"Open as Text\" action so a molecule file can be read as source instead of\n * rendered — the mirror image of `openMoleculeTab`. Both tabs can be open on one path at once:\n * `tabIds.file` and `tabIds.molecule` are separate id namespaces.\n *\n * This forces the tab *kind* only. Which viewer mounts inside `FilePanel` remains\n * `detectViewerKind`'s call — every molecule extension (and POSCAR/CONTCAR) falls through to\n * `TextViewer`, which is what makes this action meaningful for exactly those files.\n */\nexport function openTextTab(path: string, workspaceCwd: string, targetPaneId?: string): void {\n const label = path.split(\"/\").pop() || path;\n useTabStore.getState().open(\n {\n id: tabIds.file(path),\n kind: \"file\",\n label,\n closable: true,\n data: { path },\n workspaceCwd,\n },\n targetPaneId,\n );\n}\n\n/**\n * Force-open a file as a molecule tab, regardless of whether `isMoleculeFile` recognizes it.\n * Used by the file context menu's \"Open in MolViewer\" action so the user can hand any file to\n * molviewer (e.g. a LAMMPS `data` file that moleviewer's readers can still parse, but whose\n * extension isn't in `MOLECULE_EXTENSIONS`).\n */\nexport function openMoleculeTab(path: string, workspaceCwd: string, targetPaneId?: string): void {\n const label = path.split(\"/\").pop() || path;\n useTabStore.getState().open(\n {\n id: tabIds.molecule(path),\n kind: \"molecule\",\n label,\n closable: true,\n data: { path },\n workspaceCwd,\n },\n targetPaneId,\n );\n}\n","/**\n * The pane's side of a sidebar-to-pane drag: preview while an external drag is over a pane body, and\n * the open-or-move dispatch on release. `use-pane-drag.ts` is the dnd-kit counterpart for drags that\n * start on a tab already in a strip; `features/workspace/external-drag.ts` documents why this half is\n * native HTML5 DnD instead.\n *\n * The dispatch has two shapes, and what separates them is whether the dragged thing already has a tab:\n *\n * ||centre drop|edge drop|\n * |---|---|---|\n * |already open|`moveTab`|`splitWithTab`|\n * |not open yet|`open(tab, pane)`|`splitEmpty` then `open(tab, newPane)`|\n *\n * The already-open row deliberately routes through the *same* store calls an internal tab drag makes,\n * rather than re-`open`ing the tab: `tab-store.open` short-circuits to `activate` for an id that\n * already exists (ignoring `targetPaneId`), and the internal path is also where the no-op guards live\n * — dragging a chat from the sidebar onto the pane its tab already occupies must do nothing, exactly\n * as dragging its tab there does.\n *\n * `activeTabId` needs no manual resync here: it is a projection, and `tab-store`'s subscription on the\n * layout store follows every layout mutation, including these.\n *\n * swe/features/workspace-split-panes.md § Drop regions, § Splitting\n */\n\nimport { useCallback, useState, type DragEvent, type RefObject } from \"react\";\nimport {\n externalDragKind,\n readExternalDrag,\n resolveExternalDropRegion,\n type ExternalDragPayload,\n} from \"@pi-studio-ui/features/workspace/external-drag.js\";\nimport {\n containsPoint,\n isNoOpDrop,\n type DropBounds,\n type DropOutcome,\n type DropPoint,\n type DropRegion,\n} from \"@pi-studio-ui/features/workspace/pane-dnd.js\";\nimport { openFileTab } from \"@pi-studio-ui/features/files/open-file-tab.js\";\nimport { openChatTab } from \"@pi-studio-ui/features/sessions/open-chat-tab.js\";\nimport { useLayoutStore } from \"@pi-studio-ui/stores/layout-store.js\";\nimport { useSessionStore } from \"@pi-studio-ui/stores/session-store.js\";\nimport { useTabStore, tabIds } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { useHomeDir } from \"./use-home-dir.js\";\n/**\n * Marks a pane's body drop zone for the pointer lookup below, and spreads onto it. An attribute\n * rather than a ref map because the zones are already rendered from one list; nothing else needs to\n * address them.\n *\n * The name is written down exactly once, here: the zone that carries it and the selector that reads it\n * back live in different files, and a rename that hit only one of them would stop every sidebar drag\n * with nothing failing — these hooks are driven directly in tests, never rendered.\n */\nconst PANE_DROP_ATTR = \"data-pane-drop\";\n\nexport function paneDropProps(paneId: string): Record<string, string> {\n return { [PANE_DROP_ATTR]: paneId };\n}\n\nexport interface ExternalPaneDrop {\n /** The outcome a release right now would produce, or `null` when no external drag is over a pane. */\n preview: DropOutcome | null;\n onDragOver(event: DragEvent<HTMLElement>): void;\n onDragLeave(event: DragEvent<HTMLElement>): void;\n onDrop(event: DragEvent<HTMLElement>): void;\n}\n\n/**\n * The pane whose body contains the pointer, with that body's client bounds.\n *\n * Measured, not computed: a pane body's box is `paneStyle`'s percentage rect minus the strip, whose\n * height is the CSS variable `--pane-strip-height` — so the DOM is the only place the two are already\n * combined, and reproducing that arithmetic here would be a second geometry that could disagree.\n *\n * The zones are `pointer-events: none` (they must not swallow clicks into the panel beneath), which is\n * also why the handlers sit on the host and hit-test rather than being per-zone listeners.\n */\nfunction paneUnderPointer(\n host: HTMLElement | null,\n pointer: DropPoint,\n): { paneId: string; bounds: DropBounds } | null {\n if (host === null) return null;\n for (const zone of host.querySelectorAll<HTMLElement>(`[${PANE_DROP_ATTR}]`)) {\n const rect = zone.getBoundingClientRect();\n const bounds = { left: rect.left, top: rect.top, width: rect.width, height: rect.height };\n const paneId = zone.getAttribute(PANE_DROP_ATTR);\n if (paneId !== null && paneId !== \"\" && containsPoint(bounds, pointer))\n return { paneId, bounds };\n }\n return null;\n}\n\n/**\n * The tab this payload would reuse, or `null` when nothing is open for it yet. A path can be open as\n * either kind — `isMoleculeFile` decides for a fresh open, but a file already forced into MolViewer\n * through the context menu must be found too, so both ids are checked.\n */\nfunction existingTabId(payload: ExternalDragPayload): string | null {\n const { tabs } = useTabStore.getState();\n const open = (id: string): string | null => (tabs.some((t) => t.id === id) ? id : null);\n if (payload.kind === \"chat\") return open(tabIds.chat(payload.value));\n return open(tabIds.file(payload.value)) ?? open(tabIds.molecule(payload.value));\n}\n\n/**\n * Apply a resolved external drop. Exported for direct unit testing — this project's vitest config runs\n * `.test.ts` under a plain Node environment with no DOM, so the hook is verified by smoke test while\n * this, which is all of the behaviour, is driven directly.\n */\nexport function applyExternalDrop(\n payload: ExternalDragPayload,\n cwd: string,\n paneId: string,\n region: DropRegion,\n homeDir: string | null,\n): void {\n const layout = useLayoutStore.getState().layouts[cwd];\n if (layout === undefined) return;\n\n const order = useTabStore\n .getState()\n .tabs.filter((t) => t.workspaceCwd === cwd)\n .map((t) => t.id);\n const existing = existingTabId(payload);\n\n if (existing !== null) {\n if (isNoOpDrop(region, paneId, existing, layout.placement)) return;\n const { moveTab, splitWithTab } = useLayoutStore.getState();\n if (region === \"center\") moveTab(cwd, existing, paneId, order);\n else splitWithTab(cwd, existing, paneId, region, order);\n return;\n }\n\n // A fresh tab: make the pane first when an edge was chosen, so the tab opens straight into its\n // final home rather than landing in the focused pane and being moved a beat later.\n let target: string | null = paneId;\n if (region !== \"center\") target = useLayoutStore.getState().splitEmpty(cwd, paneId, region);\n if (target === null) return; // the split was refused (depth cap) — nothing to open into\n\n if (payload.kind === \"path\") {\n openFileTab(payload.value, cwd, target);\n return;\n }\n const session = useSessionStore.getState().sessions[payload.value];\n if (session !== undefined) openChatTab(session, homeDir, target);\n}\n\nexport function useExternalPaneDrop(\n cwd: string | null,\n hostRef: RefObject<HTMLElement | null>,\n): ExternalPaneDrop {\n const [preview, setPreview] = useState<DropOutcome | null>(null);\n const homeDir = useHomeDir();\n\n /** The pane and already-degraded region under this pointer, or `null` when a drop would do nothing. */\n const outcomeAt = useCallback(\n (event: DragEvent<HTMLElement>): DropOutcome | null => {\n if (cwd === null) return null;\n const layout = useLayoutStore.getState().layouts[cwd];\n if (layout === undefined) return null;\n const pointer = { x: event.clientX, y: event.clientY };\n const hit = paneUnderPointer(hostRef.current, pointer);\n if (hit === null) return null;\n return {\n paneId: hit.paneId,\n region: resolveExternalDropRegion(layout.tree, hit.paneId, pointer, hit.bounds),\n };\n },\n [cwd, hostRef],\n );\n\n const onDragOver = useCallback(\n (event: DragEvent<HTMLElement>) => {\n // Not one of ours: stay inert so the drag keeps whatever meaning it already had — and note that\n // without a `preventDefault` here the browser never fires `drop` on this element at all.\n if (externalDragKind(event.dataTransfer.types) === null) return;\n event.preventDefault();\n event.dataTransfer.dropEffect = \"copy\";\n const next = outcomeAt(event);\n // Runs per pointer frame, so only ever set a real change.\n setPreview((current) =>\n current?.paneId === next?.paneId && current?.region === next?.region ? current : next,\n );\n },\n [outcomeAt],\n );\n\n const onDragLeave = useCallback((event: DragEvent<HTMLElement>) => {\n // `dragleave` mirrors `mouseout`, not `mouseleave`: it bubbles and refires on every child-element\n // boundary crossing inside the host — moving between two panes, or between elements inside one\n // panel — so clearing on each would kill the preview a tick after `dragover` set it. Only a leave\n // whose next element is outside the host is the drag actually leaving (same guard as the Files\n // tree's container).\n if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;\n setPreview(null);\n }, []);\n\n const onDrop = useCallback(\n (event: DragEvent<HTMLElement>) => {\n const payload = readExternalDrag(event.dataTransfer.types, (mime) =>\n event.dataTransfer.getData(mime),\n );\n if (payload === null) return;\n event.preventDefault();\n setPreview(null);\n const outcome = outcomeAt(event);\n if (outcome === null || cwd === null) return;\n applyExternalDrop(payload, cwd, outcome.paneId, outcome.region, homeDir);\n },\n [cwd, homeDir, outcomeAt],\n );\n\n return { preview, onDragOver, onDragLeave, onDrop };\n}\n","/**\n * TabStrip — dnd-kit sortable tab strip (POC `renderTabs`, POC_TO_APP_PLAN_UI.md §4.2).\n * Adds drag reorder (the POC couldn't reorder) + Radix Tooltip on truncated labels.\n *\n * **Pane-scoped since sprint-049.** One strip per rendered pane, absolutely positioned across the\n * top of its pane's rect by `TabPanelHost` (a pane's rect = strip row + panel body). It shows the\n * global tab order filtered to its own pane, highlights *its* pane's active tab — not the\n * workspace-active one — and its \"+\" opens into itself. With a single pane this is pixel-identical\n * to the one global strip it replaced: same height, same position, no focus indicator.\n *\n * The `DndContext` lives in `TabPanelHost` (one context spanning every strip and body — a single\n * gesture cannot start in one drag system and finish in another), so this file owns only its own\n * `SortableContext` plus a strip-level droppable for \"dropped on the strip, not on a tab\".\n *\n * `paneId`/`cwd` are nullable for exactly one case: no workspace open at all, where the strip still\n * renders as an empty row with a disabled \"+\" exactly as before.\n */\n\nimport type { CSSProperties } from \"react\";\nimport {\n MessageSquare,\n FileText,\n GitCompare,\n TerminalSquare,\n Atom,\n X,\n Plus,\n Columns2,\n Rows2,\n} from \"lucide-react\";\nimport { clsx } from \"clsx\";\nimport * as DropdownMenu from \"@radix-ui/react-dropdown-menu\";\nimport { useDroppable } from \"@dnd-kit/core\";\nimport { MenuContent, MenuItem } from \"@pi-studio-ui/components/primitives/Menu.js\";\nimport { IconButton } from \"@pi-studio-ui/components/primitives/IconButton.js\";\nimport { SortableContext, horizontalListSortingStrategy, useSortable } from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport {\n useTabStore,\n openNewChat,\n openNewTerminal,\n openNewMolecule,\n closeTab,\n type Tab,\n type TabKind,\n} from \"@pi-studio-ui/stores/tab-store.js\";\nimport { useLayoutStore } from \"@pi-studio-ui/stores/layout-store.js\";\nimport { canSplit, type SplitRegion } from \"./pane-tree.js\";\nimport styles from \"./TabStrip.module.css\";\n\nexport const ICON_BY_KIND: Record<TabKind, typeof MessageSquare> = {\n chat: MessageSquare,\n file: FileText,\n diff: GitCompare,\n terminal: TerminalSquare,\n molecule: Atom,\n};\n\nfunction TabItem({ tab, active }: { tab: Tab; active: boolean }) {\n const activate = useTabStore((s) => s.activate);\n const Icon = ICON_BY_KIND[tab.kind];\n\n const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({\n id: tab.id,\n data: { type: \"tab\" },\n });\n\n return (\n <div\n ref={setNodeRef}\n className={clsx(styles.tab, active && styles.active)}\n style={{\n transform: CSS.Transform.toString(transform),\n transition,\n opacity: isDragging ? 0.5 : 1,\n }}\n onClick={() => activate(tab.id)}\n onAuxClick={(ev) => {\n if (ev.button === 1 && tab.closable) closeTab(tab.id);\n }}\n title={tab.label}\n {...attributes}\n {...listeners}\n >\n <span className={styles.icon}>\n <Icon size={13} />\n </span>\n <span className={styles.label}>{tab.label}</span>\n {tab.closable && (\n <span\n className={styles.close}\n onClick={(ev) => {\n ev.stopPropagation();\n closeTab(tab.id);\n }}\n >\n <X size={12} />\n </span>\n )}\n </div>\n );\n}\n\n/** Trailing \"+\" control — opens a new chat/terminal/molecule in **this pane**.\n * Rendered as a sibling of `SortableContext`, not inside it, so it's never draggable/sortable\n * and reorder/`closestCenter` collision detection never sees it (GitHub issue #8). Orphaned\n * terminals (a PTY running on the daemon with no open tab) don't need a menu entry here — they're\n * auto-reopened as tabs by `use-terminal-restore.ts` on connect, the same way chat sessions are.\n *\n * The target pane is passed explicitly rather than relying on \"the focused pane wins\", so a menu\n * click can never race focus. */\nfunction NewTabMenu({\n workspaceCwd,\n paneId,\n}: {\n workspaceCwd: string | null;\n paneId: string | null;\n}) {\n const openInPane = (open: (cwd: string, targetPaneId?: string) => void) => () => {\n // Guarded rather than defaulted: a `?? \"~\"` fallback here would silently open a chat in a\n // phantom workspace (AGENTS.md § Invariants \"Zero agents on connect ⇒ no workspace\").\n if (workspaceCwd === null) return;\n open(workspaceCwd, paneId ?? undefined);\n };\n return (\n <DropdownMenu.Root>\n <DropdownMenu.Trigger asChild>\n <IconButton\n className={styles.newTab}\n hoverBase=\"var(--pi-color-background)\"\n style={{ borderRadius: \"var(--pi-radius-sm)\" }}\n title=\"New tab\"\n disabled={!workspaceCwd}\n >\n <Plus size={14} />\n </IconButton>\n </DropdownMenu.Trigger>\n <MenuContent minWidth={160} sideOffset={4}>\n <MenuItem onSelect={openInPane(openNewChat)}>\n <MessageSquare size={13} className={styles.itemIcon} />\n New chat\n </MenuItem>\n <MenuItem onSelect={openInPane(openNewTerminal)}>\n <TerminalSquare size={13} className={styles.itemIcon} />\n New terminal\n </MenuItem>\n <MenuItem onSelect={openInPane(openNewMolecule)}>\n <Atom size={13} className={styles.itemIcon} />\n New molecule view\n </MenuItem>\n </MenuContent>\n </DropdownMenu.Root>\n );\n}\n\n/**\n * Split right / Split down on this pane, seeding the new pane with a fresh chat.\n *\n * A pane with no tabs cannot exist at rest (the assignment invariant collapses it), so the split and\n * the seed are one action: `splitEmpty` then `openNewChat` into the pane it returned. `openNewChat`\n * is this client's closest thing to a draft tab, and it opens synchronously even offline (its\n * server-side record is best-effort), so the new pane is never left empty.\n *\n * Unlike a drag, a programmatic split NEVER degrades: when `canSplit` is false for that region the\n * button is disabled with a reason. A button that quietly does something other than its label is\n * worse than one that is greyed out.\n */\nfunction SplitActions({ cwd, paneId }: { cwd: string | null; paneId: string | null }) {\n const tree = useLayoutStore((s) => (cwd === null ? undefined : s.layouts[cwd]?.tree));\n\n const split = (region: SplitRegion) => () => {\n if (cwd === null || paneId === null) return;\n const created = useLayoutStore.getState().splitEmpty(cwd, paneId, region);\n if (created !== null) openNewChat(cwd, created);\n };\n /** `null` when the split is allowed; otherwise why not — the disabled button's tooltip. A pane\n * that cannot split because no workspace is open must not claim it hit the depth cap. */\n const refusal = (region: SplitRegion): string | null => {\n if (tree === undefined || paneId === null) return \"Open a workspace to split\";\n return canSplit(tree, paneId, region) ? null : \"Maximum split depth reached\";\n };\n const rightRefusal = refusal(\"right\");\n const downRefusal = refusal(\"bottom\");\n\n return (\n <>\n <IconButton\n className={styles.splitAction}\n hoverBase=\"var(--pi-color-background)\"\n style={{ borderRadius: \"var(--pi-radius-sm)\" }}\n title={rightRefusal ?? \"Split right\"}\n disabled={rightRefusal !== null}\n onClick={split(\"right\")}\n >\n <Columns2 size={14} />\n </IconButton>\n <IconButton\n className={styles.splitAction}\n hoverBase=\"var(--pi-color-background)\"\n style={{ borderRadius: \"var(--pi-radius-sm)\" }}\n title={downRefusal ?? \"Split down\"}\n disabled={downRefusal !== null}\n onClick={split(\"bottom\")}\n >\n <Rows2 size={14} />\n </IconButton>\n </>\n );\n}\n\nexport interface TabStripProps {\n /** `null` only when no workspace is open. */\n cwd: string | null;\n /** `null` only when no workspace is open. */\n paneId: string | null;\n /** Absolute position across the top of the pane's rect, from `pane-layout-view.paneChrome`. */\n style?: CSSProperties;\n /** Show the focused-pane indicator (never with a single pane). */\n focused?: boolean;\n}\n\nexport function TabStrip({ cwd, paneId, style, focused = false }: TabStripProps) {\n const tabs = useTabStore((s) => s.tabs);\n const focusPane = useLayoutStore((s) => s.focusPane);\n const placement = useLayoutStore((s) => (cwd === null ? undefined : s.layouts[cwd]?.placement));\n const activeInPane = useLayoutStore((s) =>\n cwd === null || paneId === null ? undefined : s.layouts[cwd]?.activeByPane[paneId],\n );\n // Global tab order filtered by pane membership — a pane owns no order of its own, so reorder stays\n // `tab-store.reorder` and there is no second order to drift.\n const paneTabs = paneId === null ? [] : tabs.filter((t) => placement?.[t.id] === paneId);\n // Dropping on the strip but not on a tab moves the tab into this pane, appended last.\n const { setNodeRef } = useDroppable({\n id: `strip:${paneId ?? \"none\"}`,\n disabled: paneId === null,\n data: { type: \"strip\", paneId },\n });\n\n return (\n <div\n ref={setNodeRef}\n className={clsx(styles.strip, styles.paneStrip, focused && styles.focused)}\n style={style}\n onPointerDown={() => {\n if (cwd !== null && paneId !== null) focusPane(cwd, paneId);\n }}\n >\n <div className={styles.tabs}>\n <SortableContext items={paneTabs.map((t) => t.id)} strategy={horizontalListSortingStrategy}>\n {paneTabs.map((tab) => (\n <TabItem key={tab.id} tab={tab} active={tab.id === activeInPane} />\n ))}\n </SortableContext>\n </div>\n <NewTabMenu workspaceCwd={cwd} paneId={paneId} />\n <SplitActions cwd={cwd} paneId={paneId} />\n </div>\n );\n}\n","/**\n * Drag chrome for pane drops: the outcome preview and the floating drag chip.\n *\n * The preview is chrome, never a panel — it renders in `TabPanelHost`'s overlay layer beside the\n * dividers, never inside a panel, so it cannot affect panel mounting. Its box comes from\n * `pane-layout-view.dropPreviewStyle` and its region has already been degraded by\n * `pane-dnd.effectiveDropRegion`, which is what makes \"the preview is always the outcome\" true by\n * construction rather than by two agreeing checks.\n *\n * swe/features/workspace-split-panes.md § UI Behavior, § Drop regions\n */\n\nimport type { Tab } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { ICON_BY_KIND } from \"./TabStrip.js\";\nimport { dropPreviewStyle } from \"./pane-layout-view.js\";\nimport type { DropRegion } from \"./pane-dnd.js\";\nimport type { Rect } from \"./pane-tree.js\";\nimport styles from \"./DropPreview.module.css\";\n\nexport function DropPreview({ rect, region }: { rect: Rect; region: DropRegion }) {\n return <div className={styles.preview} style={dropPreviewStyle(rect, region)} />;\n}\n\n/** Follows the pointer inside dnd-kit's `DragOverlay` — the dragged tab, detached from its strip. */\nexport function DragChip({ tab }: { tab: Tab }) {\n const Icon = ICON_BY_KIND[tab.kind];\n return (\n <div className={styles.chip}>\n <Icon size={13} />\n <span className={styles.chipLabel}>{tab.label}</span>\n </div>\n );\n}\n","/**\n * TabPanelHost — mounts one panel per open tab via `panel-registry`, keeps inactive panels\n * mounted but hidden (`display:none`), preserving scroll/terminal state across tab switches\n * exactly like the POC's DOM-persisted panels (POC_TO_APP_PLAN_UI.md §4.2), but through React\n * reconciliation instead of `data-tab-panel` querying. This now spans workspace switches too\n * (§4.7 follow-up: workspace-scoped tabs) — ALL tabs across every workspace stay mounted; only\n * the strip/empty-state visibility is workspace-scoped, so switching workspaces never tears down\n * a live terminal or loses scroll position in another workspace's tabs.\n *\n * **Flat host, computed rectangles** (workspace-split-panes.md § Panel continuity invariant). With\n * splits, panels are NOT nested into pane subtrees: every panel stays a direct child of this one\n * container for its tab's whole life, and a rearrangement changes only its `left/top/width/height`.\n * Re-parenting would remount it, and `TerminalPanel` kills its PTY on unmount — a dragged terminal\n * would lose its process. Panels are keyed by `tab.id` in one flat `tabs.map`, so React reconciles\n * them in place no matter how the tree changes.\n * Rects come from `paneRects(effectiveTree(...))` (via `pane-layout-view.ts`, where all of this\n * file's render decisions live so they are testable without jsdom) and stay **fractional**: they are\n * emitted as percentages, so the browser's own layout resolves them against the host box. No\n * `ResizeObserver` and no measured state here — a window resize or a divider drag re-lays-out\n * without React re-rendering, and size-sensitive panels (xterm's `FitAddon`) still see their own box\n * change through the observer they already own.\n *\n * Visibility is **per pane**: a pane's active tab is on screen even when another pane is focused, so\n * it cannot be `tab.id === activeTabId` any more.\n *\n * Two distinct empty states: no workspace open at all (`activeWorkspaceCwd === null`, with an\n * \"Open Workspace\" CTA) vs. a workspace in view with no tabs opened in it yet.\n */\n\nimport { Suspense, useMemo, useRef, type CSSProperties } from \"react\";\nimport { clsx } from \"clsx\";\nimport { FolderOpen } from \"lucide-react\";\nimport { DndContext, DragOverlay, useDroppable } from \"@dnd-kit/core\";\nimport { Spinner } from \"@pi-studio-ui/components/primitives/Spinner.js\";\nimport { Button } from \"@pi-studio-ui/components/primitives/Button.js\";\nimport { useTabStore } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { useLayoutStore } from \"@pi-studio-ui/stores/layout-store.js\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useUiStore } from \"@pi-studio-ui/stores/ui-store.js\";\nimport { usePaneDrag } from \"@pi-studio-ui/hooks/use-pane-drag.js\";\nimport { PANEL_BY_KIND } from \"./panel-registry.js\";\nimport {\n occupiedPaneRects,\n paneChrome,\n paneDividers,\n paneStyle,\n panelBoxes,\n resolveOwningPaneId,\n} from \"./pane-layout-view.js\";\nimport { PaneDividers } from \"./PaneDividers.js\";\nimport { useExternalPaneDrop, paneDropProps } from \"@pi-studio-ui/hooks/use-external-pane-drop.js\";\nimport { DragChip, DropPreview } from \"./DropPreview.js\";\nimport { TabStrip } from \"./TabStrip.js\";\nimport styles from \"./TabPanelHost.module.css\";\n\n/**\n * The drop target for a pane's body. Pointer-transparent: dnd-kit hit-tests pointer coordinates\n * against measured rects, so this zone needs a box but must not swallow clicks into the panel\n * beneath it. `paneDropProps` lets a native sidebar drag find the same box by measurement, since\n * `pointer-events: none` also means native drag events never target it directly.\n */\nfunction PaneDropZone({ paneId, style }: { paneId: string; style: CSSProperties | undefined }) {\n const { setNodeRef } = useDroppable({ id: `pane:${paneId}`, data: { type: \"pane\", paneId } });\n return (\n <div ref={setNodeRef} className={styles.dropZone} style={style} {...paneDropProps(paneId)} />\n );\n}\n\nexport function TabPanelHost() {\n const tabs = useTabStore((s) => s.tabs);\n const activeWorkspaceCwd = useTabStore((s) => s.activeWorkspaceCwd);\n const layout = useLayoutStore((s) =>\n activeWorkspaceCwd === null ? undefined : s.layouts[activeWorkspaceCwd],\n );\n const status = useConnectionStore((s) => s.status);\n const openCwdPicker = useUiStore((s) => s.openCwdPicker);\n const focusPane = useLayoutStore((s) => s.focusPane);\n const hasTabsInWorkspace = tabs.some((t) => t.workspaceCwd === activeWorkspaceCwd);\n const areaRef = useRef<HTMLDivElement | null>(null);\n const boxes = useMemo(\n () => panelBoxes(tabs, activeWorkspaceCwd, layout),\n [tabs, activeWorkspaceCwd, layout],\n );\n const chrome = useMemo(() => paneChrome(layout), [layout]);\n const dividers = useMemo(() => paneDividers(layout), [layout]);\n const rects = useMemo(() => occupiedPaneRects(layout), [layout]);\n const drag = usePaneDrag(activeWorkspaceCwd);\n const externalDrop = useExternalPaneDrop(activeWorkspaceCwd, areaRef);\n // One preview slot: the two drag systems are mutually exclusive (a native drag never starts a\n // dnd-kit gesture), so whichever is live owns it.\n const preview = drag.preview ?? externalDrop.preview;\n\n // Both empty states are OVERLAYS, never an early return that would skip the panel list: every\n // panel must stay mounted for its tab's whole life, and a workspace can be brought into view\n // while it has no tabs of its own (another workspace's live terminal must survive that).\n return (\n <DndContext\n sensors={drag.sensors}\n collisionDetection={drag.collisionDetection}\n onDragStart={drag.onDragStart}\n onDragMove={drag.onDragMove}\n onDragEnd={drag.onDragEnd}\n onDragCancel={drag.onDragCancel}\n >\n <div\n className={styles.area}\n ref={areaRef}\n onDragOver={externalDrop.onDragOver}\n onDragLeave={externalDrop.onDragLeave}\n onDrop={externalDrop.onDrop}\n >\n {/* One strip per pane, or one inert strip when no workspace is open (same empty row as\n before panes existed). Strips are chrome — unlike panels, they may be created and\n destroyed freely, since nothing stateful is mounted inside one. */}\n {chrome.length === 0 ? (\n <TabStrip cwd={activeWorkspaceCwd} paneId={null} />\n ) : (\n chrome.map((pane) => (\n <TabStrip\n key={pane.paneId}\n cwd={activeWorkspaceCwd}\n paneId={pane.paneId}\n style={pane.stripStyle}\n focused={pane.focused}\n />\n ))\n )}\n {activeWorkspaceCwd !== null && dividers.length > 0 && (\n <PaneDividers cwd={activeWorkspaceCwd} dividers={dividers} hostRef={areaRef} />\n )}\n {/* Body drop zones and the outcome preview: chrome in the overlay layer, never inside a\n panel, so a drag can never affect panel mounting. */}\n {chrome.map((pane) => (\n <PaneDropZone\n key={`drop-${pane.paneId}`}\n paneId={pane.paneId}\n style={paneStyle(rects.get(pane.paneId))}\n />\n ))}\n {preview !== null && rects.has(preview.paneId) && (\n <DropPreview rect={rects.get(preview.paneId)!} region={preview.region} />\n )}\n {activeWorkspaceCwd === null && (\n <div className={styles.emptyStack}>\n <div className={styles.emptyTitle}>No workspace open</div>\n <div className={styles.emptyHint}>Open a project folder to start a chat.</div>\n <Button\n size=\"sm\"\n variant=\"secondary\"\n leftIcon={<FolderOpen size={14} />}\n disabled={status !== \"open\"}\n title={status !== \"open\" ? \"Connect to open a workspace\" : \"Open a workspace folder\"}\n onClick={() => openCwdPicker()}\n >\n Open Workspace\n </Button>\n </div>\n )}\n {activeWorkspaceCwd !== null && !hasTabsInWorkspace && (\n <div className={styles.empty}>No open tabs in this workspace</div>\n )}\n {tabs.map((tab, i) => {\n const Panel = PANEL_BY_KIND[tab.kind];\n const box = boxes[i]!;\n const pane = layout?.placement[tab.id];\n return (\n <div\n key={tab.id}\n className={clsx(styles.panel, box.visible && styles.active)}\n style={box.style}\n // Clicking anywhere in a pane's body focuses it. Bubbling phase on purpose: an\n // interactive child that stops propagation (a menu, an overlay) keeps focus where it is.\n onPointerDown={() => {\n if (activeWorkspaceCwd !== null && pane !== undefined) {\n focusPane(activeWorkspaceCwd, pane);\n }\n }}\n >\n <Suspense fallback={<Spinner size=\"md\" />}>\n <Panel tab={tab} owningPaneId={resolveOwningPaneId(tab.id, layout)} />\n </Suspense>\n </div>\n );\n })}\n </div>\n <DragOverlay dropAnimation={null}>\n {drag.draggedTab && <DragChip tab={drag.draggedTab} />}\n </DragOverlay>\n </DndContext>\n );\n}\n","/**\n * TanStack Query key factory — one namespaced key builder per RPC domain so cache\n * invalidation (e.g. the `filesChanged` signal, POC_TO_APP_PLAN_UI.md §4.5) targets exactly\n * the affected queries.\n */\n\nexport const rpcKeys = {\n fileRead: (path: string) => [\"file\", \"read\", path] as const,\n fileDownload: (path: string) => [\"file\", \"download\", path] as const,\n fileText: (path: string, objectUrl: string | null) => [\"file\", \"text\", path, objectUrl] as const,\n fileDiff: (path: string, cwd: string, staged: boolean) =>\n [\"file\", \"diff\", path, cwd, staged] as const,\n /** Invalidation prefix covering every (cwd, staged) variant of `fileDiff` for one path. */\n fileDiffByPath: (path: string) => [\"file\", \"diff\", path] as const,\n explorer: (path: string) => [\"explorer\", path] as const,\n agentList: () => [\"agents\", \"list\"] as const,\n providerModels: (provider: string) => [\"providers\", \"models\", provider] as const,\n agentTimeline: (agentId: string) => [\"agents\", \"timeline\", agentId] as const,\n agentCommands: (sessionId: string) => [\"agents\", \"commands\", sessionId] as const,\n};\n","/**\n * `use-explorer` — TanStack Query wrapper over `file_explorer_request` (POC `loadFiles`,\n * POC_TO_APP_PLAN_UI.md §4.7 / §3 `hooks/use-explorer.ts`). Directory listings are read-through\n * cached, keyed by resolved path — components resolve `~` via `resolveTildePath` before calling.\n */\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\n\nexport interface ExplorerEntry {\n name: string;\n kind: \"file\" | \"directory\" | \"symlink\" | \"other\";\n size?: number;\n mtimeMs?: number;\n}\n\ninterface RawExplorerEntry {\n name?: string;\n kind?: string;\n type?: string;\n size?: number;\n mtimeMs?: number;\n}\n\ninterface ExplorerDirectoryResult {\n ok: true;\n kind: \"directory\";\n path: string;\n resolvedPath: string;\n entries: RawExplorerEntry[];\n}\n\ninterface ExplorerTextResult {\n ok: true;\n kind: \"text\";\n path: string;\n resolvedPath: string;\n content: string;\n size: number;\n}\n\ninterface ExplorerErrorResult {\n ok: false;\n error: string;\n}\n\ntype ExplorerResult = ExplorerDirectoryResult | ExplorerTextResult | ExplorerErrorResult;\n\ninterface ExplorerRpcResponse {\n result?: ExplorerResult;\n}\n\nexport interface ExplorerListing {\n path: string;\n entries: ExplorerEntry[];\n}\n\nfunction normalizeEntry(e: RawExplorerEntry): ExplorerEntry {\n const kind = e.kind === \"directory\" || e.type === \"directory\" ? \"directory\" : \"file\";\n return { name: e.name ?? \"\", kind, size: e.size, mtimeMs: e.mtimeMs };\n}\n\n/** Dirs first (alphabetical), then files (alphabetical) — POC `fileList.sort`. */\nfunction sortEntries(entries: ExplorerEntry[]): ExplorerEntry[] {\n return entries.slice().sort((a, b) => {\n if (a.kind !== b.kind) return a.kind === \"directory\" ? -1 : 1;\n return a.name.localeCompare(b.name);\n });\n}\n\n/** Shared fetch — used by both the single-path `useExplorer` and the multi-path tree query\n * (`useExplorerTree`, one TanStack Query per expanded directory). */\nexport async function fetchExplorerListing(\n client: PiStudioClient,\n path: string,\n): Promise<ExplorerListing> {\n const response = await client.connection.request<ExplorerRpcResponse>(\"file_explorer_request\", {\n path,\n });\n const result = response.result;\n if (!result || result.ok === false) {\n throw new Error(result && \"error\" in result ? result.error : \"explorer request failed\");\n }\n const entries = \"entries\" in result ? (result.entries ?? []) : [];\n return {\n path: result.resolvedPath ?? path,\n entries: sortEntries(entries.map(normalizeEntry)),\n };\n}\n\nexport function useExplorer(path: string, enabled = true) {\n const client = useConnectionStore((s) => s.client);\n\n return useQuery({\n queryKey: rpcKeys.explorer(path),\n queryFn: () => {\n if (!client) throw new Error(\"not connected\");\n return fetchExplorerListing(client, path);\n },\n enabled: Boolean(client) && Boolean(path) && enabled,\n });\n}\n","/**\n * `use-explorer-tree` — one TanStack Query per expanded directory path (POC_TO_APP_PLAN_UI.md\n * §4.7 tree-view follow-up). `FileExplorer`'s `explorer-store` tracks *which* paths are expanded;\n * this hook fetches each of them (via the same `file_explorer_request` + `rpcKeys.explorer` cache\n * `use-explorer.ts` uses for the single-path case, so results are shared/deduped) and hands back a\n * lookup keyed by path for `flattenTree` (`features/files/file-tree.ts`) to walk into rows.\n *\n * Collapsing a directory drops it from `expanded`, so its query stops being requested here — but\n * TanStack Query keeps the cached entry around under `rpcKeys.explorer(path)`, so re-expanding is\n * instant until the cache is invalidated (`filesChanged`, an upload, a delete, ...).\n */\n\nimport { useMemo } from \"react\";\nimport { useQueries } from \"@tanstack/react-query\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\nimport { fetchExplorerListing, type ExplorerListing } from \"./use-explorer.js\";\n\nexport interface ExplorerTreeEntry {\n listing: ExplorerListing | undefined;\n isLoading: boolean;\n isError: boolean;\n error: unknown;\n}\n\nexport function useExplorerTree(expanded: Set<string>): Map<string, ExplorerTreeEntry> {\n const client = useConnectionStore((s) => s.client);\n const paths = useMemo(() => Array.from(expanded), [expanded]);\n\n const results = useQueries({\n queries: paths.map((path) => ({\n queryKey: rpcKeys.explorer(path),\n queryFn: (): Promise<ExplorerListing> => {\n if (!client) throw new Error(\"not connected\");\n return fetchExplorerListing(client, path);\n },\n enabled: Boolean(client) && Boolean(path),\n })),\n });\n\n return useMemo(() => {\n const map = new Map<string, ExplorerTreeEntry>();\n paths.forEach((path, i) => {\n const result = results[i];\n map.set(path, {\n listing: result?.data,\n isLoading: Boolean(result?.isLoading),\n isError: Boolean(result?.isError),\n error: result?.error,\n });\n });\n return map;\n }, [paths, results]);\n}\n","/**\n * `use-explorer-watch` — keeps the file tree live: subscribes each expanded directory to daemon\n * `file_changed` pushes (task-006's `FileWatchService`, a directory target — no basename filter,\n * so a child being created/deleted/renamed/modified all notify) and invalidates exactly that\n * directory's `rpcKeys.explorer(path)` query when one arrives. Reuses the `file_watch_subscribe`/\n * `file_watch_unsubscribe` RPCs from task-006 as-is — a directory is just another target, no new\n * RPC needed.\n *\n * The actual diff/subscribe/route/dispose logic lives in `createExplorerWatcher`, a plain\n * framework-free factory (mirrors task-007's `watchFile` extraction) — this repo has no jsdom/DOM\n * test environment configured, so it is the seam `use-explorer-watch.test.ts` exercises directly\n * with a fake client + a real `QueryClient`, rather than mounting the hook.\n *\n * Diffs `expanded` across calls to `sync` — subscribes newly-expanded paths, unsubscribes\n * collapsed ones, leaves unchanged ones alone — rather than tearing down and resubscribing the\n * whole set every time, which would churn every watcher on the daemon for a single `Set` identity\n * change. The hook memoizes the diff key as `Array.from(expanded).toSorted().join(\"\\0\")`; sorting\n * is THIS hook's own requirement — `use-explorer-tree.ts`'s own `Array.from(expanded)` preserves\n * `Set` insertion order and is not sorted, so two logically-identical sets can produce different\n * arrays there.\n *\n * One `onSessionMessage` handler per watcher (not one per subscribed path), matching pushes\n * against the currently-subscribed set. `too_many_watches` (task-006's per-session cap) is a soft\n * failure: that one directory stays unwatched (falls back to `invalidateAfterToolCompletion`'s\n * existing post-tool debounce) rather than throwing or retrying.\n */\n\nimport { useEffect, useRef } from \"react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\n\ninterface FileChangedMessage {\n type: \"file_changed\";\n path: string;\n}\n\nfunction isFileChangedMessage(msg: unknown): msg is FileChangedMessage {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n \"type\" in msg &&\n msg.type === \"file_changed\" &&\n \"path\" in msg &&\n typeof msg.path === \"string\"\n );\n}\n\ninterface SubscribeResponse {\n ok?: boolean;\n error?: string;\n}\n\nexport interface ExplorerWatcher {\n /** Diff `expanded` against the currently-held subscription set and subscribe/unsubscribe\n * accordingly. Safe to call repeatedly with the same set — a no-op diff issues zero RPCs. */\n sync(expanded: ReadonlySet<string>): void;\n /** Detach the message handler and unsubscribe every currently-held path. */\n dispose(): void;\n}\n\n/** Framework-free core: owns one `onSessionMessage` handler and the set of directories currently\n * subscribed, calling `onChanged(path)` for a push matching one of them. */\nexport function createExplorerWatcher(\n client: Pick<PiStudioClient, \"connection\">,\n onChanged: (path: string) => void,\n): ExplorerWatcher {\n const subscribed = new Set<string>();\n\n const unsubscribeMessages = client.connection.onSessionMessage((msg) => {\n const raw: unknown = msg;\n if (!isFileChangedMessage(raw)) return;\n if (!subscribed.has(raw.path)) return;\n onChanged(raw.path);\n });\n\n return {\n sync(expanded) {\n for (const path of subscribed) {\n if (expanded.has(path)) continue;\n subscribed.delete(path);\n void client.connection.request(\"file_watch_unsubscribe\", { path }).catch(() => {});\n }\n for (const path of expanded) {\n if (subscribed.has(path)) continue;\n subscribed.add(path);\n void client.connection\n .request(\"file_watch_subscribe\", { path })\n .then((res) => {\n const r = res as SubscribeResponse;\n if (r?.ok === false) {\n console.warn(`useExplorerWatch: ${r.error ?? \"subscribe failed\"} for \"${path}\"`);\n }\n })\n .catch(() => {});\n }\n },\n dispose() {\n unsubscribeMessages();\n for (const path of subscribed) {\n void client.connection.request(\"file_watch_unsubscribe\", { path }).catch(() => {});\n }\n subscribed.clear();\n },\n };\n}\n\nexport function useExplorerWatch(expanded: Set<string>): void {\n const client = useConnectionStore((s) => s.client);\n const queryClient = useQueryClient();\n const watcherRef = useRef<ExplorerWatcher | null>(null);\n const clientRef = useRef<Pick<PiStudioClient, \"connection\"> | null>(null);\n\n const sortedKey = Array.from(expanded).toSorted().join(\"\\0\");\n\n useEffect(() => {\n if (!client) return;\n if (clientRef.current !== client) {\n // A reconnect swapped in a new client instance — the old socket (and everything it had\n // subscribed) is already gone; dispose the old watcher and start fresh on the new one.\n watcherRef.current?.dispose();\n watcherRef.current = createExplorerWatcher(client, (path) => {\n void queryClient.invalidateQueries({ queryKey: rpcKeys.explorer(path) });\n });\n clientRef.current = client;\n }\n watcherRef.current?.sync(new Set(sortedKey ? sortedKey.split(\"\\0\") : []));\n }, [client, sortedKey, queryClient]);\n\n // Full cleanup on unmount only — independent of how many times the effect above has re-run.\n useEffect(() => {\n return () => {\n watcherRef.current?.dispose();\n watcherRef.current = null;\n clientRef.current = null;\n };\n }, []);\n}\n","/**\n * Shared `FileTransferClient` accessor — one instance per daemon connection so every\n * transfer consumer (inline viewer downloads, save-to-disk downloads, uploads) shares the\n * single inbound-frame demuxer over the one WebSocket. Mirrors `TerminalPanel.tsx`'s\n * `routerFor` pattern.\n */\n\nimport type { DaemonClient } from \"@av-pi-studio/client\";\nimport { FileTransferClient } from \"@av-pi-studio/client\";\n\nconst transferByDaemon = new WeakMap<DaemonClient, FileTransferClient>();\n\nexport function transferFor(daemon: DaemonClient): FileTransferClient {\n let transfer = transferByDaemon.get(daemon);\n if (!transfer) {\n transfer = new FileTransferClient(daemon);\n transfer.start();\n transferByDaemon.set(daemon, transfer);\n }\n return transfer;\n}\n","/**\n * `use-file-transfer` — imperative upload + save-to-disk actions for the Files sidebar\n * (features/file-explorer-transfer.md § Binary transfer frames). Unlike `use-file-download`\n * (which produces object URLs for inline viewers), these are one-shot user-triggered actions:\n * push local bytes to the daemon, or pull daemon bytes into a browser file-save.\n */\n\nimport { useCallback } from \"react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { transferFor } from \"./file-transfer-instance.js\";\n\n/** Trigger a browser \"save file\" for the bytes assembled from a daemon-side path. */\nfunction saveBlob(bytes: Uint8Array, fileName: string, mimeType?: string): void {\n const blob = new Blob([Uint8Array.from(bytes)], {\n type: mimeType || \"application/octet-stream\",\n });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement(\"a\");\n anchor.href = url;\n anchor.download = fileName;\n anchor.rel = \"noopener\";\n document.body.appendChild(anchor);\n anchor.click();\n anchor.remove();\n // Revoke on the next tick — some browsers need the URL alive until the click is processed.\n setTimeout(() => URL.revokeObjectURL(url), 0);\n}\n\nexport interface FileTransferActions {\n /** Upload one browser `File` to `<dir>/<file.name>` on the daemon. */\n upload(dir: string, file: File): Promise<void>;\n /** Download `path` from the daemon and save it via the browser. */\n saveToDisk(path: string): Promise<void>;\n}\n\nexport function useFileTransfer(): FileTransferActions {\n const daemon = useConnectionStore((s) => s.daemon);\n const queryClient = useQueryClient();\n\n const upload = useCallback(\n async (dir: string, file: File) => {\n if (!daemon) throw new Error(\"not connected\");\n const target = dir ? `${dir}/${file.name}` : file.name;\n const bytes = new Uint8Array(await file.arrayBuffer());\n await transferFor(daemon).upload(target, bytes);\n // The daemon does not push an explorer update after an upload — invalidate the listing so\n // the new file appears (mirrors `files-changed` invalidation for tool writes).\n await queryClient.invalidateQueries({ queryKey: [\"explorer\"] });\n },\n [daemon, queryClient],\n );\n\n const saveToDisk = useCallback(\n async (path: string) => {\n if (!daemon) throw new Error(\"not connected\");\n const file = await transferFor(daemon).download(path);\n saveBlob(file.bytes, file.fileName || path.split(\"/\").pop() || \"download\", file.mimeType);\n },\n [daemon],\n );\n\n return { upload, saveToDisk };\n}\n","/**\n * Git store — the right-sidebar Changes tab's checkout-status projection (POC `changeList`\n * global + `handleCheckoutStatusUpdate`, POC_TO_APP_PLAN_UI.md §4.7). `applyProjection` ports the\n * POC mapping verbatim: staged entries key off `indexStatus`, unstaged entries key off\n * `worktreeStatus`, untracked paths are always \"added\"/unstaged.\n *\n * Also retains the projection's branch metadata (branch/ahead/behind/detached/upstream/conflict\n * count/availability) for the workspace status bar's git segment (sprint-042) — previously\n * discarded here even though `use-checkout-status.ts` already receives the full projection.\n */\n\nimport { create } from \"zustand\";\n\nexport interface CheckoutFileEntry {\n path: string;\n indexStatus: string;\n worktreeStatus: string;\n}\n\nexport interface CheckoutStatusProjection {\n available: boolean;\n branch: string | null;\n upstream: string | null;\n ahead: number;\n behind: number;\n detached: boolean;\n staged: CheckoutFileEntry[];\n unstaged: CheckoutFileEntry[];\n untracked: string[];\n /** Gitignored paths (server's `--ignored=traditional` `!` lines). Optional — a daemon older\n * than this field simply omits it. Deliberately NOT folded into `changes`, which drive the\n * Changes tab and the status bar's dirty count; only the Files tree reads this, to ghost\n * ignored rows (`git-status-index.ts`). */\n ignored?: string[];\n conflicted: string[];\n hasConflicts: boolean;\n unavailableReason?: string;\n}\n\nexport interface ChangeEntry {\n path: string;\n status: \"added\" | \"deleted\" | \"modified\";\n staged: boolean;\n}\n\n/** Branch metadata reset when a cwd isn't a git repo (or has no projection yet). */\nconst EMPTY_BRANCH_META = {\n available: false,\n branch: null as string | null,\n upstream: null as string | null,\n ahead: 0,\n behind: 0,\n detached: false,\n conflictCount: 0,\n};\n\ninterface GitStoreState {\n subscribedCwd: string | null;\n changes: ChangeEntry[];\n available: boolean;\n branch: string | null;\n upstream: string | null;\n ahead: number;\n behind: number;\n detached: boolean;\n conflictCount: number;\n /** Workspace-relative gitignored paths; a wholly ignored directory arrives as one `dir/` entry. */\n ignored: string[];\n\n setSubscribedCwd(cwd: string | null): void;\n /** Map a `checkout_status_update` projection into flat change rows (POC `handleCheckoutStatusUpdate`)\n * plus the branch metadata above. */\n applyProjection(projection: CheckoutStatusProjection | null | undefined): void;\n}\n\nexport const useGitStore = create<GitStoreState>()((set) => ({\n subscribedCwd: null,\n changes: [],\n ignored: [],\n ...EMPTY_BRANCH_META,\n\n setSubscribedCwd: (cwd) => set({ subscribedCwd: cwd }),\n\n applyProjection: (projection) => {\n if (!projection || !projection.available) {\n set({ changes: [], ignored: [], ...EMPTY_BRANCH_META });\n return;\n }\n\n const changes: ChangeEntry[] = [];\n for (const f of projection.staged) {\n changes.push({\n path: f.path,\n status: f.indexStatus === \"A\" ? \"added\" : f.indexStatus === \"D\" ? \"deleted\" : \"modified\",\n staged: true,\n });\n }\n for (const f of projection.unstaged) {\n changes.push({\n path: f.path,\n status: f.worktreeStatus === \"D\" ? \"deleted\" : \"modified\",\n staged: false,\n });\n }\n for (const path of projection.untracked) {\n changes.push({ path, status: \"added\", staged: false });\n }\n set({\n changes,\n ignored: projection.ignored ?? [],\n available: true,\n branch: projection.branch,\n upstream: projection.upstream,\n ahead: projection.ahead,\n behind: projection.behind,\n detached: projection.detached,\n conflictCount: projection.conflicted.length,\n });\n },\n}));\n","/**\n * Shared workspace-path resolution — join a possibly-relative path against a workspace `base`\n * (a cwd, an explorer root, …). Lifted out of the file tab live-refresh hook's own resolver\n * (task-007/step 1 there) because a second consumer (inline chat images, task-002) needs the exact\n * same join/trailing-slash contract, and a second copy would reintroduce the tilde/absolute\n * duplicate-path bug this function exists to prevent.\n */\n\n/**\n * Absolute target for a possibly-relative `path` joined against `base`. A leading `/` or `~` is\n * returned as-is — for `~`, deliberately unexpanded: the daemon expands `~` server-side\n * (root `AGENTS.md` invariant 7), and callers that need `~` expanded for their OWN purposes (e.g.\n * a client-side cache key) go through `normalizeCwd` instead, which is a different concern from\n * what the daemon receives.\n */\nexport function resolveWorkspacePath(path: string, base: string): string | null {\n if (!path) return null;\n if (path.startsWith(\"/\") || path.startsWith(\"~\")) return path;\n if (!base) return null;\n return `${base.replace(/\\/+$/, \"\")}/${path}`;\n}\n\n/** Parent directory of an absolute path — `\"/foo/bar\"` → `\"/foo\"`, `\"/foo\"` → `\"/\"`. */\nexport function dirOf(path: string): string {\n const idx = path.lastIndexOf(\"/\");\n return idx > 0 ? path.slice(0, idx) : \"/\";\n}\n\n/** `path` relative to `root` (both absolute) — strips the root prefix and any leading slash.\n * Returns `path` unchanged if it isn't nested under `root`. */\nexport function relativeToRoot(path: string, root: string): string {\n if (!root || !path.startsWith(root)) return path;\n return path.slice(root.length).replace(/^\\/+/, \"\");\n}\n\n\n/**\n * Lexically collapses `.` and `..` segments in an absolute path without filesystem access.\n * Handles each segment in order, appending to a stack and popping for `..` (unless already at root).\n * Returns the normalized path. Input must be absolute (starts with `/`).\n *\n * Examples:\n * - `/repo/./notes.md` → `/repo/notes.md`\n * - `/repo/a/../notes.md` → `/repo/notes.md`\n * - `/repo/a/../../notes.md` → `/notes.md`\n * - `/../notes.md` → `/notes.md` (leading `..` clamped to root)\n */\nexport function collapseDotSegments(path: string): string {\n if (!path.startsWith(\"/\")) {\n return path; // Not absolute; return unchanged\n }\n\n const segments = path.split(\"/\");\n const stack: string[] = [];\n\n for (const segment of segments) {\n if (segment === \"\" || segment === \".\") {\n // Empty segments (from leading `/` or double `//`) and `.` are ignored\n continue;\n } else if (segment === \"..\") {\n // Pop the last non-root segment if available\n if (stack.length > 0) {\n stack.pop();\n }\n // else: already at root, ignore extra `..`\n } else {\n // Regular segment\n stack.push(segment);\n }\n }\n\n return \"/\" + stack.join(\"/\");\n}","/**\n * Pure drop-legality decision for the explorer's internal drag-and-drop move (task-006's handlers\n * become thin call sites over this). The web-client test setup has no jsdom environment, so any\n * rule reachable only through a DOM drag event is effectively untestable — every legality rule\n * therefore lives here, exhaustively unit-tested, rather than inside a drag event handler.\n */\n\nimport { dirOf } from \"@pi-studio-ui/lib/paths.js\";\n\nexport interface MoveTarget {\n /** Directory the drop lands in. */\n destinationDir: string;\n /** Full destination path — `destinationDir` + \"/\" + basename(sourcePath). */\n destination: string;\n}\n\n/**\n * Where a drag of `sourcePath` dropped on `row` would land, or null when the drop is illegal.\n * `rootPath` is the tree root: a drop resolving above it is rejected (the explorer is\n * workspace-scoped).\n */\nexport function resolveMoveTarget(\n sourcePath: string,\n row: { kind: string; path: string },\n rootPath: string,\n): MoveTarget | null {\n if (row.kind !== \"file\" && row.kind !== \"directory\") return null;\n\n const destinationDir = row.kind === \"directory\" ? row.path : dirOf(row.path);\n\n if (destinationDir === dirOf(sourcePath)) return null;\n if (destinationDir === sourcePath) return null;\n if (destinationDir.startsWith(`${sourcePath}/`)) return null;\n if (destinationDir !== rootPath && !destinationDir.startsWith(`${rootPath}/`)) return null;\n\n const basename = sourcePath.split(\"/\").pop();\n return { destinationDir, destination: `${destinationDir}/${basename}` };\n}\n\n/**\n * Directory an OS-file drop on `row` uploads into — a directory row is the target itself, a file\n * row's parent is (files aren't drop containers). `loading`/`error`/`draft` rows have no listing\n * to upload into and are rejected, same as an out-of-workspace resolution.\n */\nexport function resolveUploadTarget(\n row: { kind: string; path: string },\n rootPath: string,\n): string | null {\n if (row.kind !== \"file\" && row.kind !== \"directory\") return null;\n const dir = row.kind === \"directory\" ? row.path : dirOf(row.path);\n if (dir !== rootPath && !dir.startsWith(`${rootPath}/`)) return null;\n return dir;\n}\n","/**\n * Shared `file_move_request` caller for the explorer's drag-and-drop move (`FileExplorer.tsx`) —\n * one place for the wire call and the server error-code → user-facing message mapping, structured\n * the same way as the sibling `create-entry.ts`.\n */\n\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\n\nconst ERROR_MESSAGES: Record<string, string> = {\n empty_path: \"Nothing to move.\",\n invalid_name: \"Invalid name for the destination.\",\n exists: \"An item with that name already exists in the destination folder.\",\n not_found: \"That item or destination folder no longer exists.\",\n not_a_directory: \"The destination is not a folder.\",\n same_path: \"That item is already in this folder.\",\n into_descendant: \"A folder cannot be moved into itself.\",\n cross_device: \"Cannot move across filesystems.\",\n};\n\n/** Move or rename `path` to `destination`; returns the resolved destination path. Throws an\n * Error carrying a user-facing message on failure. */\nexport async function moveEntry(\n client: PiStudioClient,\n path: string,\n destination: string,\n): Promise<string> {\n const response = await client.connection.request<{\n ok: boolean;\n destination?: string;\n error?: string;\n }>(\"file_move_request\", { path, destination });\n if (!response.ok) {\n const code = response.error ?? \"\";\n throw new Error(ERROR_MESSAGES[code] ?? code ?? \"Failed to move\");\n }\n return response.destination ?? destination;\n}\n","/**\n * Pure status-line formatter shared by drag-move and rename (`FileExplorer.tsx`'s `applyMove`) —\n * both report how many diff tabs a move/rename silently closed. Closing them is correct (a\n * per-path `git diff` on the new path returns empty right after a rename, so the daemon's\n * `--no-index` fallback would render the whole file as additions — features/file-explorer-\n * improvements.md § 9, \"Decision (2026-08-03)\"); staying *silent* about it was the defect.\n */\n\n/** Append a closed-diff-tab count to a move/rename status line. `0` leaves `text` unchanged. */\nexport function withClosedDiffs(text: string, closedDiffs: number): string {\n if (closedDiffs === 0) return text;\n const noun = closedDiffs === 1 ? \"diff tab\" : \"diff tabs\";\n return `${text} — closed ${closedDiffs} ${noun}`;\n}\n","/**\n * Pure tree-flattening for the Files tab (POC_TO_APP_PLAN_UI.md §4.7 tree-view follow-up).\n * `FileExplorer.tsx` owns `expanded` (`explorer-store.ts`) and per-path listings\n * (`useExplorerTree`); this module just walks the two into the ordered row list the virtualizer\n * renders — no React, no store access, so it's cheap to unit-test in isolation.\n *\n * The workspace root is the tree's first row — a collapsible directory row naming the workspace\n * cwd, with its children at depth 1. Nothing renders above it: this is a workspace-scoped\n * browser (§4.7), not a general filesystem browser.\n */\n\nimport type { ExplorerEntry } from \"@pi-studio-ui/hooks/use-explorer.js\";\nimport type { ExplorerTreeEntry } from \"@pi-studio-ui/hooks/use-explorer-tree.js\";\n\ninterface DirRow {\n kind: \"directory\";\n /** Absolute path — also the row's stable key and the `explorer-store` toggle/query key. */\n path: string;\n name: string;\n depth: number;\n expanded: boolean;\n}\n\ninterface FileRow {\n kind: \"file\";\n path: string;\n name: string;\n depth: number;\n}\n\n/** A directory's listing is loading, rendered indented under it — not itself\n * expandable/clickable. `path` is the *directory's* path, not a real file/dir on disk — it\n * deliberately collides with that directory's own row, which is why row identity for React goes\n * through `rowKey` (below) rather than `path`. */\ninterface LoadingRow {\n kind: \"loading\";\n path: string;\n depth: number;\n}\n\n/** A directory's listing failed to load. Same non-interactive placement as `LoadingRow`. */\ninterface ErrorRow {\n kind: \"error\";\n path: string;\n depth: number;\n message: string;\n}\n\n/** The inline \"new file\"/\"new folder\" name-entry row. `path` is synthetic (never a real\n * filesystem path) so it can't be mistaken for one by the row-identity comparisons that DO key\n * off `path` — the drop-target highlight and the active-file highlight in `FileExplorer.tsx`. */\ninterface DraftRow {\n kind: \"draft\";\n path: string; // `${parentPath}::draft`\n depth: number;\n draftKind: \"file\" | \"directory\";\n parentPath: string;\n}\n\n/** A real tree row replaced in place by its rename editor. `path` and `name` are the row's\n * *current* values; `isDirectory` keeps the correct icon while editing. Unlike `DraftRow`,\n * `path` here is the real filesystem path — the editor stands in for that exact row. */\ninterface RenameRow {\n kind: \"rename\";\n path: string;\n depth: number;\n name: string;\n isDirectory: boolean;\n}\n\nexport type TreeRow = DirRow | FileRow | LoadingRow | ErrorRow | DraftRow | RenameRow;\n\n/**\n * Stable, collision-free React/virtualizer key for a row.\n *\n * `path` alone is NOT unique: a `loading`/`error` row carries its *directory's* path, so an\n * expanded-but-unsettled directory yields two rows sharing one path — and since the root is now\n * a row of its own, that pair is on screen during every workspace's first paint. Duplicate keys\n * make React orphan one of the two nodes instead of replacing it, which is exactly the \"ghost\n * text stacked on the root folder name\" symptom.\n */\nexport function rowKey(row: TreeRow): string {\n return `${row.kind}:${row.path}`;\n}\n\n/** Which directory has an in-progress inline create row, and what kind it is. */\nexport interface TreeDraft {\n parentPath: string;\n kind: \"file\" | \"directory\";\n}\n\n/** Join a directory and a basename, tolerating a trailing slash on `dir`. Exported so\n * `FileExplorer.tsx`'s rename commit can compute a same-parent destination without a second copy\n * of this logic (rename is not a `resolveMoveTarget` drop target — see `applyMove`'s caller). */\nexport function joinPath(dir: string, name: string): string {\n return dir.endsWith(\"/\") ? `${dir}${name}` : `${dir}/${name}`;\n}\n\nfunction pushChildren(\n rows: TreeRow[],\n dirPath: string,\n depth: number,\n expanded: Set<string>,\n tree: Map<string, ExplorerTreeEntry>,\n draft?: TreeDraft | null,\n renamingPath?: string | null,\n): void {\n if (draft?.parentPath === dirPath) {\n rows.push({\n kind: \"draft\",\n path: `${dirPath}::draft`,\n depth,\n draftKind: draft.kind,\n parentPath: dirPath,\n });\n }\n const node = tree.get(dirPath);\n if (!node || node.isLoading) {\n rows.push({ kind: \"loading\", path: dirPath, depth });\n return;\n }\n if (node.isError) {\n const message = node.error instanceof Error ? node.error.message : \"failed to load\";\n rows.push({ kind: \"error\", path: dirPath, depth, message });\n return;\n }\n for (const entry of node.listing?.entries ?? []) {\n pushEntry(rows, dirPath, entry, depth, expanded, tree, draft, renamingPath);\n }\n}\nfunction pushEntry(\n rows: TreeRow[],\n parentPath: string,\n entry: ExplorerEntry,\n depth: number,\n expanded: Set<string>,\n tree: Map<string, ExplorerTreeEntry>,\n draft?: TreeDraft | null,\n renamingPath?: string | null,\n): void {\n const path = joinPath(parentPath, entry.name);\n const isDirectory = entry.kind === \"directory\";\n const isExpanded = isDirectory && expanded.has(path);\n\n if (path === renamingPath) {\n rows.push({ kind: \"rename\", path, depth, name: entry.name, isDirectory });\n } else if (isDirectory) {\n rows.push({ kind: \"directory\", path, name: entry.name, depth, expanded: isExpanded });\n } else {\n rows.push({ kind: \"file\", path, name: entry.name, depth });\n }\n\n if (isExpanded) pushChildren(rows, path, depth + 1, expanded, tree, draft, renamingPath);\n}\n\n/**\n * Flatten the tree rooted at `rootPath` into the ordered rows currently visible.\n *\n * The root is itself the first row — a real, collapsible directory row whose children sit at\n * depth 1 beneath it, so the workspace being browsed is always named on screen. It is never\n * substituted by a rename row: renaming/deleting the workspace root is not offered (its row\n * opens the background-variant context menu, `FileExplorer.tsx`).\n */\nexport function flattenTree(\n rootPath: string,\n expanded: Set<string>,\n tree: Map<string, ExplorerTreeEntry>,\n draft?: TreeDraft | null,\n renamingPath?: string | null,\n): TreeRow[] {\n if (!rootPath) return [];\n const rootExpanded = expanded.has(rootPath);\n const rows: TreeRow[] = [\n {\n kind: \"directory\",\n path: rootPath,\n name: rootPath.split(\"/\").pop() || rootPath,\n depth: 0,\n expanded: rootExpanded,\n },\n ];\n if (rootExpanded) pushChildren(rows, rootPath, 1, expanded, tree, draft, renamingPath);\n return rows;\n}\n","/**\n * Git-status tinting index for the Files tree — maps an absolute tree-row path to the colour it\n * should be rendered in, VS Code style (added = green, modified = amber, deleted = red), with\n * directories inheriting the status of anything changed beneath them however deeply nested.\n *\n * The data is already live: `StatusBar` owns the single `checkout_status_subscribe` for the active\n * workspace and pushes every projection into `git-store`, so this module is pure derivation over\n * `useGitStore.changes` — no extra RPC, no server change.\n *\n * Two mismatches with the tree make this more than a `Set` lookup:\n * - `ChangeEntry.path` is **relative to the workspace root**, `TreeRow.path` is absolute. The\n * explorer's `rootPath` is the resolved form of the same cwd the status subscription uses\n * (both derive from `tab-store`'s `activeWorkspaceCwd`), so joining is safe.\n * - `git status --porcelain=v2` runs with the default `-unormal`, which **collapses a wholly\n * untracked directory into one `dir/` entry** instead of listing its files. Descendants of such\n * a directory therefore need a prefix match, not an exact one.\n */\n\nimport { dirOf } from \"@pi-studio-ui/lib/paths.js\";\nimport type { ChangeEntry } from \"@pi-studio-ui/stores/git-store.js\";\n\nexport type GitRowStatus = \"added\" | \"modified\" | \"deleted\";\n\n/** Status to tint a tree row with; `undefined` for clean, ignored, or non-repo paths. */\nexport type GitStatusLookup = (path: string) => GitRowStatus | undefined;\n\nconst NO_STATUS: GitStatusLookup = () => undefined;\n\n/**\n * Precedence when one file carries several change entries — git's `AM` (staged addition plus an\n * unstaged edit) yields both an \"added\" and a \"modified\" row in `changes`. A new file that has\n * since been edited is still fundamentally new, so \"added\" outranks \"modified\"; a deletion\n * outranks both.\n */\nfunction mergeFile(previous: GitRowStatus | undefined, next: GitRowStatus): GitRowStatus {\n if (previous === undefined) return next;\n if (previous === \"deleted\" || next === \"deleted\") return \"deleted\";\n if (previous === \"added\" || next === \"added\") return \"added\";\n return \"modified\";\n}\n\n/** A directory is green only when *everything* changed beneath it is new; any edit or deletion in\n * the subtree makes it amber. Deletions contribute \"modified\" — the folder itself still exists,\n * its contents merely changed. */\nfunction contributionToParents(status: GitRowStatus): \"added\" | \"modified\" {\n return status === \"added\" ? \"added\" : \"modified\";\n}\n\n/**\n * Build the row -> status lookup for a tree rooted at `rootPath` from the flat change list.\n * Returns a constant `undefined` lookup when there is no root or nothing has changed, so the\n * common clean-checkout case allocates nothing.\n */\nexport function buildGitStatusLookup(rootPath: string, changes: ChangeEntry[]): GitStatusLookup {\n if (!rootPath || changes.length === 0) return NO_STATUS;\n // `\"/\"` as a root would otherwise join to `//foo`; every other root loses its trailing slash.\n const base = rootPath.replace(/\\/+$/, \"\");\n\n /** Paths named directly by a change entry (files, plus collapsed untracked directories). */\n const exact = new Map<string, GitRowStatus>();\n /** Ancestor directories, rolled up from their changed descendants. */\n const ancestors = new Map<string, \"added\" | \"modified\">();\n /** Absolute paths of collapsed untracked directories, for the descendant prefix match. */\n const untrackedDirs: string[] = [];\n\n for (const change of changes) {\n const isCollapsedDir = change.path.endsWith(\"/\");\n const relative = isCollapsedDir ? change.path.slice(0, -1) : change.path;\n if (!relative) continue;\n const absolute = `${base}/${relative}`;\n\n exact.set(absolute, mergeFile(exact.get(absolute), change.status));\n if (isCollapsedDir) untrackedDirs.push(absolute);\n\n const contribution = contributionToParents(change.status);\n let dir = dirOf(absolute);\n while (dir.length > base.length && dir.startsWith(base)) {\n const previous = ancestors.get(dir);\n ancestors.set(\n dir,\n previous === undefined || previous === contribution ? contribution : \"modified\",\n );\n const parent = dirOf(dir);\n if (parent === dir) break;\n dir = parent;\n }\n }\n\n return (path) => {\n const direct = exact.get(path) ?? ancestors.get(path);\n if (direct !== undefined) return direct;\n for (const dir of untrackedDirs) {\n if (path.startsWith(`${dir}/`)) return \"added\";\n }\n return undefined;\n };\n}\n\n/** Whether a tree row is gitignored. */\nexport type IgnoredMatcher = (path: string) => boolean;\n\nconst NOTHING_IGNORED: IgnoredMatcher = () => false;\n\n/**\n * Build the \"is this row gitignored?\" predicate from `git-store.ignored`. Same two-shape input as\n * the status lookup: plain files match exactly, while a wholly ignored directory arrives collapsed\n * as `dir/` (git stops walking there) and therefore also claims everything beneath it — which is\n * the case that matters, since `node_modules/` and `dist/` are exactly what users want ghosted.\n */\nexport function buildIgnoredMatcher(rootPath: string, ignored: string[]): IgnoredMatcher {\n if (!rootPath || ignored.length === 0) return NOTHING_IGNORED;\n const base = rootPath.replace(/\\/+$/, \"\");\n\n const exact = new Set<string>();\n const dirs: string[] = [];\n for (const entry of ignored) {\n const isDir = entry.endsWith(\"/\");\n const relative = isDir ? entry.slice(0, -1) : entry;\n if (!relative) continue;\n const absolute = `${base}/${relative}`;\n exact.add(absolute);\n if (isDir) dirs.push(`${absolute}/`);\n }\n\n return (path) => {\n if (exact.has(path)) return true;\n for (const prefix of dirs) {\n if (path.startsWith(prefix)) return true;\n }\n return false;\n };\n}\n","/**\n * TreeDraftRow — the inline \"new file\"/\"new folder\" name-entry row rendered in place of a real\n * tree row (`file-tree.ts`'s `DraftRow`). Owns the input's local text state so `TreeNode` stays\n * hook-free and presentational; submits the trimmed name up to `FileExplorer` via `onSubmit`,\n * or discards the draft via `onCancel` on Escape/blur/empty-submit.\n */\n\nimport { useState, type CSSProperties } from \"react\";\nimport { Folder, File as FileIcon } from \"lucide-react\";\nimport styles from \"./FileExplorer.module.css\";\n\nexport interface TreeDraftRowProps {\n draftKind: \"file\" | \"directory\";\n indentStyle: CSSProperties;\n onSubmit(name: string): void;\n onCancel(): void;\n}\n\nexport function TreeDraftRow({ draftKind, indentStyle, onSubmit, onCancel }: TreeDraftRowProps) {\n const [name, setName] = useState(\"\");\n\n return (\n <div className={styles.item} style={indentStyle} onClick={(e) => e.stopPropagation()}>\n <span className={styles.chevronSlot} />\n <span className={styles.icon}>\n {draftKind === \"directory\" ? <Folder size={16} /> : <FileIcon size={16} />}\n </span>\n <input\n className={styles.draftInput}\n autoFocus\n spellCheck={false}\n placeholder={draftKind === \"directory\" ? \"New folder\" : \"New file\"}\n value={name}\n onChange={(e) => setName(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n const trimmed = name.trim();\n if (trimmed) onSubmit(trimmed);\n else onCancel();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n onCancel();\n }\n }}\n onBlur={onCancel}\n />\n </div>\n );\n}\n","/**\n * TreeRenameRow — the inline rename editor rendered in place of the row being renamed\n * (`file-tree.ts`'s `RenameRow`). Mirrors `TreeDraftRow`'s division of labour: owns the input's\n * local text state so `TreeNode` stays hook-free and presentational; submits the trimmed name up\n * to `FileExplorer` via `onSubmit`, or discards the edit via `onCancel` on Escape/blur/unchanged-\n * or-empty submit.\n */\n\nimport { useEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { Folder, File as FileIcon } from \"lucide-react\";\nimport styles from \"./FileExplorer.module.css\";\n\nexport interface TreeRenameRowProps {\n /** Current basename, pre-filled as the input's initial value. */\n name: string;\n isDirectory: boolean;\n indentStyle: CSSProperties;\n /** Called with the trimmed name once it is non-empty and actually different from `name`. */\n onSubmit(name: string): void;\n onCancel(): void;\n}\n\nexport function TreeRenameRow({\n name,\n isDirectory,\n indentStyle,\n onSubmit,\n onCancel,\n}: TreeRenameRowProps) {\n const [value, setValue] = useState(name);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n const input = inputRef.current;\n if (!input) return;\n input.focus();\n // Select the basename without its extension so a straight re-type doesn't clobber it. Falls\n // back to selecting the whole name for a directory (no extension to preserve) or a dotfile\n // like `.gitignore` (`lastIndexOf(\".\")` is 0, not a real extension boundary).\n const dot = name.lastIndexOf(\".\");\n const end = isDirectory || dot <= 0 ? name.length : dot;\n input.setSelectionRange(0, end);\n }, [name, isDirectory]);\n\n return (\n <div className={styles.item} style={indentStyle} onClick={(e) => e.stopPropagation()}>\n <span className={styles.chevronSlot} />\n <span className={styles.icon}>\n {isDirectory ? <Folder size={16} /> : <FileIcon size={16} />}\n </span>\n <input\n ref={inputRef}\n className={styles.draftInput}\n spellCheck={false}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n const trimmed = value.trim();\n // Bailing on empty or unchanged avoids a pointless round-trip the daemon would only\n // answer with `same_path`.\n if (!trimmed || trimmed === name) onCancel();\n else onSubmit(trimmed);\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n onCancel();\n }\n }}\n onBlur={onCancel}\n />\n </div>\n );\n}\n","/**\n * TreeNode — one row of the Files tab tree view (POC_TO_APP_PLAN_UI.md §4.7 tree-view follow-up).\n * Presentational only: `FileExplorer.tsx` computes the flattened `TreeRow[]` (`file-tree.ts`) and\n * hands each one here as a fixed-height virtualized row. A directory row toggles expansion; a\n * file row opens a tab; both show the \"⋮\" action button feeding the shared `FileContextMenu`.\n */\n\nimport { ChevronRight, Folder, File as FileIcon, MoreVertical } from \"lucide-react\";\nimport { clsx } from \"clsx\";\nimport type { TreeRow } from \"./file-tree.js\";\nimport type { GitRowStatus } from \"./git-status-index.js\";\nimport type { DragEvent } from \"react\";\nimport { IconButton } from \"@pi-studio-ui/components/primitives/IconButton.js\";\nimport { TreeDraftRow } from \"./TreeDraftRow.js\";\nimport { TreeRenameRow } from \"./TreeRenameRow.js\";\nimport styles from \"./FileExplorer.module.css\";\n\nexport interface TreeNodeProps {\n row: TreeRow;\n /** True when this row's path is the active tab's file (`FileExplorer.tsx`'s `activeFilePath`) —\n * highlighted like the sidebar's active session row. */\n active?: boolean;\n /** True when this row is the last-clicked row (`explorer-store.ts`'s `selected`) — the target\n * directory for \"New File\"/\"New Folder\". */\n selected?: boolean;\n /** True while an internal drag would land in this row (`FileExplorer.tsx`'s hovered target). */\n dropTarget?: boolean;\n /** Git status of this row's path (`git-status-index.ts`) — tints the icon + label, with\n * directories inheriting the status of anything changed beneath them. Undefined when clean,\n * ignored, or outside a repo. */\n gitStatus?: GitRowStatus;\n /** True when the entry wouldn't show in a plain listing — a dotfile, or gitignored\n * (`FileExplorer.tsx` computes both). Ghosts the row so it reads as present-but-not-normally-\n * visible; still fully interactive, and full opacity on hover/active/selected. */\n hidden?: boolean;\n /** True for the tree's own root row (the workspace cwd). Styled as a heading and NOT draggable:\n * every legal drop target lives inside it, so a root drag could only ever be refused. */\n isRoot?: boolean;\n onToggle(path: string): void;\n onOpenFile(path: string): void;\n onContextMenu(path: string, isDirectory: boolean, x: number, y: number): void;\n onSubmitDraft(parentPath: string, name: string): void;\n onCancelDraft(): void;\n onSubmitRename(path: string, name: string): void;\n onCancelRename(): void;\n onDragStartRow(path: string, isDirectory: boolean, e: DragEvent): void;\n onDragEndRow(): void;\n}\n\nconst INDENT_PX = 14;\n\n/** Same colour convention as the Changes tab's A/M/D badges (`ChangesPanel.module.css`). */\nconst GIT_STATUS_CLASS: Record<GitRowStatus, string | undefined> = {\n added: styles.gitAdded,\n modified: styles.gitModified,\n deleted: styles.gitDeleted,\n};\n\nexport function TreeNode({\n row,\n active,\n selected,\n dropTarget,\n gitStatus,\n hidden,\n isRoot,\n onToggle,\n onOpenFile,\n onContextMenu,\n onSubmitDraft,\n onCancelDraft,\n onSubmitRename,\n onCancelRename,\n onDragStartRow,\n onDragEndRow,\n}: TreeNodeProps) {\n const indent = { paddingLeft: 8 + row.depth * INDENT_PX };\n\n if (row.kind === \"loading\") {\n return (\n <div className={styles.statusRow} style={indent}>\n Loading…\n </div>\n );\n }\n if (row.kind === \"error\") {\n return (\n <div className={clsx(styles.statusRow, styles.statusRowError)} style={indent}>\n {row.message ?? \"failed to load\"}\n </div>\n );\n }\n if (row.kind === \"draft\") {\n return (\n <TreeDraftRow\n draftKind={row.draftKind}\n indentStyle={indent}\n onSubmit={(name) => onSubmitDraft(row.parentPath, name)}\n onCancel={onCancelDraft}\n />\n );\n }\n if (row.kind === \"rename\") {\n return (\n <TreeRenameRow\n name={row.name}\n isDirectory={row.isDirectory}\n indentStyle={indent}\n onSubmit={(name) => onSubmitRename(row.path, name)}\n onCancel={onCancelRename}\n />\n );\n }\n\n const isDirectory = row.kind === \"directory\";\n return (\n <div\n className={clsx(\n styles.item,\n isRoot && styles.rootItem,\n active && styles.active,\n selected && styles.selected,\n dropTarget && styles.dropTarget,\n gitStatus && GIT_STATUS_CLASS[gitStatus],\n hidden && styles.hiddenEntry,\n )}\n style={indent}\n title={row.path}\n draggable={!isRoot}\n onDragStart={(e) => onDragStartRow(row.path, isDirectory, e)}\n onDragEnd={onDragEndRow}\n onClick={() => (isDirectory ? onToggle(row.path) : onOpenFile(row.path))}\n onContextMenu={(e) => {\n e.preventDefault();\n e.stopPropagation();\n onContextMenu(row.path, isDirectory, e.clientX, e.clientY);\n }}\n >\n <span className={styles.chevronSlot}>\n {isDirectory && (\n <ChevronRight\n size={14}\n className={clsx(styles.chevron, row.expanded && styles.chevronOpen)}\n />\n )}\n </span>\n <span className={styles.icon}>\n {isDirectory ? <Folder size={16} /> : <FileIcon size={16} />}\n </span>\n <span className={isDirectory ? styles.dirName : styles.name}>{row.name}</span>\n <IconButton\n className={styles.rowAction}\n title=\"Actions\"\n onClick={(e) => {\n e.stopPropagation();\n const rect = e.currentTarget.getBoundingClientRect();\n onContextMenu(row.path, isDirectory, rect.left, rect.bottom);\n }}\n >\n <MoreVertical size={14} />\n </IconButton>\n </div>\n );\n}\n","/**\n * Clipboard write helper (file-explorer \"Copy Path\" actions, task quick-wins-1). Tries the async\n * Clipboard API first, falling back to a hidden `execCommand(\"copy\")` textarea when it's\n * unavailable — plain-http access to a self-hosted daemon over a LAN is not a secure context, so\n * `navigator.clipboard` is `undefined` there, a real deployment condition for this app, not a\n * hypothetical.\n */\n\nexport async function copyText(text: string): Promise<void> {\n if (navigator.clipboard?.writeText) {\n try {\n await navigator.clipboard.writeText(text);\n return;\n } catch {\n // Fall through to the execCommand fallback below.\n }\n }\n const textarea = document.createElement(\"textarea\");\n textarea.value = text;\n textarea.style.position = \"fixed\";\n textarea.style.opacity = \"0\";\n document.body.appendChild(textarea);\n textarea.focus();\n textarea.select();\n try {\n if (!document.execCommand(\"copy\")) throw new Error('execCommand(\"copy\") failed');\n } finally {\n textarea.remove();\n }\n}\n","/**\n * Shared `file_delete_request` caller — one place for the wire call and the server error-code →\n * user-facing message mapping, structured the same way as its siblings `create-entry.ts`,\n * `write-file.ts` and `move-entry.ts`.\n *\n * Recursive and unconfirmed server-side (`FileExplorerService.deleteFile`): confirming a\n * destructive delete with the user is the caller's job, not this module's.\n *\n * Callers own their own cache invalidation and tab cleanup — those differ per call site (the\n * context menu closes every tab under the deleted prefix; the polymer build is rolling back a file\n * no tab ever saw).\n */\n\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\n\nconst ERROR_MESSAGES: Record<string, string> = {\n empty_path: \"Nothing to delete.\",\n not_found: \"That item no longer exists.\",\n};\n\n/** Delete the file or directory at `path`. Throws an Error carrying a user-facing message on\n * failure. */\nexport async function deleteEntry(client: PiStudioClient, path: string): Promise<void> {\n const response = await client.connection.request<{ ok: boolean; error?: string }>(\n \"file_delete_request\",\n { path },\n );\n if (!response.ok) {\n const code = response.error ?? \"\";\n throw new Error(ERROR_MESSAGES[code] ?? code ?? \"Failed to delete\");\n }\n}\n","/**\n * File row context menu — Radix DropdownMenu anchored at cursor/button coordinates, mirroring\n * `SessionContextMenu.tsx`'s pattern. Two variants share this one component/menu instance\n * (`ui-store.ts`'s `fileMenu.background` flag):\n * - Row menu (background: false): Open / Open in MolViewer (files only) / Open as Text (molecule\n * files only — plain files already open that way) / New File / New Folder (directories only) /\n * Copy Absolute Path / Copy Relative Path / Download (files only) / Rename / Delete.\n * - Empty-space menu (background: true, opened by right-clicking below the last row): New File /\n * New Folder / Copy Current Directory Path / Copy Current Directory Relative Path — no\n * Open/Download/Rename/Delete, since there's no specific row under the cursor.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport * as DropdownMenu from \"@radix-ui/react-dropdown-menu\";\nimport {\n Atom,\n ExternalLink,\n FilePlus,\n FileText,\n FolderPlus,\n Copy,\n Download as DownloadIcon,\n Pencil,\n Trash2,\n} from \"lucide-react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport {\n MenuCursorTrigger,\n MenuContent,\n MenuItem,\n MenuSeparator,\n} from \"@pi-studio-ui/components/primitives/Menu.js\";\nimport { useUiStore } from \"@pi-studio-ui/stores/ui-store.js\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useExplorerStore } from \"@pi-studio-ui/stores/explorer-store.js\";\nimport { useTabStore } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { useFileTransfer } from \"@pi-studio-ui/hooks/use-file-transfer.js\";\nimport { copyText } from \"@pi-studio-ui/lib/clipboard.js\";\nimport { dirOf, relativeToRoot } from \"@pi-studio-ui/lib/paths.js\";\nimport { isMoleculeFile } from \"./viewer-registry.js\";\nimport { openFileTab, openMoleculeTab, openTextTab } from \"./open-file-tab.js\";\nimport { deleteEntry } from \"./delete-entry.js\";\n\nexport function FileContextMenu() {\n const menu = useUiStore((s) => s.fileMenu);\n const closeFileMenu = useUiStore((s) => s.closeFileMenu);\n const client = useConnectionStore((s) => s.client);\n const startDraft = useExplorerStore((s) => s.startDraft);\n const startRename = useExplorerStore((s) => s.startRename);\n const rootPath = useExplorerStore((s) => s.rootPath);\n const activeWorkspaceCwd = useTabStore((s) => s.activeWorkspaceCwd);\n const { saveToDisk } = useFileTransfer();\n const queryClient = useQueryClient();\n const triggerRef = useRef<HTMLButtonElement>(null);\n const [open, setOpen] = useState(false);\n\n useEffect(() => {\n setOpen(menu !== null);\n }, [menu]);\n\n if (!menu) return null;\n const name = menu.path.split(\"/\").pop() || menu.path;\n\n function handleOpenChange(next: boolean) {\n setOpen(next);\n if (!next) closeFileMenu();\n }\n\n function openFile() {\n if (!menu) return;\n closeFileMenu();\n openFileTab(menu.path, activeWorkspaceCwd || \"~\");\n }\n\n function openInMolviewer() {\n if (!menu) return;\n closeFileMenu();\n openMoleculeTab(menu.path, activeWorkspaceCwd || \"~\");\n }\n\n function openAsText() {\n if (!menu) return;\n closeFileMenu();\n openTextTab(menu.path, activeWorkspaceCwd || \"~\");\n }\n\n async function copyAbsolutePath() {\n if (!menu) return;\n closeFileMenu();\n try {\n await copyText(menu.path);\n } catch {\n window.alert(\"Failed to copy to clipboard.\");\n }\n }\n\n async function copyRelativePath() {\n if (!menu) return;\n closeFileMenu();\n try {\n await copyText(relativeToRoot(menu.path, rootPath) || \"/\");\n } catch {\n window.alert(\"Failed to copy to clipboard.\");\n }\n }\n\n async function download() {\n if (!menu) return;\n closeFileMenu();\n try {\n await saveToDisk(menu.path);\n } catch (err) {\n window.alert(`Failed to download: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n async function remove() {\n if (!menu) return;\n const kind = menu.isDirectory ? \"folder (and everything inside it)\" : \"file\";\n if (!window.confirm(`Permanently delete this ${kind}? \"${name}\" — this cannot be undone.`)) {\n return closeFileMenu();\n }\n closeFileMenu();\n if (!client) {\n window.alert(\"Not connected — cannot delete remotely.\");\n return;\n }\n try {\n await deleteEntry(client, menu.path);\n await queryClient.invalidateQueries({ queryKey: [\"explorer\"] });\n useTabStore.getState().closeByPathPrefix(menu.path);\n } catch (err) {\n window.alert(`Failed to delete: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n function startNew(kind: \"file\" | \"directory\") {\n if (!menu) return;\n const parent = menu.isDirectory ? menu.path : dirOf(menu.path);\n closeFileMenu();\n startDraft(parent, kind);\n }\n\n function rename() {\n if (!menu) return;\n closeFileMenu();\n startRename(menu.path);\n }\n\n return (\n <DropdownMenu.Root open={open} onOpenChange={handleOpenChange} modal={false}>\n <DropdownMenu.Trigger asChild>\n <MenuCursorTrigger ref={triggerRef} x={menu.x} y={menu.y} />\n </DropdownMenu.Trigger>\n <MenuContent minWidth={140}>\n {menu.background ? (\n <>\n <MenuItem onSelect={() => startNew(\"file\")}>\n <FilePlus size={13} />\n New File\n </MenuItem>\n <MenuItem onSelect={() => startNew(\"directory\")}>\n <FolderPlus size={13} />\n New Folder\n </MenuItem>\n <MenuSeparator />\n <MenuItem onSelect={copyAbsolutePath}>\n <Copy size={13} />\n Copy Current Directory Path\n </MenuItem>\n <MenuItem onSelect={copyRelativePath}>\n <Copy size={13} />\n Copy Current Directory Relative Path\n </MenuItem>\n </>\n ) : (\n <>\n {!menu.isDirectory && (\n <>\n <MenuItem onSelect={openFile}>\n <ExternalLink size={13} />\n Open\n </MenuItem>\n <MenuItem onSelect={openInMolviewer}>\n <Atom size={13} />\n Open in MolViewer\n </MenuItem>\n {isMoleculeFile(menu.path) && (\n <MenuItem onSelect={openAsText}>\n <FileText size={13} />\n Open as Text\n </MenuItem>\n )}\n <MenuSeparator />\n </>\n )}\n {menu.isDirectory && (\n <>\n <MenuItem onSelect={() => startNew(\"file\")}>\n <FilePlus size={13} />\n New File\n </MenuItem>\n <MenuItem onSelect={() => startNew(\"directory\")}>\n <FolderPlus size={13} />\n New Folder\n </MenuItem>\n <MenuSeparator />\n </>\n )}\n <MenuItem onSelect={copyAbsolutePath}>\n <Copy size={13} />\n Copy Absolute Path\n </MenuItem>\n <MenuItem onSelect={copyRelativePath}>\n <Copy size={13} />\n Copy Relative Path\n </MenuItem>\n <MenuSeparator />\n {!menu.isDirectory && (\n <>\n <MenuItem onSelect={download}>\n <DownloadIcon size={13} />\n Download\n </MenuItem>\n <MenuSeparator />\n </>\n )}\n <MenuItem onSelect={rename}>\n <Pencil size={13} />\n Rename\n </MenuItem>\n <MenuItem danger onSelect={remove}>\n <Trash2 size={13} />\n Delete\n </MenuItem>\n </>\n )}\n </MenuContent>\n </DropdownMenu.Root>\n );\n}\n","/**\n * Shared `file_create_request` caller for the creation affordances (workspace-picker \"new folder\",\n * the Files tree's inline \"new file\"/\"new folder\" draft row, and the molecule viewer claiming a\n * name for a freshly built polymer) — one place for the wire call and the server error-code →\n * user-facing message mapping.\n */\n\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\n\nexport type CreateEntryKind = \"file\" | \"directory\";\n\nconst ERROR_MESSAGES: Record<string, string> = {\n empty_path: \"Choose a folder first.\",\n invalid_name: 'Invalid name — cannot be empty or contain \"/\".',\n exists: \"An item with that name already exists here.\",\n not_found: \"That folder no longer exists.\",\n not_a_directory: \"That path is not a folder.\",\n unreadable: \"That folder is not readable.\",\n};\n\n/**\n * Thrown by {@link createEntry}. Carries the raw server `code` alongside the user-facing\n * `message`, because one caller has to branch on it rather than just render it: the molecule\n * viewer's polymer build walks candidate names past `exists` until one is free\n * (`polymer-file.ts`), and matching on the rendered prose instead would break the moment the\n * wording changes.\n */\nexport class CreateEntryError extends Error {\n constructor(\n readonly code: string,\n message: string,\n ) {\n super(message);\n this.name = \"CreateEntryError\";\n }\n}\n\n/** Create an empty file or a directory named `name` inside `parentPath`; returns its absolute path.\n * Throws an Error carrying a user-facing message on failure. */\nexport async function createEntry(\n client: PiStudioClient,\n parentPath: string,\n name: string,\n kind: CreateEntryKind,\n): Promise<string> {\n const response = await client.connection.request<{ ok: boolean; path?: string; error?: string }>(\n \"file_create_request\",\n { path: parentPath, name, kind },\n );\n if (!response.ok) {\n const code = response.error ?? \"\";\n throw new CreateEntryError(code, ERROR_MESSAGES[code] ?? code ?? \"Failed to create\");\n }\n return (\n response.path ?? (parentPath.endsWith(\"/\") ? `${parentPath}${name}` : `${parentPath}/${name}`)\n );\n}\n","/**\n * FileExplorer — right-sidebar Files tab, tree view (POC `loadFiles`/`renderRightPanel`,\n * chat.html ~line 1014-1053, POC_TO_APP_PLAN_UI.md §4.7 — superseded the original breadcrumb-\n * style single-directory browser with a lazy-loading tree). Each expanded directory fetches its\n * own listing (`useExplorerTree`, one `file_explorer_request` per expanded path); `file-tree.ts`\n * flattens root + expansion set + per-path listings into the ordered row list rendered here\n * through `@tanstack/react-virtual` (same virtualizer pattern as `Timeline.tsx`). The workspace\n * root is the tree's own first row: a collapsible directory row naming the cwd, styled as a\n * heading, not draggable, and wired to the *background* context-menu variant (New File/New\n * Folder/Copy path — never Rename/Delete, which make no sense for the workspace root). It also\n * carries no git tint or dot-ghosting, both of which would fire on almost every workspace.\n * Directory click toggles expand/collapse in place; file click opens a file tab. Rows are drag\n * sources for an internal move/rename (`file_move_request`, sprint-046) — dragging a row onto a\n * directory (or file, which targets its parent) moves it there. Dragging files in from the OS\n * uploads them into the hovered row's directory (same highlight + 700ms auto-expand as an\n * internal move, `resolveUploadTarget` in move-target.ts) or the workspace root when dropped on\n * empty space or the header's \"Upload\" button; `dataTransfer.types` — `\"Files\"` vs the internal\n * `application/x-pi-studio-path` MIME — discriminates the two drag kinds before any per-row state\n * is touched, so a stale internal-drag ref can never hijack an OS-file drop into the wrong\n * directory (or vice versa). Each row can be saved back to disk via its \"⋮\" menu. Downloads ride\n * the binary file-transfer frames (features/file-explorer-transfer.md).\n */\n\nimport { useEffect, useMemo, useRef, useState, type DragEvent } from \"react\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { Upload, FilePlus, FolderPlus } from \"lucide-react\";\nimport { EmptyState } from \"@pi-studio-ui/components/primitives/EmptyState.js\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useExplorerStore, resolveTildePath } from \"@pi-studio-ui/stores/explorer-store.js\";\nimport { useExplorerTree } from \"@pi-studio-ui/hooks/use-explorer-tree.js\";\nimport { useExplorerWatch } from \"@pi-studio-ui/hooks/use-explorer-watch.js\";\nimport { useFileTransfer } from \"@pi-studio-ui/hooks/use-file-transfer.js\";\nimport { useTabStore } from \"@pi-studio-ui/stores/tab-store.js\";\nimport type { DiffTabData, FileTabData, MoleculeTabData } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { useUiStore } from \"@pi-studio-ui/stores/ui-store.js\";\nimport { useGitStore } from \"@pi-studio-ui/stores/git-store.js\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\nimport { dirOf } from \"@pi-studio-ui/lib/paths.js\";\nimport { resolveMoveTarget, resolveUploadTarget } from \"./move-target.js\";\nimport { moveEntry } from \"./move-entry.js\";\nimport { withClosedDiffs } from \"./move-status.js\";\nimport { flattenTree, joinPath, rowKey } from \"./file-tree.js\";\nimport { buildGitStatusLookup, buildIgnoredMatcher } from \"./git-status-index.js\";\nimport { TreeNode } from \"./TreeNode.js\";\nimport { FileContextMenu } from \"./FileContextMenu.js\";\nimport { createEntry } from \"./create-entry.js\";\nimport { openFileTab } from \"./open-file-tab.js\";\nimport { EXTERNAL_DRAG_MIME } from \"@pi-studio-ui/features/workspace/external-drag.js\";\nimport styles from \"./FileExplorer.module.css\";\n\nconst ROW_HEIGHT_PX = 28;\n/** MIME type discriminating an internal row drag from an OS-file drag — `dataTransfer` exposes\n * the type *list* during `dragover` but not the *value* (protected mode), so this is the only\n * signal available before drop. */\nconst MOVE_MIME = \"application/x-pi-studio-path\";\n/** Standard `dataTransfer.types` entry present whenever the OS drag payload includes files —\n * browsers populate `types` with this during dragenter/dragover but only expose the actual\n * `dataTransfer.files` list at drop. */\nconst OS_FILE_TYPE = \"Files\";\n\nexport function FileExplorer() {\n const client = useConnectionStore((s) => s.client);\n const rootPath = useExplorerStore((s) => s.rootPath);\n const expanded = useExplorerStore((s) => s.expanded);\n const setRoot = useExplorerStore((s) => s.setRoot);\n const toggle = useExplorerStore((s) => s.toggle);\n const draft = useExplorerStore((s) => s.draft);\n const startDraft = useExplorerStore((s) => s.startDraft);\n const cancelDraft = useExplorerStore((s) => s.cancelDraft);\n const renaming = useExplorerStore((s) => s.renaming);\n const cancelRename = useExplorerStore((s) => s.cancelRename);\n const selected = useExplorerStore((s) => s.selected);\n const setSelected = useExplorerStore((s) => s.setSelected);\n // The workspace currently in view in the tab strip drives what Files/Changes browse — same\n // authoritative signal every tab creation site uses (§4.7 follow-up: workspace-scoped tabs).\n const activeWorkspaceCwd = useTabStore((s) => s.activeWorkspaceCwd);\n const activeTab = useTabStore((s) => s.tabs.find((t) => t.id === s.activeTabId));\n // Live checkout status for the active workspace — `StatusBar` owns the subscription, so this is\n // a pure read; the tree tints rows from it and ghosts ignored ones (no extra RPC).\n const changes = useGitStore((s) => s.changes);\n const ignored = useGitStore((s) => s.ignored);\n const { upload } = useFileTransfer();\n const openFileMenu = useUiStore((s) => s.openFileMenu);\n const queryClient = useQueryClient();\n\n const [status, setStatus] = useState<{ text: string; error: boolean } | null>(null);\n // The row currently highlighted as the drop target.\n const [dropTargetRowPath, setDropTargetRowPath] = useState<string | null>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const scrollRef = useRef<HTMLDivElement>(null);\n // Path of the row currently being dragged — `dataTransfer.getData()` returns \"\" during\n // dragover/dragenter in every browser (only dragstart/drop can read it), so target validation\n // needs the source path held in a ref instead.\n const dragSourceRef = useRef<string | null>(null);\n // Pending \"expand this collapsed directory after 700ms of hover\" timer, mid-drag.\n const autoExpandRef = useRef<{ path: string; timer: number } | null>(null);\n\n // The path of the file/molecule tab currently in view, for the active-row highlight (item 6) —\n // null for chat/terminal/diff tabs, or when nothing's active.\n const activeFilePath =\n activeTab?.kind === \"file\" || activeTab?.kind === \"molecule\"\n ? (activeTab.data as FileTabData | MoleculeTabData).path\n : null;\n\n // Where the header's \"New File\"/\"New Folder\" buttons create their entry: the selected\n // directory, or the selected file's parent — falls back to `rootPath`. The empty-space\n // context menu deliberately does NOT use this — it always targets `rootPath` (right-clicking\n // below the last row means \"here, at the root\", regardless of what's still selected above).\n const createTargetDir = selected\n ? selected.isDirectory\n ? selected.path\n : dirOf(selected.path)\n : rootPath;\n\n // Re-root whenever the active workspace changes, and seed it once on first connect.\n // `lastSeededCwd` distinguishes \"the active workspace changed\" from a re-render — only the\n // former reseeds (and restores that workspace's remembered `expanded` set, see explorer-store).\n const lastSeededCwd = useRef<string | null>(null);\n useEffect(() => {\n if (!client) return;\n const target = activeWorkspaceCwd || \"~\";\n if (lastSeededCwd.current === target) return;\n lastSeededCwd.current = target;\n void resolveTildePath(client, target).then((resolved) => setRoot(resolved));\n }, [client, activeWorkspaceCwd, setRoot]);\n\n const tree = useExplorerTree(expanded);\n useExplorerWatch(expanded);\n const rows = useMemo(\n () => flattenTree(rootPath, expanded, tree, draft, renaming),\n [rootPath, expanded, tree, draft, renaming],\n );\n const gitStatusOf = useMemo(() => buildGitStatusLookup(rootPath, changes), [rootPath, changes]);\n const isIgnored = useMemo(() => buildIgnoredMatcher(rootPath, ignored), [rootPath, ignored]);\n\n const virtualizer = useVirtualizer({\n count: rows.length,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => ROW_HEIGHT_PX,\n overscan: 12,\n getItemKey: (index) => {\n const row = rows[index];\n return row ? rowKey(row) : index;\n },\n });\n\n function handleOpenFile(path: string) {\n openFileTab(path, activeWorkspaceCwd || \"~\");\n }\n\n /**\n * A row carries up to two payloads and *where* it is dropped picks the meaning: another row is a\n * move (`MOVE_MIME`, handled here), a pane body is an open (`EXTERNAL_DRAG_MIME.path`, handled by\n * `use-external-pane-drop.ts`). A directory offers no open payload — there is no directory tab —\n * so dragging one over a pane previews nothing rather than previewing a split that would be refused.\n *\n * `effectAllowed` must cover both outcomes: this tree sets `dropEffect = \"move\"` for its own rows\n * and a pane sets `\"copy\"`, and a `dropEffect` outside `effectAllowed` is a rejected drop.\n */\n function handleDragStartRow(path: string, isDirectory: boolean, e: DragEvent) {\n e.dataTransfer.setData(MOVE_MIME, path);\n if (!isDirectory) e.dataTransfer.setData(EXTERNAL_DRAG_MIME.path, path);\n e.dataTransfer.effectAllowed = isDirectory ? \"move\" : \"copyMove\";\n dragSourceRef.current = path;\n }\n\n function handleDragEndRow() {\n dragSourceRef.current = null;\n }\n\n function clearDropState() {\n setDropTargetRowPath(null);\n if (autoExpandRef.current) {\n clearTimeout(autoExpandRef.current.timer);\n autoExpandRef.current = null;\n }\n }\n\n function scheduleAutoExpand(row: { kind: string; path: string; expanded?: boolean }) {\n if (autoExpandRef.current?.path === row.path) return;\n if (autoExpandRef.current) clearTimeout(autoExpandRef.current.timer);\n autoExpandRef.current = null;\n if (row.kind !== \"directory\" || row.expanded) return;\n const timer = window.setTimeout(() => toggle(row.path), 700);\n autoExpandRef.current = { path: row.path, timer };\n }\n\n /** Issue the move, then reconcile caches, tree state, and tabs — shared by drag-move and\n * rename (task-002: extracted before rename, its second caller, exists so the two can never\n * drift). The daemon's echoed `destination` is authoritative from here on: after the\n * trimmed-basename fix it may legitimately differ from what was requested, and repathing or\n * reopening the client-computed path instead would target a path that no longer exists. */\n async function applyMove(\n source: string,\n requestedDestination: string,\n ): Promise<{ destination: string; closedDiffs: number }> {\n if (!client) throw new Error(\"Not connected.\");\n const destination = await moveEntry(client, source, requestedDestination);\n const destinationDir = dirOf(destination);\n await queryClient.invalidateQueries({ queryKey: rpcKeys.explorer(dirOf(source)) });\n await queryClient.invalidateQueries({ queryKey: rpcKeys.explorer(destinationDir) });\n useExplorerStore.getState().repathAfterMove(source, destination, destinationDir);\n\n // Partition BEFORE closing — closeByPathPrefix removes the matching tabs, so counting\n // afterwards would always read 0. Same predicate closeByPathPrefix uses, so diff tabs under\n // a renamed/moved directory are counted too (the recorded decision: \"the same count covers\n // them\").\n const matches = useTabStore.getState().tabs.filter((t) => {\n if (t.kind !== \"file\" && t.kind !== \"diff\" && t.kind !== \"molecule\") return false;\n const path = (t.data as FileTabData | DiffTabData | MoleculeTabData).path;\n return path === source || (path?.startsWith(`${source}/`) ?? false);\n });\n const hadTab = matches.some(\n (t) =>\n (t.kind === \"file\" || t.kind === \"molecule\") &&\n (t.data as FileTabData | MoleculeTabData).path === source,\n );\n const closedDiffs = matches.filter((t) => t.kind === \"diff\").length;\n\n useTabStore.getState().closeByPathPrefix(source);\n if (hadTab) handleOpenFile(destination);\n\n return { destination, closedDiffs };\n }\n\n async function moveDropped(source: string, row: { kind: string; path: string }) {\n const target = resolveMoveTarget(source, row, rootPath);\n if (!target || !client) return;\n const basename = source.split(\"/\").pop() ?? source;\n setStatus({ text: `Moving ${basename}…`, error: false });\n try {\n const { destination, closedDiffs } = await applyMove(source, target.destination);\n setStatus({\n text: withClosedDiffs(\n `Moved to ${dirOf(destination).split(\"/\").pop() || \"/\"}`,\n closedDiffs,\n ),\n error: false,\n });\n } catch (err) {\n setStatus({ text: err instanceof Error ? err.message : \"Failed to move\", error: true });\n }\n }\n\n async function submitDraft(parentPath: string, name: string) {\n const kind = draft?.kind ?? \"file\";\n cancelDraft();\n if (!client) return;\n try {\n const created = await createEntry(client, parentPath, name, kind);\n await queryClient.invalidateQueries({ queryKey: rpcKeys.explorer(parentPath) });\n setStatus({ text: `Created ${created}`, error: false });\n if (kind === \"file\") handleOpenFile(created);\n } catch (err) {\n setStatus({ text: err instanceof Error ? err.message : \"Failed to create\", error: true });\n }\n }\n\n async function submitRename(path: string, newName: string) {\n cancelRename();\n if (!client) return;\n // Same-parent move — deliberately NOT `resolveMoveTarget`, which returns `null` for a\n // same-directory destination (correct for drag: dropping a row into its own folder is a\n // no-op; fatal here). All rename legality is decided server-side by `moveEntry`.\n const destination = joinPath(dirOf(path), newName);\n setStatus({ text: `Renaming ${path.split(\"/\").pop()}…`, error: false });\n try {\n const { destination: finalPath, closedDiffs } = await applyMove(path, destination);\n setStatus({\n text: withClosedDiffs(`Renamed to ${finalPath.split(\"/\").pop()}`, closedDiffs),\n error: false,\n });\n } catch (err) {\n setStatus({ text: err instanceof Error ? err.message : \"Failed to rename\", error: true });\n }\n }\n\n async function uploadFiles(dir: string, files: File[]) {\n if (!files.length || !dir) return;\n const existingListing = tree.get(dir)?.listing;\n const existing = new Set(existingListing?.entries.map((e) => e.name) ?? []);\n const clashes = files.filter((f) => existing.has(f.name));\n if (clashes.length > 0) {\n const names = clashes.map((f) => f.name).join(\", \");\n if (\n !window.confirm(`Overwrite ${clashes.length > 1 ? \"these files\" : \"this file\"}? ${names}`)\n ) {\n return;\n }\n }\n\n setStatus({\n text: `Uploading ${files.length} file${files.length > 1 ? \"s\" : \"\"}…`,\n error: false,\n });\n try {\n for (const file of files) await upload(dir, file);\n setStatus({\n text: `Uploaded ${files.length} file${files.length > 1 ? \"s\" : \"\"}`,\n error: false,\n });\n } catch (e) {\n setStatus({\n text: `Upload failed: ${e instanceof Error ? e.message : \"unknown\"}`,\n error: true,\n });\n }\n }\n\n function handleDrop(e: DragEvent, row?: { kind: string; path: string }) {\n e.preventDefault();\n // `dataTransfer.types` is the one signal fully owned by the browser for the CURRENT drag —\n // it can't go stale the way an app-managed ref could (e.g. a `dragend` that never fired for a\n // prior attempt) — so it alone decides which branch below runs, never `dragSourceRef` first.\n if (e.dataTransfer.types.includes(MOVE_MIME)) {\n const source = e.dataTransfer.getData(MOVE_MIME) || dragSourceRef.current;\n clearDropState();\n if (source) void moveDropped(source, row ?? { kind: \"directory\", path: rootPath });\n return;\n }\n clearDropState();\n if (!e.dataTransfer.types.includes(OS_FILE_TYPE)) return;\n const dir = row ? resolveUploadTarget(row, rootPath) : rootPath;\n if (!dir) return;\n const files = Array.from(e.dataTransfer.files);\n if (files.length) void uploadFiles(dir, files);\n }\n\n function handleDragOver(e: DragEvent) {\n const isInternalMove = e.dataTransfer.types.includes(MOVE_MIME);\n if (!isInternalMove && !e.dataTransfer.types.includes(OS_FILE_TYPE)) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = isInternalMove ? \"move\" : \"copy\";\n }\n\n if (!client) {\n return <EmptyState>Connect to browse files</EmptyState>;\n }\n\n return (\n <div\n className={styles.container}\n onDragOver={handleDragOver}\n onDragLeave={(e) => {\n // `dragleave` mirrors `mouseout`, not `mouseleave` — it bubbles and refires on every\n // child-element boundary crossing, including moving between two spans inside the SAME\n // row. Without this guard, every such micro-crossing bubbles here and wipes the\n // just-set highlight a tick after `onDragEnter` set it — the \"flashes once, then goes\n // dark\" symptom. Only clear when the drag is actually leaving the whole panel, i.e. the\n // element being entered next is outside this container.\n if (e.currentTarget.contains(e.relatedTarget as Node | null)) return;\n clearDropState();\n }}\n onDrop={(e) => handleDrop(e)}\n >\n <div className={styles.header}>\n <button\n type=\"button\"\n className={styles.uploadButton}\n onClick={() => startDraft(createTargetDir, \"file\")}\n disabled={!rootPath}\n title={\n selected\n ? `New file in ${createTargetDir.split(\"/\").pop() || \"/\"}`\n : \"New file in workspace root\"\n }\n >\n <FilePlus size={14} />\n </button>\n <button\n type=\"button\"\n className={styles.uploadButton}\n onClick={() => startDraft(createTargetDir, \"directory\")}\n disabled={!rootPath}\n title={\n selected\n ? `New folder in ${createTargetDir.split(\"/\").pop() || \"/\"}`\n : \"New folder in workspace root\"\n }\n >\n <FolderPlus size={14} />\n </button>\n <button\n type=\"button\"\n className={styles.uploadButton}\n onClick={() => fileInputRef.current?.click()}\n disabled={!rootPath}\n title=\"Upload files to the workspace root\"\n >\n <Upload size={14} />\n <span>Upload</span>\n </button>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n className={styles.hiddenInput}\n onChange={(e) => {\n void uploadFiles(rootPath, Array.from(e.target.files ?? []));\n e.target.value = \"\";\n }}\n />\n </div>\n\n {status && (\n <div className={status.error ? styles.statusError : styles.status}>{status.text}</div>\n )}\n\n <div\n className={styles.list}\n ref={scrollRef}\n onContextMenu={(e) => {\n e.preventDefault();\n openFileMenu(rootPath, true, e.clientX, e.clientY, true);\n }}\n >\n {rows.length === 0 && <EmptyState>No files loaded</EmptyState>}\n <div style={{ position: \"relative\", height: virtualizer.getTotalSize() }}>\n {virtualizer.getVirtualItems().map((virtualRow) => {\n const row = rows[virtualRow.index];\n if (!row) return null;\n const isRootRow = row.path === rootPath && row.kind === \"directory\";\n return (\n <div\n key={virtualRow.key}\n data-index={virtualRow.index}\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: virtualRow.size,\n transform: `translateY(${virtualRow.start}px)`,\n }}\n onDragEnter={(e) => {\n // Gate on `dataTransfer.types`, not just the ref — see `handleDrop`'s comment.\n if (e.dataTransfer.types.includes(MOVE_MIME)) {\n const source = dragSourceRef.current;\n if (!source) return;\n const target = resolveMoveTarget(source, row, rootPath);\n setDropTargetRowPath(target ? row.path : null);\n scheduleAutoExpand(row);\n return;\n }\n if (!e.dataTransfer.types.includes(OS_FILE_TYPE)) return;\n const target = resolveUploadTarget(row, rootPath);\n setDropTargetRowPath(target ? row.path : null);\n scheduleAutoExpand(row);\n }}\n onDrop={(e) => {\n e.stopPropagation();\n handleDrop(e, row);\n }}\n >\n <TreeNode\n row={row}\n active={\n (row.kind === \"file\" || row.kind === \"directory\") && row.path === activeFilePath\n }\n selected={\n (row.kind === \"file\" || row.kind === \"directory\") && row.path === selected?.path\n }\n gitStatus={\n !isRootRow && (row.kind === \"file\" || row.kind === \"directory\")\n ? gitStatusOf(row.path)\n : undefined\n }\n hidden={\n !isRootRow &&\n (row.kind === \"file\" || row.kind === \"directory\") &&\n (row.name.startsWith(\".\") || isIgnored(row.path))\n }\n isRoot={isRootRow}\n onToggle={(path) => {\n setSelected({ path, isDirectory: true });\n toggle(path);\n }}\n onOpenFile={(path) => {\n setSelected({ path, isDirectory: false });\n handleOpenFile(path);\n }}\n onContextMenu={(path, isDirectory, x, y) =>\n openFileMenu(path, isDirectory, x, y, isRootRow)\n }\n onSubmitDraft={(parentPath, name) => void submitDraft(parentPath, name)}\n onCancelDraft={cancelDraft}\n onSubmitRename={(path, name) => void submitRename(path, name)}\n onCancelRename={cancelRename}\n onDragStartRow={handleDragStartRow}\n onDragEndRow={handleDragEndRow}\n dropTarget={\n (row.kind === \"file\" || row.kind === \"directory\") &&\n row.path === dropTargetRowPath\n }\n />\n </div>\n );\n })}\n </div>\n </div>\n\n <FileContextMenu />\n </div>\n );\n}\n","/**\n * ChangesPanel — right-sidebar Changes tab (POC `renderRightPanel` changes branch, chat.html\n * ~line 1028-1039, POC_TO_APP_PLAN_UI.md §4.7). A/M/D badges + file path; click opens a diff tab.\n * A pure `git-store` consumer — the live checkout-status subscription for the active workspace is\n * owned solely by `StatusBar` (sprint-042; it is always mounted, unlike this panel, which only\n * exists while the Changes tab is selected). Two independent `useCheckoutStatus(cwd)` callers on\n * the same cwd would collide: the daemon's `checkout_status_subscribe`/`_unsubscribe` handlers key\n * on a flat `session:cwd` map (`packages/server/src/projects/git-checkout-rpc.ts`), not\n * reference-counted, so whichever hook instance unmounts first (e.g. switching away from this\n * tab) would silently kill the *other* instance's live feed too.\n */\n\nimport { clsx } from \"clsx\";\nimport { EmptyState } from \"@pi-studio-ui/components/primitives/EmptyState.js\";\nimport { useGitStore } from \"@pi-studio-ui/stores/git-store.js\";\nimport { useTabStore, tabIds } from \"@pi-studio-ui/stores/tab-store.js\";\nimport styles from \"./ChangesPanel.module.css\";\n\nconst BADGE_LABEL: Record<\"added\" | \"modified\" | \"deleted\", string> = {\n added: \"A\",\n modified: \"M\",\n deleted: \"D\",\n};\n\nexport function ChangesPanel() {\n const activeWorkspaceCwd = useTabStore((s) => s.activeWorkspaceCwd);\n const cwd = activeWorkspaceCwd || \"~\";\n\n const changes = useGitStore((s) => s.changes);\n const openTab = useTabStore((s) => s.open);\n\n function handleClick(path: string, staged: boolean) {\n openTab({\n id: tabIds.diff(path, staged),\n kind: \"diff\",\n label: path.split(\"/\").pop() || path,\n closable: true,\n data: { path, staged },\n workspaceCwd: cwd,\n });\n }\n\n if (changes.length === 0) {\n return <EmptyState>No changes detected</EmptyState>;\n }\n\n return (\n <div className={styles.list}>\n <div className={styles.summary}>\n {changes.length} changed file{changes.length === 1 ? \"\" : \"s\"}\n </div>\n {changes.map((change, i) => (\n <div\n key={`${change.path}-${change.staged}-${i}`}\n className={styles.item}\n onClick={() => handleClick(change.path, change.staged)}\n >\n <span className={clsx(styles.badge, styles[change.status])}>\n {BADGE_LABEL[change.status]}\n </span>\n <span className={styles.name}>{change.path}</span>\n </div>\n ))}\n </div>\n );\n}\n","/**\n * RightSidebar — Files/Changes tab bar + refresh button (POC `.sidebar-right`, chat.html\n * ~line 364-374, POC_TO_APP_PLAN_UI.md §4.7). Imported by `routes/WorkspacePage.tsx`. Used to\n * also host a Terminals tab (live PTY list + reattach/kill) — removed once the TabStrip's \"+\"\n * menu (New chat / New terminal) made per-tab terminal creation directly discoverable, leaving\n * the sidebar entry redundant (GitHub issue #8 follow-up).\n */\n\nimport { RotateCw, Folder, GitCompare } from \"lucide-react\";\nimport { clsx } from \"clsx\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { Button } from \"@pi-studio-ui/components/primitives/Button.js\";\nimport { Panel } from \"@pi-studio-ui/components/primitives/Panel.js\";\nimport { useUiStore } from \"@pi-studio-ui/stores/ui-store.js\";\nimport { useTabStore } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { FileExplorer } from \"./FileExplorer.js\";\nimport { ChangesPanel } from \"@pi-studio-ui/features/git/ChangesPanel.js\";\nimport styles from \"./RightSidebar.module.css\";\n\nexport function RightSidebar() {\n const rightSidebarTab = useUiStore((s) => s.rightSidebarTab);\n const setRightSidebarTab = useUiStore((s) => s.setRightSidebarTab);\n // Files/Changes browse the workspace currently in view in the tab strip — the same signal\n // every tab creation site uses (§4.7 follow-up: workspace-scoped tabs).\n const activeWorkspaceCwd = useTabStore((s) => s.activeWorkspaceCwd);\n const cwd = activeWorkspaceCwd || \"~\";\n const client = useConnectionStore((s) => s.client);\n const queryClient = useQueryClient();\n\n // Fires a one-shot refresh RPC (POC `checkout_refresh_request`) — subscription lifecycle for\n // live `checkout_status_update` pushes is owned solely by `StatusBar`'s `useCheckoutStatus`\n // (sprint-042: promoted from `ChangesPanel` since `StatusBar` is always mounted), so this\n // button never opens a second subscription that could outlive/race it.\n function handleRefresh() {\n void queryClient.invalidateQueries({ queryKey: [\"explorer\"] });\n if (client && cwd) {\n void client.connection.request(\"checkout_refresh_request\", { cwd }).catch(() => {});\n }\n }\n return (\n <Panel>\n <div className={styles.tabBar}>\n <button\n type=\"button\"\n className={clsx(styles.tabBtn, rightSidebarTab === \"files\" && styles.active)}\n onClick={() => setRightSidebarTab(\"files\")}\n >\n <Folder size={13} />\n Files\n </button>\n <button\n type=\"button\"\n className={clsx(styles.tabBtn, rightSidebarTab === \"changes\" && styles.active)}\n onClick={() => setRightSidebarTab(\"changes\")}\n >\n <GitCompare size={13} />\n Changes\n </button>\n <Button\n size=\"xs\"\n variant=\"ghost\"\n iconOnly\n title=\"Refresh\"\n className={styles.refreshBtn}\n onClick={handleRefresh}\n >\n <RotateCw size={13} />\n </Button>\n </div>\n <div className={styles.content}>\n {rightSidebarTab === \"files\" ? <FileExplorer /> : <ChangesPanel />}\n </div>\n </Panel>\n );\n}\n","/**\n * Stats store — per-session context/token/cost usage + poll-reconciled model, for the workspace\n * status bar (sprint-042). Sourced from `agent_session_stats_request` (SDK\n * `client.agent(id).sessionStats()`), which is pull-only: no stream event carries this data (see\n * `use-session-stats.ts`, which polls it). Keyed by **sessionId**, not `agentId` — a fresh session\n * has no `agentId` yet, and keying by sessionId lets switching back to a previously-visited\n * session show its last-known stats instantly instead of a blank flash while a fresh poll runs.\n */\n\nimport { create } from \"zustand\";\n\nexport interface SessionStats {\n contextTokens?: number;\n contextWindow?: number;\n /** 0–1 fraction (matches the wire `agentContextUsageSchema.percent` convention). */\n contextPercent?: number;\n totalTokens?: number;\n inputTokens?: number;\n outputTokens?: number;\n cost?: number;\n /** Poll-reconciled model id — see `agent_session_stats_response.payload.model` (sprint-042). */\n model?: string;\n}\n\ninterface StatsStoreState {\n bySession: Record<string, SessionStats>;\n\n /** Shallow-merge a partial update into one session's stats (missing keys are left untouched). */\n setStats(sessionId: string, partial: Partial<SessionStats>): void;\n clear(sessionId: string): void;\n}\n\nexport const useStatsStore = create<StatsStoreState>()((set) => ({\n bySession: {},\n\n setStats(sessionId, partial) {\n set((s) => ({\n bySession: {\n ...s.bySession,\n [sessionId]: { ...s.bySession[sessionId], ...partial },\n },\n }));\n },\n\n clear(sessionId) {\n set((s) => {\n const { [sessionId]: _removed, ...rest } = s.bySession;\n return { bySession: rest };\n });\n },\n}));\n","/**\n * `use-session-stats` — polls `agent_session_stats_request` (SDK `client.agent(id).sessionStats()`)\n * for one session's context/token/cost usage and poll-reconciled model, writing into `stats-store`\n * (sprint-042). This data is **pull-only**: no `AgentStreamEvent` kind carries context/token/cost\n * (see `agentStreamEventSchema` in `@av-pi-studio/protocol`), so there is no push channel to react\n * to — a poll is the only option.\n *\n * Poll triggers:\n * - once when a session with a bound `agentId` becomes the argument (mount / session switch /\n * agent bound after create),\n * - every `POLL_INTERVAL_MS` as a backstop while that stays true,\n * - immediately when the session's `status` transitions away from `\"running\"` — a proxy for\n * `turn_completed`/`turn_failed`/`turn_canceled` that avoids a second raw `agent_stream`\n * subscription: `agent-stream-events.ts` already drives exactly that status transition off those\n * stream events, so watching `SessionEntry.status` here is equivalent and simpler.\n *\n * A session with no `agentId` yet (a fresh, never-run chat) polls nothing — there is no agent to\n * ask. Failures (unsupported provider, no live session right now) are swallowed; the previous\n * cached value in `stats-store` is left untouched rather than cleared, so a transient failure\n * never blanks the status bar.\n */\n\nimport { useEffect, useRef } from \"react\";\nimport type { AgentSessionStatsResponse, AgentStatus } from \"@av-pi-studio/protocol\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useSessionStore } from \"@pi-studio-ui/stores/session-store.js\";\nimport { useStatsStore, type SessionStats } from \"@pi-studio-ui/stores/stats-store.js\";\n\nconst POLL_INTERVAL_MS = 12_000;\n\ntype SessionStatus = AgentStatus | \"idle\" | undefined;\n\n/** True exactly when a turn just ended: `prev` was `\"running\"` and `next` no longer is. Pure —\n * exported for direct unit testing without rendering the hook. */\nexport function shouldRepollOnStatusChange(prev: SessionStatus, next: SessionStatus): boolean {\n return prev === \"running\" && next !== \"running\";\n}\n\n/** Maps the RPC payload onto `stats-store`'s flatter `SessionStats` shape, writing only fields the\n * payload actually carries — `setStats`'s merge treats an explicit `undefined` value as \"clear\n * this field\", not \"leave it alone\" (object spread doesn't distinguish an absent key from one set\n * to `undefined`), so a poll response that omits e.g. `cost` must never wipe a previously known\n * cost. Also reconciles `session-store`'s `model` — the status bar's model *segment* reads\n * `SessionEntry.model` (seeded by create/restore, updated live by `agent_update`), not\n * `stats-store`, so the poll must write through to keep that value authoritative (design intent:\n * \"stats poll authoritative + agent_update for instant feedback\" — a payload `model` this session\n * never learned any other way, e.g. right after `/model` cycle, must still reach the segment).\n * Exported for direct unit testing without rendering the hook. */\nexport function applySessionStats(\n sessionId: string,\n payload: AgentSessionStatsResponse[\"payload\"],\n): void {\n const patch: Partial<SessionStats> = {};\n if (payload.contextUsage?.tokens != null) patch.contextTokens = payload.contextUsage.tokens;\n if (payload.contextUsage?.contextWindow !== undefined) {\n patch.contextWindow = payload.contextUsage.contextWindow;\n }\n if (payload.contextUsage?.percent != null) patch.contextPercent = payload.contextUsage.percent;\n if (payload.tokens?.total !== undefined) patch.totalTokens = payload.tokens.total;\n if (payload.tokens?.input !== undefined) patch.inputTokens = payload.tokens.input;\n if (payload.tokens?.output !== undefined) patch.outputTokens = payload.tokens.output;\n if (payload.cost !== undefined) patch.cost = payload.cost;\n if (payload.model !== undefined) {\n patch.model = payload.model;\n useSessionStore.getState().setModel(sessionId, payload.model);\n }\n useStatsStore.getState().setStats(sessionId, patch);\n}\n\nexport function useSessionStats(sessionId: string | null): void {\n const client = useConnectionStore((s) => s.client);\n const status = useConnectionStore((s) => s.status);\n const agentId = useSessionStore((s) =>\n sessionId ? (s.sessions[sessionId]?.agentId ?? null) : null,\n );\n const sessionStatus = useSessionStore((s) =>\n sessionId ? s.sessions[sessionId]?.status : undefined,\n );\n const inFlightRef = useRef(false);\n const prevStatusRef = useRef<typeof sessionStatus>(undefined);\n\n // Initial poll (on mount / whenever the polled agent identity changes) + interval backstop.\n useEffect(() => {\n if (status !== \"open\" || !client || !sessionId || !agentId) return;\n\n let cancelled = false;\n const poll = async () => {\n if (inFlightRef.current) return;\n inFlightRef.current = true;\n try {\n const payload = await client.agent(agentId).sessionStats();\n if (!cancelled) applySessionStats(sessionId, payload);\n } catch {\n // Best-effort — leave the prior cached value in place.\n } finally {\n inFlightRef.current = false;\n }\n };\n\n void poll();\n const interval = setInterval(() => void poll(), POLL_INTERVAL_MS);\n return () => {\n cancelled = true;\n clearInterval(interval);\n };\n }, [status, client, sessionId, agentId]);\n\n // Extra immediate poll when a turn just ended (running -> anything else).\n useEffect(() => {\n const prevStatus = prevStatusRef.current;\n prevStatusRef.current = sessionStatus;\n if (!shouldRepollOnStatusChange(prevStatus, sessionStatus)) return;\n if (status !== \"open\" || !client || !sessionId || !agentId) return;\n inFlightRef.current = false; // a stale in-flight interval poll shouldn't block this one\n void client\n .agent(agentId)\n .sessionStats()\n .then((payload) => applySessionStats(sessionId, payload))\n .catch(() => {});\n }, [sessionStatus, status, client, sessionId, agentId]);\n}\n","/**\n * `use-checkout-status` — subscribes to live git checkout status for a cwd (POC `loadChanges` +\n * `handleCheckoutStatusUpdate`, POC_TO_APP_PLAN_UI.md §4.7 / §3 `hooks/use-checkout-status.ts`).\n * On `cwd` change: unsubscribes the previous cwd (`checkout_status_unsubscribe`), subscribes the\n * new one (`checkout_status_subscribe`), and routes pushed `checkout_status_update` session\n * messages into `git-store`. Cleans up both the RPC subscription and the message handler on\n * unmount/cwd-change — the POC leaked subscriptions on cwd change (§4.7).\n */\n\nimport { useEffect, useRef } from \"react\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useGitStore, type CheckoutStatusProjection } from \"@pi-studio-ui/stores/git-store.js\";\n\ninterface CheckoutStatusUpdateMessage {\n type: \"checkout_status_update\";\n cwd: string;\n projection: CheckoutStatusProjection;\n}\n\nfunction isCheckoutStatusUpdate(msg: unknown): msg is CheckoutStatusUpdateMessage {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n \"type\" in msg &&\n msg.type === \"checkout_status_update\" &&\n \"cwd\" in msg &&\n typeof msg.cwd === \"string\" &&\n \"projection\" in msg &&\n typeof msg.projection === \"object\" &&\n msg.projection !== null\n );\n}\n\nexport interface UseCheckoutStatusResult {\n refresh(): void;\n}\n\nexport function useCheckoutStatus(cwd: string): UseCheckoutStatusResult {\n const client = useConnectionStore((s) => s.client);\n const applyProjection = useGitStore((s) => s.applyProjection);\n const setSubscribedCwd = useGitStore((s) => s.setSubscribedCwd);\n const clientRef = useRef(client);\n clientRef.current = client;\n\n useEffect(() => {\n if (!client || !cwd) return;\n\n let cancelled = false;\n void client.connection.request(\"checkout_status_subscribe\", { cwd }).catch(() => {});\n setSubscribedCwd(cwd);\n\n const unsubscribeMessages = client.connection.onSessionMessage((msg) => {\n if (cancelled) return;\n const raw: unknown = msg;\n if (!isCheckoutStatusUpdate(raw)) return;\n if (raw.cwd !== cwd) return;\n applyProjection(raw.projection);\n });\n\n void client.connection.request(\"checkout_refresh_request\", { cwd }).catch(() => {});\n\n return () => {\n cancelled = true;\n unsubscribeMessages();\n void client.connection.request(\"checkout_status_unsubscribe\", { cwd }).catch(() => {});\n setSubscribedCwd(null);\n };\n }, [client, cwd, applyProjection, setSubscribedCwd]);\n\n return {\n refresh() {\n const current = clientRef.current;\n if (!current || !cwd) return;\n void current.connection.request(\"checkout_refresh_request\", { cwd }).catch(() => {});\n },\n };\n}\n","/**\n * Pure, framework-free value formatters for the workspace status bar (sprint-042). No store/DOM\n * access — every function takes plain values and returns a display string, so each is directly\n * unit-testable without rendering anything (matches the project's `lib/`-style convention of\n * keeping core logic free of DOM dependencies).\n */\n\nconst PLACEHOLDER = \"--\";\n\n/** `undefined` → \"--\"; <1000 exact; ≥1000 → \"12.3k\"; ≥1_000_000 → \"1.2M\" (one decimal, trimmed). */\nexport function formatTokens(n: number | undefined): string {\n if (n === undefined) return PLACEHOLDER;\n if (n < 1000) return String(n);\n if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`;\n return `${(n / 1_000_000).toFixed(1)}M`;\n}\n\n/** Accepts either a 0–1 fraction or a 0–100 whole number (the wire's `agentContextUsageSchema\n * .percent` convention is not pinned to one or the other across providers) and normalizes to a\n * rounded integer percentage. `undefined`/`null` → \"--\". */\nexport function formatPercent(p: number | null | undefined): string {\n if (p === null || p === undefined) return PLACEHOLDER;\n const pct = p <= 1 ? p * 100 : p;\n return `${Math.round(pct)}%`;\n}\n\n/** `undefined` → \"--\"; small costs keep more precision (< $1 → 4 decimals), larger costs 2 dp. */\nexport function formatCost(c: number | undefined): string {\n if (c === undefined) return PLACEHOLDER;\n const dp = c < 1 ? 4 : 2;\n return `$${c.toFixed(dp)}`;\n}\n\n/** Collapses a leading `home` prefix to `~`. No-op when `home` is null/undefined or not a prefix\n * of `cwd`. Does not truncate/ellipsize — that's a CSS concern for the component to apply. */\nexport function formatCwd(cwd: string, home: string | null | undefined): string {\n if (!home) return cwd;\n if (cwd === home) return \"~\";\n if (cwd.startsWith(`${home}/`)) return `~${cwd.slice(home.length)}`;\n return cwd;\n}\n\n/** `\"↑2 ↓1\"` — omits a zero side; `\"\"` when both are zero (nothing to show). */\nexport function formatBranchMeta(ahead: number, behind: number): string {\n const parts: string[] = [];\n if (ahead > 0) parts.push(`↑${ahead}`);\n if (behind > 0) parts.push(`↓${behind}`);\n return parts.join(\" \");\n}\n","/**\n * StatusBar — full-width, 35px, y-centered powerline bar at the bottom of the workspace shell\n * (sprint-042). Renders icon-prefixed segments for the **active session**: cwd, git branch\n * (+ ahead/behind/dirty/conflict), context usage, token total, and cost. Fully swaps when\n * `activeSessionId` changes — every value is read from the active session's entry or from\n * per-session `stats-store`; nothing here is scoped to a specific tab. Mounting this component is\n * also what drives the active session's stats poll (`useSessionStats`, sprint-042/task-004) — no\n * other consumer of `stats-store` exists yet, so the poll runs exactly while the bar is on screen.\n *\n * Every segment here is read-only. The interactive model picker this bar used to host now lives\n * in the composer's bottom toolbar (`features/chat/Composer.tsx`), next to the actions it belongs\n * with; this bar owns no `ModelMenu`, no `setModel`, and no `ensureMaterialized` call.\n */\n\nimport type { ReactNode } from \"react\";\nimport { ChevronRight, Coins, DollarSign, Folder, GitBranch, Gauge } from \"lucide-react\";\nimport { useSessionStore } from \"@pi-studio-ui/stores/session-store.js\";\nimport { useTabStore } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { useGitStore } from \"@pi-studio-ui/stores/git-store.js\";\nimport { useStatsStore } from \"@pi-studio-ui/stores/stats-store.js\";\nimport { useSessionStats } from \"@pi-studio-ui/hooks/use-session-stats.js\";\nimport { useCheckoutStatus } from \"@pi-studio-ui/hooks/use-checkout-status.js\";\nimport { useHomeDir } from \"@pi-studio-ui/hooks/use-home-dir.js\";\nimport {\n formatBranchMeta,\n formatCost,\n formatCwd,\n formatPercent,\n formatTokens,\n} from \"./status-bar-format.js\";\nimport styles from \"./StatusBar.module.css\";\n\ninterface Segment {\n key: string;\n icon: ReactNode;\n text: string;\n title?: string;\n}\n\nexport function StatusBar() {\n const activeSessionId = useSessionStore((s) => s.activeSessionId);\n const session = useSessionStore((s) =>\n activeSessionId ? s.sessions[activeSessionId] : undefined,\n );\n const homeDir = useHomeDir();\n // Own the same live checkout-status subscription `ChangesPanel` uses (POC pattern,\n // `use-checkout-status.ts`), keyed off the same `activeWorkspaceCwd` — so the branch segment\n // updates live even when the Changes tab has never been opened. Subscribing to the SAME cwd\n // `ChangesPanel` would use is intentional and safe (idempotent re-subscribe); subscribing to a\n // *different* cwd than the Changes panel would corrupt its file list, since `git-store` is one\n // global singleton — this must always track the workspace-level cwd, never a per-session one.\n const activeWorkspaceCwd = useTabStore((s) => s.activeWorkspaceCwd);\n useCheckoutStatus(activeWorkspaceCwd || \"\");\n const gitAvailable = useGitStore((s) => s.available);\n const branch = useGitStore((s) => s.branch);\n const ahead = useGitStore((s) => s.ahead);\n const behind = useGitStore((s) => s.behind);\n const detached = useGitStore((s) => s.detached);\n const conflictCount = useGitStore((s) => s.conflictCount);\n const dirtyCount = useGitStore((s) => s.changes.length);\n\n const stats = useStatsStore((s) => (activeSessionId ? s.bySession[activeSessionId] : undefined));\n\n useSessionStats(activeSessionId);\n\n const segments: Segment[] = [];\n\n if (session) {\n const cwdText = formatCwd(session.cwd, homeDir);\n segments.push({ key: \"cwd\", icon: <Folder size={13} />, text: cwdText, title: session.cwd });\n }\n\n if (gitAvailable) {\n const branchMeta = formatBranchMeta(ahead, behind);\n const parts = [detached ? \"(detached)\" : (branch ?? \"?\")];\n if (branchMeta) parts.push(branchMeta);\n if (dirtyCount > 0) parts.push(`●${dirtyCount}`);\n if (conflictCount > 0) parts.push(`⚠${conflictCount}`);\n segments.push({ key: \"branch\", icon: <GitBranch size={13} />, text: parts.join(\" \") });\n }\n\n if (session) {\n const contextText = `${formatPercent(stats?.contextPercent)} (${formatTokens(stats?.contextTokens)}/${formatTokens(stats?.contextWindow)})`;\n segments.push({ key: \"context\", icon: <Gauge size={13} />, text: contextText });\n\n segments.push({\n key: \"tokens\",\n icon: <Coins size={13} />,\n text: formatTokens(stats?.totalTokens),\n title: `in ${formatTokens(stats?.inputTokens)} / out ${formatTokens(stats?.outputTokens)}`,\n });\n\n segments.push({\n key: \"cost\",\n icon: <DollarSign size={13} />,\n text: formatCost(stats?.cost),\n });\n }\n\n return (\n <div className={styles.statusBar}>\n {segments.map((seg, i) => (\n <span key={seg.key} className={styles.segmentGroup}>\n {i > 0 && <ChevronRight size={12} className={styles.chevron} aria-hidden=\"true\" />}\n <span className={styles.segment} title={seg.title}>\n <span className={styles.icon}>{seg.icon}</span>\n <span className={styles.text}>{seg.text}</span>\n </span>\n </span>\n ))}\n </div>\n );\n}\n","/**\n * Checkbox — accessible checkbox control.\n * ui-components.md § Inputs & form controls\n */\n\nimport { type InputHTMLAttributes, type ReactNode, forwardRef } from \"react\";\nimport { Check } from \"lucide-react\";\nimport styles from \"./Checkbox.module.css\";\n\nexport interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\"> {\n label?: ReactNode;\n checked?: boolean;\n}\n\nexport const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(\n { label, checked = false, disabled, className, onChange, ...rest },\n ref,\n) {\n return (\n <label className={styles.wrapper} data-disabled={disabled || undefined}>\n <input\n ref={ref}\n type=\"checkbox\"\n className={styles.nativeInput}\n checked={checked}\n disabled={disabled}\n onChange={onChange}\n {...rest}\n />\n <span className={styles.box} data-checked={checked} aria-hidden>\n {checked && <Check size={12} className={styles.checkmark} />}\n </span>\n {label && <span>{label}</span>}\n </label>\n );\n});\n","/**\n * Select + Combobox — form selection controls.\n * Select = native <select> styled for theme consistency.\n * Combobox = filterable text input + dropdown list, consuming sprint-012 combobox model.\n * ui-components.md § Inputs & form controls\n */\n\nimport {\n forwardRef,\n useState,\n useRef,\n useEffect,\n type SelectHTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport { clsx } from \"clsx\";\nimport styles from \"./Select.module.css\";\nimport {\n filterOptions,\n comboboxReducer,\n initialComboboxState,\n type ComboboxState,\n type ComboboxOption,\n type ComboboxAction,\n} from \"@pi-studio-ui/ui/combobox.js\";\n\n// ---------------------------------------------------------------------------\n// Native Select\n// ---------------------------------------------------------------------------\n\nexport interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {\n options: { value: string; label: string }[];\n placeholder?: string;\n}\n\nexport const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(\n { options, placeholder, className, ...rest },\n ref,\n) {\n return (\n <select ref={ref} className={clsx(styles.select, className)} {...rest}>\n {placeholder && (\n <option value=\"\" disabled>\n {placeholder}\n </option>\n )}\n {options.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n );\n});\n\n// ---------------------------------------------------------------------------\n// Combobox\n// ---------------------------------------------------------------------------\n\nexport interface ComboboxProps {\n options: ComboboxOption<string>[];\n value?: string;\n onSelect?: (value: string) => void;\n placeholder?: string;\n className?: string;\n}\n\nexport function Combobox({\n options,\n value,\n onSelect,\n placeholder = \"Search…\",\n className,\n}: ComboboxProps) {\n const [state, setState] = useState<ComboboxState<string>>(() => initialComboboxState(options));\n const inputRef = useRef<HTMLInputElement>(null);\n\n function dispatch(action: ComboboxAction) {\n setState((prev) => comboboxReducer(prev, action));\n }\n\n function handleSelect(opt: ComboboxOption<string>) {\n onSelect?.(opt.value);\n dispatch({ type: \"CLOSE\" });\n }\n\n // Close on outside click\n const wrapperRef = useRef<HTMLDivElement>(null);\n useEffect(() => {\n function handleClick(e: MouseEvent) {\n if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {\n dispatch({ type: \"CLOSE\" });\n }\n }\n document.addEventListener(\"mousedown\", handleClick);\n return () => document.removeEventListener(\"mousedown\", handleClick);\n }, []);\n\n return (\n <div ref={wrapperRef} className={clsx(styles.comboboxWrapper, className)}>\n <input\n ref={inputRef}\n className={styles.comboboxInput}\n value={state.query}\n placeholder={placeholder}\n onChange={(e) => dispatch({ type: \"SET_QUERY\", query: e.target.value })}\n onFocus={() => dispatch({ type: \"OPEN\" })}\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n dispatch({ type: \"ARROW_DOWN\" });\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n dispatch({ type: \"ARROW_UP\" });\n } else if (e.key === \"Enter\" && state.isOpen) {\n e.preventDefault();\n const opt = state.filtered[state.highlightedIndex];\n if (opt) handleSelect(opt);\n } else if (e.key === \"Escape\") {\n dispatch({ type: \"CLOSE\" });\n }\n }}\n role=\"combobox\"\n aria-expanded={state.isOpen}\n aria-autocomplete=\"list\"\n />\n {state.isOpen && state.filtered.length > 0 && (\n <div className={styles.comboboxList} role=\"listbox\">\n {state.filtered.map((opt, i) => (\n <button\n key={opt.value}\n className={clsx(\n styles.comboboxOption,\n i === state.highlightedIndex && styles.comboboxOptionHighlighted,\n )}\n role=\"option\"\n aria-selected={i === state.highlightedIndex}\n onMouseDown={(e) => {\n e.preventDefault();\n handleSelect(opt);\n }}\n >\n {opt.label}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n","/**\n * Dialog — generic centered modal chrome (overlay + card + header/title/close + optional footer),\n * wrapping Radix `Dialog` (design-system.md § Overlays: `@radix-ui/react-dialog` for\n * dialogs/menus/tooltips/popovers). Every feature-specific modal (workspace picker today; rename/\n * confirm dialogs later) SHOULD build on this instead of hand-rolling `Dialog.Root/Portal/Overlay/\n * Content` again — only the body content and footer actions vary per use.\n */\n\nimport { type ReactNode } from \"react\";\nimport * as RadixDialog from \"@radix-ui/react-dialog\";\nimport { X } from \"lucide-react\";\nimport { clsx } from \"clsx\";\nimport { IconButton } from \"./IconButton.js\";\nimport styles from \"./Dialog.module.css\";\n\nexport interface DialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: string;\n /** Body content between the header and the optional footer. */\n children?: ReactNode;\n /** Sticky action row rendered under the body (e.g. Cancel/Confirm buttons). */\n footer?: ReactNode;\n /** Content card width; defaults to 500px (design-system.md § Overlays desktop dialog sizing). */\n width?: number | string;\n /** Extra class applied to the content card, for callers that need to tweak body sizing. */\n className?: string;\n /**\n * Omit the header bar's reserved height and float the close button over the body instead.\n * Use for content (e.g. an image lightbox) that should sit dead-center in the viewport rather\n * than be pushed down by an asymmetric top-only header. `title` remains as an accessible\n * (visually hidden) label for the dialog.\n */\n bare?: boolean;\n}\n\n/** Re-exported so callers can wrap a footer button in `<Dialog.Close asChild>` to auto-dismiss\n * without threading `onOpenChange(false)` through their own click handler. */\nexport const DialogClose = RadixDialog.Close;\n\nexport function Dialog({\n open,\n onOpenChange,\n title,\n children,\n footer,\n width,\n className,\n bare = false,\n}: DialogProps) {\n return (\n <RadixDialog.Root open={open} onOpenChange={onOpenChange}>\n <RadixDialog.Portal>\n <RadixDialog.Overlay className={styles.overlay} />\n <div className={styles.contentWrapper}>\n <RadixDialog.Content\n className={clsx(styles.dialog, bare && styles.bare, className)}\n style={width !== undefined ? { width } : undefined}\n >\n {bare ? (\n <RadixDialog.Title className={styles.visuallyHidden}>{title}</RadixDialog.Title>\n ) : (\n <div className={styles.header}>\n <RadixDialog.Title className={styles.title}>{title}</RadixDialog.Title>\n <RadixDialog.Close asChild>\n <IconButton size=\"sm\" aria-label=\"Close\">\n <X size={16} />\n </IconButton>\n </RadixDialog.Close>\n </div>\n )}\n <div className={clsx(styles.body, bare && styles.bareBody)}>{children}</div>\n {bare && (\n <RadixDialog.Close asChild>\n <IconButton\n size=\"sm\"\n aria-label=\"Close\"\n className={styles.bareCloseBtn}\n style={{\n background: \"var(--pi-color-surface1)\",\n border: \"1px solid var(--pi-color-border)\",\n borderRadius: \"50%\",\n }}\n >\n <X size={16} />\n </IconButton>\n </RadixDialog.Close>\n )}\n {footer && <div className={styles.footer}>{footer}</div>}\n </RadixDialog.Content>\n </div>\n </RadixDialog.Portal>\n </RadixDialog.Root>\n );\n}\n","/**\n * ResizeHandle — thin draggable strip for resizing an adjacent sidebar (`WorkspacePage`'s\n * `sidebarLeft`/`sidebarRight`). Pointer-driven: `pointerdown` starts a drag, `pointermove`\n * reports the raw pixel delta since the last move, `pointerup` ends it. Width clamping and\n * persistence live in the caller (`ui-store.ts`'s `setLeftSidebarWidth`/`setRightSidebarWidth`) —\n * this component only ever reports deltas, never owns or reads the width itself.\n */\n\nimport { useCallback, useRef } from \"react\";\nimport { clsx } from \"clsx\";\nimport styles from \"./ResizeHandle.module.css\";\n\nexport interface ResizeHandleProps {\n /** Which sidebar this handle resizes. Left sidebar grows when dragged right (positive delta);\n * right sidebar shrinks when dragged right (its left edge moves right into the panel) — `side`\n * picks the correct sign so the caller only ever adds the raw delta to its current width. */\n side: \"left\" | \"right\";\n onResize(deltaX: number): void;\n className?: string;\n}\n\nexport function ResizeHandle({ side, onResize, className }: ResizeHandleProps) {\n const startXRef = useRef(0);\n\n const handlePointerMove = useCallback(\n (ev: PointerEvent) => {\n const raw = ev.clientX - startXRef.current;\n startXRef.current = ev.clientX;\n onResize(side === \"left\" ? raw : -raw);\n },\n [onResize, side],\n );\n\n const handlePointerUp = useCallback(\n (ev: PointerEvent) => {\n window.removeEventListener(\"pointermove\", handlePointerMove);\n window.removeEventListener(\"pointerup\", handlePointerUp);\n document.body.style.cursor = \"\";\n document.body.style.userSelect = \"\";\n (ev.target as HTMLElement | null)?.releasePointerCapture?.(ev.pointerId);\n },\n [handlePointerMove],\n );\n\n function handlePointerDown(ev: React.PointerEvent<HTMLDivElement>): void {\n ev.preventDefault();\n startXRef.current = ev.clientX;\n document.body.style.cursor = \"col-resize\";\n document.body.style.userSelect = \"none\";\n window.addEventListener(\"pointermove\", handlePointerMove);\n window.addEventListener(\"pointerup\", handlePointerUp);\n }\n\n return (\n <div\n className={clsx(styles.handle, className)}\n onPointerDown={handlePointerDown}\n role=\"separator\"\n aria-orientation=\"vertical\"\n />\n );\n}\n","/**\n * Shared \"open a workspace\" resolver for the folder-picker dialog: given a target cwd, either\n * activates the workspace's most recently active existing session, or creates a fresh one —\n * avoids piling up duplicate sessions when the user re-opens a folder they already have a chat\n * in. Distinct from `SessionList`'s \"+ New conversation\", which always starts a brand-new\n * session by design (POC_TO_APP_PLAN_UI.md §4.3 workspace grouping, §4.7 follow-up: open-workspace\n * dialog).\n */\n\nimport { useSessionStore } from \"@pi-studio-ui/stores/session-store.js\";\nimport { useTabStore, tabIds, openNewChat } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { useUiStore } from \"@pi-studio-ui/stores/ui-store.js\";\nimport { groupSessionsByWorkspace } from \"./workspace-grouping.js\";\n\n/**\n * Resolve `cwd` (already tilde/absolute-normalized by the caller, e.g. via `resolveTildePath`) to\n * a session: reuses the workspace's most recent session if one already exists at that cwd,\n * otherwise creates a new one. Activates the session and opens/focuses its chat tab. Also seeds\n * `useUiStore().cwd` so a subsequent bare \"+ New conversation\" defaults to this workspace.\n *\n * `homeDir` MUST be the same value the sidebar's grouping uses (`useHomeDir()`), so this matches\n * the exact workspace bucket a tilde-form and absolute-form cwd would collapse into — reusing a\n * different normalization here would resurrect the tilde/absolute duplicate-workspace bug.\n */\nexport function openWorkspace(cwd: string, homeDir: string | null): void {\n const { order, sessions } = useSessionStore.getState();\n\n const groups = groupSessionsByWorkspace(order, sessions, homeDir);\n const existing = groups.find((g) => g.cwd === cwd);\n\n if (existing?.sessions[0]) {\n const sessionId = existing.sessions[0].id;\n useSessionStore.getState().activate(sessionId);\n const session = useSessionStore.getState().sessions[sessionId];\n useTabStore.getState().open({\n id: tabIds.chat(sessionId),\n kind: \"chat\",\n label: session?.title ?? \"New chat\",\n closable: true,\n data: { sessionId },\n workspaceCwd: cwd,\n });\n } else {\n openNewChat(cwd);\n }\n\n useUiStore.getState().setCwd(cwd);\n}\n","/**\n * OpenWorkspaceDialog — directory browser for opening a new workspace (evolved from the POC\n * `#cwd-overlay`, chat.html ~line 328-336, 1122-1141; POC_TO_APP_PLAN_UI.md §4.7 follow-up:\n * toolbar \"Open Workspace\" button). Built on the generic `Dialog` primitive\n * (`components/primitives/Dialog.tsx`) — this file owns only the address input + Go/Up buttons +\n * directory list body. \"Open\" resolves the selected path to a session via `openWorkspace()` —\n * reusing the workspace's most recent existing session if one is already open there, else\n * creating a new one — then closes.\n */\n\nimport { useEffect, useState } from \"react\";\nimport { ArrowUp, Folder, FolderPlus } from \"lucide-react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { Button, Dialog, DialogClose } from \"@pi-studio-ui/components/primitives/index.js\";\nimport { TextInput } from \"@pi-studio-ui/components/primitives/TextInput.js\";\nimport { EmptyState } from \"@pi-studio-ui/components/primitives/EmptyState.js\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { rpcKeys } from \"@pi-studio-ui/lib/connection/rpc-keys.js\";\nimport { useUiStore } from \"@pi-studio-ui/stores/ui-store.js\";\nimport { useTabStore } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { normalizeCwd } from \"@pi-studio-ui/features/sessions/workspace-grouping.js\";\nimport { useExplorer } from \"@pi-studio-ui/hooks/use-explorer.js\";\nimport { useHomeDir } from \"@pi-studio-ui/hooks/use-home-dir.js\";\nimport { openWorkspace } from \"@pi-studio-ui/features/sessions/open-workspace.js\";\nimport { createEntry } from \"@pi-studio-ui/features/files/create-entry.js\";\nimport styles from \"./OpenWorkspaceDialog.module.css\";\n\nexport function OpenWorkspaceDialog() {\n const open = useUiStore((s) => s.cwdPickerOpen);\n const closeDialog = useUiStore((s) => s.closeCwdPicker);\n const activeWorkspaceCwd = useTabStore((s) => s.activeWorkspaceCwd);\n const uiCwd = useUiStore((s) => s.cwd);\n const client = useConnectionStore((s) => s.client);\n const homeDir = useHomeDir();\n\n const [path, setPathState] = useState(\"/\");\n const [inputValue, setInputValue] = useState(\"/\");\n const [creating, setCreating] = useState(false);\n const [newName, setNewName] = useState(\"\");\n const [createError, setCreateError] = useState<string | null>(null);\n const [createBusy, setCreateBusy] = useState(false);\n const queryClient = useQueryClient();\n\n // Seed the picker at the workspace currently in view each time it opens. Falls back to the\n // `?cwd=` deep-link param / last-opened workspace (`ui-store.cwd`), then home.\n useEffect(() => {\n if (!open) return;\n const seed = normalizeCwd(activeWorkspaceCwd || uiCwd || \"~\", homeDir);\n setPathState(seed);\n setInputValue(seed);\n resetCreate();\n }, [open, client, activeWorkspaceCwd, uiCwd]);\n\n const { data, isLoading, isError, error } = useExplorer(path, open && Boolean(client));\n const dirEntries = (data?.entries ?? []).filter((e) => e.kind === \"directory\");\n\n function resetCreate() {\n setCreating(false);\n setNewName(\"\");\n setCreateError(null);\n }\n\n /** Normalizes away a trailing slash so every join site (`${path}/${entry.name}`) can assume\n * `path` never ends in \"/\" — otherwise typing (or `Go`-ing) e.g. \"/tmp/\" produces a\n * doubled-slash child path (\"/tmp//pi-move-test\") the moment a subfolder is clicked. */\n function navigate(next: string) {\n const normalized = next.length > 1 ? next.replace(/\\/+$/, \"\") || \"/\" : next;\n setPathState(normalized);\n setInputValue(normalized);\n resetCreate();\n }\n\n function handleUp() {\n navigate(path.split(\"/\").slice(0, -1).join(\"/\") || \"/\");\n }\n\n function handleGo() {\n navigate(inputValue.trim());\n }\n\n async function handleCreate() {\n const name = newName.trim();\n if (!name || !client || createBusy) return;\n setCreateBusy(true);\n setCreateError(null);\n try {\n const created = await createEntry(client, path, name, \"directory\");\n await queryClient.invalidateQueries({ queryKey: rpcKeys.explorer(path) });\n navigate(created); // also resets the draft state\n } catch (err) {\n setCreateError(err instanceof Error ? err.message : \"Failed to create folder\");\n } finally {\n setCreateBusy(false);\n }\n }\n\n function handleOpen() {\n openWorkspace(path, homeDir);\n closeDialog();\n }\n\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => !next && closeDialog()}\n title=\"Open Workspace\"\n footer={\n <>\n <DialogClose asChild>\n <Button size=\"sm\" variant=\"secondary\">\n Cancel\n </Button>\n </DialogClose>\n <Button size=\"sm\" onClick={handleOpen}>\n Open this folder\n </Button>\n </>\n }\n >\n <div className={styles.pathRow}>\n <Button size=\"xs\" variant=\"ghost\" iconOnly title=\"Up\" onClick={handleUp}>\n <ArrowUp size={13} />\n </Button>\n <TextInput\n className={styles.pathInput}\n value={inputValue}\n onChange={(e) => setInputValue(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n handleGo();\n }\n }}\n />\n <Button size=\"xs\" onClick={handleGo}>\n Go\n </Button>\n <Button\n size=\"xs\"\n variant=\"ghost\"\n iconOnly\n title=\"New folder\"\n disabled={!client || isError || creating}\n onClick={() => {\n setCreating(true);\n setNewName(\"\");\n setCreateError(null);\n }}\n >\n <FolderPlus size={13} />\n </Button>\n </div>\n {creating && (\n <>\n <div className={styles.createRow}>\n <Folder size={14} />\n <TextInput\n className={styles.createInput}\n autoFocus\n placeholder=\"New folder name\"\n value={newName}\n onChange={(e) => setNewName(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n void handleCreate();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n e.stopPropagation();\n resetCreate();\n }\n }}\n />\n <Button\n size=\"xs\"\n loading={createBusy}\n disabled={!newName.trim() || createBusy}\n onClick={() => void handleCreate()}\n >\n Create\n </Button>\n <Button size=\"xs\" variant=\"secondary\" onClick={resetCreate}>\n Cancel\n </Button>\n </div>\n {createError && <div className={styles.createError}>{createError}</div>}\n </>\n )}\n <div className={styles.list}>\n {isLoading ? (\n <EmptyState>Loading...</EmptyState>\n ) : isError ? (\n <EmptyState>Error: {error instanceof Error ? error.message : \"unknown error\"}</EmptyState>\n ) : dirEntries.length === 0 ? (\n <EmptyState>No subdirectories</EmptyState>\n ) : (\n dirEntries.map((entry) => (\n <div\n key={entry.name}\n className={styles.item}\n onClick={() => navigate(path === \"/\" ? `/${entry.name}` : `${path}/${entry.name}`)}\n >\n <Folder size={14} />\n <span className={styles.name}>{entry.name}</span>\n </div>\n ))\n )}\n </div>\n </Dialog>\n );\n}\n","/**\n * WorkspacePage — the 3-column shell (POC `.main`: sidebar-left / center / sidebar-right,\n * POC_TO_APP_PLAN_UI.md §3). Feature panels are mounted here once each lands. Either sidebar can\n * be hidden via the Toolbar's panel toggles (`ui-store.ts` `leftSidebarCollapsed`/\n * `rightSidebarCollapsed`) — collapsed just means the `aside` isn't rendered, so `TabPanelHost`'s\n * `.center` column simply grows to fill the freed width. Each visible sidebar is resizable via a\n * `ResizeHandle` on its inner edge, dragging `ui-store.ts`'s `leftSidebarWidth`/\n * `rightSidebarWidth` (clamped to `[MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH]`) through the\n * delta-based `resizeLeftSidebar`/`resizeRightSidebar` actions.\n */\n\nimport { Toolbar } from \"@pi-studio-ui/features/connection/Toolbar.js\";\nimport { SessionList } from \"@pi-studio-ui/features/sessions/SessionList.js\";\n// No TabStrip here since sprint-049: strips are per pane and live inside TabPanelHost, positioned\n// across the top of each pane's rect (features/workspace/pane-layout-view.ts).\nimport { TabPanelHost } from \"@pi-studio-ui/features/workspace/TabPanelHost.js\";\nimport { RightSidebar } from \"@pi-studio-ui/features/files/RightSidebar.js\";\nimport { StatusBar } from \"@pi-studio-ui/features/workspace/StatusBar.js\";\nimport { OpenWorkspaceDialog } from \"@pi-studio-ui/features/workspace-picker/OpenWorkspaceDialog.js\";\nimport { ResizeHandle } from \"@pi-studio-ui/components/primitives/ResizeHandle.js\";\nimport { useUiStore } from \"@pi-studio-ui/stores/ui-store.js\";\nimport styles from \"./WorkspacePage.module.css\";\n\nexport function WorkspacePage() {\n const leftSidebarCollapsed = useUiStore((s) => s.leftSidebarCollapsed);\n const rightSidebarCollapsed = useUiStore((s) => s.rightSidebarCollapsed);\n const leftSidebarWidth = useUiStore((s) => s.leftSidebarWidth);\n const rightSidebarWidth = useUiStore((s) => s.rightSidebarWidth);\n const resizeLeftSidebar = useUiStore((s) => s.resizeLeftSidebar);\n const resizeRightSidebar = useUiStore((s) => s.resizeRightSidebar);\n\n return (\n <div className={styles.shell}>\n <Toolbar />\n <div className={styles.main}>\n {!leftSidebarCollapsed && (\n <aside className={styles.sidebarLeft} style={{ width: leftSidebarWidth }}>\n <SessionList />\n <ResizeHandle side=\"left\" className={styles.leftHandle} onResize={resizeLeftSidebar} />\n </aside>\n )}\n <div className={styles.center}>\n <TabPanelHost />\n </div>\n {!rightSidebarCollapsed && (\n <aside className={styles.sidebarRight} style={{ width: rightSidebarWidth }}>\n <ResizeHandle\n side=\"right\"\n className={styles.rightHandle}\n onResize={resizeRightSidebar}\n />\n <RightSidebar />\n </aside>\n )}\n </div>\n <StatusBar />\n <OpenWorkspaceDialog />\n </div>\n );\n}\n","/**\n * Boot hook: seeds the toolbar (host/password/cwd) from URL params\n * (POC `?host=&password=&cwd=&connect=1`, POC_TO_APP_PLAN_UI.md §4.1) and optionally\n * auto-connects. Runs once on mount.\n *\n * Also supports opening the web-client directly from a pairing link\n * (`pi-studio daemon pair`'s QR/link, architecture/relay-e2ee.md § Pairing):\n * - `location.hash` already carrying `#offer=...` (the web-client's own URL was used as the\n * pairing link's `baseUrl`) — connects immediately, no `connect=1` needed, since a pairing\n * link's entire purpose is a one-click connect.\n * - `?pair=<url-encoded pairing link>` — for pasting a link whose `baseUrl` is the hosted\n * `app.pi-studio.sh` landing page into `?pair=` instead of the fragment directly (mirrors the\n * existing `?host=&connect=1` convention).\n * Either form is handed to `connect()` verbatim — it detects the pairing link itself via\n * `parsePairingUrl` and branches to the relay transport when the link carries one.\n */\n\nimport { useEffect, useRef } from \"react\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useUiStore } from \"@pi-studio-ui/stores/ui-store.js\";\n\nexport function useConnectionBoot(): void {\n const ranRef = useRef(false);\n\n useEffect(() => {\n if (ranRef.current) return;\n ranRef.current = true;\n\n // A pairing link landed directly on this origin (hash carries `offer=`) — connect immediately.\n if (window.location.hash.includes(\"offer=\")) {\n const pairingUrl = window.location.href;\n useUiStore.getState().setHost(pairingUrl);\n void useConnectionStore.getState().connect({ url: pairingUrl });\n return;\n }\n\n const qs = new URLSearchParams(window.location.search);\n const host = qs.get(\"host\");\n const pair = qs.get(\"pair\");\n const password = qs.get(\"password\");\n const cwd = qs.get(\"cwd\");\n const shouldConnect = qs.get(\"connect\") === \"1\";\n\n const ui = useUiStore.getState();\n if (pair) ui.setHost(pair);\n else if (host) ui.setHost(host);\n if (password) ui.setPassword(password);\n if (cwd) ui.setCwd(cwd);\n\n if (pair) {\n // A pairing link carries its own credential (the daemon's public key) — connect\n // unconditionally, `connect=1` is only required for a plain host/password connection.\n void useConnectionStore.getState().connect({ url: pair });\n } else if (shouldConnect) {\n const { host: h, password: pw } = useUiStore.getState();\n void useConnectionStore.getState().connect({ url: h, password: pw || undefined });\n }\n }, []);\n}\n","/**\n * Pane layout persistence — one versioned, client-local record holding every workspace's pane\n * arrangement, keyed against **stable tab identities** rather than tab ids.\n *\n * Why identities: terminal tab ids are not stable across a reconnect (see `tab-store.tabIdentity`,\n * which computes them — it lives there because `tab-store` calls it on every open).\n *\n * Why the write is debounced and *also* triggered by tab-store changes: a divider drag mutates the\n * layout per pointer frame, so writes are trailing-debounced; and acquiring an identity counts as a\n * mutation, because a terminal placed by drag and never touched again would otherwise lose its pane\n * when its daemon slot finally arrives.\n *\n * swe/features/workspace-split-panes.md § Persisted layout record, § Tab identity,\n * § Data & Persistence\n */\n\nimport { localKvStore } from \"@pi-studio-ui/providers/kv-store.js\";\nimport {\n leafIds,\n PANE_LAYOUT_VERSION,\n parsePaneTree,\n type PaneNode,\n} from \"@pi-studio-ui/features/workspace/pane-tree.js\";\nimport { useLayoutStore } from \"@pi-studio-ui/stores/layout-store.js\";\nimport { tabIdentity, useTabStore, type Tab } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { useSessionStore } from \"@pi-studio-ui/stores/session-store.js\";\n\n/** Matches the `pi-studio-appearance` key convention already used by the appearance store. */\nconst STORAGE_KEY = \"pi-studio-pane-layout\";\n\n/** Short enough to survive a reload, long enough that a divider drag writes once. */\nexport const PANE_LAYOUT_WRITE_DEBOUNCE_MS = 250;\n\n// ─── Record shape ──────────────────────────────────────────────────────────────────────────\n\nexport interface PersistedWorkspaceLayout {\n /** Untrusted on load — validated by `parsePaneTree`. */\n tree: unknown;\n /** identity → pane id */\n placement: Record<string, string>;\n /** pane id → identity */\n activeByPane: Record<string, string>;\n activePaneId: string;\n}\n\nexport interface PersistedPaneLayout {\n version: typeof PANE_LAYOUT_VERSION;\n /** Keyed by normalized workspace cwd. */\n workspaces: Record<string, PersistedWorkspaceLayout>;\n /** The workspace that was in view. Optional — records written before this existed simply lack it. */\n activeWorkspaceCwd?: string;\n}\n\n/** A validated record: geometry per workspace, plus which one was in view. */\nexport interface LoadedPaneLayout {\n workspaces: Map<string, ValidatedWorkspaceLayout>;\n /** `null` when absent, or naming a workspace whose entry did not survive validation. */\n activeWorkspaceCwd: string | null;\n}\n\n/** A loaded workspace entry whose tree parsed and whose pane references are known to exist. */\nexport interface ValidatedWorkspaceLayout {\n tree: PaneNode;\n /** identity → pane id; every pane id is a leaf of `tree`. */\n placement: Record<string, string>;\n /** pane id → identity; every pane id is a leaf of `tree`. */\n activeByPane: Record<string, string>;\n /** `null` when absent or naming a pane that is not in `tree` — the caller picks a fallback. */\n activePaneId: string | null;\n}\n\n// ─── Load ──────────────────────────────────────────────────────────────────────────────────\n\n/**\n * Read the record, discarding what cannot be trusted: a `version` mismatch drops the **whole**\n * record, a structurally damaged tree drops only **that workspace's** entry, and pane references\n * that do not exist in the parsed tree are dropped individually. Never throws.\n */\nexport function loadPaneLayout(): LoadedPaneLayout {\n const workspaces = new Map<string, ValidatedWorkspaceLayout>();\n const empty: LoadedPaneLayout = { workspaces, activeWorkspaceCwd: null };\n const raw = localKvStore.get(STORAGE_KEY);\n if (!raw) return empty;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return empty; // hand-edited or truncated storage: start from a single pane\n }\n if (typeof parsed !== \"object\" || parsed === null) return empty;\n\n const record = parsed as Partial<PersistedPaneLayout>;\n if (record.version !== PANE_LAYOUT_VERSION) return empty;\n if (typeof record.workspaces !== \"object\" || record.workspaces === null) return empty;\n\n for (const [cwd, entry] of Object.entries(record.workspaces)) {\n const workspace = validateWorkspace(entry);\n if (workspace) workspaces.set(cwd, workspace);\n }\n // A workspace whose entry was dropped as damaged must not be restored as the view, or the user\n // would land on a workspace with no geometry at all.\n const active = record.activeWorkspaceCwd;\n return {\n workspaces,\n activeWorkspaceCwd: typeof active === \"string\" && workspaces.has(active) ? active : null,\n };\n}\n\nfunction validateWorkspace(entry: unknown): ValidatedWorkspaceLayout | null {\n if (typeof entry !== \"object\" || entry === null) return null;\n const candidate = entry as Partial<PersistedWorkspaceLayout>;\n const tree = parsePaneTree(candidate.tree);\n if (!tree) return null;\n\n const panes = new Set(leafIds(tree));\n const placement: Record<string, string> = {};\n for (const [identity, paneId] of stringEntries(candidate.placement)) {\n if (panes.has(paneId)) placement[identity] = paneId; // else: pane is gone, claim is worthless\n }\n const activeByPane: Record<string, string> = {};\n for (const [paneId, identity] of stringEntries(candidate.activeByPane)) {\n if (panes.has(paneId)) activeByPane[paneId] = identity;\n }\n const activePaneId = candidate.activePaneId;\n return {\n tree,\n placement,\n activeByPane,\n activePaneId: typeof activePaneId === \"string\" && panes.has(activePaneId) ? activePaneId : null,\n };\n}\n\nfunction stringEntries(value: unknown): [string, string][] {\n if (typeof value !== \"object\" || value === null) return [];\n return Object.entries(value).filter(\n (pair): pair is [string, string] => typeof pair[1] === \"string\",\n );\n}\n\n// ─── Save ──────────────────────────────────────────────────────────────────────────────────\n\n/**\n * Cancels the pending debounce, or `null` when none is in flight. Holding a closure rather than the\n * raw handle keeps the timer id's type inferred — it is a `number` in the browser and a `Timeout`\n * under Node, and this module is compiled for both.\n */\nlet cancelPendingWrite: (() => void) | null = null;\n\n/** Queue a write on a trailing debounce, so a pointer-frame burst collapses into one. */\nexport function schedulePaneLayoutWrite(): void {\n cancelPendingWrite?.();\n const handle = setTimeout(() => {\n cancelPendingWrite = null;\n writePaneLayout();\n }, PANE_LAYOUT_WRITE_DEBOUNCE_MS);\n cancelPendingWrite = () => clearTimeout(handle);\n}\n\n/** Write immediately, cancelling any pending debounce (tests, `pagehide`). */\nexport function flushPaneLayoutWrite(): void {\n cancelPendingWrite?.();\n cancelPendingWrite = null;\n writePaneLayout();\n}\n\nfunction writePaneLayout(): void {\n const identities = new Map<string, string>();\n for (const tab of useTabStore.getState().tabs) {\n const identity = tabIdentity(tab);\n if (identity !== null) identities.set(tab.id, identity);\n }\n\n const workspaces: Record<string, PersistedWorkspaceLayout> = {};\n for (const [cwd, layout] of Object.entries(useLayoutStore.getState().layouts)) {\n // Unconsumed claims are the starting point, NOT an empty object: a write that lands while restore\n // is still in flight — and the client-side tab replay triggers one immediately — would otherwise\n // persist a record describing only the tabs that happen to be open already, silently dropping\n // every pane whose chat or terminal is still on its way from the daemon. Next reload, those panes\n // have no claim and get pruned at the settle point: the split quietly collapses. Live tabs are\n // layered on top, so a tab that HAS arrived wins over the claim it consumed, and once hydration\n // settles (`pendingPlacement` emptied) this reduces to exactly the live-tab projection.\n const placement: Record<string, string> = { ...layout.pendingPlacement };\n for (const [tabId, paneId] of Object.entries(layout.placement)) {\n const identity = identities.get(tabId);\n // An identity-less tab is skipped; its pane still exists in the tree.\n if (identity !== undefined) placement[identity] = paneId;\n }\n const activeByPane: Record<string, string> = { ...layout.pendingActive };\n for (const [paneId, tabId] of Object.entries(layout.activeByPane)) {\n const identity = identities.get(tabId);\n if (identity !== undefined) activeByPane[paneId] = identity;\n }\n workspaces[cwd] = {\n tree: layout.tree,\n placement,\n activeByPane,\n activePaneId: layout.focusedPaneId,\n };\n }\n\n // Which workspace was in view is part of the layout: without it, restore lands wherever the most\n // recently active agent happens to live (`use-session-restore`'s `order[0]`), so a user with two\n // workspaces came back to the wrong one — panes correctly restored, just not the ones on screen.\n const record: PersistedPaneLayout = {\n version: PANE_LAYOUT_VERSION,\n workspaces,\n activeWorkspaceCwd: useTabStore.getState().activeWorkspaceCwd ?? undefined,\n };\n localKvStore.set(STORAGE_KEY, JSON.stringify(record));\n}\n\n// ─── Triggers ──────────────────────────────────────────────────────────────────────────────\n\n/**\n * Wire the write triggers; returns the teardown. Called once at app bootstrap.\n *\n * Layout mutations are the obvious trigger. The other two exist purely for **identity acquisition**,\n * where a tab becomes restorable without any layout state changing:\n * - `tab-store` — a terminal's daemon slot arrives via `updateData`.\n * - `session-store` — a draft chat's `createAgent` returns and `bindAgent` supplies the agent id the\n * chat's identity is keyed on (`materialize.ts`). Nothing in `tab-store` changes, so without this\n * the pane holding a brand-new conversation would stay unclaimed until some later layout mutation\n * happened to flush it. The subscription fires on every stream event too; re-deriving the\n * signature is a short string join over the open tabs, and a no-change result schedules nothing.\n */\nexport function installPaneLayoutPersistence(): () => void {\n let lastSignature = identitySignature(useTabStore.getState().tabs);\n const onIdentityMaybeChanged = (): void => {\n const signature = identitySignature(useTabStore.getState().tabs);\n if (signature === lastSignature) return;\n lastSignature = signature;\n schedulePaneLayoutWrite();\n };\n const unsubscribeLayout = useLayoutStore.subscribe(schedulePaneLayoutWrite);\n const unsubscribeTabs = useTabStore.subscribe(onIdentityMaybeChanged);\n const unsubscribeSessions = useSessionStore.subscribe(onIdentityMaybeChanged);\n return () => {\n unsubscribeLayout();\n unsubscribeTabs();\n unsubscribeSessions();\n };\n}\n\n/** Changes only when some tab gains, loses, or changes its identity. */\nfunction identitySignature(tabs: Tab[]): string {\n return tabs.map((tab) => `${tab.id}\\u0000${tabIdentity(tab) ?? \"\"}`).join(\"\\u0001\");\n}\n","/**\n * Reopen the **client-side** tabs a persisted layout still claims: `file`, `diff`, and `molecule`.\n *\n * Chats and terminals are rebuilt from the daemon's own inventory (`use-session-restore.ts`,\n * `use-terminal-restore.ts`) — the daemon owns them, and one deleted since the last load must not\n * come back. A file/diff/molecule tab has no daemon-side existence at all; it is a *view of a path*.\n * Nothing would ever reopen it, so its claim expired at the hydration settle point and its pane was\n * pruned: open two files side by side, reload, and the split silently collapsed to one pane.\n *\n * No extra persisted state is needed, because the **identity is already the descriptor**:\n * `file:<path>`, `diff:<staged|worktree>:<path>`, `molecule:<path>` (`tab-store.ts`'s `tabIdentity`).\n * A kind whose identity carries no path — a slot-less terminal, an empty molecule tab — was never\n * persisted in the first place, so there is nothing to miss.\n *\n * `tabFromIdentity` is the exact inverse of `tabIdentity` for these three kinds, and is built here\n * rather than by calling `openFileTab` precisely because that helper *dispatches*: it re-routes a\n * molecule extension to a `molecule` tab, which would turn a persisted `file:/a/x.cif` claim into a\n * `molecule:` tab, leave the claim unconsumed, and prune the pane. Round-tripping identity is the\n * whole job here, so the mapping must be literal.\n *\n * Synchronous and claim-driven, which is what lets it run at boot before a connection exists: each\n * open consumes its own claim (`layout-store.claimPaneFor`), the panels' content queries are gated on\n * the client and fire when it arrives (`use-file-read.ts`'s `enabled`), and the whole replay finishes\n * long before either daemon restore reports in — so ordering against them needs no coordination.\n *\n * swe/features/workspace-split-panes.md § Restoring a persisted layout, § Tab identity\n */\n\nimport type { ValidatedWorkspaceLayout } from \"@pi-studio-ui/lib/pane-layout-persistence.js\";\nimport { tabIds, useTabStore, type Tab } from \"@pi-studio-ui/stores/tab-store.js\";\n\n/** `diff:staged:/a/b.ts` / `diff:worktree:/a/b.ts` — the only identity with a middle segment. */\nconst DIFF_IDENTITY = /^diff:(staged|worktree):(.+)$/;\n\nexport function reopenClientTabs(loaded: ReadonlyMap<string, ValidatedWorkspaceLayout>): void {\n for (const [cwd, entry] of loaded) {\n for (const identity of Object.keys(entry.placement)) {\n const tab = tabFromIdentity(identity, cwd);\n if (tab !== null) useTabStore.getState().open(tab);\n }\n }\n}\n\n/**\n * The tab a persisted identity describes, or `null` when it names a daemon-owned kind (`agent:`,\n * `terminal:`) or anything unrecognised — a record written by a newer client can carry kinds this one\n * has never heard of, and those must be ignored rather than guessed at.\n */\nexport function tabFromIdentity(identity: string, workspaceCwd: string): Tab | null {\n const file = suffix(identity, \"file:\");\n if (file !== null) {\n return {\n id: tabIds.file(file),\n kind: \"file\",\n ...common(file),\n workspaceCwd,\n data: { path: file },\n };\n }\n const molecule = suffix(identity, \"molecule:\");\n if (molecule !== null) {\n return {\n id: tabIds.molecule(molecule),\n kind: \"molecule\",\n ...common(molecule),\n workspaceCwd,\n data: { path: molecule },\n };\n }\n const diff = DIFF_IDENTITY.exec(identity);\n if (diff !== null) {\n const path = diff[2]!;\n const staged = diff[1] === \"staged\";\n return {\n id: tabIds.diff(path, staged),\n kind: \"diff\",\n ...common(path),\n workspaceCwd,\n data: { path, staged },\n };\n }\n return null;\n}\n\n/** The path after `prefix`, or `null` when the prefix does not match or the path is empty. */\nfunction suffix(identity: string, prefix: string): string | null {\n if (!identity.startsWith(prefix)) return null;\n const path = identity.slice(prefix.length);\n return path === \"\" ? null : path;\n}\n\n/** Same basename label and closability every path-backed tab is opened with elsewhere. */\nfunction common(path: string): Pick<Tab, \"label\" | \"closable\"> {\n return { label: path.split(\"/\").pop() || path, closable: true };\n}\n","/**\n * Bring the workspace that was in view back into view after a reload.\n *\n * Panes are restored per workspace, but *which* workspace the user was looking at is a separate fact,\n * and without it restore lands wherever the most recently active agent happens to live\n * (`use-session-restore.ts` activates `order[0]`, and every tab open brings its own workspace into\n * view). With two workspaces open that is a coin flip: the panes come back correctly and the user sees\n * the wrong ones — a bug that reads as \"my layout was lost\" until you click the other workspace and\n * watch the split appear intact.\n *\n * Waits for the **hydration settle point** rather than acting on the first arriving tab: tabs arrive\n * asynchronously and out of order, and any later `open()` would switch the view again — the last\n * writer has to be this one. At the settle point every tab that will arrive has arrived.\n *\n * Two conditions to switch, both about not lying to the user:\n * - the workspace still has a layout (its entry survived validation), and\n * - it has at least one live tab, so switching does not land on an empty workspace whose sessions were\n * all deleted since the last load.\n *\n * swe/features/workspace-split-panes.md § Restoring a persisted layout\n */\n\nimport { useLayoutStore } from \"@pi-studio-ui/stores/layout-store.js\";\nimport { useSessionStore } from \"@pi-studio-ui/stores/session-store.js\";\nimport { useTabStore, type ChatTabData } from \"@pi-studio-ui/stores/tab-store.js\";\n\n/**\n * Watch for the settle point and restore the view once. Returns the unsubscribe, which is also called\n * automatically after the switch — this is a one-shot, so a later user-initiated workspace switch is\n * never undone.\n */\nexport function installActiveWorkspaceRestore(cwd: string | null): () => void {\n if (cwd === null) return () => {};\n if (isHydrated()) {\n restoreActiveWorkspace(cwd);\n return () => {};\n }\n const unsubscribe = useLayoutStore.subscribe(() => {\n if (!isHydrated()) return;\n unsubscribe();\n restoreActiveWorkspace(cwd);\n });\n return unsubscribe;\n}\n\n/** Exported for tests: the switch itself, with both guards. Returns whether it switched. */\nexport function restoreActiveWorkspace(cwd: string): boolean {\n if (useLayoutStore.getState().layouts[cwd] === undefined) return false;\n if (!useTabStore.getState().tabs.some((tab) => tab.workspaceCwd === cwd)) return false;\n const switched = useTabStore.getState().activeWorkspaceCwd !== cwd;\n if (switched) useTabStore.getState().switchWorkspace(cwd);\n adoptChatFromWorkspace(cwd);\n return switched;\n}\n\n/**\n * Keep the active conversation inside the workspace now in view.\n *\n * `switchWorkspace` already syncs it from the focused pane, but `syncActiveSession` deliberately\n * no-ops when that pane holds a terminal or a file — a terminal has no conversation, and blanking the\n * status bar would be worse. What it leaves behind, though, is whatever chat restore happened to open\n * last, which is frequently in a *different* workspace: the status bar would then report another\n * conversation's model, context, tokens and cost while the user looks at this one.\n *\n * Seeding this earlier does not work — every `open()` brings its own workspace into view and\n * re-activates its own chat, so any pre-tab seed is overwritten. This has to run after the last tab\n * has arrived, which is precisely the settle point.\n *\n * Runs even when the view did not change: the view being right already says nothing about the active\n * conversation, which `use-session-restore` seeds from a global winner.\n */\nfunction adoptChatFromWorkspace(cwd: string): void {\n const local = useTabStore\n .getState()\n .tabs.filter((tab) => tab.workspaceCwd === cwd && tab.kind === \"chat\");\n if (local.length === 0) return; // nothing local to offer; leave the status bar as it is\n const active = useSessionStore.getState().activeSessionId;\n if (local.some((tab) => (tab.data as ChatTabData).sessionId === active)) return;\n // The focused pane is not a chat (or `switchWorkspace` would have picked it), so any local\n // conversation beats a foreign one; first in tab order keeps it deterministic.\n useSessionStore.getState().activate((local[0]!.data as ChatTabData).sessionId);\n}\n\nfunction isHydrated(): boolean {\n const { sessions, terminals } = useLayoutStore.getState().hydrationSources;\n return sessions && terminals;\n}\n","/**\n * Pane-layout boot: install the persisted pane geometry, wire the write triggers, and flush on\n * `pagehide`.\n *\n * Ordering matters and is the reason this is a hook rather than a module side effect. The persisted\n * geometry must be installed *before* the restore hooks open their first tab, because every arrival\n * consumes its pane claim on the way in (`layout-store.claimPaneFor`) — a tab that arrives before\n * the claims exist lands in the default pane and the layout is lost. Both restore hooks only fire\n * once the connection reports `open`, i.e. a later commit than this mount effect, so \"install first\"\n * holds without any coordination between them.\n *\n * The record also carries the **client-side** tabs — files, diffs, molecule views — which no daemon\n * inventory can rebuild, so `reopenClientTabs` replays them. That replay waits for the connection to\n * report `open`, even though it needs no RPC of its own: reopening tabs is what brings a workspace\n * into view, and doing it at mount put a full split layout with file tabs on screen behind the\n * connect form, before there was any daemon to load them from. Restoring the UI is only meaningful\n * once there is a daemon behind it, which is the same rule the two daemon restores follow.\n *\n * It still runs before either of them: all three effects live in one component (`app.tsx`'s `Boot`),\n * so they fire in hook-declaration order within the same commit, and this one is declared first.\n * Once per page load, not per connection — the tabs stay open across a reconnect, and replaying would\n * re-activate them and yank the user's focus.\n *\n * `installPaneLayoutPersistence` is wired *after* the install so it does not schedule a write of the\n * state it just read. Writes that land before hydration settles are safe regardless: they preserve\n * unconsumed claims (see `pane-layout-persistence.ts`'s `writePaneLayout`).\n *\n * swe/features/workspace-split-panes.md § Restoring a persisted layout\n */\n\nimport { useEffect, useRef } from \"react\";\nimport {\n flushPaneLayoutWrite,\n installPaneLayoutPersistence,\n loadPaneLayout,\n type LoadedPaneLayout,\n} from \"@pi-studio-ui/lib/pane-layout-persistence.js\";\nimport { useLayoutStore } from \"@pi-studio-ui/stores/layout-store.js\";\nimport { reopenClientTabs } from \"@pi-studio-ui/features/workspace/reopen-client-tabs.js\";\nimport { installActiveWorkspaceRestore } from \"@pi-studio-ui/features/workspace/restore-active-workspace.js\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\n\nconst EMPTY_RECORD: LoadedPaneLayout = { workspaces: new Map(), activeWorkspaceCwd: null };\n\nexport function usePaneLayoutBoot(): void {\n const status = useConnectionStore((s) => s.status);\n const loadedRef = useRef<LoadedPaneLayout>(EMPTY_RECORD);\n const replayedRef = useRef(false);\n\n useEffect(() => {\n const loaded = loadPaneLayout();\n loadedRef.current = loaded;\n useLayoutStore.getState().installPersistedLayouts(loaded.workspaces, loaded.activeWorkspaceCwd);\n const uninstall = installPaneLayoutPersistence();\n // A reload inside the write debounce would otherwise drop the last mutation — a divider drag\n // followed immediately by Cmd+R being the obvious way to notice.\n const flush = () => flushPaneLayoutWrite();\n window.addEventListener(\"pagehide\", flush);\n return () => {\n window.removeEventListener(\"pagehide\", flush);\n uninstall();\n };\n }, []);\n\n useEffect(() => {\n if (status !== \"open\" || replayedRef.current) return;\n replayedRef.current = true;\n const loaded = loadedRef.current;\n reopenClientTabs(loaded.workspaces);\n // Armed only now: the settle point can only be reached once the restores are running, and arming\n // at mount would watch for a hydration cycle that a later `installPersistedLayouts` resets anyway.\n installActiveWorkspaceRestore(loaded.activeWorkspaceCwd);\n }, [status]);\n}\n","/**\n * `fetch_agent_timeline_request` returns `items: unknown[]` (not yet in the strict protocol\n * union). The POC's `restoreSessions()` observed a stable shape at runtime: each item is\n * `{kind:'other'|'assistant'|'tool_call', event?, events?}` carrying nested `AgentStreamEvent`s.\n * This flattens that shape into a plain event list the timeline reducer can replay.\n */\n\nimport type { AgentStreamEvent } from \"@av-pi-studio/protocol\";\n\ninterface TimelineItemOther {\n kind: \"other\";\n event?: AgentStreamEvent;\n}\n\ninterface TimelineItemAssistant {\n kind: \"assistant\";\n events?: AgentStreamEvent[];\n}\n\ninterface TimelineItemToolCall {\n kind: \"tool_call\";\n events?: AgentStreamEvent[];\n}\n\ntype TimelineItem = TimelineItemOther | TimelineItemAssistant | TimelineItemToolCall;\n\nfunction isTimelineItem(value: unknown): value is TimelineItem {\n return typeof value === \"object\" && value !== null && \"kind\" in value;\n}\n\n/** Flatten authoritative timeline `items` into an ordered `AgentStreamEvent[]` for replay. */\nexport function flattenTimelineItems(items: readonly unknown[]): AgentStreamEvent[] {\n const events: AgentStreamEvent[] = [];\n for (const raw of items) {\n if (!isTimelineItem(raw)) continue;\n if (raw.kind === \"other\") {\n if (raw.event) events.push(raw.event);\n } else {\n for (const event of raw.events ?? []) events.push(event);\n }\n }\n return events;\n}\n","/**\n * Drain an agent's authoritative timeline across every page.\n *\n * `fetch_agent_timeline_request` is BOUNDED: the daemon returns at most `limit` projected items\n * (default 200, `timeline-store.ts` `DEFAULT_PAGE_SIZE`) and reports `hasNewer:true` when more rows\n * exist past the page — the contract is \"clients page to completion\" (`timeline-rpc.ts`). A single\n * `direction:\"after\"` fetch therefore returns the OLDEST page only, silently dropping the newest\n * messages of any conversation longer than the cap. That's what made restored history stop partway\n * through: the daemon's rehydrated timeline (rebuilt from Pi's session file) was complete, the UI\n * just never asked for the rest.\n */\n\nimport type { AgentStreamEvent } from \"@av-pi-studio/protocol\";\nimport { flattenTimelineItems } from \"./events.js\";\n\n/** The subset of `FetchAgentTimelineResponse` paging needs (structural, so tests need no wire types). */\nexport interface TimelinePageLike {\n items: readonly unknown[];\n hasNewer: boolean;\n endCursor?: string | null;\n}\n\n/**\n * Fetch forward from the start of the timeline until the daemon reports no newer rows, flattening\n * every page into one ordered `AgentStreamEvent[]` ready for `applyStreamEvent` replay.\n *\n * `fetchPage` receives the cursor to resume from (`null` for the first page). Paging stops on\n * `hasNewer:false`, an empty page, or a cursor that fails to advance — the daemon's `endCursor` is\n * the page's max source seq and strictly increases while `hasNewer` holds, so a repeated or absent\n * cursor means a non-conforming response, not more history.\n */\nexport async function fetchTimelineEvents(\n fetchPage: (cursor: string | null) => Promise<TimelinePageLike>,\n): Promise<AgentStreamEvent[]> {\n const events: AgentStreamEvent[] = [];\n let cursor: string | null = null;\n\n for (;;) {\n const page = await fetchPage(cursor);\n if (page.items.length === 0) return events;\n events.push(...flattenTimelineItems(page.items));\n if (!page.hasNewer) return events;\n\n const next = page.endCursor;\n if (typeof next !== \"string\" || next === cursor) return events;\n cursor = next;\n }\n}\n","/**\n * Session restore — on a fresh `open` connection, fetches known agents\n * (`list_agents_request`) and hydrates each into the session store from its authoritative\n * timeline (`fetch_agent_timeline_request`). Ports POC `restoreSessions()`/`onConnected()`\n * (POC_TO_APP_PLAN_UI.md §4.3 \"Session restore\"). Live agent-stream subscription for restored\n * sessions is wired by `ChatPanel`'s `useAgentStream` once its tab mounts, keyed off `agentId` —\n * no `expectingAgent` race, since `agentId` is always known before subscribing.\n *\n * A daemon reporting zero agents restores nothing: `activeWorkspaceCwd` stays `null` and the UI\n * renders its no-workspace empty state (`TabPanelHost`) — this hook never auto-creates a session.\n * Every known agent restores, including an idle, never-used draft (eager materialization,\n * `materialize.ts`, `tab-store.ts`'s `openNewChat`) that survives to the next connect because its\n * tab was never explicitly closed — it is a real persisted session the instant it materializes,\n * not a phantom the UI should hide; only an explicit close (`closeTab`'s `discardIfEmpty`) or an\n * explicit delete removes it.\n */\n\nimport { useEffect, useRef } from \"react\";\nimport type { AgentUpdateMessage, PiStudioClient } from \"@av-pi-studio/client\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useSessionStore } from \"@pi-studio-ui/stores/session-store.js\";\nimport { useUiStore } from \"@pi-studio-ui/stores/ui-store.js\";\nimport { useLayoutStore } from \"@pi-studio-ui/stores/layout-store.js\";\nimport { resolveHome } from \"@pi-studio-ui/stores/explorer-store.js\";\nimport {\n collapseInactiveWorkspaces,\n groupSessionsByWorkspace,\n normalizeCwd,\n} from \"@pi-studio-ui/features/sessions/workspace-grouping.js\";\nimport { openChatTab } from \"@pi-studio-ui/features/sessions/open-chat-tab.js\";\nimport { EMPTY_TIMELINE, applyStreamEvent } from \"@pi-studio-ui/timeline/reducer.js\";\nimport { fetchTimelineEvents } from \"@pi-studio-ui/lib/protocol/timeline-paging.js\";\nimport type { AgentStatus } from \"@av-pi-studio/protocol\";\n\ninterface RestoredAgent {\n agentId: string;\n status?: string;\n cwd?: string;\n title?: string;\n lastActivity?: number;\n /** Live model (sprint-042), sourced server-side from the attached session's runtime info when\n * one exists, else the persisted `record.config?.model` (deferred draft or daemon restart —\n * see `list_agents_request` in `daemon/bootstrap.ts`/`dev-bootstrap.ts`). */\n model?: string;\n /** The model's OWN underlying LLM provider (e.g. `\"anthropic\"`) — always sourced from\n * `record.config?.modelProvider` (no live-session override exists for this field; a session's\n * `getRuntimeInfo().provider` is the pi-studio `AgentClient` id, a different namespace — see\n * `provider-contract.ts` `ProviderRuntimeInfo`). REQUIRED to call `setModel` again after a\n * restore without falling into `handleSelectModel`'s `if (!modelProvider) return` no-op. */\n modelProvider?: string;\n provider?: string;\n}\n\n/** Exported for direct unit testing — this project's vitest config runs `.test.ts` under a plain\n * Node environment (no DOM), so hooks with effects are verified by smoke test (task-007) rather\n * than rendered; this narrow type guard is the one piece of the listener worth a direct test. */\nexport function hasStringModel(\n msg: AgentUpdateMessage,\n): msg is AgentUpdateMessage & { model: string } {\n return typeof msg.model === \"string\";\n}\n\nexport function useSessionRestore(): void {\n const client = useConnectionStore((s) => s.client);\n const status = useConnectionStore((s) => s.status);\n const restoredRef = useRef(false);\n\n useEffect(() => {\n if (status !== \"open\" || !client) return;\n if (restoredRef.current) return; // one restore per connection lifetime\n restoredRef.current = true;\n\n void runSessionRestore(client);\n }, [status, client]);\n\n // Live model updates from an explicit `/model` set (sprint-042). `agent_update` is broadcast\n // by every RPC that changes agent state (create/status/model/…) and reaches `onSessionMessage`\n // because the daemon wraps every non-session-enveloped broadcast in `{type:\"session\", message}`\n // (`wrapSessionEnvelope`, `daemon/bootstrap.ts`) — this listener only reacts to the ones that\n // carry a `model`. Lives for the whole connection (not gated by `restoredRef`, unlike the\n // one-shot restore fetch above), so a model change on any session — active or not — is captured.\n useEffect(() => {\n if (status !== \"open\" || !client) return;\n return client.onAgentUpdate((msg) => {\n if (hasStringModel(msg)) {\n const modelProvider = typeof msg.modelProvider === \"string\" ? msg.modelProvider : undefined;\n useSessionStore.getState().setModelByAgentId(msg.agentId, msg.model, modelProvider);\n }\n });\n }, [status, client]);\n}\n\n/**\n * The one-shot restore body plus its hydration signal. Exported for direct unit testing — this\n * project's vitest config runs `.test.ts` under a plain Node environment (no DOM), so the hook\n * itself is verified by smoke test while this is driven directly.\n */\nexport async function runSessionRestore(client: PiStudioClient): Promise<void> {\n try {\n await restoreAgents(client);\n } finally {\n // Reported on every path — zero agents and a failed request included, or a persisted pane\n // would wait forever for a claim that never comes.\n useLayoutStore.getState().markHydrationSource(\"sessions\");\n }\n}\n\nasync function restoreAgents(client: PiStudioClient): Promise<void> {\n let agents: RestoredAgent[] = [];\n try {\n const result = await client.connection.request<{ agents?: RestoredAgent[] }>(\n \"list_agents_request\",\n { all: false },\n );\n agents = result.agents ?? [];\n } catch {\n agents = [];\n }\n\n agents.sort((a, b) => (b.lastActivity ?? 0) - (a.lastActivity ?? 0));\n\n const sessionStore = useSessionStore.getState();\n for (const agent of agents) {\n if (sessionStore.findByAgentId(agent.agentId)) continue;\n\n let timeline = EMPTY_TIMELINE;\n try {\n // Page to completion: one fetch returns only the OLDEST `limit` items and the newest\n // messages of a long conversation would be missing from restored history.\n const events = await fetchTimelineEvents((cursor) =>\n client.agent(agent.agentId).timeline.fetch({\n direction: \"after\",\n cursor: cursor ?? undefined,\n limit: 200,\n }),\n );\n timeline = events.reduce(applyStreamEvent, EMPTY_TIMELINE);\n } catch {\n // Best-effort: an unreadable timeline still gets an (empty) restored session.\n }\n\n // A never-used draft (eager materialization, `materialize.ts`) has no server-persisted\n // title (only an explicit rename — `SessionContextMenu.tsx` — ever sets one) and no\n // messages to derive one from; default it to \"New chat\", matching a freshly\n // `createSession`-created entry, instead of falling through to the cwd basename.\n const isEmptyDraft = timeline.rows.length === 0;\n sessionStore.hydrate({\n id: `s-${agent.agentId.slice(0, 12)}`,\n agentId: agent.agentId,\n title: agent.title || (isEmptyDraft ? \"New chat\" : agent.cwd?.split(\"/\").pop()) || \"Restored\",\n status: (agent.status as AgentStatus | undefined) ?? \"idle\",\n cwd: agent.cwd || \"~\",\n timeline,\n userMessageCount: timeline.rows.filter((r) => r.kind === \"user\").length,\n model: agent.model,\n modelProvider: agent.modelProvider,\n });\n }\n\n const { order, sessions } = useSessionStore.getState();\n const first = order.length > 0 ? sessions[order[0] ?? \"\"] : undefined;\n // Zero restored sessions: leave `tab-store.activeWorkspaceCwd` null and let `TabPanelHost`\n // render its \"no workspace open\" state. NEVER auto-create a session here — a workspace\n // exists only because the user opened a folder (or a restored agent carries a cwd).\n if (!first) return;\n\n const homeDir = await resolveHome(client).catch(() => null);\n const firstCwd = normalizeCwd(first.cwd || \"~\", homeDir);\n // Two different questions, conflated until sprint-049: which workspace was the user LOOKING at\n // (persisted since task-009, captured at boot as `pendingActiveWorkspace`), versus which agent was\n // most recently active *globally* (`first`, all this had before the layout record existed). The\n // sidebar's expanded group, the file explorer's root and the active conversation all follow the\n // former; only the fallback-tab rule below still keys off the latter. Restoring the panes of one\n // workspace while the sidebar sat expanded on another is exactly the reported \"panes come back but\n // the wrong workspace is open\" bug.\n const viewCwd = useLayoutStore.getState().pendingActiveWorkspace ?? firstCwd;\n const workspaceGroups = groupSessionsByWorkspace(order, sessions, homeDir);\n useUiStore\n .getState()\n .setCollapsedWorkspaces(collapseInactiveWorkspaces(workspaceGroups, viewCwd));\n // `order` is most-recent-first across EVERY workspace, so `first` is a global winner. Seed the\n // active conversation from the workspace coming into view instead; when its focused pane holds a\n // chat, `switchWorkspace` overwrites this at the settle point anyway, but when that pane holds a\n // terminal or a file `syncActiveSession` deliberately no-ops — and then this seed is what the\n // status bar keeps showing, so it must not be a conversation from somewhere else.\n const inView = order.find((id) => normalizeCwd(sessions[id]?.cwd || \"~\", homeDir) === viewCwd);\n useSessionStore.getState().activate(inView ?? first.id);\n useUiStore.getState().setCwd(viewCwd);\n\n // Every chat a persisted pane is still waiting for comes back, so a workspace split across two\n // conversations restores as two panes.\n const claimed = claimedChatIdentities();\n const reopened = new Set<string>();\n for (const id of order) {\n const entry = sessions[id];\n if (!entry?.agentId || !claimed.has(`agent:${entry.agentId}`)) continue;\n openChatTab(entry, homeDir);\n reopened.add(id);\n }\n // `first` — the globally most-recently-active agent, not necessarily anything the user had open —\n // is only force-opened as a fallback tab when its OWN workspace has no persisted pane-layout entry\n // to conflict with (never split, or a fresh install/cleared storage/version bump). A record for\n // some OTHER workspace must not suppress this: that workspace restores independently below, and\n // `firstCwd`'s having nothing persisted is exactly the pre-split default this falls back to.\n // Once `firstCwd` DOES have a record, this must stay claim-only: an unclaimed `first` used to\n // open unconditionally, landing in whatever pane was focused and — before `claimPaneFor`'s\n // restore-time guard existed — could silently replace a chat a claim had *just* placed there.\n //\n // Deliberately `firstCwd`, NOT `viewCwd`: this asks whether *`first`* has a record to conflict with.\n // Keying it on the workspace coming into view would silently change the question and stop opening\n // `first` whenever some other workspace had been split — the task-011 regression.\n const hasRecord = useLayoutStore.getState().layouts[firstCwd] !== undefined;\n if (!reopened.has(first.id) && !hasRecord) openChatTab(first, homeDir);\n}\n\n/**\n * The `agent:` identities persisted panes are still waiting for, across every workspace — a claim in\n * a background workspace restores its tab too, or that workspace would come back single-paned. Read\n * from the layout store's unconsumed claims, so it is empty when there was no record to install.\n */\nfunction claimedChatIdentities(): Set<string> {\n const claimed = new Set<string>();\n for (const layout of Object.values(useLayoutStore.getState().layouts)) {\n for (const identity of Object.keys(layout.pendingPlacement)) {\n if (identity.startsWith(\"agent:\")) claimed.add(identity);\n }\n }\n return claimed;\n}\n","/**\n * Terminal restore — on a fresh `open` connection, fetches every terminal currently running on\n * the daemon (`list_terminals_request`) and reopens a tab for each one it should surface,\n * mirroring `use-session-restore.ts`'s one-shot restore pattern but for terminals instead of chat\n * sessions. A terminal can outlive its tab (a daemon restart losing the tab's in-memory link, a\n * dev-mode double-mount race losing the created slot before it's persisted onto the tab, or simply\n * a terminal created outside this UI — the CLI/MCP, or another connected client) and this is what\n * makes it reappear automatically as an ordinary closable tab instead of leaking silently or\n * needing a separate \"orphaned terminals\" management view. `TerminalPanel`'s slot-creation effect\n * only fires `create_terminal_request` when a tab's `data.slot` is still `null`, so opening a tab\n * with the terminal's already-known slot here just subscribes to its existing stream (reattach),\n * never spawns a second PTY.\n *\n * **\"Reopen every terminal\" only holds for a workspace with no persisted pane-layout record.** For\n * one that has one (the ordinary case once it has been split), this is claim-driven like chat\n * restore: a terminal only gets a tab automatically if some pane there is actually waiting for it\n * (`terminal:<slot>` in that workspace's `pendingPlacement`) — checked per workspace, so one\n * workspace's record never suppresses a terminal belonging to an unrelated, never-split one. An\n * unrelated terminal left running elsewhere used to force a tab open regardless, landing in\n * whichever pane happened to be focused and — before `layout-store.ts#claimPaneFor`'s restore-time\n * guard existed — silently replacing a chat a claim had *just* placed there a moment earlier in the\n * same restore pass. Orphaned-terminal surfacing is deliberately narrower now: real for a workspace\n * nothing has ever split (nothing to conflict with), suppressed once that workspace has an\n * arrangement a stray terminal has no claim on.\n *\n * This is also one of the two halves of the pane-layout hydration settle point\n * (`layout-store.markHydrationSource`): being one-shot per connection is what makes \"initial\n * hydration complete\" a well-defined moment.\n */\n\nimport { useEffect, useRef } from \"react\";\nimport type { PiStudioClient } from \"@av-pi-studio/client\";\nimport { useConnectionStore } from \"@pi-studio-ui/lib/connection/connection-store.js\";\nimport { useTabStore, tabIds } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { normalizeCwd } from \"@pi-studio-ui/features/sessions/workspace-grouping.js\";\nimport { resolveHome } from \"@pi-studio-ui/stores/explorer-store.js\";\nimport { useLayoutStore } from \"@pi-studio-ui/stores/layout-store.js\";\n\ninterface TerminalRestoreEntry {\n slot: number;\n name: string;\n cwd?: string;\n closed: boolean;\n}\n\ninterface TerminalsListResponse {\n terminals?: TerminalRestoreEntry[];\n}\n\n/**\n * The one-shot restore body plus its hydration signal. Exported for direct unit testing — this\n * project's vitest config runs `.test.ts` under a plain Node environment (no DOM), so the hook\n * itself is verified by smoke test while this is driven directly.\n */\nexport async function runTerminalRestore(client: PiStudioClient): Promise<void> {\n try {\n let terminals: TerminalRestoreEntry[] = [];\n try {\n const res = await client.connection.request<TerminalsListResponse>(\n \"list_terminals_request\",\n {},\n );\n terminals = res.terminals ?? [];\n } catch {\n terminals = [];\n }\n if (terminals.length === 0) return;\n\n // A workspace's own persisted record is authoritative once one exists: only reopen a terminal\n // there that some pane is actually waiting for. A DIFFERENT workspace having a record must not\n // suppress this one — it restores independently below — and a workspace with no record at all\n // has nothing for a stray terminal to conflict with, so it still surfaces every terminal there,\n // matching the original \"never leak a terminal silently\" guarantee. `layouts` is snapshotted\n // once, before this loop's own `openTab` calls can start creating fresh entries for workspaces\n // that had none — a later terminal in that same NEW workspace must not see its sibling's arrival\n // as if it were a pre-existing record and get wrongly suppressed.\n const layouts = useLayoutStore.getState().layouts;\n const claimed = claimedTerminalIdentities();\n\n const homeDir = await resolveHome(client).catch(() => null);\n const openTab = useTabStore.getState().open;\n for (const entry of terminals) {\n if (entry.closed) continue;\n const workspaceCwd = normalizeCwd(entry.cwd || \"~\", homeDir);\n if (layouts[workspaceCwd] !== undefined && !claimed.has(`terminal:${entry.slot}`)) continue;\n openTab({\n id: tabIds.terminal(entry.slot),\n kind: \"terminal\",\n label: entry.name,\n closable: true,\n data: { slot: entry.slot, cwd: workspaceCwd },\n workspaceCwd,\n });\n }\n } finally {\n // Reported on every path — zero terminals and a failed request included, or a persisted pane\n // would wait forever for a claim that never comes.\n useLayoutStore.getState().markHydrationSource(\"terminals\");\n }\n}\n\n/** The `terminal:` identities persisted panes are still waiting for, across every workspace — mirrors\n * `use-session-restore.ts`'s `claimedChatIdentities`. */\nfunction claimedTerminalIdentities(): Set<string> {\n const claimed = new Set<string>();\n for (const layout of Object.values(useLayoutStore.getState().layouts)) {\n for (const identity of Object.keys(layout.pendingPlacement)) {\n if (identity.startsWith(\"terminal:\")) claimed.add(identity);\n }\n }\n return claimed;\n}\n\nexport function useTerminalRestore(): void {\n const client = useConnectionStore((s) => s.client);\n const status = useConnectionStore((s) => s.status);\n const restoredRef = useRef(false);\n\n useEffect(() => {\n if (status !== \"open\" || !client) return;\n if (restoredRef.current) return; // one restore per connection lifetime\n restoredRef.current = true;\n void runTerminalRestore(client);\n }, [status, client]);\n}\n","/**\n * Global keymap: Ctrl/Cmd+T new terminal, Ctrl/Cmd+W close active tab, Esc close overlays\n * (POC `document.addEventListener(\"keydown\", …)`, POC_TO_APP_PLAN_UI.md §4.8).\n */\n\nimport { useEffect } from \"react\";\nimport { useTabStore, openNewTerminal, closeTab } from \"@pi-studio-ui/stores/tab-store.js\";\nimport { useUiStore } from \"@pi-studio-ui/stores/ui-store.js\";\n\nexport function useShortcuts(): void {\n useEffect(() => {\n function onKeyDown(ev: KeyboardEvent) {\n const ui = useUiStore.getState();\n\n if (ev.key === \"Escape\") {\n ui.closeCwdPicker();\n ui.closeSessionMenu();\n return;\n }\n\n const mod = ev.ctrlKey || ev.metaKey;\n if (mod && ev.key.toLowerCase() === \"t\") {\n ev.preventDefault();\n // Captured synchronously here, not inside the terminal's mount effect — a rapid\n // workspace-switch-then-Ctrl+T must never race the async `create_terminal_request`.\n openNewTerminal(useTabStore.getState().activeWorkspaceCwd || \"~\");\n return;\n }\n\n if (mod && ev.key.toLowerCase() === \"w\") {\n const { activeTabId, tabs } = useTabStore.getState();\n if (!activeTabId) return;\n const tab = tabs.find((t) => t.id === activeTabId);\n if (tab?.closable !== false) {\n ev.preventDefault();\n closeTab(activeTabId);\n }\n }\n }\n\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, []);\n}\n","import { AppProviders } from \"./providers/AppProviders.js\";\nimport { WorkspacePage } from \"./routes/WorkspacePage.js\";\nimport { useConnectionBoot } from \"./hooks/use-connection.js\";\nimport { usePaneLayoutBoot } from \"./hooks/use-pane-layout.js\";\nimport { useSessionRestore } from \"./hooks/use-session-restore.js\";\nimport { useTerminalRestore } from \"./hooks/use-terminal-restore.js\";\nimport { useShortcuts } from \"./hooks/use-shortcuts.js\";\n\nfunction Boot() {\n useConnectionBoot();\n // Before the restore hooks: an arriving tab consumes its pane claim, so the claims must exist.\n usePaneLayoutBoot();\n useSessionRestore();\n useTerminalRestore();\n useShortcuts();\n return null;\n}\n\n// Root component. Connection boot + pane-layout install + session restore + global shortcuts run\n// once at the top; the workspace shell renders the 3-column layout.\nexport function App() {\n return (\n <AppProviders>\n <Boot />\n <WorkspacePage />\n </AppProviders>\n );\n}\n","/**\n * Pure, DOM-free decision core for the resume triggers (swe/features/\n * connection-resilience.md § Public Contract, § Decision table). Extracted per this repo's\n * no-jsdom testing convention (see `timeline/text-viewer-state.ts` / `hooks/molecule-reload.ts`).\n */\nimport type { ConnectionState } from \"@av-pi-studio/client\";\n\nexport type ResumeAction = \"none\" | \"reconnect-now\" | \"probe\";\n\nexport interface ResolveResumeActionInput {\n /** Current daemon connection state. */\n status: ConnectionState;\n /** `connection-store`'s `reconnection !== null` — false means the user explicitly\n * disconnected (or never connected); no resume signal may resurrect that. */\n managerActive: boolean;\n /** Whether a resume probe is already in flight (single-probe guard). */\n probeInFlight: boolean;\n}\n\n/**\n * Decision table (implement exactly — see the spec for the rationale column):\n *\n * | status | managerActive | probeInFlight | -> action |\n * |------------------------|----------------|----------------|-------------------|\n * | closed | true | – | reconnect-now |\n * | open | true | false | probe |\n * | open | true | true | none |\n * | connecting / closing | – | – | none |\n * | any | false | – | none |\n * | idle | – | – | none |\n *\n * The resume *signal* (`\"visible\"` / `\"online\"`) is deliberately not a parameter: no row branches\n * on it, so it stays at the wiring layer (`resume-triggers.ts`) where it is useful for logging.\n */\nexport function resolveResumeAction(input: ResolveResumeActionInput): ResumeAction {\n if (!input.managerActive) return \"none\"; // explicit user Disconnect — never resurrect\n if (input.status === \"closed\") return \"reconnect-now\";\n if (input.status === \"open\") return input.probeInFlight ? \"none\" : \"probe\";\n return \"none\"; // connecting / closing / idle\n}\n","/**\n * Thin DOM wiring for the resume triggers (swe/features/connection-resilience.md\n * § Purpose item 2, § Behavior & Algorithms). Subscribes to `visibilitychange` (hidden → visible\n * only) and `online`; feeds the live connection state through {@link resolveResumeAction} and\n * executes the result against the store's *live* handles — read at signal time, never captured,\n * since `connection-store` replaces `daemon`/`reconnection` on every `connect()`.\n *\n * Installed once at module scope in `main.tsx`, outside React: that file renders under\n * `<StrictMode>`, whose dev-mode double-invoked effects would otherwise attach two listener sets.\n */\nimport { useConnectionStore } from \"./connection-store.js\";\nimport { resolveResumeAction } from \"./resume-action.js\";\n\n/** Deliberately shorter than `DaemonClient.ping()`'s 10s default: the daemon answers pings from\n * its socket read loop, so this much silence on a working link is already pathological. */\nexport const PROBE_TIMEOUT_MS = 5_000;\n\n/** Subscribes to `visibilitychange` (hidden → visible only) and `online`. Returns a detach fn. */\nexport function attachResumeTriggers(): () => void {\n let probeInFlight = false;\n\n function onResume(): void {\n const { status, reconnection, daemon } = useConnectionStore.getState();\n const action = resolveResumeAction({\n status,\n managerActive: reconnection !== null,\n probeInFlight,\n });\n\n if (action === \"reconnect-now\") {\n reconnection?.reconnectNow();\n return;\n }\n\n if (action === \"probe\") {\n if (!daemon) return; // managerActive implied it, but keep the type checker honest\n probeInFlight = true;\n daemon\n .ping(PROBE_TIMEOUT_MS)\n .catch(() => {\n // Identity guard: the user may have disconnected, or reconnected to a different\n // daemon, while this probe was pending — only close the instance we actually probed.\n if (useConnectionStore.getState().daemon === daemon) {\n // close() -> \"closed\" transition -> the active ReconnectionManager's own rung-1\n // retry (~500ms) performs the reconnect. Calling reconnectNow() here would always\n // no-op (close() sets \"closing\" synchronously, so a same-tick call never sees\n // \"closed\") and would be dead code implying an immediacy that doesn't exist — see\n // the spec's § Why the probe closing the socket does NOT violate invariant 6.\n daemon.close(4000, \"stale-connection-probe\");\n }\n })\n .finally(() => {\n probeInFlight = false;\n });\n }\n }\n\n function onVisibilityChange(): void {\n if (document.visibilityState === \"visible\") onResume();\n }\n\n document.addEventListener(\"visibilitychange\", onVisibilityChange);\n window.addEventListener(\"online\", onResume);\n\n return () => {\n document.removeEventListener(\"visibilitychange\", onVisibilityChange);\n window.removeEventListener(\"online\", onResume);\n };\n}\n","import { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport \"./global.css\";\nimport { App } from \"./app.js\";\nimport { attachResumeTriggers } from \"./lib/connection/resume-triggers.js\";\n\nconst root = document.getElementById(\"root\");\nif (!root) throw new Error(\"Missing #root element\");\n\ncreateRoot(root).render(\n <StrictMode>\n <App />\n </StrictMode>,\n);\n\n// Module scope, not a React effect: this file renders under <StrictMode>, whose dev-mode\n// double-invoked effects would attach two listener sets. Module scope also matches the\n// listener's true lifetime — the document, not any component (sprint-050 connection-resilience).\nattachResumeTriggers();\n"],"file":"assets/index-CO_ZzVSZ.js"}