feat: glass ui pass

This commit is contained in:
Brooklyn Nicholson
2026-05-16 19:21:33 -05:00
parent 062eed654d
commit d67a438fec
104 changed files with 5173 additions and 1919 deletions
@@ -7,11 +7,16 @@ import type { SessionRuntimeInfo } from '@/types/hermes'
interface CwdActionsOptions {
activeSessionId: string | null
activeSessionIdRef: MutableRefObject<string | null>
currentCwd: string
onSessionRuntimeInfo?: (info: Pick<SessionRuntimeInfo, 'branch' | 'cwd'>) => void
requestGateway: <T = unknown>(method: string, params?: Record<string, unknown>) => Promise<T>
}
export function useCwdActions({ activeSessionId, activeSessionIdRef, currentCwd, requestGateway }: CwdActionsOptions) {
export function useCwdActions({
activeSessionId,
activeSessionIdRef,
onSessionRuntimeInfo,
requestGateway
}: CwdActionsOptions) {
const refreshProjectBranch = useCallback(
async (cwd: string) => {
const target = cwd.trim()
@@ -44,23 +49,15 @@ export function useCwdActions({ activeSessionId, activeSessionIdRef, currentCwd,
return
}
const persistGlobal = async () => {
const info = await requestGateway<{ branch?: string; cwd?: string; value?: string }>('config.set', {
...(activeSessionId && { session_id: activeSessionId }),
key: 'terminal.cwd',
value: trimmed
})
setCurrentCwd(info.cwd || info.value || trimmed)
if (!activeSessionId) {
setCurrentBranch(info.branch || '')
}
}
if (!activeSessionId) {
try {
await persistGlobal()
const info = await requestGateway<{ branch?: string; cwd?: string }>('config.get', {
key: 'project',
cwd: trimmed
})
setCurrentCwd(info.cwd || trimmed)
setCurrentBranch(info.branch || '')
} catch (err) {
notifyError(err, 'Working directory change failed')
}
@@ -76,44 +73,27 @@ export function useCwdActions({ activeSessionId, activeSessionIdRef, currentCwd,
setCurrentCwd(info.cwd || trimmed)
setCurrentBranch(info.branch || '')
onSessionRuntimeInfo?.({ branch: info.branch || '', cwd: info.cwd || trimmed })
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
// Older gateways without `session.cwd.set` fall back to a global write —
// user has to restart the active session for it to take effect.
if (!message.includes('unknown method')) {
notifyError(err, 'Working directory change failed')
return
}
try {
await persistGlobal()
notify({
kind: 'warning',
title: 'Working directory saved',
message: 'Restart the desktop backend to apply cwd changes to this active session.'
})
} catch (fallbackErr) {
notifyError(fallbackErr, 'Working directory change failed')
}
setCurrentCwd(trimmed)
setCurrentBranch('')
notify({
kind: 'warning',
title: 'Working directory staged',
message: 'Restart the desktop backend to apply cwd changes to this active session.'
})
}
},
[activeSessionId, requestGateway]
[activeSessionId, onSessionRuntimeInfo, requestGateway]
)
const browseSessionCwd = useCallback(async () => {
const paths = await window.hermesDesktop?.selectPaths({
title: 'Change working directory',
defaultPath: currentCwd || undefined,
directories: true,
multiple: false
})
if (paths?.[0]) {
await changeSessionCwd(paths[0])
}
}, [changeSessionCwd, currentCwd])
return { browseSessionCwd, changeSessionCwd, refreshProjectBranch }
return { changeSessionCwd, refreshProjectBranch }
}
@@ -100,7 +100,9 @@ function parseMaybeRecord(value: unknown): Record<string, unknown> {
const firstString = (...candidates: unknown[]): string => {
for (const v of candidates) {
if (typeof v === 'string' && v) return v
if (typeof v === 'string' && v) {
return v
}
}
return ''
@@ -111,7 +113,9 @@ function delegateTaskPayloads(
phase: 'running' | 'complete',
sourceEventType?: string
): Record<string, unknown>[] {
if (payload?.name !== 'delegate_task') return []
if (payload?.name !== 'delegate_task') {
return []
}
const args = parseMaybeRecord(payload.args ?? payload.input)
const result = parseMaybeRecord(payload.result)
@@ -120,6 +124,7 @@ function delegateTaskPayloads(
const status = phase === 'complete' ? (payload.error ? 'failed' : 'completed') : 'running'
const toolId = payload.tool_id || payload.tool_call_id || payload.id || 'delegate_task'
const progressText = firstString(payload.preview, payload.message, payload.context)
const eventType =
phase === 'complete'
? 'subagent.complete'
@@ -517,6 +522,8 @@ export function useMessageStream({
const runningChanged = typeof payload?.running === 'boolean'
if (apply) {
const runtimeInfo: { branch?: string; cwd?: string } = {}
if (modelChanged) {
setCurrentModel(payload!.model || '')
}
@@ -527,10 +534,20 @@ export function useMessageStream({
if (typeof payload?.cwd === 'string') {
setCurrentCwd(payload.cwd)
runtimeInfo.cwd = payload.cwd
}
if (typeof payload?.branch === 'string') {
setCurrentBranch(payload.branch)
runtimeInfo.branch = payload.branch
}
if (sessionId && (runtimeInfo.cwd !== undefined || runtimeInfo.branch !== undefined)) {
updateSessionState(sessionId, state => ({
...state,
branch: runtimeInfo.branch ?? state.branch,
cwd: runtimeInfo.cwd ?? state.cwd
}))
}
if (typeof payload?.personality === 'string') {
@@ -23,6 +23,7 @@ import {
$composerAttachments,
addComposerAttachment,
clearComposerAttachments,
terminalContextBlocksFromDraft,
type ComposerAttachment
} from '@/store/composer'
import { clearNotifications, notify, notifyError } from '@/store/notifications'
@@ -206,11 +207,13 @@ export function usePromptActions({
.map(a => a.refText)
.filter(Boolean)
.join('\n')
const terminalContextBlocks = terminalContextBlocksFromDraft(rawText).join('\n\n')
const hasImage = attachments.some(a => a.kind === 'image')
const attachmentRefs = attachments.map(attachmentDisplayText).filter((r): r is string => Boolean(r))
const text =
[contextRefs, visibleText].filter(Boolean).join('\n\n') || (hasImage ? 'What do you see in this image?' : '')
[contextRefs, terminalContextBlocks, visibleText].filter(Boolean).join('\n\n') ||
(hasImage ? 'What do you see in this image?' : '')
if (!text || busyRef.current) {
return false
@@ -12,6 +12,7 @@ import { $pinnedSessionIds } from '@/store/layout'
import { clearNotifications, notify, notifyError } from '@/store/notifications'
import { requestDesktopOnboarding } from '@/store/onboarding'
import {
$currentCwd,
$messages,
$sessions,
setActiveSessionId,
@@ -166,6 +167,7 @@ function upsertOptimisticSession(
const now = Date.now() / 1000
const session: SessionInfo = {
cwd: created.info?.cwd ?? null,
ended_at: null,
id,
input_tokens: 0,
@@ -184,11 +186,21 @@ function upsertOptimisticSession(
setSessions(prev => [session, ...prev.filter(s => s.id !== id)])
}
function applyRuntimeInfo(info: SessionCreateResponse['info'] | undefined) {
if (!info) {
function patchSessionWorkspace(sessionId: string, cwd: string | undefined) {
if (!cwd) {
return
}
setSessions(prev => prev.map(session => (session.id === sessionId ? { ...session, cwd } : session)))
}
function applyRuntimeInfo(info: SessionCreateResponse['info'] | undefined): Partial<Pick<ClientSessionState, 'branch' | 'cwd'>> | null {
if (!info) {
return null
}
const sessionState: Partial<Pick<ClientSessionState, 'branch' | 'cwd'>> = {}
if (info.credential_warning) {
requestDesktopOnboarding(info.credential_warning)
}
@@ -203,10 +215,12 @@ function applyRuntimeInfo(info: SessionCreateResponse['info'] | undefined) {
if (info.cwd) {
setCurrentCwd(info.cwd)
sessionState.cwd = info.cwd
}
if (info.branch !== undefined) {
setCurrentBranch(info.branch || '')
sessionState.branch = info.branch || ''
}
if (typeof info.personality === 'string') {
@@ -228,6 +242,8 @@ function applyRuntimeInfo(info: SessionCreateResponse['info'] | undefined) {
if (info.usage) {
setCurrentUsage(current => ({ ...current, ...info.usage }))
}
return sessionState
}
export function useSessionActions({
@@ -269,6 +285,8 @@ export function useSessionActions({
})
setSessionStartedAt(null)
setTurnStartedAt(null)
setCurrentCwd('')
setCurrentBranch('')
clearComposerDraft()
clearComposerAttachments()
setFreshDraftReady(true)
@@ -284,7 +302,8 @@ export function useSessionActions({
creatingSessionRef.current = true
try {
const created = await requestGateway<SessionCreateResponse>('session.create', { cols: 96 })
const cwd = $currentCwd.get().trim()
const created = await requestGateway<SessionCreateResponse>('session.create', { cols: 96, ...(cwd && { cwd }) })
const stored = created.stored_session_id ?? null
if (
@@ -310,7 +329,11 @@ export function useSessionActions({
setActiveSessionId(created.session_id)
setSelectedStoredSessionId(stored)
setSessionStartedAt(Date.now())
applyRuntimeInfo(created.info)
const runtimeInfo = applyRuntimeInfo(created.info)
if (runtimeInfo) {
updateSessionState(created.session_id, state => ({ ...state, ...runtimeInfo }), stored)
}
return created.session_id
} finally {
@@ -325,7 +348,8 @@ export function useSessionActions({
getRouteToken,
navigate,
requestGateway,
selectedStoredSessionIdRef
selectedStoredSessionIdRef,
updateSessionState
])
const selectSidebarItem = useCallback(
@@ -376,6 +400,8 @@ export function useSessionActions({
setActiveSessionId(cachedRuntimeId)
activeSessionIdRef.current = cachedRuntimeId
syncSessionStateToView(cachedRuntimeId, cachedState)
setCurrentCwd(cachedState.cwd)
setCurrentBranch(cachedState.branch)
setSessionStartedAt(Date.now())
clearComposerDraft()
clearComposerAttachments()
@@ -467,10 +493,15 @@ export function useSessionActions({
setActiveSessionId(resumed.session_id)
activeSessionIdRef.current = resumed.session_id
const runtimeInfo = applyRuntimeInfo(resumed.info)
patchSessionWorkspace(storedSessionId, runtimeInfo?.cwd)
updateSessionState(
resumed.session_id,
state => ({
...state,
...(runtimeInfo ?? {}),
messages: messagesForView,
busy: false,
awaitingResponse: false
@@ -479,7 +510,6 @@ export function useSessionActions({
)
clearComposerDraft()
clearComposerAttachments()
applyRuntimeInfo(resumed.info)
} catch (err) {
if (!isCurrentResume()) {
return
@@ -570,8 +600,11 @@ export function useSessionActions({
clearNotifications()
const cwd = $currentCwd.get().trim()
const branched = await requestGateway<SessionCreateResponse>('session.create', {
cols: 96,
...(cwd && { cwd }),
messages: branchMessages.map(({ content, role }) => ({ content, role })),
title: 'Branch'
})
@@ -600,7 +633,13 @@ export function useSessionActions({
clearComposerDraft()
clearComposerAttachments()
applyRuntimeInfo(branched.info)
const runtimeInfo = applyRuntimeInfo(branched.info)
patchSessionWorkspace(routedSessionId, runtimeInfo?.cwd)
if (runtimeInfo) {
updateSessionState(branched.session_id, state => ({ ...state, ...runtimeInfo }), routedSessionId)
}
return true
} catch (err) {