feat: more ui qa
This commit is contained in:
@@ -36,12 +36,7 @@ function statusGlyph(status: SubagentStatus): ReactNode {
|
||||
return <AlertCircle aria-label="Failed" className="size-3.5 shrink-0 text-destructive" />
|
||||
}
|
||||
|
||||
return (
|
||||
<CheckCircle2
|
||||
aria-label="Done"
|
||||
className="size-3.5 shrink-0 text-emerald-600/85 dark:text-emerald-400/85"
|
||||
/>
|
||||
)
|
||||
return <CheckCircle2 aria-label="Done" className="size-3.5 shrink-0 text-emerald-600/85 dark:text-emerald-400/85" />
|
||||
}
|
||||
|
||||
const STREAM_TONE: Record<SubagentStreamEntry['kind'], string> = {
|
||||
@@ -65,7 +60,11 @@ function streamGlyph(entry: SubagentStreamEntry): ReactNode {
|
||||
}
|
||||
|
||||
if (entry.kind === 'thinking') {
|
||||
return <span aria-hidden className="font-mono text-[0.7rem] leading-none text-muted-foreground/70">…</span>
|
||||
return (
|
||||
<span aria-hidden className="font-mono text-[0.7rem] leading-none text-muted-foreground/70">
|
||||
…
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return <span aria-hidden className="mt-0.5 size-1 shrink-0 rounded-full bg-muted-foreground/55" />
|
||||
@@ -103,8 +102,13 @@ export function AgentsView({ onClose }: AgentsViewProps) {
|
||||
}
|
||||
|
||||
const fmtDuration = (seconds?: number) => {
|
||||
if (!seconds || seconds <= 0) return ''
|
||||
if (seconds < 60) return `${seconds.toFixed(1)}s`
|
||||
if (!seconds || seconds <= 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (seconds < 60) {
|
||||
return `${seconds.toFixed(1)}s`
|
||||
}
|
||||
|
||||
const m = Math.floor(seconds / 60)
|
||||
const s = Math.round(seconds % 60)
|
||||
@@ -113,18 +117,29 @@ const fmtDuration = (seconds?: number) => {
|
||||
}
|
||||
|
||||
const fmtTokens = (value?: number) => {
|
||||
if (!value) return ''
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return value >= 1000 ? `${(value / 1000).toFixed(1)}k tok` : `${value} tok`
|
||||
}
|
||||
|
||||
const fmtAge = (updatedAt: number, nowMs: number) => {
|
||||
const s = Math.max(0, Math.round((nowMs - updatedAt) / 1000))
|
||||
if (s < 2) return 'now'
|
||||
if (s < 60) return `${s}s ago`
|
||||
|
||||
if (s < 2) {
|
||||
return 'now'
|
||||
}
|
||||
|
||||
if (s < 60) {
|
||||
return `${s}s ago`
|
||||
}
|
||||
|
||||
const m = Math.floor(s / 60)
|
||||
if (m < 60) return `${m}m ago`
|
||||
|
||||
if (m < 60) {
|
||||
return `${m}m ago`
|
||||
}
|
||||
|
||||
return `${Math.floor(m / 60)}h ago`
|
||||
}
|
||||
@@ -152,12 +167,14 @@ function groupDelegations(roots: readonly SubagentNode[]): RootGroup[] {
|
||||
|
||||
if (prev && sameShape && closeInTime && uniqueStep) {
|
||||
prev.nodes.push(node)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (node.taskCount > 1) {
|
||||
n += 1
|
||||
groups.push({ id: `delegation-${n}`, label: `Delegation ${n}`, nodes: [node], taskCount: node.taskCount })
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -180,7 +197,9 @@ function SubagentTree({ tree }: { tree: SubagentNode[] }) {
|
||||
const cost = flat.reduce((sum, n) => sum + (n.costUsd ?? 0), 0)
|
||||
|
||||
useEffect(() => {
|
||||
if (active <= 0 || typeof window === 'undefined') return
|
||||
if (active <= 0 || typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const id = window.setInterval(() => setNowMs(Date.now()), 500)
|
||||
|
||||
@@ -261,10 +280,7 @@ function StreamLine({
|
||||
const tone = entry.isError ? 'text-destructive' : STREAM_TONE[entry.kind]
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex min-w-0 items-baseline gap-2 text-[0.72rem] leading-relaxed"
|
||||
ref={enterRef}
|
||||
>
|
||||
<div className="flex min-w-0 items-baseline gap-2 text-[0.72rem] leading-relaxed" ref={enterRef}>
|
||||
<span className="flex h-[0.95rem] shrink-0 items-center">{streamGlyph(entry)}</span>
|
||||
<span className={cn('min-w-0 flex-1 wrap-anywhere', tone, isMono && 'font-mono text-[0.69rem]')}>
|
||||
{entry.text}
|
||||
@@ -283,13 +299,17 @@ function StreamLine({
|
||||
function SubagentRow({ node, depth = 0, nowMs }: { node: SubagentNode; depth?: number; nowMs: number }) {
|
||||
const running = node.status === 'running' || node.status === 'queued'
|
||||
const elapsed = useElapsedSeconds(running, `subagent:${node.id}`)
|
||||
|
||||
const durationSeconds =
|
||||
typeof node.durationSeconds === 'number' ? Math.max(0, Math.round(node.durationSeconds)) : elapsed
|
||||
|
||||
const [open, setOpen] = useState(() => running || depth < 2)
|
||||
const enterRef = useEnterAnimation(true, `subagent-row:${node.id}`)
|
||||
|
||||
useEffect(() => {
|
||||
if (running) setOpen(true)
|
||||
if (running) {
|
||||
setOpen(true)
|
||||
}
|
||||
}, [running])
|
||||
|
||||
const visibleRows = open ? node.stream.slice(-10) : node.stream.slice(-2)
|
||||
@@ -304,11 +324,7 @@ function SubagentRow({ node, depth = 0, nowMs }: { node: SubagentNode; depth?: n
|
||||
].filter(Boolean)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('grid min-w-0 max-w-full gap-2', depth > 0 && 'pl-4')}
|
||||
data-slot="tool-block"
|
||||
ref={enterRef}
|
||||
>
|
||||
<div className={cn('grid min-w-0 max-w-full gap-2', depth > 0 && 'pl-4')} data-slot="tool-block" ref={enterRef}>
|
||||
<button
|
||||
aria-expanded={open}
|
||||
className="group flex w-full min-w-0 items-start gap-2.5 text-left"
|
||||
@@ -374,4 +390,3 @@ function SubagentRow({ node, depth = 0, nowMs }: { node: SubagentNode; depth?: n
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { ZoomableImage } from '@/components/chat/zoomable-image'
|
||||
import { PageLoader } from '@/components/page-loader'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { CopyButton } from '@/components/ui/copy-button'
|
||||
import { TextTab, TextTabMeta } from '@/components/ui/text-tab'
|
||||
import {
|
||||
Pagination,
|
||||
PaginationButton,
|
||||
@@ -18,8 +20,7 @@ import {
|
||||
import { getSessionMessages, listSessions } from '@/hermes'
|
||||
import { sessionTitle } from '@/lib/chat-runtime'
|
||||
import { ExternalLink, ExternalLinkIcon, hostPathLabel, urlSlugTitleLabel, useLinkTitle } from '@/lib/external-link'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { FileImage, FileText, FolderOpen, Layers3, Link2 } from '@/lib/icons'
|
||||
import { FileImage, FileText, FolderOpen, Link2 } from '@/lib/icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { notifyError } from '@/store/notifications'
|
||||
import type { SessionInfo, SessionMessage } from '@/types/hermes'
|
||||
@@ -364,10 +365,7 @@ interface ArtifactsViewProps extends React.ComponentProps<'section'> {
|
||||
setStatusbarItemGroup?: SetStatusbarItemGroup
|
||||
}
|
||||
|
||||
export function ArtifactsView({
|
||||
setStatusbarItemGroup: _setStatusbarItemGroup,
|
||||
...props
|
||||
}: ArtifactsViewProps) {
|
||||
export function ArtifactsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...props }: ArtifactsViewProps) {
|
||||
const navigate = useNavigate()
|
||||
const [artifacts, setArtifacts] = useState<ArtifactRecord[] | null>(null)
|
||||
const [query, setQuery] = useState('')
|
||||
@@ -506,39 +504,26 @@ export function ArtifactsView({
|
||||
{...props}
|
||||
filters={
|
||||
<>
|
||||
<FilterButton
|
||||
active={kindFilter === 'all'}
|
||||
icon={Layers3}
|
||||
label={`All (${counts.all})`}
|
||||
onClick={() => setKindFilter('all')}
|
||||
/>
|
||||
<FilterButton
|
||||
active={kindFilter === 'image'}
|
||||
icon={FileImage}
|
||||
label={`Images (${counts.image})`}
|
||||
onClick={() => setKindFilter('image')}
|
||||
/>
|
||||
<FilterButton
|
||||
active={kindFilter === 'file'}
|
||||
icon={FileText}
|
||||
label={`Files (${counts.file})`}
|
||||
onClick={() => setKindFilter('file')}
|
||||
/>
|
||||
<FilterButton
|
||||
active={kindFilter === 'link'}
|
||||
icon={Link2}
|
||||
label={`Links (${counts.link})`}
|
||||
onClick={() => setKindFilter('link')}
|
||||
/>
|
||||
<TextTab active={kindFilter === 'all'} onClick={() => setKindFilter('all')}>
|
||||
All <TextTabMeta>({counts.all})</TextTabMeta>
|
||||
</TextTab>
|
||||
<TextTab active={kindFilter === 'image'} onClick={() => setKindFilter('image')}>
|
||||
Images <TextTabMeta>({counts.image})</TextTabMeta>
|
||||
</TextTab>
|
||||
<TextTab active={kindFilter === 'file'} onClick={() => setKindFilter('file')}>
|
||||
Files <TextTabMeta>({counts.file})</TextTabMeta>
|
||||
</TextTab>
|
||||
<TextTab active={kindFilter === 'link'} onClick={() => setKindFilter('link')}>
|
||||
Links <TextTabMeta>({counts.link})</TextTabMeta>
|
||||
</TextTab>
|
||||
</>
|
||||
}
|
||||
onSearchChange={setQuery}
|
||||
searchPlaceholder="Search artifacts..."
|
||||
searchValue={query}
|
||||
searchTrailingAction={
|
||||
<Button
|
||||
aria-label={refreshing ? 'Refreshing artifacts' : 'Refresh artifacts'}
|
||||
className="text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground"
|
||||
className="text-(--ui-text-tertiary) hover:bg-transparent hover:text-foreground"
|
||||
disabled={refreshing}
|
||||
onClick={() => void refreshArtifacts()}
|
||||
size="icon-xs"
|
||||
@@ -549,6 +534,7 @@ export function ArtifactsView({
|
||||
<Codicon name="refresh" size="0.875rem" spinning={refreshing} />
|
||||
</Button>
|
||||
}
|
||||
searchValue={query}
|
||||
>
|
||||
{!artifacts ? (
|
||||
<PageLoader label="Indexing recent session artifacts" />
|
||||
@@ -602,7 +588,7 @@ export function ArtifactsView({
|
||||
total={visibleFileArtifacts.length}
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-x-auto rounded-lg border border-(--ui-stroke-tertiary) bg-(--glass-chat-bubble-background) shadow-sm">
|
||||
<div className="overflow-x-auto rounded-lg border border-(--ui-stroke-tertiary) bg-(--ui-chat-bubble-background) shadow-sm">
|
||||
<ArtifactTable artifacts={pagedFileArtifacts} ctx={cellCtx} filter={kindFilter} />
|
||||
</div>
|
||||
</section>
|
||||
@@ -665,34 +651,6 @@ function ArtifactsPagination({ className, itemLabel, onPageChange, page, pageSiz
|
||||
)
|
||||
}
|
||||
|
||||
function FilterButton({
|
||||
active,
|
||||
icon: Icon,
|
||||
label,
|
||||
onClick
|
||||
}: {
|
||||
active: boolean
|
||||
icon: typeof Layers3
|
||||
label: string
|
||||
onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
'h-7 gap-1.5 rounded-md px-2 text-[length:var(--conversation-caption-font-size)]',
|
||||
active ? 'bg-(--ui-bg-tertiary) text-foreground' : 'text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground'
|
||||
)}
|
||||
onClick={onClick}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
interface ArtifactImageCardProps {
|
||||
artifact: ArtifactRecord
|
||||
failedImage: boolean
|
||||
@@ -704,7 +662,7 @@ function ArtifactImageCard({ artifact, failedImage, onImageError, onOpenChat }:
|
||||
return (
|
||||
<article
|
||||
className={cn(
|
||||
'group/artifact overflow-hidden rounded-lg border border-(--ui-stroke-tertiary) bg-(--glass-chat-bubble-background) shadow-sm'
|
||||
'group/artifact overflow-hidden rounded-lg border border-(--ui-stroke-tertiary) bg-(--ui-chat-bubble-background) shadow-sm'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
@@ -733,7 +691,9 @@ function ArtifactImageCard({ artifact, failedImage, onImageError, onOpenChat }:
|
||||
<FileImage className="size-3" />
|
||||
{artifact.kind}
|
||||
</div>
|
||||
<div className="truncate text-[length:var(--conversation-caption-font-size)] font-medium">{artifact.label}</div>
|
||||
<div className="truncate text-[length:var(--conversation-caption-font-size)] font-medium">
|
||||
{artifact.label}
|
||||
</div>
|
||||
<div className="mt-0.5 truncate text-[0.625rem] text-(--ui-text-tertiary)">{artifact.value}</div>
|
||||
</div>
|
||||
|
||||
@@ -769,7 +729,7 @@ function ArtifactCellAction({
|
||||
if (href) {
|
||||
return (
|
||||
<ExternalLink
|
||||
className="flex h-full w-full min-w-0 items-center gap-2 px-2.5 py-1.5 text-left text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) font-normal text-(--ui-text-secondary) no-underline transition-colors hover:text-foreground hover:underline"
|
||||
className="flex h-full w-full min-w-0 items-center gap-2 px-2.5 py-1.5 text-left text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) font-normal text-(--ui-text-secondary) no-underline underline-offset-4 decoration-current/20 transition-colors hover:text-foreground hover:underline"
|
||||
href={href}
|
||||
showExternalIcon={false}
|
||||
title={title}
|
||||
@@ -782,7 +742,7 @@ function ArtifactCellAction({
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'flex h-full w-full min-w-0 items-center gap-2 px-2.5 py-1.5 text-left text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) font-normal text-(--ui-text-secondary) no-underline transition-colors hover:text-foreground hover:underline',
|
||||
'flex h-full w-full min-w-0 items-center gap-2 px-2.5 py-1.5 text-left text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) font-normal text-(--ui-text-secondary) no-underline underline-offset-4 decoration-current/20 transition-colors hover:text-foreground hover:underline',
|
||||
'cursor-pointer'
|
||||
)}
|
||||
onClick={onClick}
|
||||
@@ -825,7 +785,10 @@ function LocationCell({ artifact }: { artifact: ArtifactRecord; ctx: CellCtx })
|
||||
return (
|
||||
<div className="group/location flex min-w-0 items-center gap-1.5">
|
||||
<div
|
||||
className={cn('min-w-0 flex-1 truncate text-[length:var(--conversation-caption-font-size)] text-(--ui-text-tertiary)', isLink ? 'font-normal' : 'font-mono')}
|
||||
className={cn(
|
||||
'min-w-0 flex-1 truncate text-[length:var(--conversation-caption-font-size)] text-(--ui-text-tertiary)',
|
||||
isLink ? 'font-normal' : 'font-mono'
|
||||
)}
|
||||
title={artifact.value}
|
||||
>
|
||||
{value}
|
||||
@@ -902,7 +865,7 @@ function ArtifactTable({
|
||||
</thead>
|
||||
<tbody className="divide-y divide-(--ui-stroke-quaternary)">
|
||||
{artifacts.map(artifact => (
|
||||
<tr className="group/artifact transition-colors hover:bg-(--chrome-action-hover)" key={artifact.id}>
|
||||
<tr className="group/artifact" key={artifact.id}>
|
||||
{ARTIFACT_COLUMNS.map(col => {
|
||||
const Cell = col.Cell
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Clipboard, FileText, FolderOpen, ImageIcon, Link, type IconComponent, MessageSquareText } from '@/lib/icons'
|
||||
import { Clipboard, FileText, FolderOpen, type IconComponent, ImageIcon, Link, MessageSquareText } from '@/lib/icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { GHOST_ICON_BTN } from './controls'
|
||||
@@ -39,7 +39,10 @@ export function ContextMenu({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-label={state.tools.label}
|
||||
className={cn(GHOST_ICON_BTN, 'data-[state=open]:bg-(--chrome-action-hover) data-[state=open]:text-foreground')}
|
||||
className={cn(
|
||||
GHOST_ICON_BTN,
|
||||
'data-[state=open]:bg-(--chrome-action-hover) data-[state=open]:text-foreground'
|
||||
)}
|
||||
disabled={!state.tools.enabled}
|
||||
size="icon"
|
||||
title={state.tools.label}
|
||||
|
||||
@@ -93,7 +93,7 @@ export function ComposerControls({
|
||||
<span className="block size-3 rounded-[0.1875rem] bg-current" />
|
||||
)
|
||||
) : (
|
||||
<Codicon name="arrow-up" size="1rem" />
|
||||
<Codicon name="arrow-up" size="1rem" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -21,7 +21,9 @@ const entryPreview = (entry: QueuedPromptEntry) =>
|
||||
export function QueuePanel({ busy, editingId, entries, onDelete, onEdit, onSendNow }: QueuePanelProps) {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
if (entries.length === 0) return null
|
||||
if (entries.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-border/65 bg-[color-mix(in_srgb,var(--dt-card)_70%,transparent)] py-0.5 shadow-[0_0_0_1px_color-mix(in_srgb,var(--dt-card)_30%,transparent)_inset]">
|
||||
|
||||
@@ -89,10 +89,7 @@ export function ComposerTriggerPopover({
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
COMPLETION_DRAWER_ROW_CLASS,
|
||||
index === activeIndex && 'bg-(--ui-bg-tertiary)'
|
||||
)}
|
||||
className={cn(COMPLETION_DRAWER_ROW_CLASS, index === activeIndex && 'bg-(--ui-bg-tertiary)')}
|
||||
data-highlighted={index === activeIndex ? '' : undefined}
|
||||
key={item.id}
|
||||
onClick={() => onPick(item)}
|
||||
@@ -102,9 +99,7 @@ export function ComposerTriggerPopover({
|
||||
<span className="grid size-3.5 shrink-0 place-items-center text-(--ui-text-tertiary)">
|
||||
<Codicon name={completionIcon(kind, item)} size="0.875rem" />
|
||||
</span>
|
||||
<span className="min-w-0 shrink truncate font-mono font-medium leading-5 text-foreground">
|
||||
{display}
|
||||
</span>
|
||||
<span className="min-w-0 shrink truncate font-mono font-medium leading-5 text-foreground">{display}</span>
|
||||
{description && (
|
||||
<span className="min-w-0 flex-1 truncate leading-5 text-(--ui-text-tertiary)">{description}</span>
|
||||
)}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { requestComposerFocus, requestComposerInsert } from '@/app/chat/composer/focus'
|
||||
import { formatRefValue } from '@/components/assistant-ui/directive-text'
|
||||
import { attachmentId, contextPath, pathLabel } from '@/lib/chat-runtime'
|
||||
import { requestComposerFocus, requestComposerInsert } from '@/app/chat/composer/focus'
|
||||
import {
|
||||
addComposerAttachment,
|
||||
setComposerTerminalSelection,
|
||||
type ComposerAttachment,
|
||||
removeComposerAttachment
|
||||
removeComposerAttachment,
|
||||
setComposerTerminalSelection
|
||||
} from '@/store/composer'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
|
||||
|
||||
@@ -11,12 +11,13 @@ import { Suspense, useMemo, useRef } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import { Thread } from '@/components/assistant-ui/thread'
|
||||
import { Backdrop } from '@/components/Backdrop'
|
||||
import { NotificationStack } from '@/components/notifications'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { getGlobalModelOptions, type HermesGateway } from '@/hermes'
|
||||
import type { ChatMessage } from '@/lib/chat-messages'
|
||||
import { quickModelOptions, sessionTitle, toRuntimeMessage } from '@/lib/chat-runtime'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { useIncrementalExternalStoreRuntime } from '@/lib/incremental-external-store-runtime'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { ComposerAttachment } from '@/store/composer'
|
||||
@@ -108,7 +109,7 @@ function ChatHeader({
|
||||
title={title}
|
||||
>
|
||||
<Button
|
||||
className="pointer-events-auto h-6 min-w-0 gap-1 rounded-md border border-transparent bg-(--ui-bg-quinary) px-2 py-0 text-(--ui-text-secondary) hover:border-(--ui-stroke-tertiary) hover:bg-(--ui-bg-tertiary) hover:text-foreground data-[state=open]:border-(--ui-stroke-tertiary) data-[state=open]:bg-(--ui-bg-tertiary) [-webkit-app-region:no-drag]"
|
||||
className="pointer-events-auto h-6 min-w-0 gap-1 rounded-md border border-transparent bg-transparent px-2 py-0 text-(--ui-text-secondary) hover:border-(--ui-stroke-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground data-[state=open]:border-(--ui-stroke-tertiary) data-[state=open]:bg-(--ui-control-active-background) [-webkit-app-region:no-drag]"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
@@ -269,10 +270,11 @@ export function ChatView({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-full min-w-0 flex-col overflow-hidden bg-(--glass-chat-surface-background)',
|
||||
'relative isolate flex h-full min-w-0 flex-col overflow-hidden bg-(--ui-chat-surface-background)',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Backdrop />
|
||||
<ChatHeader
|
||||
activeSessionId={activeSessionId}
|
||||
isRoutedSessionView={isRoutedSessionView}
|
||||
@@ -283,7 +285,7 @@ export function ChatView({
|
||||
|
||||
<NotificationStack />
|
||||
|
||||
<div className="relative min-h-0 max-w-full flex-1 overflow-hidden bg-(--glass-chat-surface-background) contain-[layout_paint]">
|
||||
<div className="relative min-h-0 max-w-full flex-1 overflow-hidden bg-(--ui-chat-surface-background) contain-[layout_paint]">
|
||||
<AssistantRuntimeProvider runtime={runtime}>
|
||||
<Thread
|
||||
clampToComposer={showChatBar}
|
||||
|
||||
@@ -2,10 +2,10 @@ import { useStore } from '@nanostores/react'
|
||||
import type { CSSProperties, MutableRefObject, PointerEvent as ReactPointerEvent, RefObject } from 'react'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
|
||||
import { requestComposerInsert } from '@/app/chat/composer/focus'
|
||||
import { CopyButton } from '@/components/ui/copy-button'
|
||||
import { PanelBottom, Send, Trash2 } from '@/lib/icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { requestComposerInsert } from '@/app/chat/composer/focus'
|
||||
import { notify } from '@/store/notifications'
|
||||
|
||||
import type { ConsoleEntry, PreviewConsoleState } from './preview-console-state'
|
||||
|
||||
@@ -100,7 +100,7 @@ export function PreviewEmptyState({
|
||||
)}
|
||||
{secondaryAction && (
|
||||
<button
|
||||
className="text-[0.6875rem] font-medium text-muted-foreground underline decoration-muted-foreground/25 underline-offset-4 transition-colors hover:text-foreground hover:decoration-foreground/55 disabled:cursor-default disabled:text-muted-foreground/55 disabled:no-underline"
|
||||
className="text-[0.6875rem] font-medium text-muted-foreground underline decoration-current/20 underline-offset-4 transition-colors hover:text-foreground disabled:cursor-default disabled:text-muted-foreground/55 disabled:no-underline"
|
||||
disabled={secondaryAction.disabled}
|
||||
onClick={secondaryAction.onClick}
|
||||
type="button"
|
||||
@@ -298,7 +298,7 @@ function PreviewToggle({ asSource, onToggle }: { asSource: boolean; onToggle: ()
|
||||
return (
|
||||
<div className="sticky top-0 z-10 flex justify-end border-b border-border/40 bg-transparent px-3 py-1 backdrop-blur">
|
||||
<button
|
||||
className="text-[0.625rem] font-bold text-muted-foreground underline decoration-muted-foreground/25 underline-offset-4 transition-colors hover:text-foreground hover:decoration-foreground/55"
|
||||
className="text-[0.625rem] font-bold text-muted-foreground underline decoration-current/20 underline-offset-4 transition-colors hover:text-foreground"
|
||||
onClick={onToggle}
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -13,7 +13,6 @@ describe('PreviewPane console state', () => {
|
||||
|
||||
const rendered = render(
|
||||
<PreviewPane
|
||||
onClose={vi.fn()}
|
||||
setTitlebarToolGroup={setTitlebarToolGroup}
|
||||
target={{
|
||||
kind: 'url',
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { PointerEvent as ReactPointerEvent } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import type { SetTitlebarToolGroup, TitlebarTool } from '@/app/shell/titlebar-controls'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { Bug } from '@/lib/icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
@@ -31,7 +30,6 @@ type PreviewWebview = HTMLElement & {
|
||||
|
||||
interface PreviewPaneProps {
|
||||
embedded?: boolean
|
||||
onClose: () => void
|
||||
onRestartServer?: (url: string, context?: string) => Promise<string>
|
||||
reloadRequest?: number
|
||||
setTitlebarToolGroup?: SetTitlebarToolGroup
|
||||
@@ -85,7 +83,7 @@ function PreviewLoadError({
|
||||
body={
|
||||
<>
|
||||
<a
|
||||
className="pointer-events-auto block cursor-pointer font-mono text-muted-foreground/90 underline decoration-muted-foreground/30 underline-offset-4 transition-colors hover:text-foreground hover:decoration-foreground/70"
|
||||
className="pointer-events-auto block cursor-pointer font-mono text-muted-foreground/90 underline decoration-current/20 underline-offset-4 transition-colors hover:text-foreground"
|
||||
href={error.url}
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
@@ -118,7 +116,6 @@ const TITLEBAR_GROUP_ID = 'preview'
|
||||
|
||||
export function PreviewPane({
|
||||
embedded = false,
|
||||
onClose,
|
||||
onRestartServer,
|
||||
reloadRequest = 0,
|
||||
setTitlebarToolGroup,
|
||||
@@ -300,35 +297,13 @@ export function PreviewPane({
|
||||
onSelect: toggleDevTools
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
icon: <Codicon name="refresh" spinning={loading} />,
|
||||
id: `${TITLEBAR_GROUP_ID}-reload`,
|
||||
label: 'Reload preview',
|
||||
onSelect: reloadPreview
|
||||
},
|
||||
{
|
||||
icon: <Codicon name="close" />,
|
||||
id: `${TITLEBAR_GROUP_ID}-close`,
|
||||
label: 'Close preview',
|
||||
onSelect: onClose
|
||||
}
|
||||
: [])
|
||||
]
|
||||
|
||||
setTitlebarToolGroup(TITLEBAR_GROUP_ID, tools)
|
||||
|
||||
return () => setTitlebarToolGroup(TITLEBAR_GROUP_ID, [])
|
||||
}, [
|
||||
consoleOpen,
|
||||
consoleState,
|
||||
devtoolsOpen,
|
||||
isWebPreview,
|
||||
loading,
|
||||
onClose,
|
||||
reloadPreview,
|
||||
setTitlebarToolGroup,
|
||||
toggleDevTools
|
||||
])
|
||||
}, [consoleOpen, consoleState, devtoolsOpen, isWebPreview, setTitlebarToolGroup, toggleDevTools])
|
||||
|
||||
useEffect(() => {
|
||||
if (!consoleOpen) {
|
||||
@@ -633,7 +608,7 @@ export function PreviewPane({
|
||||
<div className="pointer-events-none flex min-h-(--titlebar-height) items-center gap-1.5 border-b border-border/60 bg-background px-2 py-1">
|
||||
<div className="min-w-0 flex-1">
|
||||
<a
|
||||
className="pointer-events-auto inline max-w-full cursor-pointer truncate text-left text-xs font-medium text-foreground underline-offset-4 transition-colors hover:text-primary hover:underline"
|
||||
className="pointer-events-auto inline max-w-full cursor-pointer truncate text-left text-xs font-medium text-foreground underline-offset-4 decoration-current/20 transition-colors hover:text-primary hover:underline"
|
||||
href={currentUrl}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
$filePreviewTabs,
|
||||
$previewReloadRequest,
|
||||
$previewTarget,
|
||||
closeActiveRightRailTab,
|
||||
closeRightRail,
|
||||
closeRightRailTab,
|
||||
type PreviewTarget
|
||||
} from '@/store/preview'
|
||||
@@ -79,57 +79,69 @@ export function ChatPreviewRail({ onRestartServer, setTitlebarToolGroup }: ChatP
|
||||
const isPreview = activeTab.id === RIGHT_RAIL_PREVIEW_TAB_ID
|
||||
|
||||
return (
|
||||
<aside className="relative flex h-full w-full min-w-0 flex-col overflow-hidden border-l border-(--ui-stroke-tertiary) bg-(--glass-editor-surface-background) text-(--ui-text-tertiary)">
|
||||
<div
|
||||
className="flex h-(--titlebar-height) shrink-0 overflow-x-auto overflow-y-hidden overscroll-x-contain border-b border-(--ui-stroke-tertiary) bg-(--glass-sidebar-surface-background) [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
role="tablist"
|
||||
>
|
||||
{tabs.map(tab => {
|
||||
const active = tab.id === activeTab.id
|
||||
<aside className="relative flex h-full w-full min-w-0 flex-col overflow-hidden border-l border-(--ui-stroke-tertiary) bg-(--ui-editor-surface-background) text-(--ui-text-tertiary)">
|
||||
<div className="group/rail-tabs flex h-(--titlebar-height) shrink-0 border-b border-(--ui-stroke-tertiary) bg-(--ui-sidebar-surface-background)">
|
||||
<div
|
||||
className="flex min-w-0 flex-1 overflow-x-auto overflow-y-hidden overscroll-x-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
role="tablist"
|
||||
>
|
||||
{tabs.map(tab => {
|
||||
const active = tab.id === activeTab.id
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group/tab relative flex h-full min-w-0 max-w-48 shrink-0 items-center text-[0.6875rem] font-medium [-webkit-app-region:no-drag]',
|
||||
active
|
||||
? 'bg-(--glass-editor-surface-background) text-foreground [--tab-bg:var(--glass-editor-surface-background)]'
|
||||
: 'border-r border-(--ui-stroke-quaternary) text-(--ui-text-tertiary) [--tab-bg:var(--glass-sidebar-surface-background)] hover:bg-(--chrome-action-hover) hover:text-foreground hover:[--tab-bg:var(--chrome-action-hover)]'
|
||||
)}
|
||||
key={tab.id}
|
||||
>
|
||||
{active && <span aria-hidden="true" className="absolute inset-x-0 top-0 h-px bg-(--ui-stroke-primary)" />}
|
||||
<button
|
||||
aria-selected={active}
|
||||
className="flex h-full min-w-0 max-w-full items-center overflow-hidden pl-3 pr-2 text-left outline-none"
|
||||
onClick={() => selectRightRailTab(tab.id)}
|
||||
role="tab"
|
||||
title={tab.label}
|
||||
type="button"
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group/tab relative flex h-full min-w-0 max-w-48 shrink-0 items-center text-[0.6875rem] font-medium [-webkit-app-region:no-drag] last:border-r last:border-(--ui-stroke-quaternary)',
|
||||
active
|
||||
? 'bg-(--ui-editor-surface-background) text-foreground [--tab-bg:var(--ui-editor-surface-background)]'
|
||||
: 'border-r border-(--ui-stroke-quaternary) text-(--ui-text-tertiary) [--tab-bg:var(--ui-sidebar-surface-background)] hover:bg-(--chrome-action-hover) hover:text-foreground'
|
||||
)}
|
||||
key={tab.id}
|
||||
>
|
||||
<span className="block min-w-0 truncate">{tab.label}</span>
|
||||
</button>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-y-0 right-0 w-9 bg-[linear-gradient(to_right,transparent,var(--tab-bg)_55%)] opacity-0 transition-opacity group-hover/tab:opacity-100 group-focus-within/tab:opacity-100"
|
||||
/>
|
||||
<button
|
||||
aria-label={`Close ${tab.label}`}
|
||||
className="pointer-events-none absolute right-1.5 top-1/2 grid size-4 -translate-y-1/2 place-items-center rounded-sm text-(--ui-text-tertiary) opacity-0 transition-[background-color,color,opacity] hover:bg-(--ui-bg-secondary) hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 group-hover/tab:pointer-events-auto group-hover/tab:opacity-100 group-focus-within/tab:pointer-events-auto group-focus-within/tab:opacity-100"
|
||||
onClick={() => closeRightRailTab(tab.id)}
|
||||
title={`Close ${tab.label}`}
|
||||
type="button"
|
||||
>
|
||||
<Codicon name="close" size="0.75rem" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{active && (
|
||||
<span aria-hidden="true" className="absolute inset-x-0 top-0 h-px bg-(--ui-stroke-primary)" />
|
||||
)}
|
||||
<button
|
||||
aria-selected={active}
|
||||
className="flex h-full min-w-0 max-w-full items-center overflow-hidden pl-3 pr-2 text-left outline-none"
|
||||
onClick={() => selectRightRailTab(tab.id)}
|
||||
role="tab"
|
||||
title={tab.label}
|
||||
type="button"
|
||||
>
|
||||
<span className="block min-w-0 truncate">{tab.label}</span>
|
||||
</button>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-y-0 right-0 w-9 bg-[linear-gradient(to_right,transparent,var(--tab-bg)_55%)] opacity-0 transition-opacity group-hover/tab:opacity-100 group-focus-within/tab:opacity-100"
|
||||
/>
|
||||
<button
|
||||
aria-label={`Close ${tab.label}`}
|
||||
className="pointer-events-none absolute right-1.5 top-1/2 grid size-4 -translate-y-1/2 place-items-center rounded-sm text-(--ui-text-tertiary) opacity-0 transition-[background-color,color,opacity] hover:bg-(--ui-bg-secondary) hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 group-hover/tab:pointer-events-auto group-hover/tab:opacity-100 group-focus-within/tab:pointer-events-auto group-focus-within/tab:opacity-100"
|
||||
onClick={() => closeRightRailTab(tab.id)}
|
||||
title={`Close ${tab.label}`}
|
||||
type="button"
|
||||
>
|
||||
<Codicon name="close" size="0.75rem" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close preview pane"
|
||||
className="mr-1.5 grid size-6 shrink-0 self-center place-items-center rounded-md text-(--ui-text-tertiary) opacity-0 transition-opacity hover:bg-(--ui-control-hover-background) hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring group-hover/rail-tabs:opacity-100 [-webkit-app-region:no-drag]"
|
||||
onClick={closeRightRail}
|
||||
title="Close preview pane"
|
||||
type="button"
|
||||
>
|
||||
<Codicon name="close" size="0.75rem" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<PreviewPane
|
||||
embedded
|
||||
onClose={closeActiveRightRailTab}
|
||||
onRestartServer={isPreview ? onRestartServer : undefined}
|
||||
reloadRequest={previewReloadRequest}
|
||||
setTitlebarToolGroup={setTitlebarToolGroup}
|
||||
|
||||
@@ -365,31 +365,28 @@ export function CommandCenterView({
|
||||
}
|
||||
}, [])
|
||||
|
||||
const refreshUsage = useCallback(
|
||||
async (days: UsagePeriod) => {
|
||||
const requestId = usageRequestRef.current + 1
|
||||
usageRequestRef.current = requestId
|
||||
setUsageLoading(true)
|
||||
setUsageError('')
|
||||
const refreshUsage = useCallback(async (days: UsagePeriod) => {
|
||||
const requestId = usageRequestRef.current + 1
|
||||
usageRequestRef.current = requestId
|
||||
setUsageLoading(true)
|
||||
setUsageError('')
|
||||
|
||||
try {
|
||||
const response = await getUsageAnalytics(days)
|
||||
try {
|
||||
const response = await getUsageAnalytics(days)
|
||||
|
||||
if (usageRequestRef.current === requestId) {
|
||||
setUsage(response)
|
||||
}
|
||||
} catch (error) {
|
||||
if (usageRequestRef.current === requestId) {
|
||||
setUsageError(error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
} finally {
|
||||
if (usageRequestRef.current === requestId) {
|
||||
setUsageLoading(false)
|
||||
}
|
||||
if (usageRequestRef.current === requestId) {
|
||||
setUsage(response)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
} catch (error) {
|
||||
if (usageRequestRef.current === requestId) {
|
||||
setUsageError(error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
} finally {
|
||||
if (usageRequestRef.current === requestId) {
|
||||
setUsageLoading(false)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!debouncedQuery) {
|
||||
@@ -583,7 +580,10 @@ export function CommandCenterView({
|
||||
const beginAuxiliaryEdit = useCallback(
|
||||
(task: string) => {
|
||||
const current = auxiliary?.tasks.find(entry => entry.task === task)
|
||||
const initialProvider = current?.provider && current.provider !== 'auto' ? current.provider : mainModel?.provider ?? ''
|
||||
|
||||
const initialProvider =
|
||||
current?.provider && current.provider !== 'auto' ? current.provider : (mainModel?.provider ?? '')
|
||||
|
||||
const initialModel = current?.model || mainModel?.model || ''
|
||||
setAuxDraft({ provider: initialProvider, model: initialModel })
|
||||
setEditingAuxTask(task)
|
||||
@@ -658,15 +658,7 @@ export function CommandCenterView({
|
||||
{SECTIONS.map(value => (
|
||||
<OverlayNavItem
|
||||
active={section === value}
|
||||
icon={
|
||||
value === 'sessions'
|
||||
? Pin
|
||||
: value === 'system'
|
||||
? Activity
|
||||
: value === 'models'
|
||||
? Cpu
|
||||
: BarChart3
|
||||
}
|
||||
icon={value === 'sessions' ? Pin : value === 'system' ? Activity : value === 'models' ? Cpu : BarChart3}
|
||||
key={value}
|
||||
label={SECTION_LABELS[value]}
|
||||
onClick={() => setSection(value)}
|
||||
@@ -1168,7 +1160,7 @@ function UsagePanel({ error, loading, onPeriodChange, onRefresh, period, usage }
|
||||
) : (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
No usage in the last {period} days.{' '}
|
||||
<button className="underline" onClick={onRefresh} type="button">
|
||||
<button className="underline underline-offset-4 decoration-current/20" onClick={onRefresh} type="button">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { PageLoader } from '@/components/page-loader'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -24,7 +25,6 @@ import {
|
||||
triggerCronJob,
|
||||
updateCronJob
|
||||
} from '@/hermes'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { AlertTriangle, Clock, Pause, Pencil, Play, Trash2, Zap } from '@/lib/icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
@@ -216,11 +216,23 @@ function scheduleOptionForExpr(expr: string): ScheduleOption {
|
||||
return SCHEDULE_OPTIONS.find(option => option.value === 'weekdays') ?? SCHEDULE_OPTIONS[0]
|
||||
}
|
||||
|
||||
if (dayOfMonth === '*' && month === '*' && isIntegerToken(dayOfWeek) && isIntegerToken(minute) && isIntegerToken(hour)) {
|
||||
if (
|
||||
dayOfMonth === '*' &&
|
||||
month === '*' &&
|
||||
isIntegerToken(dayOfWeek) &&
|
||||
isIntegerToken(minute) &&
|
||||
isIntegerToken(hour)
|
||||
) {
|
||||
return SCHEDULE_OPTIONS.find(option => option.value === 'weekly') ?? SCHEDULE_OPTIONS[0]
|
||||
}
|
||||
|
||||
if (month === '*' && dayOfWeek === '*' && isIntegerToken(dayOfMonth) && isIntegerToken(minute) && isIntegerToken(hour)) {
|
||||
if (
|
||||
month === '*' &&
|
||||
dayOfWeek === '*' &&
|
||||
isIntegerToken(dayOfMonth) &&
|
||||
isIntegerToken(minute) &&
|
||||
isIntegerToken(hour)
|
||||
) {
|
||||
return SCHEDULE_OPTIONS.find(option => option.value === 'monthly') ?? SCHEDULE_OPTIONS[0]
|
||||
}
|
||||
|
||||
@@ -297,10 +309,7 @@ interface CronViewProps extends React.ComponentProps<'section'> {
|
||||
setStatusbarItemGroup?: SetStatusbarItemGroup
|
||||
}
|
||||
|
||||
export function CronView({
|
||||
setStatusbarItemGroup: _setStatusbarItemGroup,
|
||||
...props
|
||||
}: CronViewProps) {
|
||||
export function CronView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...props }: CronViewProps) {
|
||||
const [jobs, setJobs] = useState<CronJob[] | null>(null)
|
||||
const [query, setQuery] = useState('')
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
@@ -475,9 +484,7 @@ export function CronView({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="hidden">
|
||||
{totalCount === 0 ? 'No scheduled jobs' : `${enabledCount}/${totalCount} active`}
|
||||
</div>
|
||||
<div className="hidden">{totalCount === 0 ? 'No scheduled jobs' : `${enabledCount}/${totalCount} active`}</div>
|
||||
|
||||
<CronEditorDialog editor={editor} onClose={() => setEditor({ mode: 'closed' })} onSave={handleEditorSave} />
|
||||
|
||||
@@ -488,7 +495,8 @@ export function CronView({
|
||||
<DialogDescription>
|
||||
{pendingDelete ? (
|
||||
<>
|
||||
This will remove <span className="font-medium text-foreground">{truncate(jobTitle(pendingDelete), 60)}</span>{' '}
|
||||
This will remove{' '}
|
||||
<span className="font-medium text-foreground">{truncate(jobTitle(pendingDelete), 60)}</span>{' '}
|
||||
permanently. It will stop firing immediately.
|
||||
</>
|
||||
) : null}
|
||||
@@ -586,13 +594,14 @@ function CronJobRow({
|
||||
)
|
||||
}
|
||||
|
||||
function IconAction({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof Button>, 'size' | 'variant'>) {
|
||||
function IconAction({ children, className, ...props }: Omit<React.ComponentProps<typeof Button>, 'size' | 'variant'>) {
|
||||
return (
|
||||
<Button className={cn('size-7 text-muted-foreground hover:text-foreground', className)} size="icon" variant="ghost" {...props}>
|
||||
<Button
|
||||
className={cn('size-7 text-muted-foreground hover:text-foreground', className)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
@@ -600,7 +609,9 @@ function IconAction({
|
||||
|
||||
function StatePill({ children, tone }: { children: string; tone: keyof typeof PILL_TONE }) {
|
||||
return (
|
||||
<span className={cn('inline-flex items-center rounded-full px-1.5 py-0.5 text-[0.64rem] capitalize', PILL_TONE[tone])}>
|
||||
<span
|
||||
className={cn('inline-flex items-center rounded-full px-1.5 py-0.5 text-[0.64rem] capitalize', PILL_TONE[tone])}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { PageLoader } from '@/components/page-loader'
|
||||
import { StatusDot, type StatusTone } from '@/components/status-dot'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DisclosureCaret } from '@/components/ui/disclosure-caret'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import {
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
type MessagingPlatformInfo,
|
||||
updateMessagingPlatform
|
||||
} from '@/hermes'
|
||||
import { DisclosureCaret } from '@/components/ui/disclosure-caret'
|
||||
import { AlertTriangle, ExternalLink, Save, Trash2 } from '@/lib/icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
@@ -206,10 +206,7 @@ function fieldCopy(field: MessagingEnvVarInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
export function MessagingView({
|
||||
setStatusbarItemGroup: _setStatusbarItemGroup,
|
||||
...props
|
||||
}: MessagingViewProps) {
|
||||
export function MessagingView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...props }: MessagingViewProps) {
|
||||
const [platforms, setPlatforms] = useState<MessagingPlatformInfo[] | null>(null)
|
||||
const [edits, setEdits] = useState<EditMap>({})
|
||||
const [query, setQuery] = useState('')
|
||||
@@ -375,42 +372,42 @@ export function MessagingView({
|
||||
<PageLoader label="Loading messaging platforms..." />
|
||||
) : (
|
||||
<div className="grid h-full min-h-0 grid-cols-1 lg:grid-cols-[14rem_minmax(0,1fr)]">
|
||||
<aside className="min-h-0 overflow-y-auto border-b border-(--ui-stroke-tertiary) p-2 lg:border-b-0 lg:border-r">
|
||||
<ul className="space-y-1">
|
||||
{visiblePlatforms.map(platform => (
|
||||
<li key={platform.id}>
|
||||
<PlatformRow
|
||||
active={selected?.id === platform.id}
|
||||
onSelect={() => setSelectedId(platform.id)}
|
||||
platform={platform}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</aside>
|
||||
<aside className="min-h-0 overflow-y-auto border-b border-(--ui-stroke-tertiary) p-2 lg:border-b-0 lg:border-r">
|
||||
<ul className="space-y-1">
|
||||
{visiblePlatforms.map(platform => (
|
||||
<li key={platform.id}>
|
||||
<PlatformRow
|
||||
active={selected?.id === platform.id}
|
||||
onSelect={() => setSelectedId(platform.id)}
|
||||
platform={platform}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main className="min-h-0 overflow-hidden">
|
||||
{selected && (
|
||||
<PlatformDetail
|
||||
edits={edits[selected.id] || {}}
|
||||
onClear={key => void handleClear(selected, key)}
|
||||
onEdit={(key, value) =>
|
||||
setEdits(current => ({
|
||||
...current,
|
||||
[selected.id]: {
|
||||
...(current[selected.id] || {}),
|
||||
[key]: value
|
||||
}
|
||||
}))
|
||||
}
|
||||
onSave={() => void handleSave(selected)}
|
||||
onToggle={enabled => void handleToggle(selected, enabled)}
|
||||
platform={selected}
|
||||
saving={saving}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
<main className="min-h-0 overflow-hidden">
|
||||
{selected && (
|
||||
<PlatformDetail
|
||||
edits={edits[selected.id] || {}}
|
||||
onClear={key => void handleClear(selected, key)}
|
||||
onEdit={(key, value) =>
|
||||
setEdits(current => ({
|
||||
...current,
|
||||
[selected.id]: {
|
||||
...(current[selected.id] || {}),
|
||||
[key]: value
|
||||
}
|
||||
}))
|
||||
}
|
||||
onSave={() => void handleSave(selected)}
|
||||
onToggle={enabled => void handleToggle(selected, enabled)}
|
||||
platform={selected}
|
||||
saving={saving}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
)}
|
||||
</PageSearchShell>
|
||||
)
|
||||
@@ -429,7 +426,9 @@ function PlatformRow({
|
||||
<button
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors',
|
||||
active ? 'bg-(--ui-bg-tertiary) text-foreground' : 'text-(--ui-text-secondary) hover:bg-(--chrome-action-hover) hover:text-foreground'
|
||||
active
|
||||
? 'bg-(--ui-bg-tertiary) text-foreground'
|
||||
: 'text-(--ui-text-secondary) hover:bg-(--chrome-action-hover) hover:text-foreground'
|
||||
)}
|
||||
onClick={onSelect}
|
||||
type="button"
|
||||
@@ -490,7 +489,9 @@ function PlatformDetail({
|
||||
<PlatformAvatar platformId={platform.id} platformName={platform.name} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-[0.9375rem] font-semibold tracking-tight">{platform.name}</h3>
|
||||
<p className="mt-1 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">{platform.description}</p>
|
||||
<p className="mt-1 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">
|
||||
{platform.description}
|
||||
</p>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
<StatePill tone={stateTone(platform)}>{stateLabel(platform.state)}</StatePill>
|
||||
<SetupPill active={platform.configured}>
|
||||
@@ -511,7 +512,9 @@ function PlatformDetail({
|
||||
|
||||
<section>
|
||||
<SectionTitle>Get your credentials</SectionTitle>
|
||||
<p className="mt-1 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">{introCopy(platform)}</p>
|
||||
<p className="mt-1 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">
|
||||
{introCopy(platform)}
|
||||
</p>
|
||||
<div className="mt-3">
|
||||
<Button asChild size="sm" variant="outline">
|
||||
<a href={platform.docs_url} rel="noreferrer" target="_blank">
|
||||
@@ -591,7 +594,7 @@ function PlatformDetail({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className="border-t border-(--ui-stroke-tertiary) bg-(--glass-chat-surface-background) px-5 py-2.5">
|
||||
<footer className="border-t border-(--ui-stroke-tertiary) bg-(--ui-chat-surface-background) px-5 py-2.5">
|
||||
<div className="mx-auto flex max-w-2xl flex-wrap items-center gap-2">
|
||||
<label className="flex shrink-0 items-center gap-2 rounded-md border border-(--ui-stroke-tertiary) bg-(--ui-bg-quinary) px-2.5 py-1.5 text-[length:var(--conversation-text-font-size)]">
|
||||
<Switch
|
||||
|
||||
@@ -43,7 +43,7 @@ export function OverlaySidebar({ children, className }: OverlaySidebarProps) {
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
'flex min-h-0 flex-col gap-0.5 overflow-y-auto bg-(--glass-sidebar-surface-background) px-2.5 py-3',
|
||||
'flex min-h-0 flex-col gap-0.5 overflow-y-auto bg-(--ui-sidebar-surface-background) px-2.5 py-3',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -32,7 +32,9 @@ export function OverlayView({
|
||||
// Settings still closes the picker first instead of the underlying overlay.
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'Escape' || event.defaultPrevented) return
|
||||
if (event.key !== 'Escape' || event.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
triggerHaptic('close')
|
||||
@@ -56,7 +58,7 @@ export function OverlayView({
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-full min-h-0 flex-col overflow-hidden rounded-xl border border-(--ui-stroke-secondary) bg-(--glass-chat-surface-background) shadow-md',
|
||||
'relative flex h-full min-h-0 flex-col overflow-hidden rounded-xl border border-(--ui-stroke-secondary) bg-(--ui-chat-surface-background) shadow-md',
|
||||
rootClassName
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -26,7 +26,7 @@ export function PageSearchShell({
|
||||
return (
|
||||
<section
|
||||
{...props}
|
||||
className={cn('flex h-full min-w-0 flex-col overflow-hidden bg-(--glass-chat-surface-background)', className)}
|
||||
className={cn('flex h-full min-w-0 flex-col overflow-hidden bg-(--ui-chat-surface-background)', className)}
|
||||
>
|
||||
<div className="relative z-10 grid gap-2 border-b border-(--ui-stroke-tertiary) px-3 py-2.5">
|
||||
<PageSearchInput
|
||||
@@ -37,7 +37,7 @@ export function PageSearchShell({
|
||||
/>
|
||||
{filters ? <div className="flex flex-wrap items-center justify-center gap-1.5">{filters}</div> : null}
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-hidden bg-(--glass-chat-surface-background)">{children}</div>
|
||||
<div className="min-h-0 flex-1 overflow-hidden bg-(--ui-chat-surface-background)">{children}</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { PageLoader } from '@/components/page-loader'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
renameProfile,
|
||||
updateProfileSoul
|
||||
} from '@/hermes'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { AlertTriangle, Pencil, Save, Terminal, Trash2, Users } from '@/lib/icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
@@ -254,15 +254,7 @@ export function ProfilesView({
|
||||
)
|
||||
}
|
||||
|
||||
function ProfileRow({
|
||||
active,
|
||||
onSelect,
|
||||
profile
|
||||
}: {
|
||||
active: boolean
|
||||
onSelect: () => void
|
||||
profile: ProfileInfo
|
||||
}) {
|
||||
function ProfileRow({ active, onSelect, profile }: { active: boolean; onSelect: () => void; profile: ProfileInfo }) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
@@ -364,9 +356,7 @@ function ProfileDetail({
|
||||
{profile.model ? (
|
||||
<>
|
||||
<span className="font-mono">{profile.model}</span>
|
||||
{profile.provider && (
|
||||
<span className="text-muted-foreground"> · {profile.provider}</span>
|
||||
)}
|
||||
{profile.provider && <span className="text-muted-foreground"> · {profile.provider}</span>}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Not set</span>
|
||||
@@ -673,7 +663,8 @@ function RenameProfileDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rename profile</DialogTitle>
|
||||
<DialogDescription>
|
||||
Renaming updates the profile directory and any wrapper scripts in <span className="font-mono">~/.local/bin</span>.
|
||||
Renaming updates the profile directory and any wrapper scripts in{' '}
|
||||
<span className="font-mono">~/.local/bin</span>.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@@ -143,8 +143,8 @@ function ProjectTreeRow({
|
||||
aria-expanded={isFolder ? node.isOpen : undefined}
|
||||
aria-selected={node.isSelected}
|
||||
className={cn(
|
||||
'group/row flex h-full cursor-pointer select-none items-center gap-1 border border-transparent px-3 text-xs font-normal leading-(--file-tree-row-height) text-(--ui-text-secondary) transition-colors hover:bg-(--chrome-action-hover) hover:text-foreground',
|
||||
node.isSelected && 'bg-(--ui-bg-tertiary) text-foreground',
|
||||
'group/row flex h-full cursor-pointer select-none items-center gap-1 border border-transparent px-3 text-xs font-normal leading-(--file-tree-row-height) text-(--ui-text-secondary) transition-colors hover:bg-(--ui-row-hover-background) hover:text-foreground',
|
||||
node.isSelected && 'bg-(--ui-row-active-background) text-foreground',
|
||||
isPlaceholder && 'pointer-events-none italic text-muted-foreground/70'
|
||||
)}
|
||||
draggable={!isPlaceholder}
|
||||
@@ -192,7 +192,11 @@ function ProjectTreeRow({
|
||||
>
|
||||
{isFolder && !isPlaceholder && (
|
||||
<span aria-hidden className="flex w-3 items-center justify-center">
|
||||
<Codicon className="text-(--ui-text-tertiary)" name={node.isOpen ? 'chevron-down' : 'chevron-right'} size="0.75rem" />
|
||||
<Codicon
|
||||
className="text-(--ui-text-tertiary)"
|
||||
name={node.isOpen ? 'chevron-down' : 'chevron-right'}
|
||||
size="0.75rem"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{!isFolder && <span aria-hidden className="w-3 shrink-0" />}
|
||||
|
||||
@@ -85,7 +85,7 @@ export function RightSidebarPane({
|
||||
return (
|
||||
<aside
|
||||
aria-label="Right sidebar"
|
||||
className="relative flex h-full w-full min-w-0 flex-col overflow-hidden border-l border-(--ui-stroke-secondary) bg-(--glass-sidebar-surface-background) pt-(--titlebar-height) text-(--ui-text-tertiary) shadow-[inset_0.0625rem_0_0_color-mix(in_srgb,white_18%,transparent)]"
|
||||
className="before:pointer-events-none relative flex h-full w-full min-w-0 flex-col overflow-hidden border-l border-(--ui-stroke-secondary) bg-(--ui-sidebar-surface-background) pt-(--titlebar-height) text-(--ui-text-tertiary) shadow-[inset_0.0625rem_0_0_color-mix(in_srgb,white_18%,transparent)] before:absolute before:inset-x-0 before:top-(--titlebar-height) before:z-1 before:h-px before:bg-(--ui-stroke-tertiary)"
|
||||
>
|
||||
<RightSidebarChrome activeTab={activeTab} branch={currentBranch} />
|
||||
|
||||
@@ -116,26 +116,28 @@ export function RightSidebarPane({
|
||||
function RightSidebarChrome({ activeTab, branch }: { activeTab: RightSidebarTabId; branch: string }) {
|
||||
return (
|
||||
<header className="shrink-0 bg-transparent text-[0.75rem]">
|
||||
<div className="flex h-8 items-center gap-2 border-b border-(--ui-stroke-tertiary) px-3">
|
||||
{RIGHT_SIDEBAR_TABS.map(tab => (
|
||||
<button
|
||||
aria-label={tab.label}
|
||||
aria-pressed={tab.id === activeTab}
|
||||
className={cn(
|
||||
'grid size-6 shrink-0 place-items-center rounded-md text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring active:bg-sidebar-accent active:text-sidebar-accent-foreground',
|
||||
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground'
|
||||
)}
|
||||
data-active={tab.id === activeTab}
|
||||
key={tab.id}
|
||||
onClick={() => setRightSidebarTab(tab.id)}
|
||||
title={tab.label}
|
||||
type="button"
|
||||
>
|
||||
<Codicon name={tab.icon} size="0.875rem" />
|
||||
</button>
|
||||
))}
|
||||
<div className="flex items-center gap-2 border-b border-(--ui-stroke-tertiary) px-2.5 py-1">
|
||||
<nav aria-label="Right sidebar panels" className="flex min-w-0 items-center gap-1">
|
||||
{RIGHT_SIDEBAR_TABS.map(tab => (
|
||||
<button
|
||||
aria-label={tab.label}
|
||||
aria-pressed={tab.id === activeTab}
|
||||
className={cn(
|
||||
'grid size-6 shrink-0 place-items-center rounded-lg text-(--ui-text-tertiary) transition-colors hover:bg-(--ui-control-hover-background) hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring active:bg-(--ui-control-active-background) active:text-foreground',
|
||||
'data-[active=true]:bg-(--ui-control-active-background) data-[active=true]:text-foreground'
|
||||
)}
|
||||
data-active={tab.id === activeTab}
|
||||
key={tab.id}
|
||||
onClick={() => setRightSidebarTab(tab.id)}
|
||||
title={tab.label}
|
||||
type="button"
|
||||
>
|
||||
<Codicon name={tab.icon} size="0.875rem" />
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
{branch && (
|
||||
<span className="ml-auto flex min-w-0 items-center gap-1.5 text-[0.6875rem] text-(--ui-text-tertiary)">
|
||||
<span className="ml-auto flex min-w-0 items-center gap-1 text-[0.6875rem] text-(--ui-text-tertiary)">
|
||||
<Codicon className="shrink-0" name="git-branch" size="0.75rem" />
|
||||
<span className="truncate">{branch}</span>
|
||||
</span>
|
||||
@@ -208,7 +210,7 @@ function FilesystemTab({
|
||||
}
|
||||
|
||||
export function RightSidebarSectionHeader({ children }: { children: ReactNode }) {
|
||||
return <div className="flex h-7 shrink-0 items-center px-3">{children}</div>
|
||||
return <div className="flex h-7 shrink-0 items-center px-2">{children}</div>
|
||||
}
|
||||
|
||||
interface FileTreeBodyProps {
|
||||
|
||||
@@ -27,7 +27,12 @@ export function TerminalTab({ cwd, onAddSelectionToChat }: TerminalTabProps) {
|
||||
<div className="relative min-h-0 flex-1 px-2 pb-2">
|
||||
{status === 'starting' && (
|
||||
<div className="pointer-events-none absolute inset-0 z-10 grid place-items-center">
|
||||
<Loader className="size-8 text-(--ui-text-tertiary)" pathSteps={180} strokeScale={0.68} type="spiral-search" />
|
||||
<Loader
|
||||
className="size-8 text-(--ui-text-tertiary)"
|
||||
pathSteps={180}
|
||||
strokeScale={0.68}
|
||||
type="spiral-search"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{selection.trim() && (
|
||||
|
||||
@@ -67,7 +67,7 @@ export function AppearanceSettings() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section className="rounded-xl border border-(--ui-stroke-tertiary) bg-(--glass-chat-bubble-background) p-3 shadow-sm">
|
||||
<section className="rounded-xl border border-(--ui-stroke-tertiary) bg-(--ui-chat-bubble-background) p-3 shadow-sm">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">Color Mode</div>
|
||||
@@ -105,14 +105,16 @@ export function AppearanceSettings() {
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 text-[length:var(--conversation-text-font-size)] font-medium">{label}</div>
|
||||
<div className="mt-1 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">{description}</div>
|
||||
<div className="mt-1 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">
|
||||
{description}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border border-(--ui-stroke-tertiary) bg-(--glass-chat-bubble-background) p-3 shadow-sm">
|
||||
<section className="rounded-xl border border-(--ui-stroke-tertiary) bg-(--ui-chat-bubble-background) p-3 shadow-sm">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">Tool Call Display</div>
|
||||
@@ -160,14 +162,16 @@ export function AppearanceSettings() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">{option.description}</div>
|
||||
<div className="mt-1 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">
|
||||
{option.description}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border border-(--ui-stroke-tertiary) bg-(--glass-chat-bubble-background) p-3 shadow-sm">
|
||||
<section className="rounded-xl border border-(--ui-stroke-tertiary) bg-(--ui-chat-bubble-background) p-3 shadow-sm">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">Theme</div>
|
||||
@@ -197,7 +201,9 @@ export function AppearanceSettings() {
|
||||
<ThemePreview name={theme.name} />
|
||||
<div className="mt-3 flex items-start justify-between gap-3 px-1">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-[length:var(--conversation-text-font-size)] font-medium">{theme.label}</div>
|
||||
<div className="truncate text-[length:var(--conversation-text-font-size)] font-medium">
|
||||
{theme.label}
|
||||
</div>
|
||||
<div className="mt-0.5 line-clamp-2 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">
|
||||
{theme.description}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
Brain,
|
||||
Lock,
|
||||
type IconComponent,
|
||||
Lock,
|
||||
MessageCircle,
|
||||
Mic,
|
||||
Monitor,
|
||||
|
||||
@@ -60,7 +60,9 @@ function ModeCard({
|
||||
<span>{title}</span>
|
||||
{active ? <Check className="ml-auto size-4 text-primary" /> : null}
|
||||
</div>
|
||||
<p className="mt-1.5 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">{description}</p>
|
||||
<p className="mt-1.5 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">
|
||||
{description}
|
||||
</p>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { deleteEnvVar, getEnvVars, revealEnvVar, setEnvVar } from '@/hermes'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { Check, Eye, EyeOff, Save, Settings2, Trash2, Zap } from '@/lib/icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { OverlayActionButton, OverlayCard } from '@/app/overlays/overlay-chrome'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { getHermesConfigRecord, saveHermesConfig, type HermesGateway } from '@/hermes'
|
||||
import { getHermesConfigRecord, type HermesGateway, saveHermesConfig } from '@/hermes'
|
||||
import { Package, Wrench } from '@/lib/icons'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
import { $activeSessionId } from '@/store/session'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import type { HermesConfigRecord } from '@/types/hermes'
|
||||
|
||||
import { includesQuery } from './helpers'
|
||||
@@ -64,7 +64,10 @@ export function McpSettings({ gateway, onConfigSaved, query }: McpSettingsProps)
|
||||
|
||||
getHermesConfigRecord()
|
||||
.then(next => {
|
||||
if (cancelled) return
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
setConfig(next)
|
||||
const first = Object.keys(getServers(next)).sort()[0] ?? null
|
||||
setSelected(first)
|
||||
@@ -76,6 +79,7 @@ export function McpSettings({ gateway, onConfigSaved, query }: McpSettingsProps)
|
||||
|
||||
const servers = useMemo(() => getServers(config), [config])
|
||||
const names = useMemo(() => Object.keys(servers).sort(), [servers])
|
||||
|
||||
const filtered = useMemo(
|
||||
() => names.filter(serverName => serverMatches(serverName, servers[serverName], query.trim().toLowerCase())),
|
||||
[names, query, servers]
|
||||
@@ -97,6 +101,7 @@ export function McpSettings({ gateway, onConfigSaved, query }: McpSettingsProps)
|
||||
|
||||
if (!nextName) {
|
||||
notify({ kind: 'error', title: 'Name required', message: 'Give this MCP server a config key.' })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -112,6 +117,7 @@ export function McpSettings({ gateway, onConfigSaved, query }: McpSettingsProps)
|
||||
parsed = raw as Record<string, unknown>
|
||||
} catch (err) {
|
||||
notifyError(err, 'Invalid MCP JSON')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -161,6 +167,7 @@ export function McpSettings({ gateway, onConfigSaved, query }: McpSettingsProps)
|
||||
const reloadMcp = async () => {
|
||||
if (!gateway) {
|
||||
notify({ kind: 'warning', title: 'Gateway unavailable', message: 'Reconnect the gateway before reloading MCP.' })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,9 @@ export function NavLink({
|
||||
<Button
|
||||
className={cn(
|
||||
'flex min-h-7 w-full justify-start gap-2 rounded-md px-2 text-left text-[length:var(--conversation-text-font-size)] transition',
|
||||
active ? 'bg-(--ui-bg-tertiary) text-foreground' : 'text-(--ui-text-secondary) hover:bg-(--chrome-action-hover) hover:text-foreground'
|
||||
active
|
||||
? 'bg-(--ui-bg-tertiary) text-foreground'
|
||||
: 'text-(--ui-text-secondary) hover:bg-(--chrome-action-hover) hover:text-foreground'
|
||||
)}
|
||||
onClick={onClick}
|
||||
size="sm"
|
||||
@@ -90,7 +92,11 @@ export function ListRow({
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="text-[length:var(--conversation-text-font-size)] font-medium text-foreground">{title}</div>
|
||||
{description && <div className="mt-1 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">{description}</div>}
|
||||
{description && (
|
||||
<div className="mt-1 text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) text-(--ui-text-tertiary)">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
{hint && <div className="mt-1 block font-mono text-[0.68rem] text-muted-foreground/45">{hint}</div>}
|
||||
{below}
|
||||
</div>
|
||||
|
||||
@@ -138,7 +138,7 @@ export function AppShell({
|
||||
tools={titlebarTools}
|
||||
/>
|
||||
|
||||
<main className="relative z-3 flex min-h-0 w-full flex-1 flex-col overflow-hidden bg-(--glass-chat-surface-background) transition-none">
|
||||
<main className="relative z-3 flex min-h-0 w-full flex-1 flex-col overflow-hidden transition-none">
|
||||
<PaneShell className="min-h-0 flex-1">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -42,11 +42,13 @@ export function GatewayMenuPanel({
|
||||
const gatewayOpen = gatewayState === 'open'
|
||||
const gatewayConnecting = gatewayState === 'connecting'
|
||||
const inferenceReady = gatewayOpen && inferenceStatus?.ready === true
|
||||
|
||||
const connectionLabel = gatewayOpen
|
||||
? 'Connected'
|
||||
: gatewayConnecting
|
||||
? 'Connecting'
|
||||
: prettyState(gatewayState || 'offline')
|
||||
|
||||
const inferenceLabel = gatewayOpen
|
||||
? inferenceStatus?.ready
|
||||
? 'Inference ready'
|
||||
@@ -54,6 +56,7 @@ export function GatewayMenuPanel({
|
||||
? 'Inference not ready'
|
||||
: 'Checking inference'
|
||||
: 'Disconnected'
|
||||
|
||||
const platforms = Object.entries(statusSnapshot?.gateway_platforms || {}).sort(([l], [r]) => l.localeCompare(r))
|
||||
const recentLogs = logLines.slice(-5)
|
||||
|
||||
|
||||
@@ -89,7 +89,11 @@ export function useStatusbarItems({
|
||||
const failed = actions.filter(t => !t.status.running && (t.status.exit_code ?? 0) !== 0).length
|
||||
const previewRunning = previewServerRestartStatus === 'running' ? 1 : 0
|
||||
const previewFailed = previewServerRestartStatus === 'error' ? 1 : 0
|
||||
const subagentsRunning = Object.values(subagentsBySession).reduce((sum, items) => sum + activeSubagentCount(items), 0)
|
||||
|
||||
const subagentsRunning = Object.values(subagentsBySession).reduce(
|
||||
(sum, items) => sum + activeSubagentCount(items),
|
||||
0
|
||||
)
|
||||
|
||||
return {
|
||||
bgFailed: failed + previewFailed,
|
||||
@@ -275,16 +279,7 @@ export function useStatusbarItems({
|
||||
},
|
||||
versionItem
|
||||
],
|
||||
[
|
||||
busy,
|
||||
contextBar,
|
||||
contextUsage,
|
||||
currentModel,
|
||||
currentProvider,
|
||||
sessionStartedAt,
|
||||
turnStartedAt,
|
||||
versionItem
|
||||
]
|
||||
[busy, contextBar, contextUsage, currentModel, currentProvider, sessionStartedAt, turnStartedAt, versionItem]
|
||||
)
|
||||
|
||||
const leftStatusbarItems = useMemo(
|
||||
|
||||
@@ -2,17 +2,20 @@ import type * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type SidebarPanelLabelProps = React.ComponentProps<'span'>
|
||||
interface SidebarPanelLabelProps extends React.ComponentProps<'span'> {
|
||||
dotClassName?: string
|
||||
}
|
||||
|
||||
export function SidebarPanelLabel({ children, className, ...props }: SidebarPanelLabelProps) {
|
||||
export function SidebarPanelLabel({ children, className, dotClassName, ...props }: SidebarPanelLabelProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'flex min-w-0 items-center gap-1.5 text-[0.6875rem] font-medium text-sidebar-foreground/55',
|
||||
'flex min-w-0 items-center gap-2 pl-2 text-[0.64rem] font-semibold uppercase tracking-[0.16em] text-(--theme-primary)',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span aria-hidden="true" className={cn('dither inline-block size-2 shrink-0 rounded-[1px]', dotClassName)} />
|
||||
<span className="min-w-0 truncate leading-none">{children}</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -49,7 +49,7 @@ export function StatusbarControls({ className, leftItems = [], items = [], ...pr
|
||||
return (
|
||||
<footer
|
||||
className={cn(
|
||||
'flex h-5 shrink-0 items-stretch justify-between gap-2 border-t border-(--ui-stroke-tertiary) bg-(--glass-sidebar-surface-background) px-1 py-0 text-(--ui-text-tertiary) [-webkit-app-region:no-drag]',
|
||||
'flex h-5 shrink-0 items-stretch justify-between gap-2 border-t border-(--ui-stroke-tertiary) bg-(--ui-sidebar-surface-background) px-1 py-0 text-(--ui-text-tertiary) [-webkit-app-region:no-drag]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -183,10 +183,7 @@ function ProfilesMenuButton({ navigate }: { navigate: ReturnType<typeof useNavig
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
aria-label="Profiles"
|
||||
className={cn(
|
||||
titlebarButtonClass,
|
||||
'grid place-items-center bg-transparent select-none [&_svg]:size-4'
|
||||
)}
|
||||
className={cn(titlebarButtonClass, 'grid place-items-center bg-transparent select-none [&_svg]:size-4')}
|
||||
onPointerDown={event => event.stopPropagation()}
|
||||
title="Profiles"
|
||||
type="button"
|
||||
@@ -220,7 +217,7 @@ function TitlebarToolButton({ navigate, tool }: { navigate: ReturnType<typeof us
|
||||
const className = cn(
|
||||
titlebarButtonClass,
|
||||
'grid place-items-center bg-transparent select-none [&_svg]:size-4',
|
||||
tool.active && 'bg-(--ui-bg-tertiary)! text-foreground!',
|
||||
tool.active && 'bg-(--ui-control-active-background)! text-foreground!',
|
||||
tool.className
|
||||
)
|
||||
|
||||
|
||||
@@ -21,8 +21,6 @@ describe('titlebarControlsPosition', () => {
|
||||
})
|
||||
|
||||
it('uses the macOS fallback while the initial window state is unknown', () => {
|
||||
expect(titlebarControlsPosition(undefined).left).toBe(
|
||||
TITLEBAR_FALLBACK_WINDOW_BUTTON_X + TITLEBAR_CONTROL_OFFSET_X
|
||||
)
|
||||
expect(titlebarControlsPosition(undefined).left).toBe(TITLEBAR_FALLBACK_WINDOW_BUTTON_X + TITLEBAR_CONTROL_OFFSET_X)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,13 +13,13 @@ export const TITLEBAR_FALLBACK_WINDOW_BUTTON_X = 24
|
||||
export const TITLEBAR_EDGE_INSET = 14
|
||||
|
||||
export const titlebarButtonClass =
|
||||
'h-[var(--titlebar-control-height)] w-[var(--titlebar-control-size)] rounded-md text-muted-foreground/85 transition-colors hover:bg-(--ui-bg-tertiary) hover:text-foreground'
|
||||
'h-[var(--titlebar-control-height)] w-[var(--titlebar-control-size)] rounded-md text-muted-foreground/85 transition-colors hover:bg-(--ui-control-hover-background) hover:text-foreground'
|
||||
|
||||
export const titlebarHeaderBaseClass =
|
||||
'pointer-events-none relative z-3 flex h-(--titlebar-height) shrink-0 items-center justify-start gap-3 border-b border-(--ui-stroke-tertiary) bg-(--glass-chat-surface-background) px-[max(0.75rem,var(--titlebar-content-inset,0rem))]'
|
||||
'pointer-events-none relative z-3 flex h-(--titlebar-height) shrink-0 items-center justify-start gap-3 border-b border-(--ui-stroke-tertiary) bg-(--ui-chat-surface-background) px-[max(0.75rem,var(--titlebar-content-inset,0rem))]'
|
||||
|
||||
export const titlebarHeaderShadowClass =
|
||||
"after:pointer-events-none after:absolute after:left-0 after:right-0 after:top-full after:h-4 after:bg-linear-to-b after:from-(--glass-chat-surface-background) after:to-transparent after:content-['']"
|
||||
"after:pointer-events-none after:absolute after:left-0 after:right-0 after:top-full after:h-4 after:bg-linear-to-b after:from-(--ui-chat-surface-background) after:to-transparent after:content-['']"
|
||||
|
||||
export function titlebarControlsPosition(
|
||||
windowButtonPosition: HermesConnection['windowButtonPosition'] | undefined,
|
||||
|
||||
@@ -3,11 +3,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { PageLoader } from '@/components/page-loader'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { getSkills, getToolsets, toggleSkill } from '@/hermes'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { Brain, Wrench } from '@/lib/icons'
|
||||
import type { IconComponent } from '@/lib/icons'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { TextTab, TextTabMeta } from '@/components/ui/text-tab'
|
||||
import { getSkills, getToolsets, toggleSkill } from '@/hermes'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
import type { SkillInfo, ToolsetInfo } from '@/types/hermes'
|
||||
@@ -65,10 +64,7 @@ interface SkillsViewProps extends React.ComponentProps<'section'> {
|
||||
setStatusbarItemGroup?: SetStatusbarItemGroup
|
||||
}
|
||||
|
||||
export function SkillsView({
|
||||
setStatusbarItemGroup: _setStatusbarItemGroup,
|
||||
...props
|
||||
}: SkillsViewProps) {
|
||||
export function SkillsView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...props }: SkillsViewProps) {
|
||||
const [mode, setMode] = useRouteEnumParam('tab', SKILLS_MODES, 'skills')
|
||||
|
||||
const [query, setQuery] = useState('')
|
||||
@@ -157,31 +153,27 @@ export function SkillsView({
|
||||
{...props}
|
||||
filters={
|
||||
<>
|
||||
<div className="flex flex-wrap items-center justify-center gap-1.5">
|
||||
<ModeButton active={mode === 'skills'} icon={Brain} onClick={() => setMode('skills')} text="Skills" />
|
||||
<ModeButton
|
||||
active={mode === 'toolsets'}
|
||||
icon={Wrench}
|
||||
onClick={() => setMode('toolsets')}
|
||||
text="Toolsets"
|
||||
/>
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-2 gap-y-1">
|
||||
<TextTab active={mode === 'skills'} onClick={() => setMode('skills')}>
|
||||
Skills
|
||||
</TextTab>
|
||||
<TextTab active={mode === 'toolsets'} onClick={() => setMode('toolsets')}>
|
||||
Toolsets
|
||||
</TextTab>
|
||||
</div>
|
||||
{mode === 'skills' && categories.length > 0 && (
|
||||
<div className="flex flex-wrap justify-center gap-1.5">
|
||||
<CategoryButton
|
||||
active={activeCategory === null}
|
||||
count={totalSkills}
|
||||
label="All"
|
||||
onClick={() => setActiveCategory(null)}
|
||||
/>
|
||||
<div className="flex flex-wrap justify-center gap-x-2 gap-y-1">
|
||||
<TextTab active={activeCategory === null} onClick={() => setActiveCategory(null)}>
|
||||
All <TextTabMeta>{totalSkills}</TextTabMeta>
|
||||
</TextTab>
|
||||
{categories.map(category => (
|
||||
<CategoryButton
|
||||
<TextTab
|
||||
active={activeCategory === category.key}
|
||||
count={category.count}
|
||||
key={category.key}
|
||||
label={prettyName(category.key)}
|
||||
onClick={() => setActiveCategory(activeCategory === category.key ? null : category.key)}
|
||||
/>
|
||||
>
|
||||
{prettyName(category.key)} <TextTabMeta>{category.count}</TextTabMeta>
|
||||
</TextTab>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -192,7 +184,7 @@ export function SkillsView({
|
||||
searchTrailingAction={
|
||||
<Button
|
||||
aria-label={refreshing ? 'Refreshing skills' : 'Refresh skills'}
|
||||
className="text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground"
|
||||
className="text-(--ui-text-tertiary) hover:bg-transparent hover:text-foreground"
|
||||
disabled={refreshing}
|
||||
onClick={() => void refreshCapabilities()}
|
||||
size="icon-xs"
|
||||
@@ -205,167 +197,102 @@ export function SkillsView({
|
||||
}
|
||||
searchValue={query}
|
||||
>
|
||||
|
||||
{!skills || !toolsets ? (
|
||||
<PageLoader label="Loading capabilities..." />
|
||||
) : mode === 'skills' ? (
|
||||
<div className="h-full overflow-y-auto px-4 py-3">
|
||||
{visibleSkills.length === 0 ? (
|
||||
<EmptyState description="Try a broader search or different category." title="No skills found" />
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{skillGroups.map(([category, list]) => (
|
||||
<div className="space-y-1.5" key={category}>
|
||||
<div className="text-[0.68rem] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
|
||||
{prettyName(category)}
|
||||
</div>
|
||||
<div className="divide-y divide-(--ui-stroke-quaternary)">
|
||||
{list.map(skill => (
|
||||
<div
|
||||
className="grid gap-3 px-0 py-2.5 transition-colors hover:bg-(--ui-bg-quinary) sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center"
|
||||
key={skill.name}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-medium">{skill.name}</div>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{asText(skill.description) || 'No description.'}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={skill.enabled}
|
||||
disabled={savingSkill === skill.name}
|
||||
onCheckedChange={checked => void handleToggleSkill(skill, checked)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{!skills || !toolsets ? (
|
||||
<PageLoader label="Loading capabilities..." />
|
||||
) : mode === 'skills' ? (
|
||||
<div className="h-full overflow-y-auto px-4 py-3">
|
||||
{visibleSkills.length === 0 ? (
|
||||
<EmptyState description="Try a broader search or different category." title="No skills found" />
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{skillGroups.map(([category, list]) => (
|
||||
<div className="space-y-1.5" key={category}>
|
||||
<div className="text-[0.68rem] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
|
||||
{prettyName(category)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full overflow-y-auto px-4 py-3">
|
||||
{visibleToolsets.length === 0 ? (
|
||||
<EmptyState description="Try a broader search query." title="No toolsets found" />
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{enabledToolsets}/{toolsets.length} toolsets enabled
|
||||
</div>
|
||||
<div className="divide-y divide-(--ui-stroke-quaternary)">
|
||||
{visibleToolsets.map(toolset => {
|
||||
const tools = toolNames(toolset)
|
||||
const label = asText(toolset.label || toolset.name)
|
||||
|
||||
return (
|
||||
<div className="px-0 py-2.5 transition-colors hover:bg-(--ui-bg-quinary)" key={toolset.name}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="truncate text-sm font-medium">{label}</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<StatusPill active={toolset.enabled}>{toolset.enabled ? 'Enabled' : 'Disabled'}</StatusPill>
|
||||
<StatusPill active={toolset.configured}>
|
||||
{toolset.configured ? 'Configured' : 'Needs keys'}
|
||||
</StatusPill>
|
||||
</div>
|
||||
<div className="divide-y divide-(--ui-stroke-quaternary)">
|
||||
{list.map(skill => (
|
||||
<div
|
||||
className="grid gap-3 px-0 py-2.5 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center"
|
||||
key={skill.name}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-medium">{skill.name}</div>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{asText(skill.description) || 'No description.'}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{asText(toolset.description) || 'No description.'}
|
||||
</p>
|
||||
{tools.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{tools.map(name => (
|
||||
<span
|
||||
className="rounded-md bg-(--ui-bg-quinary) px-1.5 py-0.5 font-mono text-[0.65rem] text-(--ui-text-tertiary)"
|
||||
key={name}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Switch
|
||||
checked={skill.enabled}
|
||||
disabled={savingSkill === skill.name}
|
||||
onCheckedChange={checked => void handleToggleSkill(skill, checked)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full overflow-y-auto px-4 py-3">
|
||||
{visibleToolsets.length === 0 ? (
|
||||
<EmptyState description="Try a broader search query." title="No toolsets found" />
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{enabledToolsets}/{toolsets.length} toolsets enabled
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="divide-y divide-(--ui-stroke-quaternary)">
|
||||
{visibleToolsets.map(toolset => {
|
||||
const tools = toolNames(toolset)
|
||||
const label = asText(toolset.label || toolset.name)
|
||||
|
||||
return (
|
||||
<div className="px-0 py-2.5" key={toolset.name}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="truncate text-sm font-medium">{label}</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<StatusPill active={toolset.enabled}>{toolset.enabled ? 'Enabled' : 'Disabled'}</StatusPill>
|
||||
<StatusPill active={toolset.configured}>
|
||||
{toolset.configured ? 'Configured' : 'Needs keys'}
|
||||
</StatusPill>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{asText(toolset.description) || 'No description.'}
|
||||
</p>
|
||||
{tools.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{tools.map(name => (
|
||||
<span
|
||||
className="rounded-md bg-(--ui-bg-quinary) px-1.5 py-0.5 font-mono text-[0.65rem] text-(--ui-text-tertiary)"
|
||||
key={name}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PageSearchShell>
|
||||
)
|
||||
}
|
||||
|
||||
function ModeButton({
|
||||
active,
|
||||
icon: Icon,
|
||||
onClick,
|
||||
text
|
||||
}: {
|
||||
active: boolean
|
||||
icon: IconComponent
|
||||
onClick: () => void
|
||||
text: string
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
'h-8 gap-1.5 rounded-md px-2.5 text-xs',
|
||||
active
|
||||
? 'bg-(--ui-bg-tertiary) text-foreground'
|
||||
: 'text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground'
|
||||
)}
|
||||
onClick={onClick}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
{text}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function CategoryButton({
|
||||
active,
|
||||
count,
|
||||
label,
|
||||
onClick
|
||||
}: {
|
||||
active: boolean
|
||||
count: number
|
||||
label: string
|
||||
onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'inline-flex h-7 items-center gap-1 rounded-md bg-transparent px-1.5 text-[0.68rem] transition-colors',
|
||||
active
|
||||
? 'bg-(--ui-bg-tertiary) text-foreground'
|
||||
: 'text-(--ui-text-tertiary) hover:bg-(--chrome-action-hover) hover:text-foreground'
|
||||
)}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className={cn('underline-offset-4 decoration-current', active ? 'font-medium underline' : 'hover:underline')}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-[0.62rem] text-muted-foreground/80 no-underline">{count}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusPill({ active, children }: { active: boolean; children: string }) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full px-1.5 py-0.5 text-[0.64rem]',
|
||||
active
|
||||
? 'bg-(--ui-bg-tertiary) text-(--ui-text-secondary)'
|
||||
: 'bg-(--ui-bg-quinary) text-(--ui-text-tertiary)'
|
||||
active ? 'bg-(--ui-bg-tertiary) text-(--ui-text-secondary)' : 'bg-(--ui-bg-quinary) text-(--ui-text-tertiary)'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -52,13 +52,7 @@ export type CommandDispatchResponse =
|
||||
| SkillCommandDispatchResponse
|
||||
| SendCommandDispatchResponse
|
||||
|
||||
export type SidebarNavId =
|
||||
| 'artifacts'
|
||||
| 'command-center'
|
||||
| 'messaging'
|
||||
| 'new-session'
|
||||
| 'settings'
|
||||
| 'skills'
|
||||
export type SidebarNavId = 'artifacts' | 'command-center' | 'messaging' | 'new-session' | 'settings' | 'skills'
|
||||
|
||||
export interface SidebarNavItem {
|
||||
id: SidebarNavId
|
||||
|
||||
Reference in New Issue
Block a user