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,47 +0,0 @@
1
- import Cocoa
2
- import FinderSync
3
-
4
- class RuneFinderSync: FIFinderSync {
5
- override init() {
6
- super.init()
7
- // Monitor all directories — menu appears everywhere in Finder
8
- FIFinderSyncController.default().directoryURLs = [URL(fileURLWithPath: "/")]
9
- }
10
-
11
- override func menu(for menuKind: FIMenuKind) -> NSMenu {
12
- let menu = NSMenu(title: "Rune")
13
-
14
- // contextualMenuForContainer = background right-click (empty space)
15
- // contextualMenuForItems = right-click on selected items
16
- if menuKind == .contextualMenuForContainer || menuKind == .contextualMenuForItems {
17
- let item = NSMenuItem(
18
- title: "New Rune",
19
- action: #selector(createRune(_:)),
20
- keyEquivalent: ""
21
- )
22
- if #available(macOS 11.0, *) {
23
- item.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: "Rune")
24
- }
25
- menu.addItem(item)
26
- }
27
-
28
- return menu
29
- }
30
-
31
- @objc func createRune(_ sender: AnyObject?) {
32
- guard let target = FIFinderSyncController.default().targetedURL() else { return }
33
- let dirPath = target.path
34
-
35
- // Use shell to create .rune file and open it
36
- let script = """
37
- cd "\(dirPath)" && \
38
- /usr/local/bin/rune new agent 2>/dev/null || \
39
- ~/.rune/create-rune.sh "\(dirPath)" 2>/dev/null
40
- """
41
-
42
- let task = Process()
43
- task.executableURL = URL(fileURLWithPath: "/bin/bash")
44
- task.arguments = ["-c", script]
45
- try? task.run()
46
- }
47
- }
@@ -1,27 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
- <plist version="1.0">
4
- <dict>
5
- <key>CFBundleIdentifier</key>
6
- <string>com.studio-h.rune.finder-sync</string>
7
- <key>CFBundleName</key>
8
- <string>RuneFinderSync</string>
9
- <key>CFBundleDisplayName</key>
10
- <string>Rune Finder Extension</string>
11
- <key>CFBundleExecutable</key>
12
- <string>RuneFinderSync</string>
13
- <key>CFBundlePackageType</key>
14
- <string>XPC!</string>
15
- <key>CFBundleVersion</key>
16
- <string>0.1.0</string>
17
- <key>CFBundleShortVersionString</key>
18
- <string>0.1.0</string>
19
- <key>NSExtension</key>
20
- <dict>
21
- <key>NSExtensionPointIdentifier</key>
22
- <string>com.apple.FinderSync</string>
23
- <key>NSExtensionPrincipalClass</key>
24
- <string>RuneFinderSync.RuneFinderSync</string>
25
- </dict>
26
- </dict>
27
- </plist>
@@ -1,5 +0,0 @@
1
- import Cocoa
2
-
3
- // NSExtension entry point for Finder Sync extension
4
- let app = NSApplication.shared
5
- NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
@@ -1,12 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="ko" class="dark">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Rune</title>
7
- </head>
8
- <body class="bg-background text-foreground overflow-hidden">
9
- <div id="root"></div>
10
- <script type="module" src="/src/main.tsx"></script>
11
- </body>
12
- </html>
@@ -1,44 +0,0 @@
1
- import { useState, useEffect, useCallback } from 'react'
2
- import { Bot } from 'lucide-react'
3
- import { ChatPanel } from './features/chat/chat-panel'
4
- import { useChat } from './features/chat/use-chat'
5
-
6
- export function App() {
7
- const chat = useChat()
8
- const [showTerminal, setShowTerminal] = useState(true)
9
-
10
- // Show terminal until connected, then switch to chat
11
- useEffect(() => {
12
- const handler = (data: { port: number; connected: boolean }) => {
13
- if (data.connected) setShowTerminal(false)
14
- else setShowTerminal(true)
15
- }
16
- window.rune.on('rune:channelStatus', handler)
17
- return () => window.rune.off('rune:channelStatus', handler)
18
- }, [])
19
-
20
- const toggleTerminal = useCallback(() => setShowTerminal(prev => !prev), [])
21
-
22
- if (!chat.runeInfo) {
23
- return (
24
- <div className="flex h-screen items-center justify-center bg-background text-foreground">
25
- <div className="flex flex-col items-center gap-4">
26
- <Bot className="h-12 w-12 text-accent" />
27
- <p className="text-sm text-muted">Open a .rune file to get started</p>
28
- </div>
29
- </div>
30
- )
31
- }
32
-
33
- return (
34
- <div className="flex h-screen bg-background text-foreground text-[13px] overflow-hidden">
35
- <div className="flex-1 overflow-hidden">
36
- <ChatPanel
37
- chat={chat}
38
- showTerminal={showTerminal}
39
- onToggleTerminal={toggleTerminal}
40
- />
41
- </div>
42
- </div>
43
- )
44
- }
@@ -1,152 +0,0 @@
1
- import { useState } from 'react'
2
- import { Brain, Terminal, FileText, Pencil, Search, FolderOpen, ChevronRight, ChevronDown, Code, FileCode } from 'lucide-react'
3
- import type { ContentBlock } from './types'
4
-
5
- const TOOL_ICONS: Record<string, typeof Terminal> = {
6
- Bash: Terminal,
7
- Read: FileText,
8
- Write: FileCode,
9
- Edit: Pencil,
10
- Grep: Search,
11
- Glob: FolderOpen,
12
- Task: Code,
13
- }
14
-
15
- function getToolIcon(tool?: string) {
16
- if (!tool) return Terminal
17
- return TOOL_ICONS[tool] || Terminal
18
- }
19
-
20
- function formatArgs(args?: Record<string, unknown>): string {
21
- if (!args) return ''
22
- const entries = Object.entries(args)
23
- if (entries.length === 0) return ''
24
- return entries
25
- .map(([k, v]) => {
26
- const val = typeof v === 'string' ? v : JSON.stringify(v)
27
- const truncated = val.length > 120 ? val.slice(0, 120) + '...' : val
28
- return `${k}: ${truncated}`
29
- })
30
- .join('\n')
31
- }
32
-
33
- function ThinkingBlock({ block }: { block: ContentBlock }) {
34
- const [expanded, setExpanded] = useState(false)
35
- const hasContent = block.content && block.content.trim().length > 0
36
- const preview = block.content
37
- ? block.content.length > 80
38
- ? block.content.slice(0, 80) + '...'
39
- : block.content
40
- : ''
41
-
42
- return (
43
- <div className="activity-block activity-thinking">
44
- <button
45
- className="activity-header"
46
- onClick={() => hasContent && setExpanded(!expanded)}
47
- >
48
- <Brain className="activity-icon h-3.5 w-3.5 text-purple-400 shrink-0" />
49
- <span className="activity-label text-purple-400">Thinking</span>
50
- {!expanded && preview && (
51
- <span className="activity-preview">{preview}</span>
52
- )}
53
- {hasContent && (
54
- expanded
55
- ? <ChevronDown className="h-3 w-3 text-muted shrink-0 ml-auto" />
56
- : <ChevronRight className="h-3 w-3 text-muted shrink-0 ml-auto" />
57
- )}
58
- </button>
59
- {expanded && hasContent && (
60
- <div className="activity-body activity-thinking-body">
61
- {block.content}
62
- </div>
63
- )}
64
- </div>
65
- )
66
- }
67
-
68
- function ToolUseBlock({ block }: { block: ContentBlock }) {
69
- const [expanded, setExpanded] = useState(false)
70
- const Icon = getToolIcon(block.tool)
71
- const argsText = formatArgs(block.args)
72
-
73
- return (
74
- <div className="activity-block activity-tool-use">
75
- <button
76
- className="activity-header"
77
- onClick={() => argsText && setExpanded(!expanded)}
78
- >
79
- <Icon className="activity-icon h-3.5 w-3.5 text-accent shrink-0" />
80
- <span className="activity-label text-accent">{block.tool || 'Tool'}</span>
81
- {!expanded && block.args && (
82
- <span className="activity-preview">
83
- {Object.entries(block.args).map(([k, v]) => {
84
- const val = typeof v === 'string' ? v : JSON.stringify(v)
85
- return val.length > 60 ? val.slice(0, 60) + '...' : val
86
- }).join(' ')}
87
- </span>
88
- )}
89
- {argsText && (
90
- expanded
91
- ? <ChevronDown className="h-3 w-3 text-muted shrink-0 ml-auto" />
92
- : <ChevronRight className="h-3 w-3 text-muted shrink-0 ml-auto" />
93
- )}
94
- </button>
95
- {expanded && argsText && (
96
- <div className="activity-body">
97
- <pre className="activity-args">{argsText}</pre>
98
- </div>
99
- )}
100
- </div>
101
- )
102
- }
103
-
104
- function ToolResultBlock({ block }: { block: ContentBlock }) {
105
- const [expanded, setExpanded] = useState(false)
106
- const Icon = getToolIcon(block.tool)
107
- const hasContent = block.content && block.content.trim().length > 0
108
- const preview = block.content
109
- ? block.content.length > 100
110
- ? block.content.slice(0, 100) + '...'
111
- : block.content
112
- : ''
113
-
114
- return (
115
- <div className="activity-block activity-tool-result">
116
- <button
117
- className="activity-header"
118
- onClick={() => hasContent && setExpanded(!expanded)}
119
- >
120
- <Icon className="activity-icon h-3.5 w-3.5 text-emerald-400 shrink-0" />
121
- <span className="activity-label text-emerald-400">{block.tool || 'Result'}</span>
122
- <span className="activity-result-badge">done</span>
123
- {!expanded && preview && (
124
- <span className="activity-preview">{preview}</span>
125
- )}
126
- {hasContent && (
127
- expanded
128
- ? <ChevronDown className="h-3 w-3 text-muted shrink-0 ml-auto" />
129
- : <ChevronRight className="h-3 w-3 text-muted shrink-0 ml-auto" />
130
- )}
131
- </button>
132
- {expanded && hasContent && (
133
- <div className="activity-body">
134
- <pre className="activity-result-content">{block.content}</pre>
135
- </div>
136
- )}
137
- </div>
138
- )
139
- }
140
-
141
- export function ActivityBlock({ block }: { block: ContentBlock }) {
142
- switch (block.type) {
143
- case 'thinking':
144
- return <ThinkingBlock block={block} />
145
- case 'tool_use':
146
- return <ToolUseBlock block={block} />
147
- case 'tool_result':
148
- return <ToolResultBlock block={block} />
149
- default:
150
- return null
151
- }
152
- }
@@ -1,58 +0,0 @@
1
- import { useState } from 'react'
2
- import { Trash2, SquareTerminal } from 'lucide-react'
3
- import { cn } from '@/lib/utils'
4
- import { useIPCOn } from '@/hooks/use-ipc'
5
-
6
- interface ChatHeaderProps {
7
- name: string
8
- role?: string
9
- port: number
10
- showTerminal?: boolean
11
- onClearHistory: () => void
12
- onToggleTerminal?: () => void
13
- }
14
-
15
- export function ChatHeader({ name, role, port, showTerminal, onClearHistory, onToggleTerminal }: ChatHeaderProps) {
16
- const [connected, setConnected] = useState(false)
17
- useIPCOn('rune:channelStatus', (data: { port: number; connected: boolean }) => {
18
- if (data.port === port) setConnected(data.connected)
19
- })
20
-
21
- const dotColor = connected ? 'bg-accent' : 'bg-accent-red'
22
-
23
- return (
24
- <div
25
- className="flex items-center justify-between px-4 h-[40px] border-b border-border shrink-0"
26
- style={{ WebkitAppRegion: 'drag' as any }}
27
- >
28
- <div className="flex items-center gap-2" style={{ marginLeft: process.platform === 'darwin' ? 68 : 0 }}>
29
- <span className="text-[13px] font-medium text-foreground">{name}</span>
30
- <div className={cn('w-2 h-2 rounded-full', dotColor, !connected && 'animate-pulse')} />
31
- </div>
32
-
33
- <div className="flex items-center gap-1.5" style={{ WebkitAppRegion: 'no-drag' as any }}>
34
- {onToggleTerminal && (
35
- <button
36
- className={cn(
37
- 'inline-flex items-center justify-center rounded-md h-7 w-7 transition-colors',
38
- showTerminal
39
- ? 'text-accent bg-accent/10'
40
- : 'text-muted hover:text-foreground hover:bg-border'
41
- )}
42
- title="Toggle terminal"
43
- onClick={() => onToggleTerminal?.()}
44
- >
45
- <SquareTerminal className="h-3.5 w-3.5" />
46
- </button>
47
- )}
48
- <button
49
- className="inline-flex items-center justify-center rounded-md h-7 w-7 text-muted hover:text-foreground hover:bg-border transition-colors"
50
- title="Clear history"
51
- onClick={onClearHistory}
52
- >
53
- <Trash2 className="h-3.5 w-3.5" />
54
- </button>
55
- </div>
56
- </div>
57
- )
58
- }
@@ -1,190 +0,0 @@
1
- import { useRef, useCallback, useState, type DragEvent } from 'react'
2
- import { ArrowUp, Square, Paperclip, X } from 'lucide-react'
3
- import { cn } from '@/lib/utils'
4
-
5
- interface ChatInputProps {
6
- isStreaming: boolean
7
- disabled?: boolean
8
- onSend: (content: string, files?: string[]) => void
9
- onCancel: () => void
10
- }
11
-
12
- export function ChatInput({ isStreaming, disabled, onSend, onCancel }: ChatInputProps) {
13
- const textareaRef = useRef<HTMLTextAreaElement>(null)
14
- const fileInputRef = useRef<HTMLInputElement>(null)
15
- const [attachedFiles, setAttachedFiles] = useState<string[]>([])
16
- const [isDragOver, setIsDragOver] = useState(false)
17
-
18
- const handleSend = useCallback(() => {
19
- if (disabled) return
20
- const content = textareaRef.current?.value.trim()
21
- if (!content && attachedFiles.length === 0) return
22
- onSend(content || '', attachedFiles.length > 0 ? attachedFiles : undefined)
23
- if (textareaRef.current) {
24
- textareaRef.current.value = ''
25
- textareaRef.current.style.height = 'auto'
26
- }
27
- setAttachedFiles([])
28
- }, [disabled, onSend, attachedFiles])
29
-
30
- const handleSendOrCancel = useCallback(() => {
31
- const hasContent = textareaRef.current?.value.trim() || attachedFiles.length > 0
32
- // If streaming but user typed something, send it (will auto-cancel current stream)
33
- // If streaming with empty input, just cancel
34
- if (isStreaming && !hasContent) onCancel()
35
- else handleSend()
36
- }, [isStreaming, onCancel, handleSend, attachedFiles.length])
37
-
38
- const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
39
- if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
40
- e.preventDefault()
41
- handleSend()
42
- }
43
- }, [handleSend])
44
-
45
- const handleInput = useCallback(() => {
46
- const el = textareaRef.current
47
- if (el) {
48
- el.style.height = 'auto'
49
- el.style.height = `${Math.min(el.scrollHeight, 120)}px`
50
- }
51
- }, [])
52
-
53
- const handleFileClick = useCallback(() => {
54
- fileInputRef.current?.click()
55
- }, [])
56
-
57
- const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
58
- const files = e.target.files
59
- if (!files) return
60
- const paths: string[] = []
61
- for (let i = 0; i < files.length; i++) {
62
- const f = files[i] as any
63
- if (f.path) paths.push(f.path)
64
- }
65
- if (paths.length > 0) {
66
- setAttachedFiles(prev => [...prev, ...paths])
67
- }
68
- // Reset input so same file can be re-selected
69
- e.target.value = ''
70
- }, [])
71
-
72
- const removeFile = useCallback((index: number) => {
73
- setAttachedFiles(prev => prev.filter((_, i) => i !== index))
74
- }, [])
75
-
76
- const handleDragOver = useCallback((e: DragEvent) => {
77
- e.preventDefault()
78
- e.stopPropagation()
79
- setIsDragOver(true)
80
- }, [])
81
-
82
- const handleDragLeave = useCallback((e: DragEvent) => {
83
- e.preventDefault()
84
- e.stopPropagation()
85
- // Only set false if leaving the container (not entering a child)
86
- if (e.currentTarget && !e.currentTarget.contains(e.relatedTarget as Node)) {
87
- setIsDragOver(false)
88
- }
89
- }, [])
90
-
91
- const handleDrop = useCallback((e: DragEvent) => {
92
- e.preventDefault()
93
- e.stopPropagation()
94
- setIsDragOver(false)
95
- const files = e.dataTransfer?.files
96
- if (!files || files.length === 0) return
97
- const paths: string[] = []
98
- for (let i = 0; i < files.length; i++) {
99
- const f = files[i] as any
100
- if (f.path) paths.push(f.path)
101
- }
102
- if (paths.length > 0) {
103
- setAttachedFiles(prev => [...prev, ...paths])
104
- }
105
- }, [])
106
-
107
- const getFileName = (filePath: string) => {
108
- return filePath.split('/').pop() || filePath
109
- }
110
-
111
- return (
112
- <div
113
- className={cn(
114
- 'flex flex-col gap-2 px-4 py-3.5 border-t shrink-0 relative transition-colors',
115
- isDragOver ? 'border-accent bg-accent/5' : 'border-border'
116
- )}
117
- onDragOver={handleDragOver}
118
- onDragLeave={handleDragLeave}
119
- onDrop={handleDrop}
120
- >
121
- {isDragOver && (
122
- <div className="absolute inset-0 flex items-center justify-center bg-accent/10 border-2 border-dashed border-accent rounded-lg z-10 pointer-events-none">
123
- <span className="text-[12px] font-medium text-accent">Drop files here</span>
124
- </div>
125
- )}
126
- {attachedFiles.length > 0 && (
127
- <div className="flex flex-wrap gap-1.5">
128
- {attachedFiles.map((file, i) => (
129
- <div
130
- key={i}
131
- className="flex items-center gap-1.5 bg-muted/30 border border-border rounded-lg px-2.5 py-1.5 text-[11px] text-muted-foreground max-w-[200px]"
132
- >
133
- <Paperclip className="h-3 w-3 shrink-0 opacity-50" />
134
- <span className="truncate">{getFileName(file)}</span>
135
- <button
136
- onClick={() => removeFile(i)}
137
- className="shrink-0 hover:text-foreground transition-colors"
138
- >
139
- <X className="h-3 w-3" />
140
- </button>
141
- </div>
142
- ))}
143
- </div>
144
- )}
145
- <div className="flex gap-2.5 items-end">
146
- <button
147
- className="inline-flex items-center justify-center rounded-xl h-[38px] w-[38px] shrink-0 transition-colors text-muted-foreground hover:text-foreground hover:bg-muted/30"
148
- onClick={handleFileClick}
149
- title="Attach file"
150
- >
151
- <Paperclip className="h-4 w-4" />
152
- </button>
153
- <input
154
- ref={fileInputRef}
155
- type="file"
156
- multiple
157
- className="hidden"
158
- onChange={handleFileChange}
159
- />
160
- <textarea
161
- ref={textareaRef}
162
- className={cn(
163
- 'flex-1 rounded-xl border border-input bg-transparent px-3.5 py-2.5 text-[13px] text-foreground resize-none outline-none min-h-[42px] max-h-[120px] leading-[1.5] transition-colors placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-0',
164
- disabled && 'opacity-50 cursor-not-allowed'
165
- )}
166
- placeholder={disabled ? 'Waiting for channel connection...' : 'Type a message...'}
167
- rows={1}
168
- spellCheck={false}
169
- disabled={disabled}
170
- onKeyDown={handleKeyDown}
171
- onInput={handleInput}
172
- />
173
- <button
174
- className={cn(
175
- 'inline-flex items-center justify-center rounded-xl h-[38px] w-[38px] shrink-0 transition-colors',
176
- disabled
177
- ? 'bg-muted text-muted-foreground cursor-not-allowed'
178
- : isStreaming
179
- ? 'bg-accent-red text-white hover:bg-accent-red/90'
180
- : 'bg-accent text-accent-foreground hover:bg-accent/90'
181
- )}
182
- onClick={handleSendOrCancel}
183
- disabled={disabled}
184
- >
185
- {isStreaming ? <Square className="h-3.5 w-3.5" /> : <ArrowUp className="h-4 w-4 stroke-[2.5]" />}
186
- </button>
187
- </div>
188
- </div>
189
- )
190
- }