@@ -364,15 +398,20 @@ export function ToolCallRenderer({ toolCall, result, isError, needsApproval, onA
)
}
- case 'ls': {
+ case "ls": {
if (Array.isArray(result)) {
- const dirs = result.filter((f: { is_dir?: boolean } | string) => typeof f === 'object' && f.is_dir).length
+ const dirs = result.filter(
+ (f: { is_dir?: boolean } | string) => typeof f === "object" && f.is_dir
+ ).length
const files = result.length - dirs
return (
- {files} file{files !== 1 ? 's' : ''}{dirs > 0 ? `, ${dirs} folder${dirs !== 1 ? 's' : ''}` : ''}
+
+ {files} file{files !== 1 ? "s" : ""}
+ {dirs > 0 ? `, ${dirs} folder${dirs !== 1 ? "s" : ""}` : ""}
+
@@ -381,13 +420,15 @@ export function ToolCallRenderer({ toolCall, result, isError, needsApproval, onA
return null
}
- case 'glob': {
+ case "glob": {
if (Array.isArray(result)) {
return (
- Found {result.length} match{result.length !== 1 ? 'es' : ''}
+
+ Found {result.length} match{result.length !== 1 ? "es" : ""}
+
@@ -396,14 +437,17 @@ export function ToolCallRenderer({ toolCall, result, isError, needsApproval, onA
return null
}
- case 'grep': {
+ case "grep": {
if (Array.isArray(result)) {
const fileCount = new Set(result.map((m: { path: string }) => m.path)).size
return (
- {result.length} match{result.length !== 1 ? 'es' : ''} in {fileCount} file{fileCount !== 1 ? 's' : ''}
+
+ {result.length} match{result.length !== 1 ? "es" : ""} in {fileCount} file
+ {fileCount !== 1 ? "s" : ""}
+
@@ -412,10 +456,10 @@ export function ToolCallRenderer({ toolCall, result, isError, needsApproval, onA
return null
}
- case 'execute': {
+ case "execute": {
// When expanded, output is shown in CommandDisplay - just show status
// When collapsed, show the output preview
- const output = typeof result === 'string' ? result : JSON.stringify(result)
+ const output = typeof result === "string" ? result : JSON.stringify(result)
if (isExpanded) {
return (
@@ -434,7 +478,7 @@ export function ToolCallRenderer({ toolCall, result, isError, needsApproval, onA
{output.slice(0, 500)}
- {output.length > 500 && '...'}
+ {output.length > 500 && "..."}
)
@@ -447,14 +491,14 @@ export function ToolCallRenderer({ toolCall, result, isError, needsApproval, onA
)
}
- case 'write_todos':
+ case "write_todos":
// Already shown in Tasks panel
return null
- case 'write_file':
- case 'edit_file': {
+ case "write_file":
+ case "edit_file": {
// Show confirmation message for file operations
- if (typeof result === 'string' && result.trim()) {
+ if (typeof result === "string" && result.trim()) {
return (
@@ -470,9 +514,9 @@ export function ToolCallRenderer({ toolCall, result, isError, needsApproval, onA
)
}
- case 'task': {
+ case "task": {
// Subagent task completion
- if (typeof result === 'string' && result.trim()) {
+ if (typeof result === "string" && result.trim()) {
return (
@@ -481,7 +525,7 @@ export function ToolCallRenderer({ toolCall, result, isError, needsApproval, onA
{result.slice(0, 500)}
- {result.length > 500 && '...'}
+ {result.length > 500 && "..."}
)
@@ -496,11 +540,14 @@ export function ToolCallRenderer({ toolCall, result, isError, needsApproval, onA
default: {
// Generic success for unknown tools
- if (typeof result === 'string' && result.trim()) {
+ if (typeof result === "string" && result.trim()) {
return (
- {result.slice(0, 100)}{result.length > 100 ? '...' : ''}
+
+ {result.slice(0, 100)}
+ {result.length > 100 ? "..." : ""}
+
)
}
@@ -519,10 +566,14 @@ export function ToolCallRenderer({ toolCall, result, isError, needsApproval, onA
const hasFormattedDisplay = formattedContent || formattedResult
return (
-
+
{/* Header */}
setIsExpanded(!isExpanded)}
@@ -534,7 +585,9 @@ export function ToolCallRenderer({ toolCall, result, isError, needsApproval, onA
)}
-
+
{label}
@@ -557,8 +610,8 @@ export function ToolCallRenderer({ toolCall, result, isError, needsApproval, onA
)}
{result !== undefined && !needsApproval && (
-
- {isError ? 'ERROR' : 'OK'}
+
+ {isError ? "ERROR" : "OK"}
)}
@@ -628,11 +681,13 @@ export function ToolCallRenderer({ toolCall, result, isError, needsApproval, onA
{result !== undefined && (
RAW RESULT
-
- {typeof result === 'string' ? result : JSON.stringify(result, null, 2)}
+
+ {typeof result === "string" ? result : JSON.stringify(result, null, 2)}
)}
@@ -640,4 +695,4 @@ export function ToolCallRenderer({ toolCall, result, isError, needsApproval, onA
)}
)
-}
\ No newline at end of file
+}
diff --git a/src/renderer/src/components/chat/WorkspacePicker.tsx b/src/renderer/src/components/chat/WorkspacePicker.tsx
index c8db25f..fa2dbbf 100644
--- a/src/renderer/src/components/chat/WorkspacePicker.tsx
+++ b/src/renderer/src/components/chat/WorkspacePicker.tsx
@@ -1,14 +1,10 @@
-import { selectWorkspaceFolder } from '@/lib/workspace-utils'
-import { Check, ChevronDown, Folder } from 'lucide-react'
-import { useState, useEffect } from 'react'
-import { Button } from '@/components/ui/button'
-import {
- Popover,
- PopoverContent,
- PopoverTrigger
-} from '@/components/ui/popover'
-import { useCurrentThread } from '@/lib/thread-context'
-import { cn } from '@/lib/utils'
+import { selectWorkspaceFolder } from "@/lib/workspace-utils"
+import { Check, ChevronDown, Folder } from "lucide-react"
+import { useState, useEffect } from "react"
+import { Button } from "@/components/ui/button"
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
+import { useCurrentThread } from "@/lib/thread-context"
+import { cn } from "@/lib/utils"
interface WorkspacePickerProps {
threadId: string
@@ -43,7 +39,7 @@ export function WorkspacePicker({ threadId }: WorkspacePickerProps): React.JSX.E
await selectWorkspaceFolder(threadId, setWorkspacePath, setWorkspaceFiles, setLoading, setOpen)
}
- const folderName = workspacePath?.split('/').pop()
+ const folderName = workspacePath?.split("/").pop()
return (
@@ -52,14 +48,14 @@ export function WorkspacePicker({ threadId }: WorkspacePickerProps): React.JSX.E
variant="ghost"
size="sm"
className={cn(
- 'h-7 px-2 text-xs gap-1.5',
- workspacePath ? 'text-foreground' : 'text-amber-500'
+ "h-7 px-2 text-xs gap-1.5",
+ workspacePath ? "text-foreground" : "text-amber-500"
)}
disabled={!threadId}
>
- {workspacePath ? folderName : 'Select workspace'}
+ {workspacePath ? folderName : "Select workspace"}
@@ -94,7 +90,8 @@ export function WorkspacePicker({ threadId }: WorkspacePickerProps): React.JSX.E
) : (
- Select a folder for the agent to work in. The agent will read and write files directly to this location.
+ Select a folder for the agent to work in. The agent will read and write files
+ directly to this location.
>(new Set())
const [loading, setLoading] = useState(false)
-
+
// Load workspace path for current thread
useEffect(() => {
async function loadWorkspacePath() {
@@ -28,14 +36,14 @@ export function FilesystemPanel() {
loadWorkspacePath()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentThreadId])
-
+
// Auto-expand root when workspace path changes
useEffect(() => {
if (workspacePath) {
setExpandedDirs(new Set([workspacePath]))
}
}, [workspacePath])
-
+
// Listen for file changes from the main process
useEffect(() => {
if (!setWorkspaceFiles) return
@@ -43,26 +51,26 @@ export function FilesystemPanel() {
const cleanup = window.api.workspace.onFilesChanged(async (data) => {
// Only refresh if this is the current thread
if (data.threadId === currentThreadId) {
- console.log('[FilesystemPanel] Files changed, refreshing...')
+ console.log("[FilesystemPanel] Files changed, refreshing...")
try {
const result = await window.api.workspace.loadFromDisk(data.threadId)
if (result.success) {
setWorkspaceFiles(result.files)
}
} catch (e) {
- console.error('[FilesystemPanel] Error refreshing files:', e)
+ console.error("[FilesystemPanel] Error refreshing files:", e)
}
}
})
-
+
return cleanup
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentThreadId])
-
+
// Handle selecting a workspace folder
async function handleSelectFolder() {
if (!currentThreadId || !setWorkspacePath || !setWorkspaceFiles) return
-
+
setLoading(true)
try {
const path = await window.api.workspace.select(currentThreadId)
@@ -75,16 +83,16 @@ export function FilesystemPanel() {
}
}
} catch (e) {
- console.error('[FilesystemPanel] Select folder error:', e)
+ console.error("[FilesystemPanel] Select folder error:", e)
} finally {
setLoading(false)
}
}
-
+
// Handle refreshing files from disk
async function handleRefresh() {
if (!currentThreadId || !setWorkspaceFiles) return
-
+
setLoading(true)
try {
const result = await window.api.workspace.loadFromDisk(currentThreadId)
@@ -92,20 +100,20 @@ export function FilesystemPanel() {
setWorkspaceFiles(result.files)
}
} catch (e) {
- console.error('[FilesystemPanel] Refresh error:', e)
+ console.error("[FilesystemPanel] Refresh error:", e)
} finally {
setLoading(false)
}
}
// Normalize path to always start with /
- const normalizePath = (p: string) => p.startsWith('/') ? p : '/' + p
+ const normalizePath = (p: string) => (p.startsWith("/") ? p : "/" + p)
// Get parent path, always returns / for root-level items
const getParentPath = (p: string) => {
const normalized = normalizePath(p)
- const lastSlash = normalized.lastIndexOf('/')
- if (lastSlash <= 0) return '/'
+ const lastSlash = normalized.lastIndexOf("/")
+ if (lastSlash <= 0) return "/"
return normalized.substring(0, lastSlash)
}
@@ -113,48 +121,48 @@ export function FilesystemPanel() {
const buildTree = (files: FileInfo[]) => {
const tree: Map = new Map()
const allDirs = new Set()
-
+
// First pass: collect all directories (both explicit and implicit)
- files.forEach(file => {
+ files.forEach((file) => {
const normalized = normalizePath(file.path)
-
+
// Walk up the path to collect all parent directories
let current = getParentPath(normalized)
- while (current !== '/') {
+ while (current !== "/") {
allDirs.add(current)
current = getParentPath(current)
}
-
+
// If this is an explicit directory entry, add it
if (file.is_dir) {
- const dirPath = normalized.endsWith('/') ? normalized.slice(0, -1) : normalized
+ const dirPath = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized
allDirs.add(dirPath)
}
})
-
+
// Second pass: add files and directories to their parent's children list
- files.forEach(file => {
- const normalized = normalizePath(file.path.endsWith('/') ? file.path.slice(0, -1) : file.path)
+ files.forEach((file) => {
+ const normalized = normalizePath(file.path.endsWith("/") ? file.path.slice(0, -1) : file.path)
const parentPath = getParentPath(normalized)
-
+
if (!tree.has(parentPath)) {
tree.set(parentPath, [])
}
-
+
// Use normalized path in the file info for consistent tree lookups
tree.get(parentPath)!.push({
...file,
path: normalized
})
})
-
+
// Third pass: add implicit directories as entries
- allDirs.forEach(dir => {
+ allDirs.forEach((dir) => {
const parentPath = getParentPath(dir)
-
+
// Check if this directory is already in parent's children
const siblings = tree.get(parentPath) || []
- if (!siblings.some(f => f.path === dir)) {
+ if (!siblings.some((f) => f.path === dir)) {
if (!tree.has(parentPath)) {
tree.set(parentPath, [])
}
@@ -164,7 +172,7 @@ export function FilesystemPanel() {
})
}
})
-
+
// Sort children: directories first, then alphabetically
tree.forEach((children) => {
children.sort((a, b) => {
@@ -173,14 +181,14 @@ export function FilesystemPanel() {
return a.path.localeCompare(b.path)
})
})
-
+
return tree
}
const tree = buildTree(workspaceFiles)
const toggleDir = (path: string) => {
- setExpandedDirs(prev => {
+ setExpandedDirs((prev) => {
const next = new Set(prev)
if (next.has(path)) {
next.delete(path)
@@ -192,7 +200,7 @@ export function FilesystemPanel() {
}
const renderNode = (file: FileInfo, depth: number = 0) => {
- const name = file.path.split('/').pop() || file.path
+ const name = file.path.split("/").pop() || file.path
const isExpanded = expandedDirs.has(file.path)
const children = tree.get(file.path) || []
@@ -201,7 +209,7 @@ export function FilesystemPanel() {
file.is_dir && toggleDir(file.path)}
className={cn(
- "flex w-full items-center gap-2 px-3 py-1.5 text-sm hover:bg-background-interactive transition-colors",
+ "flex w-full items-center gap-2 px-3 py-1.5 text-sm hover:bg-background-interactive transition-colors"
)}
style={{ paddingLeft: `${depth * 16 + 12}px` }}
>
@@ -227,14 +235,14 @@ export function FilesystemPanel() {
)}
-
- {file.is_dir && isExpanded && children.map(child => renderNode(child, depth + 1))}
+
+ {file.is_dir && isExpanded && children.map((child) => renderNode(child, depth + 1))}
)
}
// Get root level items (all paths are normalized to start with /)
- const rootItems = tree.get('/') || []
+ const rootItems = tree.get("/") || []
// If no workspace is selected, show selection prompt
if (!workspacePath) {
@@ -274,8 +282,11 @@ export function FilesystemPanel() {
WORKSPACE
-
- {workspacePath.split('/').pop()}
+
+ {workspacePath.split("/").pop()}
-
+
{rootItems.length === 0 ? (
@@ -316,7 +327,7 @@ export function FilesystemPanel() {
) : (
- rootItems.map(file => renderNode(file))
+ rootItems.map((file) => renderNode(file))
)}
diff --git a/src/renderer/src/components/panels/RightPanel.tsx b/src/renderer/src/components/panels/RightPanel.tsx
index 424f286..a938902 100644
--- a/src/renderer/src/components/panels/RightPanel.tsx
+++ b/src/renderer/src/components/panels/RightPanel.tsx
@@ -1,4 +1,4 @@
-import { useState, useRef, useCallback, useEffect, useMemo } from 'react'
+import { useState, useRef, useCallback, useEffect, useMemo } from "react"
import {
ListTodo,
FolderTree,
@@ -22,13 +22,13 @@ import {
FileJson,
Image,
FileType
-} from 'lucide-react'
-import { cn } from '@/lib/utils'
-import { useAppStore } from '@/lib/store'
-import { useThreadState } from '@/lib/thread-context'
-import { Badge } from '@/components/ui/badge'
-import { Button } from '@/components/ui/button'
-import type { Todo } from '@/types'
+} from "lucide-react"
+import { cn } from "@/lib/utils"
+import { useAppStore } from "@/lib/store"
+import { useThreadState } from "@/lib/thread-context"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import type { Todo } from "@/types"
const HEADER_HEIGHT = 40 // px
const HANDLE_HEIGHT = 6 // px
@@ -58,8 +58,8 @@ function SectionHeader({
>
@@ -90,16 +90,16 @@ function ResizeHandle({ onDrag }: ResizeHandleProps): React.JSX.Element {
}
const handleMouseUp = (): void => {
- document.removeEventListener('mousemove', handleMouseMove)
- document.removeEventListener('mouseup', handleMouseUp)
- document.body.style.cursor = ''
- document.body.style.userSelect = ''
+ document.removeEventListener("mousemove", handleMouseMove)
+ document.removeEventListener("mouseup", handleMouseUp)
+ document.body.style.cursor = ""
+ document.body.style.userSelect = ""
}
- document.addEventListener('mousemove', handleMouseMove)
- document.addEventListener('mouseup', handleMouseUp)
- document.body.style.cursor = 'row-resize'
- document.body.style.userSelect = 'none'
+ document.addEventListener("mousemove", handleMouseMove)
+ document.addEventListener("mouseup", handleMouseUp)
+ document.body.style.cursor = "row-resize"
+ document.body.style.userSelect = "none"
},
[onDrag]
)
@@ -291,8 +291,8 @@ export function RightPanel(): React.JSX.Element {
const handleMouseUp = (): void => {
dragStartHeights.current = null
}
- document.addEventListener('mouseup', handleMouseUp)
- return () => document.removeEventListener('mouseup', handleMouseUp)
+ document.addEventListener("mouseup", handleMouseUp)
+ return () => document.removeEventListener("mouseup", handleMouseUp)
}, [])
// Reset heights when panels open/close to redistribute
@@ -375,27 +375,27 @@ export function RightPanel(): React.JSX.Element {
const STATUS_CONFIG = {
pending: {
icon: Circle,
- badge: 'outline' as const,
- label: 'PENDING',
- color: 'text-muted-foreground'
+ badge: "outline" as const,
+ label: "PENDING",
+ color: "text-muted-foreground"
},
in_progress: {
icon: Clock,
- badge: 'info' as const,
- label: 'IN PROGRESS',
- color: 'text-status-info'
+ badge: "info" as const,
+ label: "IN PROGRESS",
+ color: "text-status-info"
},
completed: {
icon: CheckCircle2,
- badge: 'nominal' as const,
- label: 'DONE',
- color: 'text-status-nominal'
+ badge: "nominal" as const,
+ label: "DONE",
+ color: "text-status-nominal"
},
cancelled: {
icon: XCircle,
- badge: 'critical' as const,
- label: 'CANCELLED',
- color: 'text-muted-foreground'
+ badge: "critical" as const,
+ label: "CANCELLED",
+ color: "text-muted-foreground"
}
}
@@ -415,10 +415,10 @@ function TasksContent(): React.JSX.Element {
)
}
- const inProgress = todos.filter((t) => t.status === 'in_progress')
- const pending = todos.filter((t) => t.status === 'pending')
- const completed = todos.filter((t) => t.status === 'completed')
- const cancelled = todos.filter((t) => t.status === 'cancelled')
+ const inProgress = todos.filter((t) => t.status === "in_progress")
+ const pending = todos.filter((t) => t.status === "pending")
+ const completed = todos.filter((t) => t.status === "completed")
+ const cancelled = todos.filter((t) => t.status === "cancelled")
// Completed section includes both completed and cancelled
const doneItems = [...completed, ...cancelled]
@@ -490,17 +490,17 @@ function TasksContent(): React.JSX.Element {
function TaskItem({ todo }: { todo: Todo }): React.JSX.Element {
const config = STATUS_CONFIG[todo.status]
const Icon = config.icon
- const isDone = todo.status === 'completed' || todo.status === 'cancelled'
+ const isDone = todo.status === "completed" || todo.status === "cancelled"
return (
-
- {todo.content}
+
+ {todo.content}
{config.label}
@@ -545,7 +545,7 @@ function FilesContent(): React.JSX.Element {
const cleanup = window.api.workspace.onFilesChanged(async (data) => {
// Only reload if the event is for the current thread
if (data.threadId === currentThreadId) {
- console.log('[FilesContent] Files changed, reloading...', data)
+ console.log("[FilesContent] Files changed, reloading...", data)
const result = await window.api.workspace.loadFromDisk(currentThreadId)
if (result.success && result.files) {
setWorkspaceFiles(result.files)
@@ -572,7 +572,7 @@ function FilesContent(): React.JSX.Element {
}
}
} catch (e) {
- console.error('[FilesContent] Select folder error:', e)
+ console.error("[FilesContent] Select folder error:", e)
} finally {
setSyncing(false)
}
@@ -590,7 +590,7 @@ function FilesContent(): React.JSX.Element {
}
// syncToDisk is not yet implemented
- console.warn('[FilesContent] syncToDisk is not yet implemented')
+ console.warn("[FilesContent] syncToDisk is not yet implemented")
}
return (
@@ -601,7 +601,7 @@ function FilesContent(): React.JSX.Element {
className="text-[10px] text-muted-foreground truncate flex-1"
title={workspacePath || undefined}
>
- {workspacePath ? workspacePath.split('/').pop() : 'No folder linked'}
+ {workspacePath ? workspacePath.split("/").pop() : "No folder linked"}
0
? workspacePath
? `Sync to ${workspacePath}`
- : 'Sync files to disk'
+ : "Sync files to disk"
: workspacePath
? `Change folder`
- : 'Link sync folder'
+ : "Link sync folder"
}
>
{syncing ? (
@@ -629,7 +629,7 @@ function FilesContent(): React.JSX.Element {
)}
- {workspaceFiles.length > 0 ? 'Sync' : workspacePath ? 'Change' : 'Link'}
+ {workspaceFiles.length > 0 ? "Sync" : workspacePath ? "Change" : "Link"}
@@ -641,7 +641,7 @@ function FilesContent(): React.JSX.Element {
No workspace files
{workspacePath
- ? `Linked to ${workspacePath.split('/').pop()}`
+ ? `Linked to ${workspacePath.split("/").pop()}`
: 'Click "Link" to set a sync folder'}
@@ -686,8 +686,8 @@ function buildFileTree(files: FileInfo[]): TreeNode[] {
for (const file of sortedFiles) {
// Normalize path - remove leading slash
- const normalizedPath = file.path.startsWith('/') ? file.path.slice(1) : file.path
- const parts = normalizedPath.split('/')
+ const normalizedPath = file.path.startsWith("/") ? file.path.slice(1) : file.path
+ const parts = normalizedPath.split("/")
const fileName = parts[parts.length - 1]
const node: TreeNode = {
@@ -704,7 +704,7 @@ function buildFileTree(files: FileInfo[]): TreeNode[] {
nodeMap.set(normalizedPath, node)
} else {
// Nested item - find or create parent directories
- let currentPath = ''
+ let currentPath = ""
let parentChildren = root
for (let i = 0; i < parts.length - 1; i++) {
@@ -715,7 +715,7 @@ function buildFileTree(files: FileInfo[]): TreeNode[] {
// Create implicit directory node
parentNode = {
name: parts[i],
- path: '/' + currentPath,
+ path: "/" + currentPath,
is_dir: true,
children: []
}
@@ -815,7 +815,7 @@ function FileTreeNode({
@@ -881,38 +881,38 @@ function FileIcon({
}
// Get file extension
- const ext = name.includes('.') ? name.split('.').pop()?.toLowerCase() : ''
+ const ext = name.includes(".") ? name.split(".").pop()?.toLowerCase() : ""
// Map extensions to icons and colors
switch (ext) {
- case 'ts':
- case 'tsx':
+ case "ts":
+ case "tsx":
return
- case 'js':
- case 'jsx':
+ case "js":
+ case "jsx":
return
- case 'json':
+ case "json":
return
- case 'md':
- case 'mdx':
+ case "md":
+ case "mdx":
return
- case 'py':
+ case "py":
return
- case 'css':
- case 'scss':
- case 'sass':
+ case "css":
+ case "scss":
+ case "sass":
return
- case 'html':
+ case "html":
return
- case 'svg':
- case 'png':
- case 'jpg':
- case 'jpeg':
- case 'gif':
- case 'webp':
+ case "svg":
+ case "png":
+ case "jpg":
+ case "jpeg":
+ case "gif":
+ case "webp":
return
- case 'yml':
- case 'yaml':
+ case "yml":
+ case "yaml":
return
default:
return
@@ -943,11 +943,11 @@ function AgentsContent(): React.JSX.Element {
{agent.name}
{agent.status.toUpperCase()}
diff --git a/src/renderer/src/components/panels/SubagentPanel.tsx b/src/renderer/src/components/panels/SubagentPanel.tsx
index 6b9eced..0953990 100644
--- a/src/renderer/src/components/panels/SubagentPanel.tsx
+++ b/src/renderer/src/components/panels/SubagentPanel.tsx
@@ -7,14 +7,14 @@ import {
Sparkles,
Search,
FileCheck
-} from 'lucide-react'
-import { ScrollArea } from '@/components/ui/scroll-area'
-import { Badge } from '@/components/ui/badge'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
-import { cn } from '@/lib/utils'
-import { useAppStore } from '@/lib/store'
-import { useThreadState } from '@/lib/thread-context'
-import type { Subagent } from '@/types'
+} from "lucide-react"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { Badge } from "@/components/ui/badge"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { cn } from "@/lib/utils"
+import { useAppStore } from "@/lib/store"
+import { useThreadState } from "@/lib/thread-context"
+import type { Subagent } from "@/types"
// Icon component for subagent type (avoid creating components during render)
function SubagentTypeIcon({
@@ -25,11 +25,11 @@ function SubagentTypeIcon({
className?: string
}): React.JSX.Element {
switch (subagentType) {
- case 'correctness-checker':
+ case "correctness-checker":
return
- case 'final-reviewer':
+ case "final-reviewer":
return
- case 'research':
+ case "research":
return
default:
return
@@ -39,16 +39,16 @@ function SubagentTypeIcon({
// Get badge variant for subagent type
function getSubagentTypeBadge(subagentType?: string): string {
switch (subagentType) {
- case 'correctness-checker':
- return 'CHECKER'
- case 'final-reviewer':
- return 'REVIEWER'
- case 'research':
- return 'RESEARCH'
- case 'general-purpose':
- return 'GENERAL'
+ case "correctness-checker":
+ return "CHECKER"
+ case "final-reviewer":
+ return "REVIEWER"
+ case "research":
+ return "RESEARCH"
+ case "general-purpose":
+ return "GENERAL"
default:
- return subagentType?.toUpperCase() || 'TASK'
+ return subagentType?.toUpperCase() || "TASK"
}
}
@@ -58,8 +58,8 @@ export function SubagentPanel(): React.JSX.Element {
const subagents = threadState?.subagents ?? []
// Count by status
- const runningCount = subagents.filter((s) => s.status === 'running').length
- const completedCount = subagents.filter((s) => s.status === 'completed').length
+ const runningCount = subagents.filter((s) => s.status === "running").length
+ const completedCount = subagents.filter((s) => s.status === "completed").length
return (
@@ -110,18 +110,18 @@ export function SubagentPanel(): React.JSX.Element {
function SubagentCard({ subagent }: { subagent: Subagent }): React.JSX.Element {
const getStatusConfig = (): {
icon: React.ElementType
- badge: 'outline' | 'info' | 'nominal' | 'critical'
+ badge: "outline" | "info" | "nominal" | "critical"
label: string
} => {
switch (subagent.status) {
- case 'pending':
- return { icon: Clock, badge: 'outline' as const, label: 'PENDING' }
- case 'running':
- return { icon: Loader2, badge: 'info' as const, label: 'RUNNING' }
- case 'completed':
- return { icon: CheckCircle2, badge: 'nominal' as const, label: 'DONE' }
- case 'failed':
- return { icon: XCircle, badge: 'critical' as const, label: 'FAILED' }
+ case "pending":
+ return { icon: Clock, badge: "outline" as const, label: "PENDING" }
+ case "running":
+ return { icon: Loader2, badge: "info" as const, label: "RUNNING" }
+ case "completed":
+ return { icon: CheckCircle2, badge: "nominal" as const, label: "DONE" }
+ case "failed":
+ return { icon: XCircle, badge: "critical" as const, label: "FAILED" }
}
}
@@ -142,22 +142,22 @@ function SubagentCard({ subagent }: { subagent: Subagent }): React.JSX.Element {
const duration = getDuration()
return (
-
+
{subagent.name}
{config.label}
@@ -179,7 +179,7 @@ function SubagentCard({ subagent }: { subagent: Subagent }): React.JSX.Element {
)}
{duration && (
- {subagent.status === 'running' ? (
+ {subagent.status === "running" ? (
) : (
diff --git a/src/renderer/src/components/panels/TodoPanel.tsx b/src/renderer/src/components/panels/TodoPanel.tsx
index de7a251..34095a4 100644
--- a/src/renderer/src/components/panels/TodoPanel.tsx
+++ b/src/renderer/src/components/panels/TodoPanel.tsx
@@ -1,36 +1,36 @@
-import { useState } from 'react'
-import { CheckCircle2, Circle, Clock, XCircle, ChevronRight, ChevronDown } from 'lucide-react'
-import { ScrollArea } from '@/components/ui/scroll-area'
-import { Badge } from '@/components/ui/badge'
-import { useAppStore } from '@/lib/store'
-import { useThreadState } from '@/lib/thread-context'
-import { cn } from '@/lib/utils'
-import type { Todo } from '@/types'
+import { useState } from "react"
+import { CheckCircle2, Circle, Clock, XCircle, ChevronRight, ChevronDown } from "lucide-react"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { Badge } from "@/components/ui/badge"
+import { useAppStore } from "@/lib/store"
+import { useThreadState } from "@/lib/thread-context"
+import { cn } from "@/lib/utils"
+import type { Todo } from "@/types"
const STATUS_CONFIG = {
pending: {
icon: Circle,
- badge: 'outline' as const,
- label: 'PENDING',
- color: 'text-muted-foreground'
+ badge: "outline" as const,
+ label: "PENDING",
+ color: "text-muted-foreground"
},
in_progress: {
icon: Clock,
- badge: 'info' as const,
- label: 'IN PROGRESS',
- color: 'text-status-info'
+ badge: "info" as const,
+ label: "IN PROGRESS",
+ color: "text-status-info"
},
completed: {
icon: CheckCircle2,
- badge: 'nominal' as const,
- label: 'DONE',
- color: 'text-status-nominal'
+ badge: "nominal" as const,
+ label: "DONE",
+ color: "text-status-nominal"
},
cancelled: {
icon: XCircle,
- badge: 'critical' as const,
- label: 'CANCELLED',
- color: 'text-muted-foreground'
+ badge: "critical" as const,
+ label: "CANCELLED",
+ color: "text-muted-foreground"
}
}
@@ -41,10 +41,10 @@ export function TodoPanel(): React.JSX.Element {
const [completedExpanded, setCompletedExpanded] = useState(false)
// Group todos by status
- const inProgress = todos.filter((t) => t.status === 'in_progress')
- const pending = todos.filter((t) => t.status === 'pending')
- const completed = todos.filter((t) => t.status === 'completed')
- const cancelled = todos.filter((t) => t.status === 'cancelled')
+ const inProgress = todos.filter((t) => t.status === "in_progress")
+ const pending = todos.filter((t) => t.status === "pending")
+ const completed = todos.filter((t) => t.status === "completed")
+ const cancelled = todos.filter((t) => t.status === "cancelled")
// Completed section includes both completed and cancelled
const doneItems = [...completed, ...cancelled]
@@ -139,17 +139,17 @@ function TodoItem({ todo }: { todo: Todo }): React.JSX.Element {
return (
-
+
{todo.content}
diff --git a/src/renderer/src/components/sidebar/ThreadSidebar.tsx b/src/renderer/src/components/sidebar/ThreadSidebar.tsx
index 67eeed7..54f4184 100644
--- a/src/renderer/src/components/sidebar/ThreadSidebar.tsx
+++ b/src/renderer/src/components/sidebar/ThreadSidebar.tsx
@@ -1,18 +1,18 @@
-import { useState } from 'react'
-import { Plus, MessageSquare, Trash2, Pencil, Loader2 } from 'lucide-react'
-import { Button } from '@/components/ui/button'
-import { ScrollArea } from '@/components/ui/scroll-area'
-import { useAppStore } from '@/lib/store'
-import { useThreadStream } from '@/lib/thread-context'
-import { cn, formatRelativeTime, truncate } from '@/lib/utils'
+import { useState } from "react"
+import { Plus, MessageSquare, Trash2, Pencil, Loader2 } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { useAppStore } from "@/lib/store"
+import { useThreadStream } from "@/lib/thread-context"
+import { cn, formatRelativeTime, truncate } from "@/lib/utils"
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger
-} from '@/components/ui/context-menu'
-import type { Thread } from '@/types'
+} from "@/components/ui/context-menu"
+import type { Thread } from "@/types"
// Thread loading indicator that subscribes to the stream context
function ThreadLoadingIcon({ threadId }: { threadId: string }): React.JSX.Element {
@@ -53,10 +53,10 @@ function ThreadListItem({
{
if (!isEditing) {
@@ -73,8 +73,8 @@ function ThreadListItem({
onChange={(e) => onEditingTitleChange(e.target.value)}
onBlur={onSaveTitle}
onKeyDown={(e) => {
- if (e.key === 'Enter') onSaveTitle()
- if (e.key === 'Escape') onCancelEditing()
+ if (e.key === "Enter") onSaveTitle()
+ if (e.key === "Escape") onCancelEditing()
}}
className="w-full bg-background border border-border rounded px-1 py-0.5 text-sm outline-none focus:ring-1 focus:ring-ring"
autoFocus
@@ -110,10 +110,7 @@ function ThreadListItem({
Rename
-
+
Delete
@@ -123,21 +120,15 @@ function ThreadListItem({
}
export function ThreadSidebar(): React.JSX.Element {
- const {
- threads,
- currentThreadId,
- createThread,
- selectThread,
- deleteThread,
- updateThread
- } = useAppStore()
+ const { threads, currentThreadId, createThread, selectThread, deleteThread, updateThread } =
+ useAppStore()
const [editingThreadId, setEditingThreadId] = useState(null)
- const [editingTitle, setEditingTitle] = useState('')
+ const [editingTitle, setEditingTitle] = useState("")
const startEditing = (threadId: string, currentTitle: string): void => {
setEditingThreadId(threadId)
- setEditingTitle(currentTitle || '')
+ setEditingTitle(currentTitle || "")
}
const saveTitle = async (): Promise => {
@@ -145,12 +136,12 @@ export function ThreadSidebar(): React.JSX.Element {
await updateThread(editingThreadId, { title: editingTitle.trim() })
}
setEditingThreadId(null)
- setEditingTitle('')
+ setEditingTitle("")
}
const cancelEditing = (): void => {
setEditingThreadId(null)
- setEditingTitle('')
+ setEditingTitle("")
}
const handleNewThread = async (): Promise => {
@@ -160,8 +151,13 @@ export function ThreadSidebar(): React.JSX.Element {
return (
{/* New Thread Button - with dynamic safe area padding when zoomed out */}
-
-
+
+
New Thread
@@ -179,7 +175,7 @@ export function ThreadSidebar(): React.JSX.Element {
editingTitle={editingTitle}
onSelect={() => selectThread(thread.thread_id)}
onDelete={() => deleteThread(thread.thread_id)}
- onStartEditing={() => startEditing(thread.thread_id, thread.title || '')}
+ onStartEditing={() => startEditing(thread.thread_id, thread.title || "")}
onSaveTitle={saveTitle}
onCancelEditing={cancelEditing}
onEditingTitleChange={setEditingTitle}
diff --git a/src/renderer/src/components/tabs/BinaryFileViewer.tsx b/src/renderer/src/components/tabs/BinaryFileViewer.tsx
index d60a260..b5a4f52 100644
--- a/src/renderer/src/components/tabs/BinaryFileViewer.tsx
+++ b/src/renderer/src/components/tabs/BinaryFileViewer.tsx
@@ -1,5 +1,5 @@
-import { File, Download } from 'lucide-react'
-import { Button } from '@/components/ui/button'
+import { File, Download } from "lucide-react"
+import { Button } from "@/components/ui/button"
interface BinaryFileViewerProps {
filePath: string
@@ -7,11 +7,11 @@ interface BinaryFileViewerProps {
}
export function BinaryFileViewer({ filePath, size }: BinaryFileViewerProps): React.JSX.Element {
- const fileName = filePath.split('/').pop() || filePath
- const ext = fileName.includes('.') ? fileName.split('.').pop()?.toUpperCase() : 'FILE'
+ const fileName = filePath.split("/").pop() || filePath
+ const ext = fileName.includes(".") ? fileName.split(".").pop()?.toUpperCase() : "FILE"
const formatSize = (bytes?: number): string => {
- if (!bytes) return 'Unknown size'
+ if (!bytes) return "Unknown size"
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
@@ -45,7 +45,8 @@ export function BinaryFileViewer({ filePath, size }: BinaryFileViewerProps): Rea
{ext} file • {formatSize(size)}
- This file type cannot be previewed in the viewer. You can open it with an external application.
+ This file type cannot be previewed in the viewer. You can open it with an external
+ application.
diff --git a/src/renderer/src/components/tabs/CodeViewer.tsx b/src/renderer/src/components/tabs/CodeViewer.tsx
index 8253bd2..2f36dce 100644
--- a/src/renderer/src/components/tabs/CodeViewer.tsx
+++ b/src/renderer/src/components/tabs/CodeViewer.tsx
@@ -1,22 +1,22 @@
-import { useEffect, useState, useMemo } from 'react'
-import { ScrollArea } from '@/components/ui/scroll-area'
-import { createHighlighterCore, type HighlighterCore } from 'shiki/core'
-import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
+import { useEffect, useState, useMemo } from "react"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { createHighlighterCore, type HighlighterCore } from "shiki/core"
+import { createJavaScriptRegexEngine } from "shiki/engine/javascript"
// Import bundled themes and languages
-import githubDarkDefault from 'shiki/themes/github-dark-default.mjs'
-import langTypescript from 'shiki/langs/typescript.mjs'
-import langTsx from 'shiki/langs/tsx.mjs'
-import langJavascript from 'shiki/langs/javascript.mjs'
-import langJsx from 'shiki/langs/jsx.mjs'
-import langPython from 'shiki/langs/python.mjs'
-import langJson from 'shiki/langs/json.mjs'
-import langCss from 'shiki/langs/css.mjs'
-import langHtml from 'shiki/langs/html.mjs'
-import langMarkdown from 'shiki/langs/markdown.mjs'
-import langYaml from 'shiki/langs/yaml.mjs'
-import langBash from 'shiki/langs/bash.mjs'
-import langSql from 'shiki/langs/sql.mjs'
+import githubDarkDefault from "shiki/themes/github-dark-default.mjs"
+import langTypescript from "shiki/langs/typescript.mjs"
+import langTsx from "shiki/langs/tsx.mjs"
+import langJavascript from "shiki/langs/javascript.mjs"
+import langJsx from "shiki/langs/jsx.mjs"
+import langPython from "shiki/langs/python.mjs"
+import langJson from "shiki/langs/json.mjs"
+import langCss from "shiki/langs/css.mjs"
+import langHtml from "shiki/langs/html.mjs"
+import langMarkdown from "shiki/langs/markdown.mjs"
+import langYaml from "shiki/langs/yaml.mjs"
+import langBash from "shiki/langs/bash.mjs"
+import langSql from "shiki/langs/sql.mjs"
// Singleton highlighter instance (using JS engine - no WASM needed)
let highlighterPromise: Promise | null = null
@@ -26,9 +26,18 @@ async function getHighlighter(): Promise {
highlighterPromise = createHighlighterCore({
themes: [githubDarkDefault],
langs: [
- langTypescript, langTsx, langJavascript, langJsx,
- langPython, langJson, langCss, langHtml,
- langMarkdown, langYaml, langBash, langSql
+ langTypescript,
+ langTsx,
+ langJavascript,
+ langJsx,
+ langPython,
+ langJson,
+ langCss,
+ langHtml,
+ langMarkdown,
+ langYaml,
+ langBash,
+ langSql
],
engine: createJavaScriptRegexEngine()
})
@@ -43,33 +52,43 @@ interface CodeViewerProps {
// Map file extensions to Shiki language identifiers (only languages we've loaded)
const SUPPORTED_LANGS = new Set([
- 'typescript', 'tsx', 'javascript', 'jsx', 'python', 'json',
- 'css', 'html', 'markdown', 'yaml', 'bash', 'sql'
+ "typescript",
+ "tsx",
+ "javascript",
+ "jsx",
+ "python",
+ "json",
+ "css",
+ "html",
+ "markdown",
+ "yaml",
+ "bash",
+ "sql"
])
function getLanguage(ext: string | undefined): string | null {
const langMap: Record = {
- 'ts': 'typescript',
- 'tsx': 'tsx',
- 'js': 'javascript',
- 'jsx': 'jsx',
- 'mjs': 'javascript',
- 'cjs': 'javascript',
- 'py': 'python',
- 'json': 'json',
- 'css': 'css',
- 'html': 'html',
- 'htm': 'html',
- 'md': 'markdown',
- 'mdx': 'markdown',
- 'yaml': 'yaml',
- 'yml': 'yaml',
- 'sh': 'bash',
- 'bash': 'bash',
- 'zsh': 'bash',
- 'sql': 'sql'
+ ts: "typescript",
+ tsx: "tsx",
+ js: "javascript",
+ jsx: "jsx",
+ mjs: "javascript",
+ cjs: "javascript",
+ py: "python",
+ json: "json",
+ css: "css",
+ html: "html",
+ htm: "html",
+ md: "markdown",
+ mdx: "markdown",
+ yaml: "yaml",
+ yml: "yaml",
+ sh: "bash",
+ bash: "bash",
+ zsh: "bash",
+ sql: "sql"
}
-
+
const lang = ext ? langMap[ext] : null
return lang && SUPPORTED_LANGS.has(lang) ? lang : null
}
@@ -78,8 +97,8 @@ export function CodeViewer({ filePath, content }: CodeViewerProps) {
const [highlightedHtml, setHighlightedHtml] = useState(null)
// Get file extension for syntax highlighting
- const fileName = filePath.split('/').pop() || filePath
- const ext = fileName.includes('.') ? fileName.split('.').pop()?.toLowerCase() : undefined
+ const fileName = filePath.split("/").pop() || filePath
+ const ext = fileName.includes(".") ? fileName.split(".").pop()?.toLowerCase() : undefined
const language = useMemo(() => getLanguage(ext), [ext])
// Highlight code with Shiki
@@ -93,22 +112,22 @@ export function CodeViewer({ filePath, content }: CodeViewerProps) {
}
try {
- console.log('[CodeViewer] Starting highlight for', language)
+ console.log("[CodeViewer] Starting highlight for", language)
const highlighter = await getHighlighter()
-
+
if (cancelled) return
-
+
const html = highlighter.codeToHtml(content, {
lang: language,
- theme: 'github-dark-default'
+ theme: "github-dark-default"
})
-
+
if (cancelled) return
-
- console.log('[CodeViewer] Highlighting complete, html length:', html.length)
+
+ console.log("[CodeViewer] Highlighting complete, html length:", html.length)
setHighlightedHtml(html)
} catch (e) {
- console.error('[CodeViewer] Shiki highlighting failed:', e)
+ console.error("[CodeViewer] Shiki highlighting failed:", e)
setHighlightedHtml(null)
}
}
@@ -120,7 +139,7 @@ export function CodeViewer({ filePath, content }: CodeViewerProps) {
}
}, [content, language])
- const lineCount = content?.split('\n').length ?? 0
+ const lineCount = content?.split("\n").length ?? 0
return (
@@ -130,17 +149,14 @@ export function CodeViewer({ filePath, content }: CodeViewerProps) {
•
{lineCount} lines
•
- {language || 'plain text'}
+ {language || "plain text"}
{/* File content with syntax highlighting */}
{highlightedHtml ? (
-
+
) : (
// Fallback plain text rendering
diff --git a/src/renderer/src/components/tabs/FileViewer.tsx b/src/renderer/src/components/tabs/FileViewer.tsx
index 94eadbe..02bba9d 100644
--- a/src/renderer/src/components/tabs/FileViewer.tsx
+++ b/src/renderer/src/components/tabs/FileViewer.tsx
@@ -1,12 +1,12 @@
-import { useEffect, useState, useMemo } from 'react'
-import { Loader2, AlertCircle, FileCode } from 'lucide-react'
-import { useCurrentThread } from '@/lib/thread-context'
-import { getFileType, isBinaryFile } from '@/lib/file-types'
-import { CodeViewer } from './CodeViewer'
-import { ImageViewer } from './ImageViewer'
-import { MediaViewer } from './MediaViewer'
-import { PDFViewer } from './PDFViewer'
-import { BinaryFileViewer } from './BinaryFileViewer'
+import { useEffect, useState, useMemo } from "react"
+import { Loader2, AlertCircle, FileCode } from "lucide-react"
+import { useCurrentThread } from "@/lib/thread-context"
+import { getFileType, isBinaryFile } from "@/lib/file-types"
+import { CodeViewer } from "./CodeViewer"
+import { ImageViewer } from "./ImageViewer"
+import { MediaViewer } from "./MediaViewer"
+import { PDFViewer } from "./PDFViewer"
+import { BinaryFileViewer } from "./BinaryFileViewer"
interface FileViewerProps {
filePath: string
@@ -21,7 +21,7 @@ export function FileViewer({ filePath, threadId }: FileViewerProps): React.JSX.E
const [fileSize, setFileSize] = useState()
// Get file type info
- const fileName = filePath.split('/').pop() || filePath
+ const fileName = filePath.split("/").pop() || filePath
const fileTypeInfo = useMemo(() => getFileType(fileName), [fileName])
const isBinary = useMemo(() => isBinaryFile(fileName), [fileName])
@@ -54,7 +54,7 @@ export function FileViewer({ filePath, threadId }: FileViewerProps): React.JSX.E
setBinaryContent(result.content)
setFileSize(result.size)
} else {
- setError(result.error || 'Failed to read file')
+ setError(result.error || "Failed to read file")
}
} else {
// Read as text file
@@ -63,11 +63,11 @@ export function FileViewer({ filePath, threadId }: FileViewerProps): React.JSX.E
setFileContents(filePath, result.content)
setFileSize(result.size)
} else {
- setError(result.error || 'Failed to read file')
+ setError(result.error || "Failed to read file")
}
}
} catch (e) {
- setError(e instanceof Error ? e.message : 'Failed to read file')
+ setError(e instanceof Error ? e.message : "Failed to read file")
} finally {
setIsLoading(false)
}
@@ -107,43 +107,43 @@ export function FileViewer({ filePath, threadId }: FileViewerProps): React.JSX.E
}
// Route to appropriate viewer based on file type
- if (fileTypeInfo.type === 'image' && binaryContent) {
+ if (fileTypeInfo.type === "image" && binaryContent) {
return (
)
}
- if (fileTypeInfo.type === 'video' && binaryContent) {
+ if (fileTypeInfo.type === "video" && binaryContent) {
return (
)
}
- if (fileTypeInfo.type === 'audio' && binaryContent) {
+ if (fileTypeInfo.type === "audio" && binaryContent) {
return (
)
}
- if (fileTypeInfo.type === 'pdf' && binaryContent) {
+ if (fileTypeInfo.type === "pdf" && binaryContent) {
return
}
- if (fileTypeInfo.type === 'binary') {
+ if (fileTypeInfo.type === "binary") {
return
}
diff --git a/src/renderer/src/components/tabs/ImageViewer.tsx b/src/renderer/src/components/tabs/ImageViewer.tsx
index a1bc326..e033d4f 100644
--- a/src/renderer/src/components/tabs/ImageViewer.tsx
+++ b/src/renderer/src/components/tabs/ImageViewer.tsx
@@ -1,7 +1,7 @@
-import { useState, useRef } from 'react'
-import { ZoomIn, ZoomOut, Maximize2, RotateCw, Hand } from 'lucide-react'
-import { ScrollArea } from '@/components/ui/scroll-area'
-import { Button } from '@/components/ui/button'
+import { useState, useRef } from "react"
+import { ZoomIn, ZoomOut, Maximize2, RotateCw, Hand } from "lucide-react"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { Button } from "@/components/ui/button"
interface ImageViewerProps {
filePath: string
@@ -9,7 +9,11 @@ interface ImageViewerProps {
mimeType: string
}
-export function ImageViewer({ filePath, base64Content, mimeType }: ImageViewerProps): React.JSX.Element {
+export function ImageViewer({
+ filePath,
+ base64Content,
+ mimeType
+}: ImageViewerProps): React.JSX.Element {
const [zoom, setZoom] = useState(100)
const [rotation, setRotation] = useState(0)
const [isPanning, setIsPanning] = useState(false)
@@ -17,7 +21,7 @@ export function ImageViewer({ filePath, base64Content, mimeType }: ImageViewerPr
const [panOffset, setPanOffset] = useState({ x: 0, y: 0 })
const containerRef = useRef(null)
- const fileName = filePath.split('/').pop() || filePath
+ const fileName = filePath.split("/").pop() || filePath
const imageUrl = `data:${mimeType};base64,${base64Content}`
const handleZoomIn = (): void => {
@@ -73,7 +77,6 @@ export function ImageViewer({ filePath, base64Content, mimeType }: ImageViewerPr
// Reset pan when zoom changes to 100 or less
-
const canPan = zoom > 100
return (
@@ -106,9 +109,7 @@ export function ImageViewer({ filePath, base64Content, mimeType }: ImageViewerPr
-
- {zoom}%
-
+ {zoom}%
-
+
-
+
@@ -150,8 +141,8 @@ export function ImageViewer({ filePath, base64Content, mimeType }: ImageViewerPr
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
style={{
- cursor: canPan ? (isPanning ? 'grabbing' : 'grab') : 'default',
- userSelect: 'none'
+ cursor: canPan ? (isPanning ? "grabbing" : "grab") : "default",
+ userSelect: "none"
}}
>
100 ? 'pixelated' : 'auto'
+ imageRendering: zoom > 100 ? "pixelated" : "auto"
}}
draggable={false}
/>
diff --git a/src/renderer/src/components/tabs/MediaViewer.tsx b/src/renderer/src/components/tabs/MediaViewer.tsx
index 965edcb..d87bb7b 100644
--- a/src/renderer/src/components/tabs/MediaViewer.tsx
+++ b/src/renderer/src/components/tabs/MediaViewer.tsx
@@ -1,15 +1,20 @@
-import { Music, Video } from 'lucide-react'
-import { ScrollArea } from '@/components/ui/scroll-area'
+import { Music, Video } from "lucide-react"
+import { ScrollArea } from "@/components/ui/scroll-area"
interface MediaViewerProps {
filePath: string
base64Content: string
mimeType: string
- mediaType: 'video' | 'audio'
+ mediaType: "video" | "audio"
}
-export function MediaViewer({ filePath, base64Content, mimeType, mediaType }: MediaViewerProps): React.JSX.Element {
- const fileName = filePath.split('/').pop() || filePath
+export function MediaViewer({
+ filePath,
+ base64Content,
+ mimeType,
+ mediaType
+}: MediaViewerProps): React.JSX.Element {
+ const fileName = filePath.split("/").pop() || filePath
const mediaUrl = `data:${mimeType};base64,${base64Content}`
return (
@@ -24,7 +29,7 @@ export function MediaViewer({ filePath, base64Content, mimeType, mediaType }: Me
{/* Media player */}
- {mediaType === 'video' ? (
+ {mediaType === "video" ? (
<>
Audio File
-
+
Your browser does not support the audio tag.
diff --git a/src/renderer/src/components/tabs/PDFViewer.tsx b/src/renderer/src/components/tabs/PDFViewer.tsx
index a3c8df1..b4dd24b 100644
--- a/src/renderer/src/components/tabs/PDFViewer.tsx
+++ b/src/renderer/src/components/tabs/PDFViewer.tsx
@@ -1,6 +1,6 @@
-import { FileText, ExternalLink } from 'lucide-react'
-import { ScrollArea } from '@/components/ui/scroll-area'
-import { Button } from '@/components/ui/button'
+import { FileText, ExternalLink } from "lucide-react"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { Button } from "@/components/ui/button"
interface PDFViewerProps {
filePath: string
@@ -8,12 +8,12 @@ interface PDFViewerProps {
}
export function PDFViewer({ filePath, base64Content }: PDFViewerProps): React.JSX.Element {
- const fileName = filePath.split('/').pop() || filePath
+ const fileName = filePath.split("/").pop() || filePath
const pdfUrl = `data:application/pdf;base64,${base64Content}`
const handleOpenExternal = (): void => {
// Open in system default PDF viewer
- const link = document.createElement('a')
+ const link = document.createElement("a")
link.href = pdfUrl
link.download = fileName
link.click()
@@ -29,12 +29,7 @@ export function PDFViewer({ filePath, base64Content }: PDFViewerProps): React.JS
PDF Document
-
+
Download
@@ -43,11 +38,7 @@ export function PDFViewer({ filePath, base64Content }: PDFViewerProps): React.JS
{/* PDF embed */}
-
+
{/* Fallback if PDF can't be displayed inline */}
diff --git a/src/renderer/src/components/tabs/TabBar.tsx b/src/renderer/src/components/tabs/TabBar.tsx
index f8c62df..051787e 100644
--- a/src/renderer/src/components/tabs/TabBar.tsx
+++ b/src/renderer/src/components/tabs/TabBar.tsx
@@ -1,14 +1,17 @@
-import { Bot, X, FileCode, FileText, FileJson, File } from 'lucide-react'
-import { cn } from '@/lib/utils'
-import { useAppStore } from '@/lib/store'
-import { useThreadState, type OpenFile } from '@/lib/thread-context'
+import { Bot, X, FileCode, FileText, FileJson, File } from "lucide-react"
+import { cn } from "@/lib/utils"
+import { useAppStore } from "@/lib/store"
+import { useThreadState, type OpenFile } from "@/lib/thread-context"
interface TabBarProps {
className?: string
threadId?: string
}
-export function TabBar({ className, threadId: propThreadId }: TabBarProps): React.JSX.Element | null {
+export function TabBar({
+ className,
+ threadId: propThreadId
+}: TabBarProps): React.JSX.Element | null {
const { currentThreadId } = useAppStore()
const threadId = propThreadId ?? currentThreadId
const threadState = useThreadState(threadId)
@@ -20,16 +23,18 @@ export function TabBar({ className, threadId: propThreadId }: TabBarProps): Reac
const { openFiles, activeTab, setActiveTab, closeFile } = threadState
return (
-
+
{/* Agent Tab - Always first and prominent */}
setActiveTab('agent')}
+ onClick={() => setActiveTab("agent")}
className={cn(
"flex items-center gap-2 px-4 h-full text-sm font-medium transition-colors shrink-0 border-r border-border",
- activeTab === 'agent'
+ activeTab === "agent"
? "bg-primary/15 text-primary border-b-2 border-b-primary"
: "text-muted-foreground hover:text-foreground hover:bg-background-interactive"
)}
@@ -104,23 +109,23 @@ function FileTab({ file, isActive, onSelect, onClose }: FileTabProps): React.JSX
}
function FileIcon({ name }: { name: string }): React.JSX.Element {
- const ext = name.includes('.') ? name.split('.').pop()?.toLowerCase() : ''
+ const ext = name.includes(".") ? name.split(".").pop()?.toLowerCase() : ""
switch (ext) {
- case 'ts':
- case 'tsx':
- case 'js':
- case 'jsx':
- case 'py':
- case 'css':
- case 'scss':
- case 'html':
+ case "ts":
+ case "tsx":
+ case "js":
+ case "jsx":
+ case "py":
+ case "css":
+ case "scss":
+ case "html":
return
- case 'json':
+ case "json":
return
- case 'md':
- case 'mdx':
- case 'txt':
+ case "md":
+ case "mdx":
+ case "txt":
return
default:
return
diff --git a/src/renderer/src/components/tabs/TabbedPanel.tsx b/src/renderer/src/components/tabs/TabbedPanel.tsx
index 47263c8..11768fb 100644
--- a/src/renderer/src/components/tabs/TabbedPanel.tsx
+++ b/src/renderer/src/components/tabs/TabbedPanel.tsx
@@ -1,7 +1,7 @@
-import { useCurrentThread } from '@/lib/thread-context'
-import { TabBar } from './TabBar'
-import { FileViewer } from './FileViewer'
-import { ChatContainer } from '@/components/chat/ChatContainer'
+import { useCurrentThread } from "@/lib/thread-context"
+import { TabBar } from "./TabBar"
+import { FileViewer } from "./FileViewer"
+import { ChatContainer } from "@/components/chat/ChatContainer"
interface TabbedPanelProps {
threadId: string
@@ -12,7 +12,7 @@ export function TabbedPanel({ threadId, showTabBar = true }: TabbedPanelProps):
const { activeTab, openFiles } = useCurrentThread(threadId)
// Determine what to render based on active tab
- const isAgentTab = activeTab === 'agent'
+ const isAgentTab = activeTab === "agent"
const activeFile = openFiles.find((f) => f.path === activeTab)
return (
diff --git a/src/renderer/src/components/tabs/index.ts b/src/renderer/src/components/tabs/index.ts
index c8a964b..c788287 100644
--- a/src/renderer/src/components/tabs/index.ts
+++ b/src/renderer/src/components/tabs/index.ts
@@ -1,8 +1,8 @@
-export { TabBar } from './TabBar'
-export { FileViewer } from './FileViewer'
-export { TabbedPanel } from './TabbedPanel'
-export { CodeViewer } from './CodeViewer'
-export { ImageViewer } from './ImageViewer'
-export { MediaViewer } from './MediaViewer'
-export { PDFViewer } from './PDFViewer'
-export { BinaryFileViewer } from './BinaryFileViewer'
+export { TabBar } from "./TabBar"
+export { FileViewer } from "./FileViewer"
+export { TabbedPanel } from "./TabbedPanel"
+export { CodeViewer } from "./CodeViewer"
+export { ImageViewer } from "./ImageViewer"
+export { MediaViewer } from "./MediaViewer"
+export { PDFViewer } from "./PDFViewer"
+export { BinaryFileViewer } from "./BinaryFileViewer"
diff --git a/src/renderer/src/components/ui/badge.tsx b/src/renderer/src/components/ui/badge.tsx
index 2d9f6f2..f92f86e 100644
--- a/src/renderer/src/components/ui/badge.tsx
+++ b/src/renderer/src/components/ui/badge.tsx
@@ -15,23 +15,20 @@ const badgeVariants = cva(
nominal: "border-status-nominal/30 bg-status-nominal/15 text-status-nominal",
warning: "border-status-warning/30 bg-status-warning/15 text-status-warning",
critical: "border-status-critical/30 bg-status-critical/15 text-status-critical",
- info: "border-status-info/30 bg-status-info/15 text-status-info",
- },
+ info: "border-status-info/30 bg-status-info/15 text-status-info"
+ }
},
defaultVariants: {
- variant: "default",
- },
+ variant: "default"
+ }
}
)
export interface BadgeProps
- extends React.HTMLAttributes,
- VariantProps { }
+ extends React.HTMLAttributes, VariantProps {}
function Badge({ className, variant, ...props }: BadgeProps) {
- return (
-
- )
+ return
}
// eslint-disable-next-line react-refresh/only-export-components
diff --git a/src/renderer/src/components/ui/button.tsx b/src/renderer/src/components/ui/button.tsx
index 998454a..56fb8bb 100644
--- a/src/renderer/src/components/ui/button.tsx
+++ b/src/renderer/src/components/ui/button.tsx
@@ -18,26 +18,25 @@ const buttonVariants = cva(
nominal: "bg-status-nominal text-background hover:bg-status-nominal/90",
warning: "bg-status-warning text-background hover:bg-status-warning/90",
critical: "bg-status-critical text-white hover:bg-status-critical/90",
- info: "bg-status-info text-white hover:bg-status-info/90",
+ info: "bg-status-info text-white hover:bg-status-info/90"
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 px-3 text-xs",
lg: "h-10 px-6",
icon: "size-9",
- "icon-sm": "size-8",
- },
+ "icon-sm": "size-8"
+ }
},
defaultVariants: {
variant: "default",
- size: "default",
- },
+ size: "default"
+ }
}
)
export interface ButtonProps
- extends React.ButtonHTMLAttributes,
- VariantProps {
+ extends React.ButtonHTMLAttributes, VariantProps {
asChild?: boolean
}
@@ -45,11 +44,7 @@ const Button = React.forwardRef(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
-
+
)
}
)
diff --git a/src/renderer/src/components/ui/card.tsx b/src/renderer/src/components/ui/card.tsx
index fb7e7b0..51b71a6 100644
--- a/src/renderer/src/components/ui/card.tsx
+++ b/src/renderer/src/components/ui/card.tsx
@@ -1,78 +1,51 @@
import * as React from "react"
import { cn } from "@/lib/utils"
-const Card = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
+const Card = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
Card.displayName = "Card"
-const CardHeader = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
+const CardHeader = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
CardHeader.displayName = "CardHeader"
-const CardTitle = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
+const CardTitle = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
-
+
))
CardDescription.displayName = "CardDescription"
-const CardContent = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
+const CardContent = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
CardContent.displayName = "CardContent"
-const CardFooter = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
+const CardFooter = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+)
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/src/renderer/src/components/ui/context-menu.tsx b/src/renderer/src/components/ui/context-menu.tsx
index 2cc244b..3da1806 100644
--- a/src/renderer/src/components/ui/context-menu.tsx
+++ b/src/renderer/src/components/ui/context-menu.tsx
@@ -6,51 +6,32 @@ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
-function ContextMenu({
- ...props
-}: React.ComponentProps) {
+function ContextMenu({ ...props }: React.ComponentProps) {
return
}
function ContextMenuTrigger({
...props
}: React.ComponentProps) {
- return (
-
- )
+ return
}
-function ContextMenuGroup({
- ...props
-}: React.ComponentProps) {
- return (
-
- )
+function ContextMenuGroup({ ...props }: React.ComponentProps) {
+ return
}
-function ContextMenuPortal({
- ...props
-}: React.ComponentProps) {
- return (
-
- )
+function ContextMenuPortal({ ...props }: React.ComponentProps) {
+ return
}
-function ContextMenuSub({
- ...props
-}: React.ComponentProps) {
+function ContextMenuSub({ ...props }: React.ComponentProps) {
return
}
function ContextMenuRadioGroup({
...props
}: React.ComponentProps) {
- return (
-
- )
+ return
}
function ContextMenuSubTrigger({
@@ -195,10 +176,7 @@ function ContextMenuLabel({
)
@@ -217,17 +195,11 @@ function ContextMenuSeparator({
)
}
-function ContextMenuShortcut({
- className,
- ...props
-}: React.ComponentProps<"span">) {
+function ContextMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
return (
)
@@ -248,5 +220,5 @@ export {
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
- ContextMenuRadioGroup,
+ ContextMenuRadioGroup
}
diff --git a/src/renderer/src/components/ui/dialog.tsx b/src/renderer/src/components/ui/dialog.tsx
index 0dee4b4..a05499e 100644
--- a/src/renderer/src/components/ui/dialog.tsx
+++ b/src/renderer/src/components/ui/dialog.tsx
@@ -1,7 +1,7 @@
-import * as React from 'react'
-import * as DialogPrimitive from '@radix-ui/react-dialog'
-import { X } from 'lucide-react'
-import { cn } from '@/lib/utils'
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
@@ -18,7 +18,7 @@ const DialogOverlay = React.forwardRef<
) => (
-
+const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
+
)
-DialogHeader.displayName = 'DialogHeader'
+DialogHeader.displayName = "DialogHeader"
-const DialogFooter = ({
- className,
- ...props
-}: React.HTMLAttributes) => (
+const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
)
-DialogFooter.displayName = 'DialogFooter'
+DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef,
@@ -78,7 +69,7 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
))
@@ -90,7 +81,7 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
))
diff --git a/src/renderer/src/components/ui/input.tsx b/src/renderer/src/components/ui/input.tsx
index e4515fa..03e50f7 100644
--- a/src/renderer/src/components/ui/input.tsx
+++ b/src/renderer/src/components/ui/input.tsx
@@ -1,8 +1,7 @@
import * as React from "react"
import { cn } from "@/lib/utils"
-export interface InputProps
- extends React.InputHTMLAttributes {}
+export interface InputProps extends React.InputHTMLAttributes {}
const Input = React.forwardRef(
({ className, type, ...props }, ref) => {
diff --git a/src/renderer/src/components/ui/popover.tsx b/src/renderer/src/components/ui/popover.tsx
index 55596e1..ff12cd8 100644
--- a/src/renderer/src/components/ui/popover.tsx
+++ b/src/renderer/src/components/ui/popover.tsx
@@ -3,15 +3,11 @@ import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
-function Popover({
- ...props
-}: React.ComponentProps) {
+function Popover({ ...props }: React.ComponentProps) {
return
}
-function PopoverTrigger({
- ...props
-}: React.ComponentProps) {
+function PopoverTrigger({ ...props }: React.ComponentProps) {
return
}
@@ -37,9 +33,7 @@ function PopoverContent({
)
}
-function PopoverAnchor({
- ...props
-}: React.ComponentProps) {
+function PopoverAnchor({ ...props }: React.ComponentProps) {
return
}
diff --git a/src/renderer/src/components/ui/resizable.tsx b/src/renderer/src/components/ui/resizable.tsx
index ea93093..f4033d7 100644
--- a/src/renderer/src/components/ui/resizable.tsx
+++ b/src/renderer/src/components/ui/resizable.tsx
@@ -9,29 +9,32 @@ interface ResizeHandleProps {
export function ResizeHandle({ onDrag }: ResizeHandleProps) {
const startXRef = useRef(0)
-
- const handleMouseDown = useCallback((e: React.MouseEvent) => {
- e.preventDefault()
- startXRef.current = e.clientX
-
- const handleMouseMove = (e: MouseEvent) => {
- // Calculate total delta from drag start
- const totalDelta = e.clientX - startXRef.current
- onDrag(totalDelta)
- }
-
- const handleMouseUp = () => {
- document.removeEventListener('mousemove', handleMouseMove)
- document.removeEventListener('mouseup', handleMouseUp)
- document.body.style.cursor = ''
- document.body.style.userSelect = ''
- }
-
- document.addEventListener('mousemove', handleMouseMove)
- document.addEventListener('mouseup', handleMouseUp)
- document.body.style.cursor = 'col-resize'
- document.body.style.userSelect = 'none'
- }, [onDrag])
+
+ const handleMouseDown = useCallback(
+ (e: React.MouseEvent) => {
+ e.preventDefault()
+ startXRef.current = e.clientX
+
+ const handleMouseMove = (e: MouseEvent) => {
+ // Calculate total delta from drag start
+ const totalDelta = e.clientX - startXRef.current
+ onDrag(totalDelta)
+ }
+
+ const handleMouseUp = () => {
+ document.removeEventListener("mousemove", handleMouseMove)
+ document.removeEventListener("mouseup", handleMouseUp)
+ document.body.style.cursor = ""
+ document.body.style.userSelect = ""
+ }
+
+ document.addEventListener("mousemove", handleMouseMove)
+ document.addEventListener("mouseup", handleMouseUp)
+ document.body.style.cursor = "col-resize"
+ document.body.style.userSelect = "none"
+ },
+ [onDrag]
+ )
return (
,
React.ComponentPropsWithoutRef
->(
- (
- { className, orientation = "horizontal", decorative = true, ...props },
- ref
- ) => (
-
- )
-)
+>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
+
+))
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }
diff --git a/src/renderer/src/index.css b/src/renderer/src/index.css
index 3923672..dde8139 100644
--- a/src/renderer/src/index.css
+++ b/src/renderer/src/index.css
@@ -64,50 +64,50 @@
:root {
/* Foundation */
--radius: 3px;
- --background: #0D0D0F;
+ --background: #0d0d0f;
--background-elevated: #141418;
- --background-interactive: #1C1C22;
- --foreground: #E8E8EC;
+ --background-interactive: #1c1c22;
+ --foreground: #e8e8ec;
/* Borders */
- --border: #2A2A32;
- --border-emphasis: #3A3A45;
- --input: #2A2A32;
- --ring: #3B82F6;
+ --border: #2a2a32;
+ --border-emphasis: #3a3a45;
+ --input: #2a2a32;
+ --ring: #3b82f6;
/* Text hierarchy */
--muted: #141418;
- --muted-foreground: #8A8A96;
- --tertiary-foreground: #5A5A66;
+ --muted-foreground: #8a8a96;
+ --tertiary-foreground: #5a5a66;
/* Semantic mapping for shadcn compatibility */
--card: #141418;
- --card-foreground: #E8E8EC;
+ --card-foreground: #e8e8ec;
--popover: #141418;
- --popover-foreground: #E8E8EC;
- --primary: #3B82F6;
- --primary-foreground: #E8E8EC;
- --secondary: #1C1C22;
- --secondary-foreground: #E8E8EC;
- --accent: #FB923C;
- --accent-foreground: #0D0D0F;
- --destructive: #E53E3E;
+ --popover-foreground: #e8e8ec;
+ --primary: #3b82f6;
+ --primary-foreground: #e8e8ec;
+ --secondary: #1c1c22;
+ --secondary-foreground: #e8e8ec;
+ --accent: #fb923c;
+ --accent-foreground: #0d0d0f;
+ --destructive: #e53e3e;
/* Status colors */
- --status-critical: #E53E3E;
- --status-warning: #F59E0B;
- --status-nominal: #22C55E;
- --status-info: #3B82F6;
+ --status-critical: #e53e3e;
+ --status-warning: #f59e0b;
+ --status-nominal: #22c55e;
+ --status-info: #3b82f6;
/* Sidebar */
--sidebar: #141418;
- --sidebar-foreground: #E8E8EC;
- --sidebar-primary: #3B82F6;
- --sidebar-primary-foreground: #E8E8EC;
- --sidebar-accent: #1C1C22;
- --sidebar-accent-foreground: #E8E8EC;
- --sidebar-border: #2A2A32;
- --sidebar-ring: #3B82F6;
+ --sidebar-foreground: #e8e8ec;
+ --sidebar-primary: #3b82f6;
+ --sidebar-primary-foreground: #e8e8ec;
+ --sidebar-accent: #1c1c22;
+ --sidebar-accent-foreground: #e8e8ec;
+ --sidebar-border: #2a2a32;
+ --sidebar-ring: #3b82f6;
}
@layer base {
@@ -116,7 +116,7 @@
}
body {
@apply bg-background text-foreground antialiased;
- font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', ui-monospace, monospace;
+ font-family: "JetBrains Mono", "Fira Code", "SF Mono", ui-monospace, monospace;
}
}
@@ -188,7 +188,8 @@
/* Pulse animation for live indicators */
@keyframes tactical-pulse {
- 0%, 100% {
+ 0%,
+ 100% {
opacity: 1;
}
50% {
@@ -253,12 +254,20 @@
margin-bottom: 0.5em;
}
-.streaming-markdown h1 { font-size: 1.5em; }
-.streaming-markdown h2 { font-size: 1.25em; }
-.streaming-markdown h3 { font-size: 1.125em; }
+.streaming-markdown h1 {
+ font-size: 1.5em;
+}
+.streaming-markdown h2 {
+ font-size: 1.25em;
+}
+.streaming-markdown h3 {
+ font-size: 1.125em;
+}
.streaming-markdown h4,
.streaming-markdown h5,
-.streaming-markdown h6 { font-size: 1em; }
+.streaming-markdown h6 {
+ font-size: 1em;
+}
.streaming-markdown ul,
.streaming-markdown ol {
@@ -387,7 +396,7 @@
.app-badge-version {
font-size: 11px;
- font-family: 'JetBrains Mono', monospace;
+ font-family: "JetBrains Mono", monospace;
color: color-mix(in srgb, var(--primary) 70%, transparent);
line-height: 1;
}
@@ -406,7 +415,7 @@
}
.shiki-content code {
- font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', ui-monospace, monospace;
+ font-family: "JetBrains Mono", "Fira Code", "SF Mono", ui-monospace, monospace;
font-size: inherit;
background: transparent !important;
}
diff --git a/src/renderer/src/lib/electron-transport.ts b/src/renderer/src/lib/electron-transport.ts
index a4bc69b..bf32b1a 100644
--- a/src/renderer/src/lib/electron-transport.ts
+++ b/src/renderer/src/lib/electron-transport.ts
@@ -1,7 +1,7 @@
-import type { UseStreamTransport } from '@langchain/langgraph-sdk/react'
-import type { ToolCall, ToolCallChunk } from '@langchain/core/messages'
-import type { StreamPayload, StreamEvent, IPCEvent, IPCStreamEvent } from '../../../types'
-import type { Subagent } from '../types'
+import type { UseStreamTransport } from "@langchain/langgraph-sdk/react"
+import type { ToolCall, ToolCallChunk } from "@langchain/core/messages"
+import type { StreamPayload, StreamEvent, IPCEvent, IPCStreamEvent } from "../../../types"
+import type { Subagent } from "../types"
/**
* Usage metadata from LangChain model responses.
@@ -102,7 +102,7 @@ export class ElectronIPCTransport implements UseStreamTransport {
const threadId = payload.config?.configurable?.thread_id
const modelId = payload.config?.configurable?.model_id as string | undefined
if (!threadId) {
- return this.createErrorGenerator('MISSING_THREAD_ID', 'Thread ID is required')
+ return this.createErrorGenerator("MISSING_THREAD_ID", "Thread ID is required")
}
// Check if this is a resume command (no message needed)
@@ -114,21 +114,27 @@ export class ElectronIPCTransport implements UseStreamTransport {
| null
| undefined
const messages = input?.messages ?? []
- const lastHumanMessage = messages.find((m) => m.type === 'human')
- const messageContent = lastHumanMessage?.content ?? ''
+ const lastHumanMessage = messages.find((m) => m.type === "human")
+ const messageContent = lastHumanMessage?.content ?? ""
// Only require message content if not resuming
if (!messageContent && !hasResumeCommand) {
- return this.createErrorGenerator('MISSING_MESSAGE', 'Message content is required')
+ return this.createErrorGenerator("MISSING_MESSAGE", "Message content is required")
}
// Create an async generator that bridges IPC events
- return this.createStreamGenerator(threadId, messageContent, payload.command, payload.signal, modelId)
+ return this.createStreamGenerator(
+ threadId,
+ messageContent,
+ payload.command,
+ payload.signal,
+ modelId
+ )
}
private async *createErrorGenerator(code: string, message: string): AsyncGenerator {
yield {
- event: 'error',
+ event: "error",
data: { error: code, message }
}
}
@@ -151,7 +157,7 @@ export class ElectronIPCTransport implements UseStreamTransport {
// Emit metadata event first to establish run context
yield {
- event: 'metadata',
+ event: "metadata",
data: {
run_id: runId,
thread_id: threadId
@@ -159,31 +165,37 @@ export class ElectronIPCTransport implements UseStreamTransport {
}
// Start the stream via IPC (pass modelId to use the selected model)
- const cleanup = window.api.agent.streamAgent(threadId, message, command, (ipcEvent) => {
- // Convert IPC events to SDK format
- const sdkEvents = this.convertToSDKEvents(ipcEvent as IPCEvent, threadId)
+ const cleanup = window.api.agent.streamAgent(
+ threadId,
+ message,
+ command,
+ (ipcEvent) => {
+ // Convert IPC events to SDK format
+ const sdkEvents = this.convertToSDKEvents(ipcEvent as IPCEvent, threadId)
- for (const sdkEvent of sdkEvents) {
- if (sdkEvent.event === 'done' || sdkEvent.event === 'error') {
- isDone = true
- hasError = sdkEvent.event === 'error'
- }
+ for (const sdkEvent of sdkEvents) {
+ if (sdkEvent.event === "done" || sdkEvent.event === "error") {
+ isDone = true
+ hasError = sdkEvent.event === "error"
+ }
- // If someone is waiting for the next event, resolve immediately
- if (resolveNext) {
- const resolve = resolveNext
- resolveNext = null
- resolve(sdkEvent)
- } else {
- // Otherwise queue the event
- eventQueue.push(sdkEvent)
+ // If someone is waiting for the next event, resolve immediately
+ if (resolveNext) {
+ const resolve = resolveNext
+ resolveNext = null
+ resolve(sdkEvent)
+ } else {
+ // Otherwise queue the event
+ eventQueue.push(sdkEvent)
+ }
}
- }
- }, modelId)
+ },
+ modelId
+ )
// Handle abort signal
if (signal) {
- signal.addEventListener('abort', () => {
+ signal.addEventListener("abort", () => {
cleanup()
isDone = true
if (resolveNext) {
@@ -199,10 +211,10 @@ export class ElectronIPCTransport implements UseStreamTransport {
// Check for queued events first
if (eventQueue.length > 0) {
const event = eventQueue.shift()!
- if (event.event === 'done') {
+ if (event.event === "done") {
break
}
- if (event.event !== 'error' || hasError) {
+ if (event.event !== "error" || hasError) {
yield event
}
if (hasError) {
@@ -220,13 +232,13 @@ export class ElectronIPCTransport implements UseStreamTransport {
break
}
- if (event.event === 'done') {
+ if (event.event === "done") {
break
}
yield event
- if (event.event === 'error') {
+ if (event.event === "error") {
break
}
}
@@ -241,29 +253,29 @@ export class ElectronIPCTransport implements UseStreamTransport {
switch (event.type) {
// Raw stream events from LangGraph - parse and convert
- case 'stream': {
+ case "stream": {
const streamEvents = this.processStreamEvent(event)
events.push(...streamEvents)
break
}
// Legacy: Token streaming for real-time typing effect
- case 'token':
+ case "token":
events.push({
- event: 'messages',
+ event: "messages",
data: [
- { id: event.messageId, type: 'ai', content: event.token },
- { langgraph_node: 'agent' }
+ { id: event.messageId, type: "ai", content: event.token },
+ { langgraph_node: "agent" }
]
})
break
// Legacy: Tool call chunks
- case 'tool_call':
+ case "tool_call":
events.push({
- event: 'custom',
+ event: "custom",
data: {
- type: 'tool_call',
+ type: "tool_call",
messageId: event.messageId,
tool_calls: event.tool_calls
}
@@ -271,14 +283,14 @@ export class ElectronIPCTransport implements UseStreamTransport {
break
// Legacy: Full state values
- case 'values': {
+ case "values": {
const { todos, files, workspacePath, subagents, interrupt } = event.data
// Only emit values event if todos is defined
// Avoid emitting { todos: [] } when undefined, which would wipe out existing todos
if (todos !== undefined) {
events.push({
- event: 'values',
+ event: "values",
data: { todos }
})
}
@@ -291,15 +303,15 @@ export class ElectronIPCTransport implements UseStreamTransport {
path,
is_dir: false,
size:
- typeof (data as { content?: string })?.content === 'string'
+ typeof (data as { content?: string })?.content === "string"
? (data as { content: string }).content.length
: undefined
}))
if (filesList.length) {
events.push({
- event: 'custom',
- data: { type: 'workspace', files: filesList, path: workspacePath || '/' }
+ event: "custom",
+ data: { type: "workspace", files: filesList, path: workspacePath || "/" }
})
}
}
@@ -307,8 +319,8 @@ export class ElectronIPCTransport implements UseStreamTransport {
// Emit subagents
if (subagents?.length) {
events.push({
- event: 'custom',
- data: { type: 'subagents', subagents }
+ event: "custom",
+ data: { type: "subagents", subagents }
})
}
@@ -327,9 +339,9 @@ export class ElectronIPCTransport implements UseStreamTransport {
)
events.push({
- event: 'custom',
+ event: "custom",
data: {
- type: 'interrupt',
+ type: "interrupt",
request: {
id: firstAction.id || crypto.randomUUID(),
tool_call: {
@@ -338,9 +350,9 @@ export class ElectronIPCTransport implements UseStreamTransport {
args: firstAction.args || {}
},
allowed_decisions: reviewConfig?.allowedDecisions || [
- 'approve',
- 'reject',
- 'edit'
+ "approve",
+ "reject",
+ "edit"
]
}
}
@@ -349,13 +361,13 @@ export class ElectronIPCTransport implements UseStreamTransport {
} else if (interrupt.tool_call) {
// Legacy format with direct tool_call property
events.push({
- event: 'custom',
+ event: "custom",
data: {
- type: 'interrupt',
+ type: "interrupt",
request: {
id: interrupt.id || crypto.randomUUID(),
tool_call: interrupt.tool_call,
- allowed_decisions: ['approve', 'reject', 'edit']
+ allowed_decisions: ["approve", "reject", "edit"]
}
}
})
@@ -364,25 +376,25 @@ export class ElectronIPCTransport implements UseStreamTransport {
break
}
- case 'error':
+ case "error":
events.push({
- event: 'error',
- data: { error: 'STREAM_ERROR', message: event.error }
+ event: "error",
+ data: { error: "STREAM_ERROR", message: event.error }
})
break
- case 'done':
+ case "done":
events.push({
- event: 'done',
+ event: "done",
data: { thread_id: threadId }
})
break
}
console.log(
- '[Transport] convertToSDKEvents total:',
+ "[Transport] convertToSDKEvents total:",
events.length,
- 'events',
+ "events",
events.map((e) => e.event)
)
return events
@@ -395,20 +407,20 @@ export class ElectronIPCTransport implements UseStreamTransport {
const events: StreamEvent[] = []
const { mode, data } = event
- if (mode === 'messages') {
+ if (mode === "messages") {
// Messages mode returns [message, metadata] tuples
const [msgChunk, metadata] = data as [SerializedMessageChunk, MessageMetadata]
// LangChain serialization: actual data is in kwargs
const kwargs = msgChunk?.kwargs || {}
const classId = Array.isArray(msgChunk?.id) ? msgChunk.id : []
- const className = classId[classId.length - 1] || ''
+ const className = classId[classId.length - 1] || ""
// Check if this is a ToolMessage (class name contains 'ToolMessage')
- const isToolMessage = className.includes('ToolMessage') && !!kwargs.tool_call_id
+ const isToolMessage = className.includes("ToolMessage") && !!kwargs.tool_call_id
// Check if this is an AI message (class name contains 'AI')
- const isAIMessage = className.includes('AI') || className.includes('AIMessageChunk')
+ const isAIMessage = className.includes("AI") || className.includes("AIMessageChunk")
if (isAIMessage) {
const content = this.extractContent(kwargs.content)
@@ -417,16 +429,16 @@ export class ElectronIPCTransport implements UseStreamTransport {
if (content || kwargs.tool_calls?.length) {
events.push({
- event: 'messages',
+ event: "messages",
data: [
{
id: msgId,
- type: 'ai',
- content: content || '',
+ type: "ai",
+ content: content || "",
// Include tool_calls if present
...(kwargs.tool_calls?.length && { tool_calls: kwargs.tool_calls })
},
- { langgraph_node: metadata?.langgraph_node || 'agent' }
+ { langgraph_node: metadata?.langgraph_node || "agent" }
]
})
}
@@ -437,9 +449,9 @@ export class ElectronIPCTransport implements UseStreamTransport {
events.push(...subagentEvents)
events.push({
- event: 'custom',
+ event: "custom",
data: {
- type: 'tool_call',
+ type: "tool_call",
messageId: this.currentMessageId,
tool_calls: kwargs.tool_call_chunks
}
@@ -465,7 +477,7 @@ export class ElectronIPCTransport implements UseStreamTransport {
// Usage metadata is present on completed AI messages (not streaming chunks)
const usageMetadata = kwargs.usage_metadata || kwargs.response_metadata?.usage
if (usageMetadata) {
- console.log('[ElectronTransport] Found usage_metadata:', {
+ console.log("[ElectronTransport] Found usage_metadata:", {
input_tokens: usageMetadata.input_tokens,
output_tokens: usageMetadata.output_tokens,
total_tokens: usageMetadata.total_tokens,
@@ -475,9 +487,9 @@ export class ElectronIPCTransport implements UseStreamTransport {
// Only emit if we have actual token counts (not on every chunk)
if (usageMetadata.input_tokens !== undefined && usageMetadata.input_tokens > 0) {
events.push({
- event: 'custom',
+ event: "custom",
data: {
- type: 'token_usage',
+ type: "token_usage",
usage: {
inputTokens: usageMetadata.input_tokens,
outputTokens: usageMetadata.output_tokens,
@@ -498,26 +510,26 @@ export class ElectronIPCTransport implements UseStreamTransport {
// Emit tool message to the stream
events.push({
- event: 'messages',
+ event: "messages",
data: [
{
id: msgId,
- type: 'tool',
+ type: "tool",
content,
tool_call_id: kwargs.tool_call_id,
name: kwargs.name
},
- { langgraph_node: metadata?.langgraph_node || 'tools' }
+ { langgraph_node: metadata?.langgraph_node || "tools" }
]
})
// Handle subagent task completion
- if (kwargs.name === 'task') {
+ if (kwargs.name === "task") {
const completionEvents = this.processToolMessage(kwargs.tool_call_id)
events.push(...completionEvents)
}
}
- } else if (mode === 'values') {
+ } else if (mode === "values") {
// Values mode returns full state with serialized LangChain messages
const state = data as {
messages?: SerializedMessageChunk[]
@@ -538,13 +550,13 @@ export class ElectronIPCTransport implements UseStreamTransport {
for (const msg of state.messages) {
const kwargs = msg.kwargs || {}
const classId = Array.isArray(msg.id) ? msg.id : []
- const className = classId[classId.length - 1] || ''
+ const className = classId[classId.length - 1] || ""
// Check for task tool calls in AI messages
if (kwargs.tool_calls?.length) {
for (const toolCall of kwargs.tool_calls) {
if (
- toolCall.name === 'task' &&
+ toolCall.name === "task" &&
toolCall.id &&
!this.activeSubagents.has(toolCall.id)
) {
@@ -558,10 +570,10 @@ export class ElectronIPCTransport implements UseStreamTransport {
}
// Check for ToolMessage (subagent completion)
- if (className.includes('ToolMessage') && kwargs.tool_call_id && kwargs.name === 'task') {
+ if (className.includes("ToolMessage") && kwargs.tool_call_id && kwargs.name === "task") {
const subagent = this.activeSubagents.get(kwargs.tool_call_id)
- if (subagent && subagent.status === 'running') {
- subagent.status = 'completed'
+ if (subagent && subagent.status === "running") {
+ subagent.status = "completed"
subagent.completedAt = new Date()
}
}
@@ -578,17 +590,17 @@ export class ElectronIPCTransport implements UseStreamTransport {
const transformedMessages = state.messages
?.filter((msg) => {
const classId = Array.isArray(msg.id) ? msg.id : []
- const className = classId[classId.length - 1] || ''
+ const className = classId[classId.length - 1] || ""
// Filter out HumanMessage
- return !className.includes('Human')
+ return !className.includes("Human")
})
.map((msg) => {
const kwargs = msg.kwargs || {}
const classId = Array.isArray(msg.id) ? msg.id : []
- const className = classId[classId.length - 1] || ''
+ const className = classId[classId.length - 1] || ""
// Determine message type from class name
- const type: 'ai' | 'tool' = className.includes('Tool') ? 'tool' : 'ai'
+ const type: "ai" | "tool" = className.includes("Tool") ? "tool" : "ai"
const content = this.extractContent(kwargs.content)
return {
@@ -596,10 +608,10 @@ export class ElectronIPCTransport implements UseStreamTransport {
type,
content,
// Include tool_calls for AI messages
- ...(type === 'ai' && kwargs.tool_calls && { tool_calls: kwargs.tool_calls }),
+ ...(type === "ai" && kwargs.tool_calls && { tool_calls: kwargs.tool_calls }),
// Include tool_call_id and name for tool messages
- ...(type === 'tool' && kwargs.tool_call_id && { tool_call_id: kwargs.tool_call_id }),
- ...(type === 'tool' && kwargs.name && { name: kwargs.name })
+ ...(type === "tool" && kwargs.tool_call_id && { tool_call_id: kwargs.tool_call_id }),
+ ...(type === "tool" && kwargs.name && { name: kwargs.name })
}
})
@@ -619,7 +631,7 @@ export class ElectronIPCTransport implements UseStreamTransport {
// Only emit if we have something to update
if (Object.keys(valuesData).length > 0) {
events.push({
- event: 'values',
+ event: "values",
data: valuesData
})
}
@@ -632,15 +644,15 @@ export class ElectronIPCTransport implements UseStreamTransport {
path,
is_dir: false,
size:
- typeof (fileData as { content?: string })?.content === 'string'
+ typeof (fileData as { content?: string })?.content === "string"
? (fileData as { content: string }).content.length
: undefined
}))
if (filesList.length) {
events.push({
- event: 'custom',
- data: { type: 'workspace', files: filesList, path: state.workspacePath || '/' }
+ event: "custom",
+ data: { type: "workspace", files: filesList, path: state.workspacePath || "/" }
})
}
}
@@ -670,9 +682,9 @@ export class ElectronIPCTransport implements UseStreamTransport {
}
events.push({
- event: 'custom',
+ event: "custom",
data: {
- type: 'interrupt',
+ type: "interrupt",
request: {
id: toolCallId || crypto.randomUUID(),
tool_call: {
@@ -680,7 +692,7 @@ export class ElectronIPCTransport implements UseStreamTransport {
name: firstAction.name,
args: firstAction.args || {}
},
- allowed_decisions: reviewConfig?.allowedDecisions || ['approve', 'reject', 'edit']
+ allowed_decisions: reviewConfig?.allowedDecisions || ["approve", "reject", "edit"]
}
}
})
@@ -697,16 +709,16 @@ export class ElectronIPCTransport implements UseStreamTransport {
private extractContent(
content: string | Array<{ type: string; text?: string }> | undefined
): string {
- if (typeof content === 'string') {
+ if (typeof content === "string") {
return content
}
if (Array.isArray(content)) {
return content
- .filter((block): block is { type: 'text'; text: string } => block.type === 'text')
+ .filter((block): block is { type: "text"; text: string } => block.type === "text")
.map((block) => block.text)
- .join('')
+ .join("")
}
- return ''
+ return ""
}
/**
@@ -724,7 +736,7 @@ export class ElectronIPCTransport implements UseStreamTransport {
// Get or create accumulated tool call
let accumulated = this.accumulatedToolCalls.get(chunk.id)
if (!accumulated) {
- accumulated = { id: chunk.id, name: chunk.name || '', args: '' }
+ accumulated = { id: chunk.id, name: chunk.name || "", args: "" }
this.accumulatedToolCalls.set(chunk.id, accumulated)
}
@@ -739,7 +751,7 @@ export class ElectronIPCTransport implements UseStreamTransport {
}
// Check if this is a "task" tool call and try to parse args
- if (accumulated.name === 'task') {
+ if (accumulated.name === "task") {
try {
const args = JSON.parse(accumulated.args)
// Only process if we haven't already created a subagent for this tool call
@@ -769,7 +781,7 @@ export class ElectronIPCTransport implements UseStreamTransport {
if (!toolCall.id || !toolCall.name) continue
// Check if this is a "task" tool call
- if (toolCall.name === 'task' && !this.activeSubagents.has(toolCall.id)) {
+ if (toolCall.name === "task" && !this.activeSubagents.has(toolCall.id)) {
const args = toolCall.args || {}
if (args.subagent_type || args.description) {
const subagent = this.createSubagentFromTask(toolCall.id, args)
@@ -791,7 +803,7 @@ export class ElectronIPCTransport implements UseStreamTransport {
// Check if this tool_call_id corresponds to an active subagent
const subagent = this.activeSubagents.get(toolCallId)
if (subagent) {
- subagent.status = 'completed'
+ subagent.status = "completed"
subagent.completedAt = new Date()
events.push(this.createSubagentEvent())
}
@@ -803,16 +815,16 @@ export class ElectronIPCTransport implements UseStreamTransport {
* Create a Subagent object from task tool call args
*/
private createSubagentFromTask(toolCallId: string, args: Record): Subagent {
- const subagentType = (args.subagent_type as string) || 'general-purpose'
- const description = (args.description as string) || 'Executing task...'
+ const subagentType = (args.subagent_type as string) || "general-purpose"
+ const description = (args.description as string) || "Executing task..."
// Generate a friendly name from the subagent type
const nameMap: Record = {
- 'general-purpose': 'General Purpose Agent',
- 'correctness-checker': 'Correctness Checker',
- 'final-reviewer': 'Final Reviewer',
- 'code-reviewer': 'Code Reviewer',
- research: 'Research Agent'
+ "general-purpose": "General Purpose Agent",
+ "correctness-checker": "Correctness Checker",
+ "final-reviewer": "Final Reviewer",
+ "code-reviewer": "Code Reviewer",
+ research: "Research Agent"
}
return {
@@ -820,7 +832,7 @@ export class ElectronIPCTransport implements UseStreamTransport {
toolCallId,
name: nameMap[subagentType] || this.formatSubagentName(subagentType),
description,
- status: 'running',
+ status: "running",
startedAt: new Date(),
subagentType
}
@@ -831,9 +843,9 @@ export class ElectronIPCTransport implements UseStreamTransport {
*/
private formatSubagentName(subagentType: string): string {
return subagentType
- .split('-')
+ .split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
- .join(' ')
+ .join(" ")
}
/**
@@ -841,9 +853,9 @@ export class ElectronIPCTransport implements UseStreamTransport {
*/
private createSubagentEvent(): StreamEvent {
return {
- event: 'custom',
+ event: "custom",
data: {
- type: 'subagents',
+ type: "subagents",
subagents: Array.from(this.activeSubagents.values())
}
}
diff --git a/src/renderer/src/lib/file-types.ts b/src/renderer/src/lib/file-types.ts
index d151240..f3ad081 100644
--- a/src/renderer/src/lib/file-types.ts
+++ b/src/renderer/src/lib/file-types.ts
@@ -1,4 +1,4 @@
-export type FileType = 'image' | 'video' | 'audio' | 'pdf' | 'code' | 'text' | 'binary'
+export type FileType = "image" | "video" | "audio" | "pdf" | "code" | "text" | "binary"
interface FileTypeInfo {
type: FileType
@@ -7,50 +7,93 @@ interface FileTypeInfo {
}
const IMAGE_EXTENSIONS = new Set([
- 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'ico', 'tiff', 'tif'
+ "png",
+ "jpg",
+ "jpeg",
+ "gif",
+ "svg",
+ "webp",
+ "bmp",
+ "ico",
+ "tiff",
+ "tif"
])
-const VIDEO_EXTENSIONS = new Set([
- 'mp4', 'webm', 'ogg', 'ogv', 'mov', 'avi', 'wmv', 'flv', 'mkv'
-])
+const VIDEO_EXTENSIONS = new Set(["mp4", "webm", "ogg", "ogv", "mov", "avi", "wmv", "flv", "mkv"])
-const AUDIO_EXTENSIONS = new Set([
- 'mp3', 'wav', 'ogg', 'oga', 'm4a', 'flac', 'aac', 'weba'
-])
+const AUDIO_EXTENSIONS = new Set(["mp3", "wav", "ogg", "oga", "m4a", "flac", "aac", "weba"])
-const PDF_EXTENSIONS = new Set(['pdf'])
+const PDF_EXTENSIONS = new Set(["pdf"])
const CODE_EXTENSIONS = new Set([
- 'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs',
- 'py', 'java', 'c', 'cpp', 'h', 'hpp',
- 'cs', 'go', 'rs', 'rb', 'php',
- 'json', 'xml', 'yaml', 'yml', 'toml',
- 'css', 'scss', 'sass', 'less',
- 'html', 'htm', 'vue', 'svelte',
- 'md', 'mdx', 'markdown',
- 'sh', 'bash', 'zsh', 'fish',
- 'sql', 'graphql', 'proto',
- 'dockerfile', 'makefile'
+ "ts",
+ "tsx",
+ "js",
+ "jsx",
+ "mjs",
+ "cjs",
+ "py",
+ "java",
+ "c",
+ "cpp",
+ "h",
+ "hpp",
+ "cs",
+ "go",
+ "rs",
+ "rb",
+ "php",
+ "json",
+ "xml",
+ "yaml",
+ "yml",
+ "toml",
+ "css",
+ "scss",
+ "sass",
+ "less",
+ "html",
+ "htm",
+ "vue",
+ "svelte",
+ "md",
+ "mdx",
+ "markdown",
+ "sh",
+ "bash",
+ "zsh",
+ "fish",
+ "sql",
+ "graphql",
+ "proto",
+ "dockerfile",
+ "makefile"
])
const TEXT_EXTENSIONS = new Set([
- 'txt', 'log', 'csv', 'tsv',
- 'env', 'gitignore', 'editorconfig',
- 'conf', 'config', 'ini', 'cfg'
+ "txt",
+ "log",
+ "csv",
+ "tsv",
+ "env",
+ "gitignore",
+ "editorconfig",
+ "conf",
+ "config",
+ "ini",
+ "cfg"
])
export function getFileType(fileName: string): FileTypeInfo {
- const ext = fileName.includes('.')
- ? fileName.split('.').pop()?.toLowerCase()
- : undefined
+ const ext = fileName.includes(".") ? fileName.split(".").pop()?.toLowerCase() : undefined
if (!ext) {
- return { type: 'text', canPreview: true }
+ return { type: "text", canPreview: true }
}
if (IMAGE_EXTENSIONS.has(ext)) {
return {
- type: 'image',
+ type: "image",
mimeType: getMimeType(ext),
canPreview: true
}
@@ -58,7 +101,7 @@ export function getFileType(fileName: string): FileTypeInfo {
if (VIDEO_EXTENSIONS.has(ext)) {
return {
- type: 'video',
+ type: "video",
mimeType: getMimeType(ext),
canPreview: true
}
@@ -66,7 +109,7 @@ export function getFileType(fileName: string): FileTypeInfo {
if (AUDIO_EXTENSIONS.has(ext)) {
return {
- type: 'audio',
+ type: "audio",
mimeType: getMimeType(ext),
canPreview: true
}
@@ -74,28 +117,28 @@ export function getFileType(fileName: string): FileTypeInfo {
if (PDF_EXTENSIONS.has(ext)) {
return {
- type: 'pdf',
- mimeType: 'application/pdf',
+ type: "pdf",
+ mimeType: "application/pdf",
canPreview: true
}
}
if (CODE_EXTENSIONS.has(ext)) {
return {
- type: 'code',
+ type: "code",
canPreview: true
}
}
if (TEXT_EXTENSIONS.has(ext)) {
return {
- type: 'text',
+ type: "text",
canPreview: true
}
}
return {
- type: 'binary',
+ type: "binary",
canPreview: false
}
}
@@ -103,45 +146,47 @@ export function getFileType(fileName: string): FileTypeInfo {
function getMimeType(ext: string): string {
const mimeTypes: Record = {
// Images
- 'png': 'image/png',
- 'jpg': 'image/jpeg',
- 'jpeg': 'image/jpeg',
- 'gif': 'image/gif',
- 'svg': 'image/svg+xml',
- 'webp': 'image/webp',
- 'bmp': 'image/bmp',
- 'ico': 'image/x-icon',
- 'tiff': 'image/tiff',
- 'tif': 'image/tiff',
-
+ png: "image/png",
+ jpg: "image/jpeg",
+ jpeg: "image/jpeg",
+ gif: "image/gif",
+ svg: "image/svg+xml",
+ webp: "image/webp",
+ bmp: "image/bmp",
+ ico: "image/x-icon",
+ tiff: "image/tiff",
+ tif: "image/tiff",
+
// Video
- 'mp4': 'video/mp4',
- 'webm': 'video/webm',
- 'ogg': 'video/ogg',
- 'ogv': 'video/ogg',
- 'mov': 'video/quicktime',
- 'avi': 'video/x-msvideo',
- 'wmv': 'video/x-ms-wmv',
- 'flv': 'video/x-flv',
- 'mkv': 'video/x-matroska',
-
+ mp4: "video/mp4",
+ webm: "video/webm",
+ ogg: "video/ogg",
+ ogv: "video/ogg",
+ mov: "video/quicktime",
+ avi: "video/x-msvideo",
+ wmv: "video/x-ms-wmv",
+ flv: "video/x-flv",
+ mkv: "video/x-matroska",
+
// Audio
- 'mp3': 'audio/mpeg',
- 'wav': 'audio/wav',
- 'oga': 'audio/ogg',
- 'm4a': 'audio/mp4',
- 'flac': 'audio/flac',
- 'aac': 'audio/aac',
- 'weba': 'audio/webm',
-
+ mp3: "audio/mpeg",
+ wav: "audio/wav",
+ oga: "audio/ogg",
+ m4a: "audio/mp4",
+ flac: "audio/flac",
+ aac: "audio/aac",
+ weba: "audio/webm",
+
// PDF
- 'pdf': 'application/pdf'
+ pdf: "application/pdf"
}
- return mimeTypes[ext] || 'application/octet-stream'
+ return mimeTypes[ext] || "application/octet-stream"
}
export function isBinaryFile(fileName: string): boolean {
const { type } = getFileType(fileName)
- return type === 'image' || type === 'video' || type === 'audio' || type === 'pdf' || type === 'binary'
+ return (
+ type === "image" || type === "video" || type === "audio" || type === "pdf" || type === "binary"
+ )
}
diff --git a/src/renderer/src/lib/store.ts b/src/renderer/src/lib/store.ts
index 83b6480..b0ebc84 100644
--- a/src/renderer/src/lib/store.ts
+++ b/src/renderer/src/lib/store.ts
@@ -1,5 +1,5 @@
-import { create } from 'zustand'
-import type { Thread, ModelConfig, Provider } from '@/types'
+import { create } from "zustand"
+import type { Thread, ModelConfig, Provider } from "@/types"
interface AppState {
// Threads
@@ -11,7 +11,7 @@ interface AppState {
providers: Provider[]
// Right panel state (UI state, not thread data)
- rightPanelTab: 'todos' | 'files' | 'subagents'
+ rightPanelTab: "todos" | "files" | "subagents"
// Settings dialog state
settingsOpen: boolean
@@ -34,7 +34,7 @@ interface AppState {
deleteApiKey: (providerId: string) => Promise
// Panel actions
- setRightPanelTab: (tab: 'todos' | 'files' | 'subagents') => void
+ setRightPanelTab: (tab: "todos" | "files" | "subagents") => void
// Settings actions
setSettingsOpen: (open: boolean) => void
@@ -50,7 +50,7 @@ export const useAppStore = create((set, get) => ({
currentThreadId: null,
models: [],
providers: [],
- rightPanelTab: 'todos',
+ rightPanelTab: "todos",
settingsOpen: false,
sidebarCollapsed: false,
@@ -80,10 +80,10 @@ export const useAppStore = create((set, get) => ({
},
deleteThread: async (threadId: string) => {
- console.log('[Store] Deleting thread:', threadId)
+ console.log("[Store] Deleting thread:", threadId)
try {
await window.api.threads.delete(threadId)
- console.log('[Store] Thread deleted from backend')
+ console.log("[Store] Thread deleted from backend")
set((state) => {
const threads = state.threads.filter((t) => t.thread_id !== threadId)
@@ -98,7 +98,7 @@ export const useAppStore = create((set, get) => ({
}
})
} catch (error) {
- console.error('[Store] Failed to delete thread:', error)
+ console.error("[Store] Failed to delete thread:", error)
}
},
@@ -114,7 +114,7 @@ export const useAppStore = create((set, get) => ({
const generatedTitle = await window.api.threads.generateTitle(content)
await get().updateThread(threadId, { title: generatedTitle })
} catch (error) {
- console.error('[Store] Failed to generate title:', error)
+ console.error("[Store] Failed to generate title:", error)
}
},
@@ -130,16 +130,16 @@ export const useAppStore = create((set, get) => ({
},
setApiKey: async (providerId: string, apiKey: string) => {
- console.log('[Store] setApiKey called:', { providerId, keyLength: apiKey.length })
+ console.log("[Store] setApiKey called:", { providerId, keyLength: apiKey.length })
try {
await window.api.models.setApiKey(providerId, apiKey)
- console.log('[Store] API key saved via IPC')
+ console.log("[Store] API key saved via IPC")
// Reload providers and models to update availability
await get().loadProviders()
await get().loadModels()
- console.log('[Store] Providers and models reloaded')
+ console.log("[Store] Providers and models reloaded")
} catch (e) {
- console.error('[Store] Failed to set API key:', e)
+ console.error("[Store] Failed to set API key:", e)
throw e
}
},
@@ -152,7 +152,7 @@ export const useAppStore = create((set, get) => ({
},
// Panel actions
- setRightPanelTab: (tab: 'todos' | 'files' | 'subagents') => {
+ setRightPanelTab: (tab: "todos" | "files" | "subagents") => {
set({ rightPanelTab: tab })
},
diff --git a/src/renderer/src/lib/thread-context.tsx b/src/renderer/src/lib/thread-context.tsx
index ae95e80..6368a6c 100644
--- a/src/renderer/src/lib/thread-context.tsx
+++ b/src/renderer/src/lib/thread-context.tsx
@@ -8,13 +8,13 @@ import {
useEffect,
useSyncExternalStore,
type ReactNode
-} from 'react'
+} from "react"
/* eslint-disable react-refresh/only-export-components */
-import { useStream } from '@langchain/langgraph-sdk/react'
-import { ElectronIPCTransport } from './electron-transport'
-import type { Message, Todo, FileInfo, Subagent, HITLRequest } from '@/types'
-import type { DeepAgent } from '../../../main/agent/types'
+import { useStream } from "@langchain/langgraph-sdk/react"
+import { ElectronIPCTransport } from "./electron-transport"
+import type { Message, Todo, FileInfo, Subagent, HITLRequest } from "@/types"
+import type { DeepAgent } from "../../../main/agent/types"
// Open file tab type
export interface OpenFile {
@@ -43,7 +43,7 @@ export interface ThreadState {
error: string | null
currentModel: string
openFiles: OpenFile[]
- activeTab: 'agent' | string
+ activeTab: "agent" | string
fileContents: Record
tokenUsage: TokenUsage | null
}
@@ -53,7 +53,7 @@ type StreamInstance = ReturnType>
// Stream data that we want to be reactive
interface StreamData {
- messages: StreamInstance['messages']
+ messages: StreamInstance["messages"]
isLoading: boolean
stream: StreamInstance | null
}
@@ -72,7 +72,7 @@ export interface ThreadActions {
setCurrentModel: (modelId: string) => void
openFile: (path: string, name: string) => void
closeFile: (path: string) => void
- setActiveTab: (tab: 'agent' | string) => void
+ setActiveTab: (tab: "agent" | string) => void
setFileContents: (path: string, content: string) => void
}
@@ -96,9 +96,9 @@ const createDefaultThreadState = (): ThreadState => ({
subagents: [],
pendingApproval: null,
error: null,
- currentModel: 'claude-sonnet-4-5-20250929',
+ currentModel: "claude-sonnet-4-5-20250929",
openFiles: [],
- activeTab: 'agent',
+ activeTab: "agent",
fileContents: {},
tokenUsage: null
})
@@ -162,7 +162,7 @@ function ThreadStreamHolder({
const stream = useStream({
transport,
threadId,
- messagesKey: 'messages',
+ messagesKey: "messages",
onCustomEvent: (data) => {
onCustomEventRef.current(data as CustomEventData)
},
@@ -211,7 +211,7 @@ function ThreadStreamHolder({
return null
}
-export function ThreadProvider({ children }: { children: ReactNode }): React.JSX.Element {
+export function ThreadProvider({ children }: { children: ReactNode }) {
const [threadStates, setThreadStates] = useState>({})
const [activeThreadIds, setActiveThreadIds] = useState>(new Set())
const initializedThreadsRef = useRef>(new Set())
@@ -259,7 +259,11 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
(threadId: string): ThreadState => {
const state = threadStates[threadId] || createDefaultThreadState()
if (state.pendingApproval) {
- console.log('[ThreadContext] getThreadState returning pendingApproval for:', threadId, state.pendingApproval)
+ console.log(
+ "[ThreadContext] getThreadState returning pendingApproval for:",
+ threadId,
+ state.pendingApproval
+ )
}
return state
},
@@ -282,10 +286,12 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
// Parse error messages into user-friendly format
const parseErrorMessage = useCallback((error: Error | string): string => {
- const errorMessage = typeof error === 'string' ? error : error.message
+ const errorMessage = typeof error === "string" ? error : error.message
// Check for context window exceeded errors
- const contextWindowMatch = errorMessage.match(/prompt is too long: (\d+) tokens > (\d+) maximum/i)
+ const contextWindowMatch = errorMessage.match(
+ /prompt is too long: (\d+) tokens > (\d+) maximum/i
+ )
if (contextWindowMatch) {
const [, usedTokens, maxTokens] = contextWindowMatch
const usedK = Math.round(parseInt(usedTokens) / 1000)
@@ -294,13 +300,17 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
}
// Check for rate limit errors
- if (errorMessage.includes('rate_limit') || errorMessage.includes('429')) {
- return 'Rate limit exceeded. Please wait a moment before sending another message.'
+ if (errorMessage.includes("rate_limit") || errorMessage.includes("429")) {
+ return "Rate limit exceeded. Please wait a moment before sending another message."
}
// Check for authentication errors
- if (errorMessage.includes('401') || errorMessage.includes('invalid_api_key') || errorMessage.includes('authentication')) {
- return 'Authentication failed. Please check your API key in settings.'
+ if (
+ errorMessage.includes("401") ||
+ errorMessage.includes("invalid_api_key") ||
+ errorMessage.includes("authentication")
+ ) {
+ return "Authentication failed. Please check your API key in settings."
}
// Return the original message for other errors
@@ -310,7 +320,7 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
// Handle errors from ThreadStreamHolder
const handleError = useCallback(
(threadId: string, error: Error) => {
- console.error('[ThreadContext] Stream error:', { threadId, error })
+ console.error("[ThreadContext] Stream error:", { threadId, error })
const userFriendlyMessage = parseErrorMessage(error)
updateThreadState(threadId, () => ({ error: userFriendlyMessage }))
},
@@ -320,15 +330,19 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
// Handle custom events from ThreadStreamHolder (interrupts, workspace updates, etc.)
const handleCustomEvent = useCallback(
(threadId: string, data: CustomEventData) => {
- console.log('[ThreadContext] Custom event received:', { threadId, type: data.type, data })
+ console.log("[ThreadContext] Custom event received:", { threadId, type: data.type, data })
switch (data.type) {
- case 'interrupt':
+ case "interrupt":
if (data.request) {
- console.log('[ThreadContext] Setting pendingApproval for thread:', threadId, data.request)
+ console.log(
+ "[ThreadContext] Setting pendingApproval for thread:",
+ threadId,
+ data.request
+ )
updateThreadState(threadId, () => ({ pendingApproval: data.request }))
}
break
- case 'workspace':
+ case "workspace":
if (Array.isArray(data.files)) {
updateThreadState(threadId, (state) => {
const fileMap = new Map(state.workspaceFiles.map((f) => [f.path, f]))
@@ -342,25 +356,25 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
updateThreadState(threadId, () => ({ workspacePath: data.path }))
}
break
- case 'subagents':
+ case "subagents":
if (Array.isArray(data.subagents)) {
updateThreadState(threadId, () => ({
subagents: data.subagents!.map((s) => ({
id: s.id || crypto.randomUUID(),
- name: s.name || 'Subagent',
- description: s.description || '',
- status: (s.status || 'pending') as 'pending' | 'running' | 'completed' | 'failed',
+ name: s.name || "Subagent",
+ description: s.description || "",
+ status: (s.status || "pending") as "pending" | "running" | "completed" | "failed",
startedAt: s.startedAt,
completedAt: s.completedAt
}))
}))
}
break
- case 'token_usage':
+ case "token_usage":
// Only update if we have meaningful token values (> 0)
// This prevents resetting the usage when streaming ends
if (data.usage && data.usage.inputTokens !== undefined && data.usage.inputTokens > 0) {
- console.log('[ThreadContext] Token usage update:', {
+ console.log("[ThreadContext] Token usage update:", {
threadId,
inputTokens: data.usage.inputTokens,
outputTokens: data.usage.outputTokens,
@@ -419,7 +433,7 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
},
setWorkspaceFiles: (files: FileInfo[] | ((prev: FileInfo[]) => FileInfo[])) => {
updateThreadState(threadId, (state) => ({
- workspaceFiles: typeof files === 'function' ? files(state.workspaceFiles) : files
+ workspaceFiles: typeof files === "function" ? files(state.workspaceFiles) : files
}))
},
setWorkspacePath: (path: string | null) => {
@@ -465,14 +479,18 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
let newActiveTab = state.activeTab
if (state.activeTab === path) {
const closedIndex = state.openFiles.findIndex((f) => f.path === path)
- if (newOpenFiles.length === 0) newActiveTab = 'agent'
+ if (newOpenFiles.length === 0) newActiveTab = "agent"
else if (closedIndex > 0) newActiveTab = newOpenFiles[closedIndex - 1].path
else newActiveTab = newOpenFiles[0].path
}
- return { openFiles: newOpenFiles, activeTab: newActiveTab, fileContents: newFileContents }
+ return {
+ openFiles: newOpenFiles,
+ activeTab: newActiveTab,
+ fileContents: newFileContents
+ }
})
},
- setActiveTab: (tab: 'agent' | string) => {
+ setActiveTab: (tab: "agent" | string) => {
updateThreadState(threadId, () => ({ activeTab: tab }))
},
setFileContents: (path: string, content: string) => {
@@ -510,7 +528,7 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
}
}
} catch (error) {
- console.error('[ThreadContext] Failed to load thread details:', error)
+ console.error("[ThreadContext] Failed to load thread details:", error)
}
// Load thread history from checkpoints
@@ -551,31 +569,31 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
if (channelValues?.messages && Array.isArray(channelValues.messages)) {
const messages: Message[] = channelValues.messages.map((msg, index) => {
- let role: 'user' | 'assistant' | 'system' | 'tool' = 'assistant'
- if (typeof msg._getType === 'function') {
+ let role: "user" | "assistant" | "system" | "tool" = "assistant"
+ if (typeof msg._getType === "function") {
const type = msg._getType()
- if (type === 'human') role = 'user'
- else if (type === 'ai') role = 'assistant'
- else if (type === 'system') role = 'system'
- else if (type === 'tool') role = 'tool'
+ if (type === "human") role = "user"
+ else if (type === "ai") role = "assistant"
+ else if (type === "system") role = "system"
+ else if (type === "tool") role = "tool"
} else if (msg.type) {
- if (msg.type === 'human') role = 'user'
- else if (msg.type === 'ai') role = 'assistant'
- else if (msg.type === 'system') role = 'system'
- else if (msg.type === 'tool') role = 'tool'
+ if (msg.type === "human") role = "user"
+ else if (msg.type === "ai") role = "assistant"
+ else if (msg.type === "system") role = "system"
+ else if (msg.type === "tool") role = "tool"
}
- let content: Message['content'] = ''
- if (typeof msg.content === 'string') content = msg.content
- else if (Array.isArray(msg.content)) content = msg.content as Message['content']
+ let content: Message["content"] = ""
+ if (typeof msg.content === "string") content = msg.content
+ else if (Array.isArray(msg.content)) content = msg.content as Message["content"]
return {
id: msg.id || `msg-${index}`,
role,
content,
- tool_calls: msg.tool_calls as Message['tool_calls'],
- ...(role === 'tool' && msg.tool_call_id && { tool_call_id: msg.tool_call_id }),
- ...(role === 'tool' && msg.name && { name: msg.name }),
+ tool_calls: msg.tool_calls as Message["tool_calls"],
+ ...(role === "tool" && msg.tool_call_id && { tool_call_id: msg.tool_call_id }),
+ ...(role === "tool" && msg.name && { name: msg.name }),
created_at: new Date()
}
})
@@ -585,8 +603,8 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
if (channelValues?.todos && Array.isArray(channelValues.todos)) {
const todos: Todo[] = channelValues.todos.map((todo, index) => ({
id: todo.id || `todo-${index}`,
- content: todo.content || '',
- status: (todo.status as Todo['status']) || 'pending'
+ content: todo.content || "",
+ status: (todo.status as Todo["status"]) || "pending"
}))
actions.setTodos(todos)
}
@@ -608,7 +626,7 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
name: req.action,
args: req.args
},
- allowed_decisions: ['approve', 'reject', 'edit']
+ allowed_decisions: ["approve", "reject", "edit"]
}
actions.setPendingApproval(hitlRequest)
} else if (reviewConfigs && reviewConfigs.length > 0) {
@@ -621,17 +639,17 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
name: config.toolName,
args: config.toolArgs
},
- allowed_decisions: ['approve', 'reject', 'edit']
+ allowed_decisions: ["approve", "reject", "edit"]
}
actions.setPendingApproval(hitlRequest)
}
}
}
} catch (error) {
- console.error('[ThreadContext] Failed to load thread history:', error)
+ console.error("[ThreadContext] Failed to load thread history:", error)
}
},
- [getThreadActions]
+ [getThreadActions, updateThreadState]
)
const initializeThread = useCallback(
@@ -663,7 +681,8 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
return next
})
setThreadStates((prev) => {
- const { [threadId]: _, ...rest } = prev
+ const { [threadId]: _removed, ...rest } = prev
+ void _removed // Explicitly mark as intentionally unused
return rest
})
}, [])
@@ -677,7 +696,14 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
subscribeToStream,
getStreamData
}),
- [getThreadState, getThreadActions, initializeThread, cleanupThread, subscribeToStream, getStreamData]
+ [
+ getThreadState,
+ getThreadActions,
+ initializeThread,
+ cleanupThread,
+ subscribeToStream,
+ getStreamData
+ ]
)
return (
@@ -697,10 +723,9 @@ export function ThreadProvider({ children }: { children: ReactNode }): React.JSX
)
}
-// eslint-disable-next-line react-refresh/only-export-components
export function useThreadContext(): ThreadContextValue {
const context = useContext(ThreadContext)
- if (!context) throw new Error('useThreadContext must be used within a ThreadProvider')
+ if (!context) throw new Error("useThreadContext must be used within a ThreadProvider")
return context
}
diff --git a/src/renderer/src/lib/utils.ts b/src/renderer/src/lib/utils.ts
index bf46ef9..e1d0658 100644
--- a/src/renderer/src/lib/utils.ts
+++ b/src/renderer/src/lib/utils.ts
@@ -6,36 +6,36 @@ export function cn(...inputs: ClassValue[]) {
}
export function formatDate(date: Date | string): string {
- const d = typeof date === 'string' ? new Date(date) : date
- return d.toLocaleDateString('en-US', {
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit'
+ const d = typeof date === "string" ? new Date(date) : date
+ return d.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit"
})
}
export function formatRelativeTime(date: Date | string): string {
- const d = typeof date === 'string' ? new Date(date) : date
+ const d = typeof date === "string" ? new Date(date) : date
const now = new Date()
const diff = now.getTime() - d.getTime()
-
+
const seconds = Math.floor(diff / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
- if (seconds < 60) return 'just now'
+ if (seconds < 60) return "just now"
if (minutes < 60) return `${minutes}m ago`
if (hours < 24) return `${hours}h ago`
if (days < 7) return `${days}d ago`
-
+
return formatDate(d)
}
export function truncate(str: string, length: number): string {
if (str.length <= length) return str
- return str.slice(0, length) + '...'
+ return str.slice(0, length) + "..."
}
export function generateId(): string {
diff --git a/src/renderer/src/lib/workspace-utils.ts b/src/renderer/src/lib/workspace-utils.ts
index 677077f..13b6462 100644
--- a/src/renderer/src/lib/workspace-utils.ts
+++ b/src/renderer/src/lib/workspace-utils.ts
@@ -1,7 +1,7 @@
export async function selectWorkspaceFolder(
currentThreadId: string | null,
setWorkspacePath: (path: string | null) => void,
- setWorkspaceFiles: (files: any[]) => void,
+ setWorkspaceFiles: (files: Array<{ path: string; is_dir?: boolean; size?: number }>) => void,
setLoading: (loading: boolean) => void,
setOpen?: (open: boolean) => void
): Promise {
@@ -18,7 +18,7 @@ export async function selectWorkspaceFolder(
}
if (setOpen) setOpen(false)
} catch (e) {
- console.error('[WorkspacePicker] Select folder error:', e)
+ console.error("[WorkspacePicker] Select folder error:", e)
} finally {
setLoading(false)
}
diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx
index 4a1b150..44814f6 100644
--- a/src/renderer/src/main.tsx
+++ b/src/renderer/src/main.tsx
@@ -1,9 +1,9 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import App from './App'
-import './index.css'
+import React from "react"
+import ReactDOM from "react-dom/client"
+import App from "./App"
+import "./index.css"
-ReactDOM.createRoot(document.getElementById('root')!).render(
+ReactDOM.createRoot(document.getElementById("root")!).render(
diff --git a/src/renderer/src/types.ts b/src/renderer/src/types.ts
index 5343eef..08033b4 100644
--- a/src/renderer/src/types.ts
+++ b/src/renderer/src/types.ts
@@ -1,5 +1,5 @@
// Re-export types from electron for use in renderer
-export type ThreadStatus = 'idle' | 'busy' | 'interrupted' | 'error'
+export type ThreadStatus = "idle" | "busy" | "interrupted" | "error"
export interface Thread {
thread_id: string
@@ -11,7 +11,7 @@ export interface Thread {
title?: string
}
-export type RunStatus = 'pending' | 'running' | 'error' | 'success' | 'interrupted'
+export type RunStatus = "pending" | "running" | "error" | "success" | "interrupted"
export interface Run {
run_id: string
@@ -24,7 +24,7 @@ export interface Run {
}
// Provider configuration
-export type ProviderId = 'anthropic' | 'openai' | 'google' | 'ollama'
+export type ProviderId = "anthropic" | "openai" | "google" | "ollama"
export interface Provider {
id: ProviderId
@@ -46,7 +46,7 @@ export interface Subagent {
id: string
name: string
description: string
- status: 'pending' | 'running' | 'completed' | 'failed'
+ status: "pending" | "running" | "completed" | "failed"
startedAt?: Date
completedAt?: Date
// Used to correlate task tool calls with their responses
@@ -56,20 +56,20 @@ export interface Subagent {
}
export type StreamEvent =
- | { type: 'message'; message: Message }
- | { type: 'tool_call'; toolCall: ToolCall }
- | { type: 'tool_result'; toolResult: ToolResult }
- | { type: 'interrupt'; request: HITLRequest }
- | { type: 'token'; token: string }
- | { type: 'todos'; todos: Todo[] }
- | { type: 'workspace'; files: FileInfo[]; path: string }
- | { type: 'subagents'; subagents: Subagent[] }
- | { type: 'done'; result: unknown }
- | { type: 'error'; error: string }
+ | { type: "message"; message: Message }
+ | { type: "tool_call"; toolCall: ToolCall }
+ | { type: "tool_result"; toolResult: ToolResult }
+ | { type: "interrupt"; request: HITLRequest }
+ | { type: "token"; token: string }
+ | { type: "todos"; todos: Todo[] }
+ | { type: "workspace"; files: FileInfo[]; path: string }
+ | { type: "subagents"; subagents: Subagent[] }
+ | { type: "done"; result: unknown }
+ | { type: "error"; error: string }
export interface Message {
id: string
- role: 'user' | 'assistant' | 'system' | 'tool'
+ role: "user" | "assistant" | "system" | "tool"
content: string | ContentBlock[]
tool_calls?: ToolCall[]
// For tool messages - links result to its tool call
@@ -80,7 +80,7 @@ export interface Message {
}
export interface ContentBlock {
- type: 'text' | 'image' | 'tool_use' | 'tool_result'
+ type: "text" | "image" | "tool_use" | "tool_result"
text?: string
tool_use_id?: string
name?: string
@@ -103,11 +103,11 @@ export interface ToolResult {
export interface HITLRequest {
id: string
tool_call: ToolCall
- allowed_decisions: HITLDecision['type'][]
+ allowed_decisions: HITLDecision["type"][]
}
export interface HITLDecision {
- type: 'approve' | 'reject' | 'edit'
+ type: "approve" | "reject" | "edit"
tool_call_id: string
edited_args?: Record
feedback?: string
@@ -116,7 +116,7 @@ export interface HITLDecision {
export interface Todo {
id: string
content: string
- status: 'pending' | 'in_progress' | 'completed' | 'cancelled'
+ status: "pending" | "in_progress" | "completed" | "cancelled"
}
export interface FileInfo {
diff --git a/src/types.ts b/src/types.ts
index 214aab4..34fddae 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,6 +1,6 @@
-import type { UseStreamTransport } from '@langchain/langgraph-sdk/react'
+import type { UseStreamTransport } from "@langchain/langgraph-sdk/react"
-export type StreamPayload = Parameters[0]
+export type StreamPayload = Parameters[0]
export type StreamEvent = {
id?: string
@@ -11,13 +11,13 @@ export type StreamEvent = {
// Types for the IPC events from main process
export interface IPCMessage {
id: string
- type: 'human' | 'ai' | 'tool' | 'system'
+ type: "human" | "ai" | "tool" | "system"
content: string
tool_calls?: { id: string; name: string; args: Record }[]
}
export interface IPCValuesEvent {
- type: 'values'
+ type: "values"
data: {
messages?: IPCMessage[]
todos?: { id?: string; content?: string; status?: string }[]
@@ -36,30 +36,30 @@ export interface IPCValuesEvent {
}
export interface IPCTokenEvent {
- type: 'token'
+ type: "token"
messageId: string
token: string
}
export interface IPCToolCallEvent {
- type: 'tool_call'
+ type: "tool_call"
messageId: string | null
tool_calls: Array<{ id?: string; name?: string; args?: string }>
}
// Raw stream event - forwards LangGraph stream chunks directly
export interface IPCStreamEvent {
- type: 'stream'
- mode: 'messages' | 'values'
+ type: "stream"
+ mode: "messages" | "values"
data: unknown
}
export interface IPCDoneEvent {
- type: 'done'
+ type: "done"
}
export interface IPCErrorEvent {
- type: 'error'
+ type: "error"
error: string
}
diff --git a/test_data/generate_users.py b/test_data/generate_users.py
deleted file mode 100644
index eb68c94..0000000
--- a/test_data/generate_users.py
+++ /dev/null
@@ -1,153 +0,0 @@
-import json
-import random
-import string
-from datetime import datetime, timedelta
-
-# Lists for generating realistic gaming data
-ADJECTIVES = ["Shadow", "Dark", "Cyber", "Neon", "Toxic", "Epic", "Legendary", "Swift", "Silent", "Deadly",
- "Mystic", "Frozen", "Blazing", "Storm", "Thunder", "Iron", "Steel", "Crystal", "Void", "Chaos",
- "Alpha", "Omega", "Prime", "Ultra", "Hyper", "Mega", "Super", "Quantum", "Cosmic", "Astral",
- "Savage", "Brutal", "Fierce", "Wild", "Rogue", "Ghost", "Phantom", "Stealth", "Ninja", "Samurai"]
-
-NOUNS = ["Wolf", "Dragon", "Phoenix", "Hawk", "Tiger", "Viper", "Cobra", "Panther", "Falcon", "Raven",
- "Knight", "Warrior", "Hunter", "Slayer", "Sniper", "Assassin", "Ninja", "Samurai", "Gladiator", "Spartan",
- "Gamer", "Player", "Master", "Lord", "King", "Queen", "Prince", "Champion", "Legend", "Hero",
- "Wizard", "Mage", "Sorcerer", "Demon", "Angel", "Reaper", "Titan", "Giant", "Beast", "Monster"]
-
-CLAN_NAMES = ["Elite Squad", "Dark Legion", "Phoenix Rising", "Shadow Warriors", "Cyber Knights",
- "Neon Ninjas", "Thunder Gods", "Ice Dragons", "Fire Hawks", "Storm Riders",
- "Void Walkers", "Chaos Syndicate", "Alpha Pack", "Omega Force", "Prime Division",
- "Quantum Collective", "Astral Guardians", "Savage Beasts", "Ghost Protocol", "Stealth Ops",
- "Night Stalkers", "Day Breakers", "Moon Runners", "Sun Chasers", "Star Lords",
- "Diamond Dogs", "Golden Eagles", "Silver Wolves", "Bronze Bulls", "Platinum Panthers",
- "Crimson Tide", "Azure Knights", "Emerald Empire", "Obsidian Order", "Jade Dragons",
- None, None, None, None, None] # Some users not in clans
-
-ACHIEVEMENTS = [
- "First Blood", "Double Kill", "Triple Kill", "Quad Kill", "Pentakill",
- "Unstoppable", "Godlike", "Legendary", "Dominating", "Rampage",
- "Sharpshooter", "Headhunter", "Sniper Elite", "Quick Draw", "Dead Eye",
- "Survivor", "Last One Standing", "Victory Royale", "Champion", "Undefeated",
- "Speed Demon", "Marathon Runner", "Night Owl", "Early Bird", "Dedicated",
- "Collector", "Completionist", "Explorer", "Adventurer", "Pioneer",
- "Team Player", "MVP", "Clutch Master", "Support Hero", "Tank Commander",
- "First Win", "10 Wins", "50 Wins", "100 Wins", "500 Wins",
- "Level 10", "Level 25", "Level 50", "Level 100", "Max Level",
- "Social Butterfly", "Lone Wolf", "Clan Leader", "Veteran", "Newcomer",
- "Premium Member", "Beta Tester", "Alpha Tester", "Founder", "VIP",
- "Holiday Hero 2023", "Summer Slayer", "Winter Warrior", "Spring Striker", "Fall Fighter"
-]
-
-RANKS = ["Bronze I", "Bronze II", "Bronze III", "Bronze IV",
- "Silver I", "Silver II", "Silver III", "Silver IV",
- "Gold I", "Gold II", "Gold III", "Gold IV",
- "Platinum I", "Platinum II", "Platinum III", "Platinum IV",
- "Diamond I", "Diamond II", "Diamond III", "Diamond IV",
- "Master", "Grandmaster", "Challenger", "Legend", "Immortal"]
-
-EMAIL_DOMAINS = ["gmail.com", "yahoo.com", "hotmail.com", "outlook.com", "protonmail.com",
- "icloud.com", "aol.com", "mail.com", "zoho.com", "fastmail.com"]
-
-def generate_gamertag():
- style = random.choice(['adj_noun', 'adj_noun_num', 'word_num', 'xxx_style', 'name_style'])
-
- if style == 'adj_noun':
- return f"{random.choice(ADJECTIVES)}{random.choice(NOUNS)}"
- elif style == 'adj_noun_num':
- return f"{random.choice(ADJECTIVES)}{random.choice(NOUNS)}{random.randint(1, 9999)}"
- elif style == 'word_num':
- return f"{random.choice(NOUNS)}{random.randint(1, 9999)}"
- elif style == 'xxx_style':
- sep = random.choice(['_', 'x', 'X', '-', ''])
- return f"x{sep}{random.choice(NOUNS)}{sep}x{random.randint(1, 99)}"
- else:
- first_names = ["Alex", "Jordan", "Taylor", "Morgan", "Casey", "Riley", "Quinn", "Avery",
- "Max", "Sam", "Charlie", "Jamie", "Drew", "Blake", "Skyler", "Dakota"]
- return f"{random.choice(first_names)}{random.choice(['_', '', 'x', 'X'])}{random.choice(NOUNS)}{random.randint(1, 999)}"
-
-def generate_email(gamertag):
- clean_tag = ''.join(c.lower() for c in gamertag if c.isalnum())
- variation = random.choice([
- clean_tag,
- f"{clean_tag}{random.randint(1, 999)}",
- f"{clean_tag}_{random.randint(1, 99)}",
- f"gaming_{clean_tag}",
- f"{clean_tag}_gaming"
- ])
- return f"{variation}@{random.choice(EMAIL_DOMAINS)}"
-
-def generate_user(user_id):
- gamertag = generate_gamertag()
-
- # Generate correlated stats
- level = random.randint(1, 100)
- xp = level * random.randint(800, 1200) + random.randint(0, 999)
-
- games_played = random.randint(10, 5000)
-
- # Win rate varies by skill level (higher levels tend to have better win rates)
- base_win_rate = 0.3 + (level / 100) * 0.3 + random.uniform(-0.15, 0.15)
- base_win_rate = max(0.1, min(0.9, base_win_rate))
-
- wins = int(games_played * base_win_rate)
- losses = games_played - wins
-
- # Rank correlates with level
- rank_index = min(len(RANKS) - 1, int((level / 100) * len(RANKS) * 0.8) + random.randint(-3, 3))
- rank_index = max(0, min(len(RANKS) - 1, rank_index))
-
- # Achievements based on level and games played
- num_achievements = min(len(ACHIEVEMENTS), random.randint(1, 5) + level // 10 + games_played // 500)
- achievements = random.sample(ACHIEVEMENTS, num_achievements)
-
- # Friends count
- friends_count = random.randint(0, 500)
-
- # Account dates
- days_since_created = random.randint(1, 2000)
- account_created = datetime.now() - timedelta(days=days_since_created)
-
- days_since_online = random.choices(
- [0, random.randint(1, 7), random.randint(8, 30), random.randint(31, 365)],
- weights=[0.4, 0.3, 0.2, 0.1]
- )[0]
- last_online = datetime.now() - timedelta(days=days_since_online, hours=random.randint(0, 23), minutes=random.randint(0, 59))
-
- # Playtime correlates with games played and account age
- avg_game_length = random.uniform(0.2, 1.5) # hours per game
- total_playtime_hours = round(games_played * avg_game_length + random.uniform(-50, 200), 1)
- total_playtime_hours = max(1, total_playtime_hours)
-
- # Premium status
- premium_subscriber = random.random() < 0.25 # 25% are premium
-
- return {
- "id": user_id,
- "gamertag": gamertag,
- "email": generate_email(gamertag),
- "level": level,
- "xp": xp,
- "rank": RANKS[rank_index],
- "games_played": games_played,
- "wins": wins,
- "losses": losses,
- "achievements": achievements,
- "friends_count": friends_count,
- "clan_name": random.choice(CLAN_NAMES),
- "account_created": account_created.strftime("%Y-%m-%dT%H:%M:%SZ"),
- "last_online": last_online.strftime("%Y-%m-%dT%H:%M:%SZ"),
- "premium_subscriber": premium_subscriber,
- "total_playtime_hours": total_playtime_hours
- }
-
-def main():
- num_users = 1050
- users = [generate_user(i + 1) for i in range(num_users)]
-
- with open('/Users/hunter/Projects/github/langchain-ai/openwork/test_data/users_9.json', 'w') as f:
- json.dump(users, f, indent=2)
-
- print(f"Generated {num_users} users successfully!")
-
-if __name__ == "__main__":
- main()