openrune 1.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +5 -76
- package/README.md +7 -80
- package/bin/rune.js +18 -653
- package/package.json +10 -40
- package/.claude-plugin/marketplace.json +0 -17
- package/.claude-plugin/plugin.json +0 -24
- package/.mcp.json +0 -16
- package/bootstrap.js +0 -8
- package/channel/rune-channel.ts +0 -486
- package/electron-builder.yml +0 -61
- package/finder-extension/FinderSync.swift +0 -47
- package/finder-extension/RuneFinderSync.appex/Contents/Info.plist +0 -27
- package/finder-extension/RuneFinderSync.appex/Contents/MacOS/RuneFinderSync +0 -0
- package/finder-extension/main.swift +0 -5
- package/renderer/index.html +0 -12
- package/renderer/src/App.tsx +0 -44
- package/renderer/src/features/chat/activity-block.tsx +0 -152
- package/renderer/src/features/chat/chat-header.tsx +0 -58
- package/renderer/src/features/chat/chat-input.tsx +0 -190
- package/renderer/src/features/chat/chat-panel.tsx +0 -151
- package/renderer/src/features/chat/markdown-renderer.tsx +0 -26
- package/renderer/src/features/chat/message-bubble.tsx +0 -79
- package/renderer/src/features/chat/message-list.tsx +0 -178
- package/renderer/src/features/chat/types.ts +0 -32
- package/renderer/src/features/chat/use-chat.ts +0 -260
- package/renderer/src/features/terminal/terminal-panel.tsx +0 -155
- package/renderer/src/global.d.ts +0 -29
- package/renderer/src/globals.css +0 -92
- package/renderer/src/hooks/use-ipc.ts +0 -24
- package/renderer/src/lib/markdown.ts +0 -83
- package/renderer/src/lib/utils.ts +0 -6
- package/renderer/src/main.tsx +0 -10
- package/renderer/tsconfig.json +0 -16
- package/renderer/vite.config.ts +0 -23
- package/screenshot-chatting-ui.png +0 -0
- package/src/main.ts +0 -796
- package/src/preload.ts +0 -58
- package/tsconfig.json +0 -14
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState, useCallback } from 'react'
|
|
2
|
-
import { Toaster, toast } from 'sonner'
|
|
3
|
-
import { ShieldAlert } from 'lucide-react'
|
|
4
|
-
import { ChatHeader } from './chat-header'
|
|
5
|
-
import { MessageList } from './message-list'
|
|
6
|
-
import { ChatInput } from './chat-input'
|
|
7
|
-
import { TerminalPanel } from '../terminal/terminal-panel'
|
|
8
|
-
import type { useChat } from './use-chat'
|
|
9
|
-
|
|
10
|
-
interface PermissionPrompt {
|
|
11
|
-
ptyId: string
|
|
12
|
-
context: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface ChatPanelProps {
|
|
16
|
-
chat: ReturnType<typeof useChat>
|
|
17
|
-
showTerminal?: boolean
|
|
18
|
-
onToggleTerminal?: () => void
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function ChatPanel({ chat, showTerminal, onToggleTerminal }: ChatPanelProps) {
|
|
22
|
-
const [permissionPrompt, setPermissionPrompt] = useState<PermissionPrompt | null>(null)
|
|
23
|
-
|
|
24
|
-
// Listen for permission prompts
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
const handler = (data: { id: string; context: string }) => {
|
|
27
|
-
setPermissionPrompt({ ptyId: data.id, context: data.context || '' })
|
|
28
|
-
}
|
|
29
|
-
window.rune.on('rune:permissionNeeded', handler)
|
|
30
|
-
return () => window.rune.off('rune:permissionNeeded', handler)
|
|
31
|
-
}, [])
|
|
32
|
-
|
|
33
|
-
const handlePermissionResponse = useCallback((response: 'allow' | 'always' | 'deny') => {
|
|
34
|
-
if (!permissionPrompt) return
|
|
35
|
-
window.rune.send('rune:permissionRespond', { ptyId: permissionPrompt.ptyId, allow: response !== 'deny', response })
|
|
36
|
-
setPermissionPrompt(null)
|
|
37
|
-
}, [permissionPrompt])
|
|
38
|
-
|
|
39
|
-
// Toast notifications for channel status
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
const handler = (data: { port: number; connected: boolean }) => {
|
|
42
|
-
if (data.connected) {
|
|
43
|
-
toast.success(`Connected to channel :${data.port}`, { duration: 3000 })
|
|
44
|
-
} else {
|
|
45
|
-
toast.error(`Disconnected from channel :${data.port}`, { duration: 4000 })
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
window.rune.on('rune:channelStatus', handler)
|
|
49
|
-
return () => window.rune.off('rune:channelStatus', handler)
|
|
50
|
-
}, [])
|
|
51
|
-
|
|
52
|
-
const channelCommand = chat.runeInfo
|
|
53
|
-
? `RUNE_CHANNEL_PORT=${chat.runeInfo.port} RUNE_FOLDER_PATH=${chat.runeInfo.folderPath} claude --permission-mode auto --enable-auto-mode --dangerously-load-development-channels server:rune-channel`
|
|
54
|
-
: ''
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<div className="flex flex-col h-full bg-background text-foreground relative">
|
|
58
|
-
<ChatHeader
|
|
59
|
-
name={chat.runeInfo?.name || 'Rune'}
|
|
60
|
-
role={chat.runeInfo?.role}
|
|
61
|
-
port={chat.runeInfo?.port || 0}
|
|
62
|
-
showTerminal={showTerminal}
|
|
63
|
-
onClearHistory={chat.clearHistory}
|
|
64
|
-
onToggleTerminal={onToggleTerminal}
|
|
65
|
-
/>
|
|
66
|
-
<div className="flex-1 overflow-hidden relative">
|
|
67
|
-
{/* Terminal */}
|
|
68
|
-
{chat.runeInfo && (
|
|
69
|
-
<div className="absolute inset-0 z-10" style={{ display: showTerminal ? 'block' : 'none' }}>
|
|
70
|
-
<TerminalPanel
|
|
71
|
-
cwd={chat.runeInfo.folderPath}
|
|
72
|
-
autoCommand={channelCommand}
|
|
73
|
-
visible={showTerminal}
|
|
74
|
-
/>
|
|
75
|
-
</div>
|
|
76
|
-
)}
|
|
77
|
-
{/* Chat view */}
|
|
78
|
-
<div className="absolute inset-0 flex flex-col" style={{ display: showTerminal ? 'none' : 'flex' }}>
|
|
79
|
-
<MessageList
|
|
80
|
-
messages={chat.messages}
|
|
81
|
-
isStreaming={chat.isStreaming}
|
|
82
|
-
streamingDisplayText={chat.streamingDisplayText}
|
|
83
|
-
streamingActivities={chat.streamingActivities}
|
|
84
|
-
port={chat.runeInfo?.port}
|
|
85
|
-
folderPath={chat.runeInfo?.folderPath}
|
|
86
|
-
connected={chat.connected}
|
|
87
|
-
visible={!showTerminal}
|
|
88
|
-
/>
|
|
89
|
-
|
|
90
|
-
{/* Permission prompt banner */}
|
|
91
|
-
{permissionPrompt && !showTerminal && (
|
|
92
|
-
<div className="mx-3 mb-2 rounded-lg border border-amber-500/30 bg-amber-500/10 overflow-hidden">
|
|
93
|
-
<div className="flex items-center gap-2 px-3 py-2">
|
|
94
|
-
<ShieldAlert className="h-4 w-4 text-amber-400 shrink-0" />
|
|
95
|
-
<span className="text-[12px] font-medium text-amber-300">Permission Required</span>
|
|
96
|
-
</div>
|
|
97
|
-
{permissionPrompt.context && (
|
|
98
|
-
<pre className="px-3 pb-2 text-[11px] text-muted leading-relaxed max-h-[160px] overflow-y-auto whitespace-pre-wrap break-words">
|
|
99
|
-
{permissionPrompt.context.split('\n').slice(-12).join('\n')}
|
|
100
|
-
</pre>
|
|
101
|
-
)}
|
|
102
|
-
<div className="flex gap-2 px-3 pb-3">
|
|
103
|
-
<button
|
|
104
|
-
onClick={() => handlePermissionResponse('allow')}
|
|
105
|
-
className="flex-1 px-3 py-1.5 rounded-md text-[12px] font-medium bg-accent text-white hover:brightness-110 transition-all"
|
|
106
|
-
>
|
|
107
|
-
Allow
|
|
108
|
-
</button>
|
|
109
|
-
<button
|
|
110
|
-
onClick={() => handlePermissionResponse('always')}
|
|
111
|
-
className="flex-1 px-3 py-1.5 rounded-md text-[12px] font-medium bg-accent/20 text-accent hover:bg-accent/30 border border-accent/30 transition-all"
|
|
112
|
-
>
|
|
113
|
-
Always
|
|
114
|
-
</button>
|
|
115
|
-
<button
|
|
116
|
-
onClick={() => handlePermissionResponse('deny')}
|
|
117
|
-
className="px-3 py-1.5 rounded-md text-[12px] font-medium bg-red-500/20 text-red-400 hover:bg-red-500/30 border border-red-500/30 transition-all"
|
|
118
|
-
>
|
|
119
|
-
Deny
|
|
120
|
-
</button>
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
123
|
-
)}
|
|
124
|
-
|
|
125
|
-
<Toaster
|
|
126
|
-
position="top-center"
|
|
127
|
-
style={{ top: '56px' }}
|
|
128
|
-
toastOptions={{
|
|
129
|
-
style: {
|
|
130
|
-
fontSize: '12px',
|
|
131
|
-
padding: '8px 14px',
|
|
132
|
-
background: 'oklch(0.25 0.065 300)',
|
|
133
|
-
color: 'oklch(0.93 0.015 300)',
|
|
134
|
-
border: '1px solid oklch(1 0.03 300 / 10%)',
|
|
135
|
-
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
|
|
136
|
-
},
|
|
137
|
-
}}
|
|
138
|
-
/>
|
|
139
|
-
{(chat.messages.length > 0 || chat.isStreaming) && (
|
|
140
|
-
<ChatInput
|
|
141
|
-
isStreaming={chat.isStreaming}
|
|
142
|
-
disabled={!chat.connected}
|
|
143
|
-
onSend={chat.sendMessage}
|
|
144
|
-
onCancel={chat.cancelStream}
|
|
145
|
-
/>
|
|
146
|
-
)}
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
)
|
|
151
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'react'
|
|
2
|
-
import { cn } from '@/lib/utils'
|
|
3
|
-
import { renderMarkdown } from '@/lib/markdown'
|
|
4
|
-
|
|
5
|
-
interface MarkdownRendererProps {
|
|
6
|
-
text: string
|
|
7
|
-
isStreaming?: boolean
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function MarkdownRenderer({ text, isStreaming }: MarkdownRendererProps) {
|
|
11
|
-
const html = useMemo(() => {
|
|
12
|
-
if (!text) return ''
|
|
13
|
-
return renderMarkdown(text)
|
|
14
|
-
}, [text])
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<div
|
|
18
|
-
className={cn(
|
|
19
|
-
'msg-content rendered leading-[1.65] break-words text-[13px]',
|
|
20
|
-
isStreaming && 'streaming',
|
|
21
|
-
isStreaming && !text && 'empty-streaming'
|
|
22
|
-
)}
|
|
23
|
-
dangerouslySetInnerHTML={text ? { __html: html } : undefined}
|
|
24
|
-
/>
|
|
25
|
-
)
|
|
26
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { Paperclip, FileText } from 'lucide-react'
|
|
2
|
-
import { MarkdownRenderer } from './markdown-renderer'
|
|
3
|
-
|
|
4
|
-
interface MessageBubbleProps {
|
|
5
|
-
role: 'user' | 'assistant' | 'system'
|
|
6
|
-
text: string
|
|
7
|
-
files?: string[]
|
|
8
|
-
isStreaming?: boolean
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'])
|
|
12
|
-
|
|
13
|
-
function isImage(filePath: string): boolean {
|
|
14
|
-
const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase()
|
|
15
|
-
return IMAGE_EXTS.has(ext)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function getFileName(filePath: string) {
|
|
19
|
-
return filePath.split('/').pop() || filePath
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function FileAttachment({ file }: { file: string }) {
|
|
23
|
-
if (isImage(file)) {
|
|
24
|
-
return (
|
|
25
|
-
<div className="rounded-lg overflow-hidden border border-accent/10 max-w-[200px]">
|
|
26
|
-
<img
|
|
27
|
-
src={`file://${file}`}
|
|
28
|
-
alt={getFileName(file)}
|
|
29
|
-
className="max-w-full max-h-[160px] object-contain bg-black/20"
|
|
30
|
-
draggable={false}
|
|
31
|
-
/>
|
|
32
|
-
<div className="flex items-center gap-1 px-2 py-1 text-[10px] text-muted-foreground truncate">
|
|
33
|
-
<Paperclip className="h-2.5 w-2.5 shrink-0 opacity-50" />
|
|
34
|
-
{getFileName(file)}
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<span className="inline-flex items-center gap-1 bg-accent/10 rounded-md px-2 py-0.5 text-[11px] text-accent">
|
|
42
|
-
<FileText className="h-2.5 w-2.5" />
|
|
43
|
-
{getFileName(file)}
|
|
44
|
-
</span>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function MessageBubble({ role, text, files, isStreaming }: MessageBubbleProps) {
|
|
49
|
-
if (role === 'system') {
|
|
50
|
-
return (
|
|
51
|
-
<div className="flex justify-center">
|
|
52
|
-
<span className="text-[11px] text-muted/50">{text}</span>
|
|
53
|
-
</div>
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (role === 'user') {
|
|
58
|
-
return (
|
|
59
|
-
<div className="flex justify-end">
|
|
60
|
-
<div className="leading-[1.7] whitespace-pre-wrap break-words text-[13px] bg-user-bg border border-accent/15 rounded-2xl rounded-br-md px-4 py-3 max-w-[85%]">
|
|
61
|
-
{files && files.length > 0 && (
|
|
62
|
-
<div className="flex flex-wrap gap-1.5 mb-2">
|
|
63
|
-
{files.map((file, i) => (
|
|
64
|
-
<FileAttachment key={i} file={file} />
|
|
65
|
-
))}
|
|
66
|
-
</div>
|
|
67
|
-
)}
|
|
68
|
-
{text}
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
<div className="flex flex-col">
|
|
76
|
-
<MarkdownRenderer text={text} isStreaming={isStreaming} />
|
|
77
|
-
</div>
|
|
78
|
-
)
|
|
79
|
-
}
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { useRef, useEffect, useState, useCallback, useMemo } from 'react'
|
|
2
|
-
import { Zap, Loader2 } from 'lucide-react'
|
|
3
|
-
import { MessageBubble } from './message-bubble'
|
|
4
|
-
import { ActivityBlock } from './activity-block'
|
|
5
|
-
import { useIPCOn } from '@/hooks/use-ipc'
|
|
6
|
-
import type { ChatMessage, ContentBlock } from './types'
|
|
7
|
-
|
|
8
|
-
interface ToolActivity {
|
|
9
|
-
tool: string
|
|
10
|
-
status: 'running' | 'done'
|
|
11
|
-
preview?: string
|
|
12
|
-
ts: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface MessageListProps {
|
|
16
|
-
messages: ChatMessage[]
|
|
17
|
-
isStreaming: boolean
|
|
18
|
-
streamingDisplayText: string
|
|
19
|
-
streamingActivities?: ContentBlock[]
|
|
20
|
-
port?: number
|
|
21
|
-
folderPath?: string
|
|
22
|
-
connected?: boolean
|
|
23
|
-
visible?: boolean
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function MessageList({ messages, isStreaming, streamingDisplayText, streamingActivities, port, folderPath, connected = false, visible }: MessageListProps) {
|
|
27
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
|
28
|
-
const bottomRef = useRef<HTMLDivElement>(null)
|
|
29
|
-
const shouldAutoScrollRef = useRef(true)
|
|
30
|
-
const [toolActivities, setToolActivities] = useState<ToolActivity[]>([])
|
|
31
|
-
|
|
32
|
-
useIPCOn('rune:toolActivity', (data: { port: number; type: 'tool_start' | 'tool_end'; tool: string; preview?: string }) => {
|
|
33
|
-
if (data.port !== port) return
|
|
34
|
-
if (data.type === 'tool_start') {
|
|
35
|
-
setToolActivities(prev => [...prev.slice(-4), { tool: data.tool, status: 'running', ts: Date.now() }])
|
|
36
|
-
} else {
|
|
37
|
-
setToolActivities(prev => {
|
|
38
|
-
const idx = prev.findIndex(a => a.tool === data.tool && a.status === 'running')
|
|
39
|
-
if (idx === -1) return prev
|
|
40
|
-
const updated = [...prev]
|
|
41
|
-
updated[idx] = { ...updated[idx], status: 'done', preview: data.preview }
|
|
42
|
-
return updated
|
|
43
|
-
})
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
// Clear old done activities
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
if (toolActivities.length === 0) return
|
|
50
|
-
const timer = setInterval(() => {
|
|
51
|
-
const cutoff = Date.now() - 5000
|
|
52
|
-
setToolActivities(prev => prev.filter(a => a.status === 'running' || a.ts > cutoff))
|
|
53
|
-
}, 1000)
|
|
54
|
-
return () => clearInterval(timer)
|
|
55
|
-
}, [toolActivities.length])
|
|
56
|
-
|
|
57
|
-
const handleScroll = useCallback(() => {
|
|
58
|
-
const el = containerRef.current
|
|
59
|
-
if (!el) return
|
|
60
|
-
shouldAutoScrollRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80
|
|
61
|
-
}, [])
|
|
62
|
-
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
if (!shouldAutoScrollRef.current) return
|
|
65
|
-
requestAnimationFrame(() => {
|
|
66
|
-
bottomRef.current?.scrollIntoView({ behavior: 'instant' })
|
|
67
|
-
})
|
|
68
|
-
}, [messages.length, streamingDisplayText, streamingActivities?.length])
|
|
69
|
-
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
shouldAutoScrollRef.current = true
|
|
72
|
-
requestAnimationFrame(() => {
|
|
73
|
-
bottomRef.current?.scrollIntoView({ behavior: 'instant' })
|
|
74
|
-
})
|
|
75
|
-
}, [messages.length])
|
|
76
|
-
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
if (visible === false) return
|
|
79
|
-
shouldAutoScrollRef.current = true
|
|
80
|
-
requestAnimationFrame(() => {
|
|
81
|
-
bottomRef.current?.scrollIntoView({ behavior: 'instant' })
|
|
82
|
-
})
|
|
83
|
-
}, [visible])
|
|
84
|
-
|
|
85
|
-
const loadingMessages = useMemo(() => [
|
|
86
|
-
'Preparing your assistant...',
|
|
87
|
-
'Waking up the agent...',
|
|
88
|
-
'Getting things ready...',
|
|
89
|
-
'Waiting for assistant response...',
|
|
90
|
-
'Setting up the workspace...',
|
|
91
|
-
'Almost there...',
|
|
92
|
-
'Connecting the dots...',
|
|
93
|
-
'Brewing some intelligence...',
|
|
94
|
-
], [])
|
|
95
|
-
|
|
96
|
-
const [loadingMsgIdx, setLoadingMsgIdx] = useState(() => Math.floor(Math.random() * 8))
|
|
97
|
-
|
|
98
|
-
useEffect(() => {
|
|
99
|
-
if (messages.length > 0 || isStreaming) return
|
|
100
|
-
const timer = setInterval(() => {
|
|
101
|
-
setLoadingMsgIdx(prev => {
|
|
102
|
-
let next: number
|
|
103
|
-
do { next = Math.floor(Math.random() * loadingMessages.length) } while (next === prev)
|
|
104
|
-
return next
|
|
105
|
-
})
|
|
106
|
-
}, 3000)
|
|
107
|
-
return () => clearInterval(timer)
|
|
108
|
-
}, [messages.length, isStreaming, loadingMessages])
|
|
109
|
-
|
|
110
|
-
if (messages.length === 0 && !isStreaming) {
|
|
111
|
-
return (
|
|
112
|
-
<div ref={containerRef} className="flex-1 overflow-y-auto flex flex-col items-center justify-center gap-3 px-10">
|
|
113
|
-
<Loader2 className="h-6 w-6 text-muted animate-spin" />
|
|
114
|
-
<p className="text-[12px] text-muted transition-opacity duration-500">
|
|
115
|
-
{!connected ? 'Connecting to channel...' : loadingMessages[loadingMsgIdx]}
|
|
116
|
-
</p>
|
|
117
|
-
</div>
|
|
118
|
-
)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
<div ref={containerRef} className="flex-1 overflow-y-auto px-5 py-5 flex flex-col gap-5 min-h-0" onScroll={handleScroll}>
|
|
123
|
-
{!connected && (
|
|
124
|
-
<div className="flex items-center gap-2 px-3 py-2 bg-accent-red/8 border border-accent-red/20 rounded-lg text-[12px] text-accent-red shrink-0">
|
|
125
|
-
<Zap className="h-3.5 w-3.5 shrink-0" />
|
|
126
|
-
<span>Channel disconnected — waiting for Claude CLI on port :{port}</span>
|
|
127
|
-
</div>
|
|
128
|
-
)}
|
|
129
|
-
{messages.map((msg, i) => {
|
|
130
|
-
const isStreamingMsg = isStreaming && msg.role === 'assistant' && i === messages.length - 1
|
|
131
|
-
return (
|
|
132
|
-
<div key={i} className="flex flex-col gap-1.5">
|
|
133
|
-
{/* Show activity blocks for completed messages */}
|
|
134
|
-
{!isStreamingMsg && msg.blocks && msg.blocks.length > 0 && (
|
|
135
|
-
<div className="flex flex-col gap-1">
|
|
136
|
-
{msg.blocks.map((block, bi) => (
|
|
137
|
-
<ActivityBlock key={`${block.type}-${block.ts}-${bi}`} block={block} />
|
|
138
|
-
))}
|
|
139
|
-
</div>
|
|
140
|
-
)}
|
|
141
|
-
{/* Show streaming activity blocks */}
|
|
142
|
-
{isStreamingMsg && streamingActivities && streamingActivities.length > 0 && (
|
|
143
|
-
<div className="flex flex-col gap-1">
|
|
144
|
-
{streamingActivities.map((block, bi) => (
|
|
145
|
-
<ActivityBlock key={`${block.type}-${block.ts}-${bi}`} block={block} />
|
|
146
|
-
))}
|
|
147
|
-
</div>
|
|
148
|
-
)}
|
|
149
|
-
<MessageBubble
|
|
150
|
-
role={msg.role}
|
|
151
|
-
text={isStreamingMsg ? streamingDisplayText : msg.text}
|
|
152
|
-
files={msg.files}
|
|
153
|
-
isStreaming={isStreamingMsg}
|
|
154
|
-
/>
|
|
155
|
-
</div>
|
|
156
|
-
)
|
|
157
|
-
})}
|
|
158
|
-
{toolActivities.length > 0 && (
|
|
159
|
-
<div className="flex flex-col gap-1 px-1">
|
|
160
|
-
{toolActivities.map((activity, i) => (
|
|
161
|
-
<div key={`${activity.tool}-${activity.ts}-${i}`} className="flex items-center gap-2 text-[11px] text-muted animate-in fade-in duration-200">
|
|
162
|
-
{activity.status === 'running' ? (
|
|
163
|
-
<Loader2 className="h-3 w-3 text-accent animate-spin shrink-0" />
|
|
164
|
-
) : (
|
|
165
|
-
<div className="h-3 w-3 flex items-center justify-center shrink-0">
|
|
166
|
-
<div className="h-1.5 w-1.5 rounded-full bg-accent/50" />
|
|
167
|
-
</div>
|
|
168
|
-
)}
|
|
169
|
-
<span className={activity.status === 'running' ? 'text-accent' : 'text-muted'}>{activity.tool}</span>
|
|
170
|
-
{activity.preview && <span className="text-muted truncate max-w-[280px]">— {activity.preview}</span>}
|
|
171
|
-
</div>
|
|
172
|
-
))}
|
|
173
|
-
</div>
|
|
174
|
-
)}
|
|
175
|
-
<div ref={bottomRef} />
|
|
176
|
-
</div>
|
|
177
|
-
)
|
|
178
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export interface ContentBlock {
|
|
2
|
-
type: 'thinking' | 'tool_use' | 'tool_result'
|
|
3
|
-
content?: string
|
|
4
|
-
tool?: string
|
|
5
|
-
args?: Record<string, unknown>
|
|
6
|
-
ts: number
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ChatMessage {
|
|
10
|
-
role: 'user' | 'assistant' | 'system'
|
|
11
|
-
text: string
|
|
12
|
-
files?: string[]
|
|
13
|
-
blocks?: ContentBlock[]
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface RuneInfo {
|
|
17
|
-
filePath: string
|
|
18
|
-
folderPath: string
|
|
19
|
-
port: number
|
|
20
|
-
name: string
|
|
21
|
-
role: string
|
|
22
|
-
icon?: string
|
|
23
|
-
history: { role: 'user' | 'assistant'; text: string; ts: number }[]
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface ChatState {
|
|
27
|
-
messages: ChatMessage[]
|
|
28
|
-
isStreaming: boolean
|
|
29
|
-
shownText: string
|
|
30
|
-
typeQueue: string
|
|
31
|
-
activityBlocks: ContentBlock[]
|
|
32
|
-
}
|