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
+125 -1
View File
@@ -4,7 +4,7 @@ import { triggerHaptic } from '@/lib/haptics'
export interface ComposerAttachment {
id: string
kind: 'image' | 'file' | 'folder' | 'url'
kind: 'image' | 'file' | 'folder' | 'terminal' | 'url'
label: string
detail?: string
refText?: string
@@ -15,11 +15,38 @@ export interface ComposerAttachment {
export const $composerDraft = atom('')
export const $composerAttachments = atom<ComposerAttachment[]>([])
export const $composerTerminalSelections = atom<Record<string, string>>({})
export function setComposerDraft(value: string) {
$composerDraft.set(value)
}
export function appendComposerDraft(value: string) {
const text = value.trim()
if (!text) {
return
}
const current = $composerDraft.get()
const separator = current && !current.endsWith('\n') ? '\n\n' : ''
$composerDraft.set(`${current}${separator}${text}`)
}
export function appendComposerInline(value: string) {
const text = value.trim()
if (!text) {
return
}
const current = $composerDraft.get().trimEnd()
const separator = current ? ' ' : ''
$composerDraft.set(`${current}${separator}${text}`)
}
export function clearComposerDraft() {
$composerDraft.set('')
}
@@ -46,6 +73,103 @@ export function clearComposerAttachments() {
$composerAttachments.set([])
}
const TERMINAL_REF_RE = /@terminal:(`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|\S+)/g
function unquoteRefValue(raw: string) {
const head = raw[0]
const tail = raw[raw.length - 1]
const quoted = (head === '`' && tail === '`') || (head === '"' && tail === '"') || (head === "'" && tail === "'")
return (quoted ? raw.slice(1, -1) : raw).replace(/[,.;!?]+$/, '').trim()
}
function terminalLabelsFromDraft(draft: string) {
const labels: string[] = []
const seen = new Set<string>()
for (const match of draft.matchAll(TERMINAL_REF_RE)) {
const label = unquoteRefValue(match[1] || '')
if (!label || seen.has(label)) {
continue
}
seen.add(label)
labels.push(label)
}
return labels
}
export function setComposerTerminalSelection(label: string, text: string) {
const nextLabel = label.trim()
const nextText = text.trim()
if (!nextLabel || !nextText) {
return
}
const current = $composerTerminalSelections.get()
if (current[nextLabel] === nextText) {
return
}
$composerTerminalSelections.set({
...current,
[nextLabel]: nextText
})
}
export function reconcileComposerTerminalSelections(draft: string) {
const current = $composerTerminalSelections.get()
const labels = new Set(terminalLabelsFromDraft(draft))
let changed = false
const next: Record<string, string> = {}
for (const [label, text] of Object.entries(current)) {
if (!labels.has(label)) {
changed = true
continue
}
next[label] = text
}
if (changed) {
$composerTerminalSelections.set(next)
}
}
export function terminalContextBlocksFromDraft(draft: string) {
const labels = terminalLabelsFromDraft(draft)
if (labels.length === 0) {
return []
}
const selections = $composerTerminalSelections.get()
return labels.flatMap(label => {
const text = selections[label]?.trim()
if (!text) {
return []
}
return `\`\`\`terminal\n${text}\n\`\`\``
})
}
export function clearComposerTerminalSelections() {
if (Object.keys($composerTerminalSelections.get()).length === 0) {
return
}
$composerTerminalSelections.set({})
}
function upsertAttachment(attachments: ComposerAttachment[], attachment: ComposerAttachment) {
const index = attachments.findIndex(item => item.id === attachment.id)
+38 -3
View File
@@ -1,16 +1,19 @@
import { atom, computed, type ReadableAtom } from 'nanostores'
import { arraysEqual, insertUniqueId, persistStringArray, storedStringArray } from '@/lib/storage'
import { arraysEqual, insertUniqueId, persistBoolean, persistStringArray, storedBoolean, storedStringArray } from '@/lib/storage'
import { $paneStates, ensurePaneRegistered, setPaneOpen, setPaneWidthOverride, togglePane } from './panes'
export const SIDEBAR_DEFAULT_WIDTH = 224
export const SIDEBAR_MAX_WIDTH = 320
export const SIDEBAR_DEFAULT_WIDTH = 237
export const SIDEBAR_MAX_WIDTH = 360
export const FILE_BROWSER_DEFAULT_WIDTH = '17rem'
export const FILE_BROWSER_MIN_WIDTH = '14rem'
export const FILE_BROWSER_MAX_WIDTH = '20rem'
export const SIDEBAR_SESSIONS_PAGE_SIZE = 50
const SIDEBAR_PINNED_STORAGE_KEY = 'hermes.desktop.pinnedSessions'
const SIDEBAR_AGENTS_GROUPED_STORAGE_KEY = 'hermes.desktop.agentsGroupedByWorkspace'
export const CHAT_SIDEBAR_PANE_ID = 'chat-sidebar'
export const FILE_BROWSER_PANE_ID = 'file-browser'
@@ -42,9 +45,12 @@ export const $sidebarWidth: ReadableAtom<number> = computed($paneStates, states
export const $pinnedSessionIds = atom(storedStringArray(SIDEBAR_PINNED_STORAGE_KEY))
export const $sidebarPinsOpen = atom(true)
export const $sidebarRecentsOpen = atom(true)
export const $sidebarAgentsGrouped = atom(storedBoolean(SIDEBAR_AGENTS_GROUPED_STORAGE_KEY, false))
export const $isSidebarResizing = atom(false)
export const $sessionsLimit = atom(SIDEBAR_SESSIONS_PAGE_SIZE)
$pinnedSessionIds.subscribe(ids => persistStringArray(SIDEBAR_PINNED_STORAGE_KEY, [...ids]))
$sidebarAgentsGrouped.subscribe(grouped => persistBoolean(SIDEBAR_AGENTS_GROUPED_STORAGE_KEY, grouped))
export function setSidebarWidth(width: number) {
const bounded = Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_DEFAULT_WIDTH, width))
@@ -75,6 +81,10 @@ export function setSidebarRecentsOpen(open: boolean) {
$sidebarRecentsOpen.set(open)
}
export function setSidebarAgentsGrouped(grouped: boolean) {
$sidebarAgentsGrouped.set(grouped)
}
export function setSidebarResizing(resizing: boolean) {
$isSidebarResizing.set(resizing)
}
@@ -96,3 +106,28 @@ export function unpinSession(sessionId: string) {
$pinnedSessionIds.set(next)
}
}
export function reorderPinnedSession(sessionId: string, targetIndex: number) {
const prev = $pinnedSessionIds.get()
if (!prev.includes(sessionId)) {
return
}
const next = insertUniqueId(prev, sessionId, targetIndex)
if (!arraysEqual(prev, next)) {
$pinnedSessionIds.set(next)
}
}
export function bumpSessionsLimit(step: number = SIDEBAR_SESSIONS_PAGE_SIZE) {
const safeStep = Math.max(1, Math.floor(step))
$sessionsLimit.set($sessionsLimit.get() + safeStep)
}
export function resetSessionsLimit() {
if ($sessionsLimit.get() !== SIDEBAR_SESSIONS_PAGE_SIZE) {
$sessionsLimit.set(SIDEBAR_SESSIONS_PAGE_SIZE)
}
}
+2
View File
@@ -19,6 +19,7 @@ function updateAtom<T>(store: AppAtom<T>, next: Updater<T>) {
export const $connection = atom<HermesConnection | null>(null)
export const $gatewayState = atom('idle')
export const $sessions = atom<SessionInfo[]>([])
export const $sessionsTotal = atom<number>(0)
export const $sessionsLoading = atom(true)
export const $workingSessionIds = atom<string[]>([])
export const $activeSessionId = atom<string | null>(null)
@@ -52,6 +53,7 @@ export const $modelPickerOpen = atom(false)
export const setConnection = (next: Updater<HermesConnection | null>) => updateAtom($connection, next)
export const setGatewayState = (next: Updater<string>) => updateAtom($gatewayState, next)
export const setSessions = (next: Updater<SessionInfo[]>) => updateAtom($sessions, next)
export const setSessionsTotal = (next: Updater<number>) => updateAtom($sessionsTotal, next)
export const setSessionsLoading = (next: Updater<boolean>) => updateAtom($sessionsLoading, next)
export const setWorkingSessionIds = (next: Updater<string[]>) => updateAtom($workingSessionIds, next)
export const setActiveSessionId = (next: Updater<string | null>) => updateAtom($activeSessionId, next)