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.
Files changed (38) hide show
  1. package/README.ko.md +5 -76
  2. package/README.md +7 -80
  3. package/bin/rune.js +18 -653
  4. package/package.json +10 -40
  5. package/.claude-plugin/marketplace.json +0 -17
  6. package/.claude-plugin/plugin.json +0 -24
  7. package/.mcp.json +0 -16
  8. package/bootstrap.js +0 -8
  9. package/channel/rune-channel.ts +0 -486
  10. package/electron-builder.yml +0 -61
  11. package/finder-extension/FinderSync.swift +0 -47
  12. package/finder-extension/RuneFinderSync.appex/Contents/Info.plist +0 -27
  13. package/finder-extension/RuneFinderSync.appex/Contents/MacOS/RuneFinderSync +0 -0
  14. package/finder-extension/main.swift +0 -5
  15. package/renderer/index.html +0 -12
  16. package/renderer/src/App.tsx +0 -44
  17. package/renderer/src/features/chat/activity-block.tsx +0 -152
  18. package/renderer/src/features/chat/chat-header.tsx +0 -58
  19. package/renderer/src/features/chat/chat-input.tsx +0 -190
  20. package/renderer/src/features/chat/chat-panel.tsx +0 -151
  21. package/renderer/src/features/chat/markdown-renderer.tsx +0 -26
  22. package/renderer/src/features/chat/message-bubble.tsx +0 -79
  23. package/renderer/src/features/chat/message-list.tsx +0 -178
  24. package/renderer/src/features/chat/types.ts +0 -32
  25. package/renderer/src/features/chat/use-chat.ts +0 -260
  26. package/renderer/src/features/terminal/terminal-panel.tsx +0 -155
  27. package/renderer/src/global.d.ts +0 -29
  28. package/renderer/src/globals.css +0 -92
  29. package/renderer/src/hooks/use-ipc.ts +0 -24
  30. package/renderer/src/lib/markdown.ts +0 -83
  31. package/renderer/src/lib/utils.ts +0 -6
  32. package/renderer/src/main.tsx +0 -10
  33. package/renderer/tsconfig.json +0 -16
  34. package/renderer/vite.config.ts +0 -23
  35. package/screenshot-chatting-ui.png +0 -0
  36. package/src/main.ts +0 -796
  37. package/src/preload.ts +0 -58
  38. 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
- }