feat: better composer etc

This commit is contained in:
Brooklyn Nicholson
2026-05-04 22:19:16 -05:00
parent 42db075e10
commit fcce49db3f
50 changed files with 1299 additions and 668 deletions
@@ -40,13 +40,19 @@ export function useContextSuggestions({
cwd: cwd || undefined
})
if (stillCurrent()) {setContextSuggestions((result.items || []).filter(i => i.text))}
if (stillCurrent()) {
setContextSuggestions((result.items || []).filter(i => i.text))
}
} catch {
if (stillCurrent()) {setContextSuggestions([])}
if (stillCurrent()) {
setContextSuggestions([])
}
}
}, [activeSessionId, activeSessionIdRef, currentCwd, requestGateway])
useEffect(() => {
if (gatewayState === 'open' && activeSessionId) {void refresh()}
if (gatewayState === 'open' && activeSessionId) {
void refresh()
}
}, [activeSessionId, gatewayState, refresh])
}
@@ -16,10 +16,15 @@ export function useCwdActions({ activeSessionId, activeSessionIdRef, currentCwd,
async (cwd: string) => {
const target = cwd.trim()
if (!target || activeSessionIdRef.current) {return}
if (!target || activeSessionIdRef.current) {
return
}
try {
const info = await requestGateway<{ branch?: string; cwd?: string }>('config.get', { key: 'project', cwd: target })
const info = await requestGateway<{ branch?: string; cwd?: string }>('config.get', {
key: 'project',
cwd: target
})
if (!activeSessionIdRef.current && ($currentCwd.get() || target) === (info.cwd || target)) {
setCurrentBranch(info.branch || '')
@@ -35,7 +40,9 @@ export function useCwdActions({ activeSessionId, activeSessionIdRef, currentCwd,
async (cwd: string) => {
const trimmed = cwd.trim()
if (!trimmed) {return}
if (!trimmed) {
return
}
const persistGlobal = async () => {
const info = await requestGateway<{ branch?: string; cwd?: string; value?: string }>('config.set', {
@@ -46,7 +53,9 @@ export function useCwdActions({ activeSessionId, activeSessionIdRef, currentCwd,
setCurrentCwd(info.cwd || info.value || trimmed)
if (!activeSessionId) {setCurrentBranch(info.branch || '')}
if (!activeSessionId) {
setCurrentBranch(info.branch || '')
}
}
if (!activeSessionId) {
@@ -101,7 +110,9 @@ export function useCwdActions({ activeSessionId, activeSessionIdRef, currentCwd,
multiple: false
})
if (paths?.[0]) {await changeSessionCwd(paths[0])}
if (paths?.[0]) {
await changeSessionCwd(paths[0])
}
}, [changeSessionCwd, currentCwd])
return { browseSessionCwd, changeSessionCwd, refreshProjectBranch }
@@ -25,7 +25,9 @@ export function useModelControls({ activeSessionId, queryClient, requestGateway
queryClient.setQueryData<ModelOptionsResponse>(['model-options', activeSessionId || 'global'], patch)
if (includeGlobal) {queryClient.setQueryData<ModelOptionsResponse>(['model-options', 'global'], patch)}
if (includeGlobal) {
queryClient.setQueryData<ModelOptionsResponse>(['model-options', 'global'], patch)
}
},
[activeSessionId, queryClient]
)
@@ -34,9 +36,13 @@ export function useModelControls({ activeSessionId, queryClient, requestGateway
try {
const result = await getGlobalModelInfo()
if (typeof result.model === 'string') {setCurrentModel(result.model)}
if (typeof result.model === 'string') {
setCurrentModel(result.model)
}
if (typeof result.provider === 'string') {setCurrentProvider(result.provider)}
if (typeof result.provider === 'string') {
setCurrentProvider(result.provider)
}
} catch {
// The delayed session.info event still updates this once the agent is ready.
}
@@ -56,7 +62,9 @@ export function useModelControls({ activeSessionId, queryClient, requestGateway
command: `/model ${selection.model} --provider ${selection.provider}${selection.persistGlobal ? ' --global' : ''}`
})
if (selection.persistGlobal) {void refreshCurrentModel()}
if (selection.persistGlobal) {
void refreshCurrentModel()
}
void queryClient.invalidateQueries({
queryKey: selection.persistGlobal ? ['model-options'] : ['model-options', activeSessionId]
})
@@ -87,7 +87,13 @@ describe('usePreviewRouting', () => {
const target = previewTarget('/work/demo.html')
registerSessionPreview('session-1', target, 'tool-result')
render(<PreviewRoutingHarness onEvent={handler => { handleEvent = handler }} />)
render(
<PreviewRoutingHarness
onEvent={handler => {
handleEvent = handler
}}
/>
)
await waitFor(() => {
expect($previewTarget.get()).toEqual({ ...target, renderMode: 'preview' })
@@ -95,7 +101,13 @@ describe('usePreviewRouting', () => {
})
it('does not infer previews from assistant prose', async () => {
render(<PreviewRoutingHarness onEvent={handler => { handleEvent = handler }} />)
render(
<PreviewRoutingHarness
onEvent={handler => {
handleEvent = handler
}}
/>
)
act(() => {
$messages.set([
@@ -109,7 +121,13 @@ describe('usePreviewRouting', () => {
})
it('registers structured tool-result preview targets', async () => {
render(<PreviewRoutingHarness onEvent={handler => { handleEvent = handler }} />)
render(
<PreviewRoutingHarness
onEvent={handler => {
handleEvent = handler
}}
/>
)
act(() =>
handleEvent({
@@ -127,7 +145,13 @@ describe('usePreviewRouting', () => {
})
it('registers html previews from edit inline diffs', async () => {
render(<PreviewRoutingHarness onEvent={handler => { handleEvent = handler }} />)
render(
<PreviewRoutingHarness
onEvent={handler => {
handleEvent = handler
}}
/>
)
act(() =>
handleEvent({
@@ -113,18 +113,32 @@ export function usePreviewRouting({
const registerStructuredPreview = useCallback(
async (event: RpcEvent) => {
if (event.session_id && event.session_id !== activeSessionIdRef.current && event.session_id !== previewSessionId) {return}
if (
event.session_id &&
event.session_id !== activeSessionIdRef.current &&
event.session_id !== previewSessionId
) {
return
}
if (!event.type.startsWith('tool.')) {return}
if (!event.type.startsWith('tool.')) {
return
}
if (!previewSessionId) {return}
if (!previewSessionId) {
return
}
const candidate = structuredPreviewCandidate(event.payload)
if (!candidate) {return}
if (!candidate) {
return
}
const desktop = window.hermesDesktop
if (!desktop?.normalizePreviewTarget) {return}
if (!desktop?.normalizePreviewTarget) {
return
}
const sessionId = previewSessionId
const cwd = currentCwd || ''
const target = await desktop.normalizePreviewTarget(candidate, cwd || undefined).catch(() => null)
@@ -146,7 +160,9 @@ export function usePreviewRouting({
async (url: string, context?: string) => {
const sessionId = activeSessionIdRef.current
if (!sessionId) {throw new Error('No active session for background restart')}
if (!sessionId) {
throw new Error('No active session for background restart')
}
const cwd = $currentCwd.get() || currentCwd || ''
@@ -159,7 +175,9 @@ export function usePreviewRouting({
const taskId = result.task_id || ''
if (!taskId) {throw new Error('Background restart did not return a task id')}
if (!taskId) {
throw new Error('Background restart did not return a task id')
}
beginPreviewServerRestart(taskId, url)
@@ -175,18 +193,26 @@ export function usePreviewRouting({
if (event.type === 'preview.restart.complete') {
const { task_id, text } = asRecord(event.payload)
if (typeof task_id === 'string' && task_id) {completePreviewServerRestart(task_id, typeof text === 'string' ? text : '')}
if (typeof task_id === 'string' && task_id) {
completePreviewServerRestart(task_id, typeof text === 'string' ? text : '')
}
} else if (event.type === 'preview.restart.progress') {
const { task_id, text } = asRecord(event.payload)
if (typeof task_id === 'string' && task_id) {progressPreviewServerRestart(task_id, typeof text === 'string' ? text : '')}
if (typeof task_id === 'string' && task_id) {
progressPreviewServerRestart(task_id, typeof text === 'string' ? text : '')
}
}
if (event.session_id && event.session_id !== activeSessionIdRef.current) {return}
if (event.session_id && event.session_id !== activeSessionIdRef.current) {
return
}
void registerStructuredPreview(event)
if ($previewTarget.get()?.kind === 'url' && gatewayEventCompletedFileDiff(event)) {requestPreviewReload()}
if ($previewTarget.get()?.kind === 'url' && gatewayEventCompletedFileDiff(event)) {
requestPreviewReload()
}
},
[activeSessionIdRef, baseHandleGatewayEvent, registerStructuredPreview]
)
@@ -22,10 +22,14 @@ interface RouteResumeOptions {
// parsed. If the hash references a real session, defer; resume picks it up
// next tick. Without this, ctrl+R on `#/:sessionId` flashes 5 loading states.
function rawHashLooksLikeSession(): boolean {
if (typeof window === 'undefined') {return false}
if (typeof window === 'undefined') {
return false
}
const hash = window.location.hash.replace(/^#/, '')
if (!hash || hash === '/') {return false}
if (!hash || hash === '/') {
return false
}
return !hash.startsWith('/settings') && !hash.startsWith('/skills') && !hash.startsWith('/artifacts')
}
@@ -46,7 +50,9 @@ export function useRouteResume({
startFreshSessionDraft
}: RouteResumeOptions) {
useEffect(() => {
if (currentView !== 'chat' || gatewayState !== 'open') {return}
if (currentView !== 'chat' || gatewayState !== 'open') {
return
}
if (routedSessionId) {
const cachedRuntime = runtimeIdByStoredSessionIdRef.current.get(routedSessionId)
@@ -56,7 +62,9 @@ export function useRouteResume({
Boolean(cachedRuntime) &&
cachedRuntime === activeSessionIdRef.current
if (!alreadyActive) {void resumeSession(routedSessionId, true)}
if (!alreadyActive) {
void resumeSession(routedSessionId, true)
}
return
}
@@ -655,16 +655,16 @@ export function useSessionActions({
setFreshDraftReady(false)
setSelectedStoredSessionId(storedSessionId)
selectedStoredSessionIdRef.current = storedSessionId
const stored = $sessions.get().find(session => session.id === storedSessionId)
const stored = $sessions.get().find(session => session.id === storedSessionId)
if (stored) {
setCurrentUsage(current => ({
...current,
input: stored.input_tokens || 0,
output: stored.output_tokens || 0,
total: (stored.input_tokens || 0) + (stored.output_tokens || 0)
}))
}
if (stored) {
setCurrentUsage(current => ({
...current,
input: stored.input_tokens || 0,
output: stored.output_tokens || 0,
total: (stored.input_tokens || 0) + (stored.output_tokens || 0)
}))
}
setMessages(previousMessages)
navigate(sessionRoute(storedSessionId), { replace: true })