Skip to content

Commit 15b395b

Browse files
committed
refactor: polish UI, add session controls, and improve message filtering
This commit includes multiple improvements: - Format session list filtering code for better readability - Add resizable panel layout persistence with local storage - Add keyboard shortcuts for common actions - Add review stage banner with actionable steps - Add viewport controls to preview panel - Add session file path copying to inspect panel - Add latest user note display in agent shell - Filter chat messages to show only relevant review content - Update styles for new UI components and layout - Rename "Open checks" to "Blockers" in inspect panel
1 parent 55e6931 commit 15b395b

6 files changed

Lines changed: 556 additions & 21 deletions

File tree

apps/agent-html-app/src/app.tsx

Lines changed: 293 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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"
2634
import {
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

98107
type 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+
131153
export 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

Comments
 (0)