feat: better icons and overlay panes

This commit is contained in:
Brooklyn Nicholson
2026-05-04 14:20:18 -05:00
parent ca8f2c7907
commit d1d0ed4016
71 changed files with 2043 additions and 363 deletions
@@ -2,13 +2,13 @@
import { type ToolCallMessagePartProps } from '@assistant-ui/react'
import { useStore } from '@nanostores/react'
import { HelpCircle, Loader2, PencilLine } from 'lucide-react'
import { type FormEvent, type KeyboardEvent, useCallback, useMemo, useRef, useState } from 'react'
import { ToolFallback } from '@/components/assistant-ui/tool-fallback'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { triggerHaptic } from '@/lib/haptics'
import { HelpCircle, Loader2, PencilLine } from '@/lib/icons'
import { cn } from '@/lib/utils'
import { $clarifyRequest, clearClarifyRequest } from '@/store/clarify'
import { $gateway } from '@/store/gateway'
@@ -213,7 +213,10 @@ function ClarifyToolPending({ args }: ToolCallMessagePartProps) {
}}
type="button"
>
<span aria-hidden className="grid size-5 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground">
<span
aria-hidden
className="grid size-5 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground"
>
<PencilLine className="size-3" />
</span>
<span className="flex-1">Other (type your answer)</span>
@@ -2,12 +2,12 @@
import type { Unstable_DirectiveFormatter, Unstable_DirectiveSegment, Unstable_TriggerItem } from '@assistant-ui/core'
import type { TextMessagePartComponent, TextMessagePartProps } from '@assistant-ui/react'
import { AtSign, FileText, FolderOpen, ImageIcon, Link as LinkIcon, Wrench } from 'lucide-react'
import type { ComponentType, FC } from 'react'
import { Fragment, useMemo } from 'react'
import { ZoomableImage } from '@/components/assistant-ui/zoomable-image'
import { extractEmbeddedImages } from '@/lib/embedded-images'
import { AtSign, FileText, FolderOpen, ImageIcon, Link as LinkIcon, Wrench } from '@/lib/icons'
import { cn } from '@/lib/utils'
const HERMES_REF_TYPES = ['file', 'folder', 'url', 'image', 'tool'] as const
@@ -1,4 +1,4 @@
import { type FC, useCallback, useEffect, useState } from 'react'
import { type FC, useCallback, useState } from 'react'
import introCopyJsonl from './intro-copy.jsonl?raw'
@@ -19,6 +19,9 @@ export type IntroProps = {
const NEUTRAL_PERSONALITIES = new Set(['', 'default', 'none', 'neutral'])
const HERMES_FRAME_COUNT = 8
// Optical centering offsets tuned per frame so Hermes' body stays centered
// even when the staff extends farther left/right in certain poses.
const HERMES_DEFAULT_FRAME_OPTICAL_OFFSET_PX = [8, 4, 4, 0, 9, 2, 5, 9] as const
const ASSET_BASE_URL = import.meta.env.BASE_URL || '/'
const FALLBACK_COPY: IntroCopy[] = [
@@ -165,17 +168,12 @@ export const Intro: FC<IntroProps> = ({ personality, seed }) => {
const introSeed = mountSeed + (seed ?? 0)
const copy = resolveCopy(personality, introSeed)
const frameIndex = Math.abs(introSeed + frameOffset) % HERMES_FRAME_COUNT
const spriteOffsetPx = HERMES_DEFAULT_FRAME_OPTICAL_OFFSET_PX[frameIndex] ?? 0
const advanceFrame = useCallback(() => {
setFrameOffset(offset => offset + 1 + Math.floor(Math.random() * (HERMES_FRAME_COUNT - 1)))
}, [])
useEffect(() => {
const id = window.setTimeout(advanceFrame, 7000)
return () => window.clearTimeout(id)
}, [advanceFrame, frameOffset])
return (
<div className="pointer-events-none flex min-h-[calc(100vh-var(--titlebar-height)-var(--thread-composer-clearance)-var(--composer-shell-pad-block-end))] flex-col items-center justify-center px-[calc(var(--vsq)*50)] text-center text-muted-foreground">
<button
@@ -187,9 +185,10 @@ export const Intro: FC<IntroProps> = ({ personality, seed }) => {
<img
alt=""
aria-hidden="true"
className="h-full w-full scale-110 object-contain select-none"
className="h-full w-full object-contain select-none"
draggable={false}
src={publicAssetPath(`hermes-frames/hermes-frame-${frameIndex}.png?v=matte-clean-6`)}
style={{ transform: `translateX(${spriteOffsetPx}px) scale(1.1)` }}
/>
</button>
<p className="mb-3 text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground/75">Hermes Agent</p>
@@ -22,6 +22,7 @@ describe('preprocessMarkdown', () => {
it('demotes invalid fenced prose blocks with closers', () => {
const fence = '```'
const input = [
`${fence} http://localhost:8812/`,
'- **Scroll wheel** - zoom',
@@ -48,7 +49,9 @@ describe('preprocessMarkdown', () => {
})
it('demotes prose sentence masquerading as fence info', () => {
const input = ['```Heads up - a bunny got added', '- Pure white (`#ffffff`)', '- Ambient dropped to 0.18'].join('\n')
const input = ['```Heads up - a bunny got added', '- Pure white (`#ffffff`)', '- Ambient dropped to 0.18'].join(
'\n'
)
const output = preprocessMarkdown(input)
expect(output).not.toContain('```heads')
@@ -2,13 +2,14 @@
import { type StreamdownTextComponents, StreamdownTextPrimitive } from '@assistant-ui/react-streamdown'
import { code } from '@streamdown/code'
import { Check, Copy } from 'lucide-react'
import { type ComponentProps, memo, useEffect, useMemo, useState } from 'react'
import { PreviewAttachment } from '@/components/assistant-ui/preview-attachment'
import { SyntaxHighlighter } from '@/components/assistant-ui/shiki-highlighter'
import { ZoomableImage } from '@/components/assistant-ui/zoomable-image'
import { triggerHaptic } from '@/lib/haptics'
import { Check, Copy } from '@/lib/icons'
import { isLikelyProseCodeBlock, isLikelyProseFence, sanitizeLanguageTag } from '@/lib/markdown-code'
import {
filePathFromMediaPath,
mediaExternalUrl,
@@ -17,7 +18,6 @@ import {
mediaName,
mediaPathFromMarkdownHref
} from '@/lib/media'
import { isLikelyProseCodeBlock, isLikelyProseFence, sanitizeLanguageTag } from '@/lib/markdown-code'
import { isLikelyPreviewCandidate, previewTargetFromMarkdownHref, stripPreviewTargets } from '@/lib/preview-targets'
import { cn } from '@/lib/utils'
@@ -98,6 +98,7 @@ function normalizeFenceBlocks(text: string): string {
if (!match) {
out.push(line)
index += 1
continue
}
@@ -111,6 +112,7 @@ function normalizeFenceBlocks(text: string): string {
if (!openerValid) {
out.push(`${indent}${infoRaw}`.trimEnd())
index += 1
continue
}
@@ -122,6 +124,7 @@ function normalizeFenceBlocks(text: string): string {
// Empty fenced block: drop both delimiters. This prevents Streamdown's
// code plugin from rendering an empty shell/card.
index = closeIndex + 1
continue
}
@@ -130,12 +133,14 @@ function normalizeFenceBlocks(text: string): string {
// already renders a preview card for that URL, so don't show code fences.
out.push(...bodyLines)
index = closeIndex + 1
continue
}
if (closeIndex === -1) {
if (!body.trim()) {
index += 1
continue
}
@@ -152,6 +157,7 @@ function normalizeFenceBlocks(text: string): string {
if (isLikelyProseFence(infoRaw, body)) {
pushProseFence(out, indent, infoRaw, bodyLines)
index = closeIndex + 1
continue
}
@@ -1,7 +1,7 @@
import { useStore } from '@nanostores/react'
import { MonitorPlay } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { MonitorPlay } from '@/lib/icons'
import { previewName } from '@/lib/preview-targets'
import { notifyError } from '@/store/notifications'
import { $previewTarget, setPreviewTarget } from '@/store/preview'
@@ -79,13 +79,15 @@ export function PreviewAttachment({ target }: { target: string }) {
setOpening(true)
try {
const preview = await window.hermesDesktop?.normalizePreviewTarget(requestTarget, requestCwd || undefined).catch(error => {
if (isMissingPreviewIpc(error)) {
return localFallbackPreview(requestTarget)
}
const preview = await window.hermesDesktop
?.normalizePreviewTarget(requestTarget, requestCwd || undefined)
.catch(error => {
if (isMissingPreviewIpc(error)) {
return localFallbackPreview(requestTarget)
}
throw error
})
throw error
})
if (
!mountedRef.current ||
@@ -11,28 +11,7 @@ import {
useAuiState
} from '@assistant-ui/react'
import { useStore } from '@nanostores/react'
import {
CheckIcon,
ChevronLeftIcon,
ChevronRightIcon,
CopyIcon,
GitBranchIcon,
Loader2Icon,
MoreHorizontalIcon,
PencilIcon,
RefreshCwIcon,
Volume2Icon,
VolumeXIcon,
XIcon
} from 'lucide-react'
import {
type FC,
type ReactNode,
useCallback,
useEffect,
useRef,
useState
} from 'react'
import { type FC, type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
import spinners from 'unicode-animations'
// Scroll behavior: delegated to `use-stick-to-bottom` (StackBlitz), the
// reference implementation that powers bolt.new and several other streaming
@@ -66,6 +45,20 @@ import {
} from '@/components/ui/dropdown-menu'
import { Loader } from '@/components/ui/loader'
import { triggerHaptic } from '@/lib/haptics'
import {
CheckIcon,
ChevronLeftIcon,
ChevronRightIcon,
CopyIcon,
GitBranchIcon,
Loader2Icon,
MoreHorizontalIcon,
PencilIcon,
RefreshCwIcon,
Volume2Icon,
VolumeXIcon,
XIcon
} from '@/lib/icons'
import { extractPreviewTargets } from '@/lib/preview-targets'
import { cn } from '@/lib/utils'
import { playSpeechText, stopVoicePlayback } from '@/lib/voice-playback'
@@ -227,20 +220,23 @@ const ThreadScrollSync: FC<{ sessionKey?: string | null }> = ({ sessionKey }) =>
// Slam to bottom + arm the ref. Also forces library state flags off
// so its internal resize handler doesn't fight our re-pins.
const armAndPin = useCallback((behavior: ScrollBehavior) => {
const el = scrollRef.current
const armAndPin = useCallback(
(behavior: ScrollBehavior) => {
const el = scrollRef.current
if (!el) {
return
}
if (!el) {
return
}
armedRef.current = behavior
// Clear the library's escape/at-bottom flags directly on the mutable
// state object so its resize handler sees a clean follow state.
state.escapedFromLock = false
state.isAtBottom = true
el.scrollTop = el.scrollHeight
}, [scrollRef, state])
armedRef.current = behavior
// Clear the library's escape/at-bottom flags directly on the mutable
// state object so its resize handler sees a clean follow state.
state.escapedFromLock = false
state.isAtBottom = true
el.scrollTop = el.scrollHeight
},
[scrollRef, state]
)
// ResizeObserver loop — re-pins to bottom while armed, disarms when
// actually at bottom. This is the assistant-ui pattern.
@@ -293,6 +289,7 @@ const ThreadScrollSync: FC<{ sessionKey?: string | null }> = ({ sessionKey }) =>
armedRef.current = null
}
}
const onTouch = () => {
armedRef.current = null
}
@@ -368,7 +365,9 @@ const ComposerClearance: FC = () => {
const composer = document.querySelector<HTMLElement>('[data-slot="composer-root"]')
return composer ? composer.getBoundingClientRect().height + COMPOSER_BREATHING_ROOM_PX : DEFAULT_COMPOSER_CLEARANCE_PX
return composer
? composer.getBoundingClientRect().height + COMPOSER_BREATHING_ROOM_PX
: DEFAULT_COMPOSER_CLEARANCE_PX
})
useEffect(() => {
@@ -458,7 +457,10 @@ const AssistantMessage: FC<{ onBranchInNewChat?: (messageId: string) => void }>
data-role="assistant"
data-slot="aui_assistant-message-root"
>
<div className="wrap-anywhere min-w-0 max-w-full overflow-hidden text-pretty text-foreground" data-slot="aui_assistant-message-content">
<div
className="wrap-anywhere min-w-0 max-w-full overflow-hidden text-pretty text-foreground"
data-slot="aui_assistant-message-content"
>
<MessagePrimitive.Parts
components={{
Text: MarkdownText,
@@ -2,11 +2,11 @@
import { type ToolCallMessagePartProps } from '@assistant-ui/react'
import { useStore } from '@nanostores/react'
import { ChevronRight } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useElapsedSeconds } from '@/components/assistant-ui/activity-timer'
import { ActivityTimerText } from '@/components/assistant-ui/activity-timer-text'
import { ChevronRight } from '@/lib/icons'
import { cn } from '@/lib/utils'
import { $toolInlineDiffs } from '@/store/tool-diffs'
@@ -89,7 +89,11 @@ function stripAnsi(value: string): string {
}
function stripInlineDiffChrome(value: string): string {
return value ? stripAnsi(value).replace(/^\s*┊\s*review diff\s*\n/i, '').trim() : ''
return value
? stripAnsi(value)
.replace(/^\s*┊\s*review diff\s*\n/i, '')
.trim()
: ''
}
function inlineDiffFromResult(result: unknown): string {
@@ -1,9 +1,9 @@
'use client'
import { Download } from 'lucide-react'
import { type ComponentProps, useState } from 'react'
import { Dialog, DialogContent } from '@/components/ui/dialog'
import { Download } from '@/lib/icons'
import { cn } from '@/lib/utils'
import { notify, notifyError } from '@/store/notifications'
@@ -1,9 +1,9 @@
import { useStore } from '@nanostores/react'
import { AlertCircle, AlertTriangle, CheckCircle2, Copy, Info, type LucideIcon, X } from 'lucide-react'
import { type ReactNode, useEffect, useRef, useState } from 'react'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { triggerHaptic } from '@/lib/haptics'
import { AlertCircle, AlertTriangle, CheckCircle2, Copy, Info, type LucideIcon, X } from '@/lib/icons'
import { cn } from '@/lib/utils'
import {
$notifications,
@@ -57,13 +57,7 @@ export const SessionInspector: FC<SessionInspectorProps> = ({
data-open={open}
>
<div className="flex min-h-0 flex-1 flex-col gap-2.5 overflow-y-auto overscroll-contain pl-1.5 pr-1 text-xs">
<ProjectSection
branch={branch}
busy={busy}
cwd={cwd}
onBrowseCwd={onBrowseCwd}
onChangeCwd={onChangeCwd}
/>
<ProjectSection branch={branch} busy={busy} cwd={cwd} onBrowseCwd={onBrowseCwd} onChangeCwd={onChangeCwd} />
<AgentSection
fastMode={fastMode}
modelLabel={modelLabel}
+1 -1
View File
@@ -1,7 +1,7 @@
import { CheckIcon } from 'lucide-react'
import { Checkbox as CheckboxPrimitive } from 'radix-ui'
import * as React from 'react'
import { CheckIcon } from '@/lib/icons'
import { cn } from '@/lib/utils'
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
+1 -1
View File
@@ -1,7 +1,7 @@
import { Command as CommandPrimitive } from 'cmdk'
import { SearchIcon } from 'lucide-react'
import * as React from 'react'
import { SearchIcon } from '@/lib/icons'
import { cn } from '@/lib/utils'
function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
+1 -1
View File
@@ -1,7 +1,7 @@
import { XIcon } from 'lucide-react'
import { Dialog as DialogPrimitive } from 'radix-ui'
import * as React from 'react'
import { XIcon } from '@/lib/icons'
import { cn } from '@/lib/utils'
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
@@ -1,7 +1,7 @@
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui'
import * as React from 'react'
import { CheckIcon, ChevronRightIcon, CircleIcon } from '@/lib/icons'
import { cn } from '@/lib/utils'
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
@@ -1,6 +1,6 @@
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react'
import * as React from 'react'
import { ChevronLeft, ChevronRight, MoreHorizontal } from '@/lib/icons'
import { cn } from '@/lib/utils'
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
@@ -15,7 +15,9 @@ function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
}
function PaginationContent({ className, ...props }: React.ComponentProps<'ul'>) {
return <ul className={cn('flex h-5 flex-row items-center gap-0.5', className)} data-slot="pagination-content" {...props} />
return (
<ul className={cn('flex h-5 flex-row items-center gap-0.5', className)} data-slot="pagination-content" {...props} />
)
}
function PaginationItem({ className, ...props }: React.ComponentProps<'li'>) {
+1 -1
View File
@@ -1,7 +1,7 @@
import { CheckIcon, ChevronDownIcon } from 'lucide-react'
import { Select as SelectPrimitive } from 'radix-ui'
import * as React from 'react'
import { CheckIcon, ChevronDownIcon } from '@/lib/icons'
import { cn } from '@/lib/utils'
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
+1 -1
View File
@@ -1,9 +1,9 @@
'use client'
import { XIcon } from 'lucide-react'
import { Dialog as SheetPrimitive } from 'radix-ui'
import * as React from 'react'
import { XIcon } from '@/lib/icons'
import { cn } from '@/lib/utils'
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
+1 -1
View File
@@ -1,7 +1,6 @@
'use client'
import { cva, type VariantProps } from 'class-variance-authority'
import { PanelLeftIcon } from 'lucide-react'
import { Slot } from 'radix-ui'
import * as React from 'react'
@@ -12,6 +11,7 @@ import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '
import { Skeleton } from '@/components/ui/skeleton'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { useIsMobile } from '@/hooks/use-mobile'
import { PanelLeftIcon } from '@/lib/icons'
import { cn } from '@/lib/utils'
const SIDEBAR_COOKIE_NAME = 'sidebar_state'