feat: better icons and overlay panes
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { appendAssistantTextPart, chatMessageText, renderMediaTags, toChatMessages, upsertToolPart } from './chat-messages'
|
||||
import {
|
||||
appendAssistantTextPart,
|
||||
chatMessageText,
|
||||
renderMediaTags,
|
||||
toChatMessages,
|
||||
upsertToolPart
|
||||
} from './chat-messages'
|
||||
|
||||
describe('toChatMessages', () => {
|
||||
it('keeps a turn with interleaved tool-only rows in a single bubble', () => {
|
||||
@@ -65,8 +71,7 @@ describe('toChatMessages', () => {
|
||||
const [message] = toChatMessages([
|
||||
{
|
||||
content: {
|
||||
text:
|
||||
'look\n\n--- Attached Context ---\n\n📄 @file:foo.ts (10 tokens)\n```ts\nconst x = 1\n```'
|
||||
text: 'look\n\n--- Attached Context ---\n\n📄 @file:foo.ts (10 tokens)\n```ts\nconst x = 1\n```'
|
||||
},
|
||||
role: 'user',
|
||||
timestamp: 1
|
||||
|
||||
@@ -52,8 +52,7 @@ export function reasoningPart(text: string): ChatMessagePart {
|
||||
return { type: 'reasoning', text }
|
||||
}
|
||||
|
||||
const MEDIA_LINE_RE =
|
||||
/(^|\n)[\t ]*[`"']?MEDIA:\s*(?<line>`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|\S+)[`"']?[\t ]*(\n|$)/g
|
||||
const MEDIA_LINE_RE = /(^|\n)[\t ]*[`"']?MEDIA:\s*(?<line>`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|\S+)[`"']?[\t ]*(\n|$)/g
|
||||
|
||||
const MEDIA_TAG_RE = /[`"']?MEDIA:\s*(?<inline>`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|\S+)[`"']?/g
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
desktopSkinSlashCompletions,
|
||||
desktopSlashDescription,
|
||||
desktopSlashUnavailableMessage,
|
||||
desktopSkinSlashCompletions,
|
||||
filterDesktopCommandsCatalog,
|
||||
isDesktopSlashCommand,
|
||||
isDesktopSlashSuggestion
|
||||
|
||||
@@ -196,6 +196,7 @@ export function desktopSkinSlashCompletions(
|
||||
argPrefix: string
|
||||
): DesktopSlashCompletion[] {
|
||||
const prefix = argPrefix.trim().toLowerCase()
|
||||
|
||||
const commands: DesktopSlashCompletion[] = [
|
||||
{
|
||||
text: '/skin list',
|
||||
@@ -243,9 +244,5 @@ export function filterDesktopCommandsCatalog(catalog: CommandsCatalogLike): Comm
|
||||
}
|
||||
|
||||
function isKnownHermesSlashCommand(command: string): boolean {
|
||||
return (
|
||||
DESKTOP_COMMANDS.has(command) ||
|
||||
DESKTOP_ALIASES.has(command) ||
|
||||
BLOCKED_COMMANDS.has(command)
|
||||
)
|
||||
return DESKTOP_COMMANDS.has(command) || DESKTOP_ALIASES.has(command) || BLOCKED_COMMANDS.has(command)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ export function extractEmbeddedImages(text: string): EmbeddedImageExtraction {
|
||||
}
|
||||
|
||||
const images: string[] = []
|
||||
|
||||
const cleanedText = text
|
||||
.replace(EMBEDDED_IMAGE_RE, (_match, _open, dataUrl: string) => {
|
||||
images.push(dataUrl)
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import {
|
||||
IconActivity as Activity,
|
||||
IconAlertCircle as AlertCircle,
|
||||
IconAlertTriangle as AlertTriangle,
|
||||
IconArrowUp as ArrowUp,
|
||||
IconAt as AtSign,
|
||||
IconWaveSine as AudioLines,
|
||||
IconBrain as Brain,
|
||||
IconBug as Bug,
|
||||
IconCheck as Check,
|
||||
IconCircleCheck as CheckCircle2,
|
||||
IconCheck as CheckIcon,
|
||||
IconChevronDown as ChevronDown,
|
||||
IconChevronDown as ChevronDownIcon,
|
||||
IconChevronLeft as ChevronLeft,
|
||||
IconChevronLeft as ChevronLeftIcon,
|
||||
IconChevronRight as ChevronRight,
|
||||
IconChevronRight as ChevronRightIcon,
|
||||
IconCircle as CircleIcon,
|
||||
IconClipboard as Clipboard,
|
||||
IconCommand as Command,
|
||||
IconCopy as Copy,
|
||||
IconCopy as CopyIcon,
|
||||
IconCpu as Cpu,
|
||||
IconDownload as Download,
|
||||
IconExternalLink as ExternalLink,
|
||||
IconEye as Eye,
|
||||
IconEyeOff as EyeOff,
|
||||
IconPhoto as FileImage,
|
||||
IconFileText as FileText,
|
||||
IconFolderOpen as FolderOpen,
|
||||
IconGitBranch as GitBranch,
|
||||
IconGitBranch as GitBranchIcon,
|
||||
IconGlobe as Globe,
|
||||
IconHelpCircle as HelpCircle,
|
||||
IconPhoto as ImageIcon,
|
||||
IconInfoCircle as Info,
|
||||
IconKey as KeyRound,
|
||||
IconLayersIntersect2 as Layers3,
|
||||
IconLink as Link,
|
||||
IconLink as Link2,
|
||||
IconLink as LinkIcon,
|
||||
IconLoader2 as Loader2,
|
||||
IconLoader2 as Loader2Icon,
|
||||
IconLock as Lock,
|
||||
IconMessageCircle as MessageCircle,
|
||||
IconMessage2 as MessageSquareText,
|
||||
IconMicrophone as Mic,
|
||||
IconMicrophoneOff as MicOff,
|
||||
IconDeviceDesktop as Monitor,
|
||||
IconDeviceDesktopAnalytics as MonitorPlay,
|
||||
IconMoon as Moon,
|
||||
IconDots as MoreHorizontal,
|
||||
IconDots as MoreHorizontalIcon,
|
||||
IconDotsVertical as MoreVertical,
|
||||
IconNotebook as NotebookTabs,
|
||||
IconPackage as Package,
|
||||
IconPalette as Palette,
|
||||
IconLayoutBottombar as PanelBottom,
|
||||
IconLayoutSidebar as PanelLeftIcon,
|
||||
IconPencil as Pencil,
|
||||
IconPencil as PencilIcon,
|
||||
IconPencil as PencilLine,
|
||||
IconPin as Pin,
|
||||
IconPlus as Plus,
|
||||
IconRefresh as RefreshCw,
|
||||
IconRefresh as RefreshCwIcon,
|
||||
IconDeviceFloppy as Save,
|
||||
IconSearch as Search,
|
||||
IconSearch as SearchIcon,
|
||||
IconSend as Send,
|
||||
IconSettings as Settings,
|
||||
IconSettings2 as Settings2,
|
||||
IconAdjustmentsHorizontal as SlidersHorizontal,
|
||||
IconSparkles as Sparkles,
|
||||
IconSquare as Square,
|
||||
IconSun as Sun,
|
||||
IconTrash as Trash2,
|
||||
IconVolume2 as Volume2,
|
||||
IconVolume2 as Volume2Icon,
|
||||
IconVolumeOff as VolumeX,
|
||||
IconVolumeOff as VolumeXIcon,
|
||||
IconTool as Wrench,
|
||||
IconX as X,
|
||||
IconX as XIcon,
|
||||
IconBolt as Zap
|
||||
} from '@tabler/icons-react'
|
||||
|
||||
export {
|
||||
Activity,
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
ArrowUp,
|
||||
AtSign,
|
||||
AudioLines,
|
||||
Brain,
|
||||
Bug,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
CheckIcon,
|
||||
ChevronDown,
|
||||
ChevronDownIcon,
|
||||
ChevronLeft,
|
||||
ChevronLeftIcon,
|
||||
ChevronRight,
|
||||
ChevronRightIcon,
|
||||
CircleIcon,
|
||||
Clipboard,
|
||||
Command,
|
||||
Copy,
|
||||
CopyIcon,
|
||||
Cpu,
|
||||
Download,
|
||||
ExternalLink,
|
||||
Eye,
|
||||
EyeOff,
|
||||
FileImage,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
GitBranch,
|
||||
GitBranchIcon,
|
||||
Globe,
|
||||
HelpCircle,
|
||||
ImageIcon,
|
||||
Info,
|
||||
KeyRound,
|
||||
Layers3,
|
||||
Link,
|
||||
Link2,
|
||||
LinkIcon,
|
||||
Loader2,
|
||||
Loader2Icon,
|
||||
Lock,
|
||||
MessageCircle,
|
||||
MessageSquareText,
|
||||
Mic,
|
||||
MicOff,
|
||||
Monitor,
|
||||
MonitorPlay,
|
||||
Moon,
|
||||
MoreHorizontal,
|
||||
MoreHorizontalIcon,
|
||||
MoreVertical,
|
||||
NotebookTabs,
|
||||
Package,
|
||||
Palette,
|
||||
PanelBottom,
|
||||
PanelLeftIcon,
|
||||
Pencil,
|
||||
PencilIcon,
|
||||
PencilLine,
|
||||
Pin,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
RefreshCwIcon,
|
||||
Save,
|
||||
Search,
|
||||
SearchIcon,
|
||||
Send,
|
||||
Settings,
|
||||
Settings2,
|
||||
SlidersHorizontal,
|
||||
Sparkles,
|
||||
Square,
|
||||
Sun,
|
||||
Trash2,
|
||||
Volume2,
|
||||
Volume2Icon,
|
||||
VolumeX,
|
||||
VolumeXIcon,
|
||||
Wrench,
|
||||
X,
|
||||
XIcon,
|
||||
Zap
|
||||
}
|
||||
|
||||
export type { LucideIcon } from 'lucide-react'
|
||||
@@ -1,5 +1,6 @@
|
||||
const VALID_LANGUAGE_RE = /^[a-z0-9][a-z0-9+#-]*$/i
|
||||
const NON_CODE_FENCE_LANGUAGES = new Set(['', 'text', 'plain', 'plaintext', 'md', 'markdown'])
|
||||
|
||||
const COMMON_CODE_LANGUAGES = new Set([
|
||||
'bash',
|
||||
'c',
|
||||
@@ -48,14 +49,11 @@ export function sanitizeLanguageTag(tag: string): string {
|
||||
}
|
||||
|
||||
function proseLineCount(body: string): number {
|
||||
return body
|
||||
.split('\n')
|
||||
.filter(line => {
|
||||
const trimmed = line.trim()
|
||||
return body.split('\n').filter(line => {
|
||||
const trimmed = line.trim()
|
||||
|
||||
return Boolean(trimmed) && /^[A-Za-z0-9"'`*-]/.test(trimmed)
|
||||
})
|
||||
.length
|
||||
return Boolean(trimmed) && /^[A-Za-z0-9"'`*-]/.test(trimmed)
|
||||
}).length
|
||||
}
|
||||
|
||||
const CODE_SIGNAL_RE = [
|
||||
|
||||
@@ -97,7 +97,12 @@ function isLocalPreviewUrl(value: string): boolean {
|
||||
export function isLikelyPreviewCandidate(value: string): boolean {
|
||||
const trimmed = stripTrailingPunctuation(value.trim())
|
||||
|
||||
return isHtmlFileUrl(trimmed) || HTML_EXT_RE.test(trimmed) || isPreviewDirectoryCandidate(trimmed) || isLocalPreviewUrl(trimmed)
|
||||
return (
|
||||
isHtmlFileUrl(trimmed) ||
|
||||
HTML_EXT_RE.test(trimmed) ||
|
||||
isPreviewDirectoryCandidate(trimmed) ||
|
||||
isLocalPreviewUrl(trimmed)
|
||||
)
|
||||
}
|
||||
|
||||
function collectPreviewMatches(text: string): PreviewCandidateMatch[] {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { SessionInfo } from '@/hermes'
|
||||
import { getSessionMessages } from '@/hermes'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
|
||||
interface ExportSessionParams {
|
||||
sessionId: string
|
||||
title?: string | null
|
||||
session?: SessionInfo
|
||||
}
|
||||
|
||||
function sanitizeFilenamePart(value: string) {
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9._-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 48)
|
||||
}
|
||||
|
||||
function sessionExportFilename(sessionId: string, title?: string | null) {
|
||||
const titlePart = title ? sanitizeFilenamePart(title) : ''
|
||||
const idPart = sanitizeFilenamePart(sessionId).slice(0, 8) || 'session'
|
||||
|
||||
return `${titlePart || 'session'}-${idPart}.json`
|
||||
}
|
||||
|
||||
export async function exportSession(sessionId: string, params: Omit<ExportSessionParams, 'sessionId'> = {}) {
|
||||
if (!sessionId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { messages } = await getSessionMessages(sessionId)
|
||||
|
||||
const payload = {
|
||||
exported_at: new Date().toISOString(),
|
||||
session_id: sessionId,
|
||||
title: params.title ?? null,
|
||||
session: params.session ?? null,
|
||||
message_count: messages.length,
|
||||
messages
|
||||
}
|
||||
|
||||
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
|
||||
const downloadUrl = URL.createObjectURL(blob)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = downloadUrl
|
||||
anchor.download = sessionExportFilename(sessionId, params.title)
|
||||
anchor.click()
|
||||
URL.revokeObjectURL(downloadUrl)
|
||||
|
||||
notify({ kind: 'success', message: 'Session exported', durationMs: 2_000 })
|
||||
} catch (err) {
|
||||
notifyError(err, 'Could not export session')
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user