@@ -22,7 +22,15 @@ import {
2222 type ReviewFocusIntent ,
2323 type ReviewFocusTarget ,
2424} from "./lib/review-focus"
25- import type { ReviewTimelineActionConfig } from "./lib/review-flow"
25+ import {
26+ findLatestProposalDecision ,
27+ findRecentProposalDecisions ,
28+ getCurrentReviewStage ,
29+ getProposalDecisionTrend ,
30+ getReviewTimeline ,
31+ getReviewTimelineActionConfig ,
32+ type ReviewTimelineActionConfig ,
33+ } from "./lib/review-flow"
2634import {
2735 createSourceFocusTargetFromDiagnostic ,
2836 createSourceFocusTargetFromGroup ,
@@ -94,6 +102,7 @@ import type {
94102 RuntimeReport ,
95103 WorkbenchView ,
96104} from "./lib/types"
105+ import { copyText } from "./lib/utils"
97106
98107type CommandState = {
99108 loading : boolean
@@ -128,9 +137,26 @@ type HydratedSessionState = {
128137 previewHtml ?: string
129138}
130139
140+ type PanelLayoutState = {
141+ sessions : number
142+ workbench : number
143+ shell : number
144+ }
145+
146+ const panelLayoutStorageKey = "agent-html-app:panel-layout:v1"
147+ const defaultPanelLayout : PanelLayoutState = {
148+ sessions : 18 ,
149+ workbench : 52 ,
150+ shell : 30 ,
151+ }
152+
131153export function App ( ) {
132154 const [ appState , setAppState ] = useState < AppState > ( mockAppState )
133155 const [ sessionDrafts , setSessionDrafts ] = useState < Record < string , string > > ( { } )
156+ const [ sessionSearchFocusKey , setSessionSearchFocusKey ] = useState < string > ( )
157+ const [ panelLayout , setPanelLayout ] = useState < PanelLayoutState > ( ( ) =>
158+ readStoredPanelLayout ( ) ,
159+ )
134160 const [ agentShellReviewIntent , setAgentShellReviewIntent ] =
135161 useState < ReviewFocusIntent > ( )
136162 const [ agentShellClearReviewFocusKey , setAgentShellClearReviewFocusKey ] =
@@ -161,9 +187,57 @@ export function App() {
161187 appState . chat ,
162188 currentDraftSource ,
163189 )
164- const latestProposalText = [ ...appState . chat ]
190+ const latestProposal = [ ...appState . chat ]
165191 . reverse ( )
166- . find ( ( message ) => message . kind === "proposal-placeholder" ) ?. text
192+ . find ( ( message ) => message . kind === "proposal-placeholder" )
193+ const latestProposalText = latestProposal ?. text
194+ const latestProposalDecision = findLatestProposalDecision ( appState . chat )
195+ const recentProposalDecisions = findRecentProposalDecisions ( appState . chat )
196+ const proposalDecisionTrend = getProposalDecisionTrend (
197+ recentProposalDecisions ,
198+ )
199+ const latestProposalIsStale = latestProposal
200+ ? appState . currentSession . summary . updatedAt > latestProposal . createdAt
201+ : false
202+ const currentReviewStage = getCurrentReviewStage ( {
203+ build : appState . currentBuild ,
204+ hasUnsavedSourceChanges,
205+ inspect : appState . currentInspect ,
206+ latestProposalExists : Boolean ( latestProposal ) ,
207+ latestProposalIsStale,
208+ proposalComparison,
209+ session : appState . currentSession ,
210+ sourceValidation : currentSourceValidation ,
211+ } )
212+ const reviewTimeline = getReviewTimeline ( {
213+ build : appState . currentBuild ,
214+ hasUnsavedSourceChanges,
215+ inspect : appState . currentInspect ,
216+ latestProposal,
217+ latestProposalDecision,
218+ proposalDecisionTrend,
219+ latestProposalIsStale,
220+ messages : appState . chat ,
221+ proposalComparison,
222+ session : appState . currentSession ,
223+ sourceValidation : currentSourceValidation ,
224+ } )
225+ const currentStageItem =
226+ reviewTimeline . find ( ( item ) => item . id === currentReviewStage ) ??
227+ reviewTimeline [ 0 ]
228+ const currentStageAction = getReviewTimelineActionConfig ( {
229+ stage : currentReviewStage ,
230+ activeView,
231+ build : appState . currentBuild ,
232+ hasUnsavedSourceChanges,
233+ inspect : appState . currentInspect ,
234+ latestProposalExists : Boolean ( latestProposal ) ,
235+ latestProposalDecision,
236+ latestProposalIsStale,
237+ proposalComparison,
238+ sessionHasPreview : appState . currentSession . summary . hasPreview ,
239+ sourceValidation : currentSourceValidation ,
240+ } )
167241 const availableReviewFocusTargets = getAvailableReviewFocusTargets ( {
168242 proposalText : latestProposalText ,
169243 build : appState . currentBuild ,
@@ -219,6 +293,72 @@ export function App() {
219293 }
220294 } , [ ] )
221295
296+ useEffect ( ( ) => {
297+ const handleKeydown = ( event : KeyboardEvent ) => {
298+ if ( event . defaultPrevented ) {
299+ return
300+ }
301+
302+ const modifier = event . metaKey || event . ctrlKey
303+ if ( ! modifier ) {
304+ return
305+ }
306+
307+ const key = event . key . toLowerCase ( )
308+
309+ switch ( key ) {
310+ case "k" :
311+ event . preventDefault ( )
312+ setSessionSearchFocusKey ( `search-${ Date . now ( ) } ` )
313+ break
314+ case "s" :
315+ if ( ! hasUnsavedSourceChanges || commandState . savingSource ) {
316+ return
317+ }
318+ event . preventDefault ( )
319+ void handleSaveSource ( currentDraftSource )
320+ break
321+ case "enter" :
322+ if ( commandState . runningBuild ) {
323+ return
324+ }
325+ event . preventDefault ( )
326+ void handleBuild ( )
327+ break
328+ case "i" :
329+ if ( ! event . shiftKey || commandState . runningInspect ) {
330+ return
331+ }
332+ event . preventDefault ( )
333+ void handleInspect ( )
334+ break
335+ case "1" :
336+ event . preventDefault ( )
337+ void handleViewChange ( "preview" )
338+ break
339+ case "2" :
340+ event . preventDefault ( )
341+ void handleViewChange ( "source" )
342+ break
343+ case "3" :
344+ event . preventDefault ( )
345+ void handleViewChange ( "inspect" )
346+ break
347+ }
348+ }
349+
350+ window . addEventListener ( "keydown" , handleKeydown )
351+ return ( ) => {
352+ window . removeEventListener ( "keydown" , handleKeydown )
353+ }
354+ } , [
355+ commandState . runningBuild ,
356+ commandState . runningInspect ,
357+ commandState . savingSource ,
358+ currentDraftSource ,
359+ hasUnsavedSourceChanges ,
360+ ] )
361+
222362 useEffect ( ( ) => {
223363 setAgentShellReviewIntent ( undefined )
224364 setAgentShellClearReviewFocusKey ( undefined )
@@ -1429,10 +1569,67 @@ export function App() {
14291569 < RuntimeBanner report = { appState . runtimeReport } />
14301570 ) : null }
14311571
1432- < ResizablePanelGroup className = "app-shell" orientation = "horizontal" >
1433- < ResizablePanel className = "app-pane" defaultSize = { 18 } minSize = { 14 } >
1572+ < SurfaceCard className = "stage-banner" variant = "banner" >
1573+ < SurfaceCardHeader title = { currentStageItem ?. label ?? "Current stage" } >
1574+ < div className = "stage-banner-meta" >
1575+ < StatusBadge
1576+ tone = { statusToneForStage ( currentStageItem ?. pillClassName ) }
1577+ >
1578+ { currentStageItem ?. statusLabel ?? "Current" }
1579+ </ StatusBadge >
1580+ < span className = "inline-meta" >
1581+ { appState . currentSession . summary . name }
1582+ </ span >
1583+ < span className = "inline-meta" > View { activeView } </ span >
1584+ </ div >
1585+ </ SurfaceCardHeader >
1586+ < SurfaceCardBody className = "stage-banner-body" padding = "compact" >
1587+ < p > { currentStageItem ?. summary } </ p >
1588+ < div className = "stage-banner-actions" >
1589+ { currentStageAction ? (
1590+ < Button
1591+ onClick = { ( ) => {
1592+ void handleRunReviewAction ( currentStageAction . handler )
1593+ } }
1594+ size = "sm"
1595+ type = "button"
1596+ >
1597+ { currentStageAction . label }
1598+ </ Button >
1599+ ) : null }
1600+ < Button
1601+ onClick = { ( ) => {
1602+ void copyText ( appState . currentSession . sourcePath )
1603+ } }
1604+ size = "sm"
1605+ type = "button"
1606+ variant = "outline"
1607+ >
1608+ Copy source path
1609+ </ Button >
1610+ </ div >
1611+ </ SurfaceCardBody >
1612+ </ SurfaceCard >
1613+
1614+ < ResizablePanelGroup
1615+ className = "app-shell"
1616+ defaultLayout = { toGroupLayout ( panelLayout ) }
1617+ onLayoutChanged = { ( layout ) => {
1618+ const nextLayout = normalizePanelLayout ( layout )
1619+ setPanelLayout ( nextLayout )
1620+ persistPanelLayout ( nextLayout )
1621+ } }
1622+ orientation = "horizontal"
1623+ >
1624+ < ResizablePanel
1625+ className = "app-pane"
1626+ defaultSize = { panelLayout . sessions }
1627+ id = "sessions"
1628+ minSize = { 14 }
1629+ >
14341630 < SessionsSidebar
14351631 activeSessionId = { appState . currentSession . summary . id }
1632+ focusSearchKey = { sessionSearchFocusKey }
14361633 isBusy = { isSidebarBusy }
14371634 onCreateSession = { ( ) => {
14381635 void handleCreateSession ( )
@@ -1453,7 +1650,12 @@ export function App() {
14531650 />
14541651 </ ResizablePanel >
14551652 < ResizableHandle className = "app-shell-handle" withHandle />
1456- < ResizablePanel className = "app-pane" defaultSize = { 52 } minSize = { 34 } >
1653+ < ResizablePanel
1654+ className = "app-pane"
1655+ defaultSize = { panelLayout . workbench }
1656+ id = "workbench"
1657+ minSize = { 34 }
1658+ >
14571659 < Workbench
14581660 activeView = { activeView }
14591661 activeReviewFocus = { agentShellReviewFocus }
@@ -1494,7 +1696,12 @@ export function App() {
14941696 />
14951697 </ ResizablePanel >
14961698 < ResizableHandle className = "app-shell-handle" withHandle />
1497- < ResizablePanel className = "app-pane" defaultSize = { 30 } minSize = { 20 } >
1699+ < ResizablePanel
1700+ className = "app-pane"
1701+ defaultSize = { panelLayout . shell }
1702+ id = "shell"
1703+ minSize = { 20 }
1704+ >
14981705 < AgentShell
14991706 activeView = { activeView }
15001707 activeSourceFocusReviewStatus = { sourceFocusReviewStatus }
@@ -1633,11 +1840,88 @@ function RuntimeBanner({ report }: { report: RuntimeReport }) {
16331840 < StatusBadge > ok { report . counts . ok } </ StatusBadge >
16341841 < StatusBadge tone = "dirty" > warn { report . counts . warn } </ StatusBadge >
16351842 < StatusBadge tone = "error" > fail { report . counts . fail } </ StatusBadge >
1843+ < span className = "inline-meta" > v{ report . packageVersion } </ span >
16361844 </ div >
16371845 </ SurfaceCardHeader >
1638- < SurfaceCardBody className = "sr-only" padding = "none" >
1639- Runtime health summary
1846+ < SurfaceCardBody className = "runtime-banner-body" padding = "compact" >
1847+ < span className = "inline-meta" > { report . runtimeRoot } </ span >
1848+ < span className = "inline-meta" > { report . outputDir } </ span >
16401849 </ SurfaceCardBody >
16411850 </ SurfaceCard >
16421851 )
16431852}
1853+
1854+ function readStoredPanelLayout ( ) : PanelLayoutState {
1855+ if ( typeof window === "undefined" ) {
1856+ return defaultPanelLayout
1857+ }
1858+
1859+ try {
1860+ const raw = window . localStorage . getItem ( panelLayoutStorageKey )
1861+ if ( ! raw ) {
1862+ return defaultPanelLayout
1863+ }
1864+
1865+ const parsed = JSON . parse ( raw ) as Partial < PanelLayoutState >
1866+ if (
1867+ typeof parsed . sessions === "number" &&
1868+ typeof parsed . workbench === "number" &&
1869+ typeof parsed . shell === "number"
1870+ ) {
1871+ return {
1872+ sessions : parsed . sessions ,
1873+ workbench : parsed . workbench ,
1874+ shell : parsed . shell ,
1875+ }
1876+ }
1877+ } catch {
1878+ return defaultPanelLayout
1879+ }
1880+
1881+ return defaultPanelLayout
1882+ }
1883+
1884+ function persistPanelLayout ( layout : PanelLayoutState ) {
1885+ if ( typeof window === "undefined" ) {
1886+ return
1887+ }
1888+
1889+ window . localStorage . setItem ( panelLayoutStorageKey , JSON . stringify ( layout ) )
1890+ }
1891+
1892+ function normalizePanelLayout (
1893+ layout : Record < string , number > ,
1894+ ) : PanelLayoutState {
1895+ return {
1896+ sessions : layout . sessions ?? defaultPanelLayout . sessions ,
1897+ workbench : layout . workbench ?? defaultPanelLayout . workbench ,
1898+ shell : layout . shell ?? defaultPanelLayout . shell ,
1899+ }
1900+ }
1901+
1902+ function toGroupLayout ( layout : PanelLayoutState ) {
1903+ return {
1904+ sessions : layout . sessions ,
1905+ workbench : layout . workbench ,
1906+ shell : layout . shell ,
1907+ }
1908+ }
1909+
1910+ function statusToneForStage (
1911+ className ?: string ,
1912+ ) : "default" | "accent" | "ready" | "dirty" | "error" | "building" {
1913+ switch ( className ) {
1914+ case "status-ready" :
1915+ return "ready"
1916+ case "status-dirty" :
1917+ return "dirty"
1918+ case "status-error" :
1919+ return "error"
1920+ case "status-building" :
1921+ return "building"
1922+ case "accent" :
1923+ return "accent"
1924+ default :
1925+ return "default"
1926+ }
1927+ }
0 commit comments