feat: glass ui pass
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user