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