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,260 +0,0 @@
1
- import { useState, useCallback, useRef, useEffect } from 'react'
2
- import { useIPCOn, useIPCSend } from '@/hooks/use-ipc'
3
- import type { ChatState, ChatMessage, ContentBlock, RuneInfo } from './types'
4
-
5
- export function useChat() {
6
- const [runeInfo, setRuneInfo] = useState<RuneInfo | null>(null)
7
- const [connected, setConnected] = useState(false)
8
- const [state, setState] = useState<ChatState>({
9
- messages: [],
10
- isStreaming: false,
11
- shownText: '',
12
- typeQueue: '',
13
- activityBlocks: [],
14
- })
15
- const typeTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
16
- const send = useIPCSend()
17
-
18
- // Track channel connection status
19
- const lastStatusRef = useRef<{ port: number; connected: boolean } | null>(null)
20
- useIPCOn('rune:channelStatus', (data: { port: number; connected: boolean }) => {
21
- lastStatusRef.current = data
22
- if (runeInfo && data.port === runeInfo.port) setConnected(data.connected)
23
- })
24
-
25
- // Apply buffered status when runeInfo arrives
26
- useEffect(() => {
27
- if (runeInfo && lastStatusRef.current && lastStatusRef.current.port === runeInfo.port) {
28
- setConnected(lastStatusRef.current.connected)
29
- }
30
- }, [runeInfo])
31
-
32
- // Initialize from main process
33
- useIPCOn('rune:init', (data: RuneInfo) => {
34
- setRuneInfo(data)
35
- // Restore history
36
- if (data.history && data.history.length > 0) {
37
- setState(prev => ({
38
- ...prev,
39
- messages: data.history.map(h => ({ role: h.role, text: h.text })),
40
- }))
41
- }
42
- })
43
-
44
- // Typewriter engine
45
- const startTyping = useCallback(() => {
46
- if (typeTimerRef.current) return
47
- typeTimerRef.current = setInterval(() => {
48
- setState(prev => {
49
- if (!prev.typeQueue) {
50
- if (typeTimerRef.current) {
51
- clearInterval(typeTimerRef.current)
52
- typeTimerRef.current = null
53
- }
54
- return prev
55
- }
56
- const batch = Math.min(prev.typeQueue.length, 4)
57
- return {
58
- ...prev,
59
- shownText: prev.shownText + prev.typeQueue.slice(0, batch),
60
- typeQueue: prev.typeQueue.slice(batch),
61
- }
62
- })
63
- }, 10)
64
- }, [])
65
-
66
- const appendChunk = useCallback((text: string) => {
67
- setState(prev => ({ ...prev, typeQueue: prev.typeQueue + text }))
68
- startTyping()
69
- }, [startTyping])
70
-
71
- const finishStream = useCallback(() => {
72
- if (typeTimerRef.current) {
73
- clearInterval(typeTimerRef.current)
74
- typeTimerRef.current = null
75
- }
76
- setState(prev => {
77
- const fullText = prev.shownText + prev.typeQueue
78
- const messages = [...prev.messages]
79
- if (messages.length > 0 && messages[messages.length - 1].role === 'assistant') {
80
- messages[messages.length - 1] = {
81
- role: 'assistant',
82
- text: fullText,
83
- blocks: prev.activityBlocks.length > 0 ? [...prev.activityBlocks] : undefined,
84
- }
85
- }
86
- return { ...prev, messages, isStreaming: false, shownText: '', typeQueue: '', activityBlocks: [] }
87
- })
88
- }, [])
89
-
90
- // IPC event listeners
91
- useIPCOn('rune:streamStart', () => {
92
- setState(prev => ({
93
- ...prev,
94
- isStreaming: true,
95
- shownText: '',
96
- typeQueue: '',
97
- activityBlocks: [],
98
- messages: [...prev.messages, { role: 'assistant', text: '' }],
99
- }))
100
- })
101
-
102
- useIPCOn('rune:activity', (data: { port: number; activityType: string; content?: string; tool?: string; args?: Record<string, unknown> }) => {
103
- const block: ContentBlock = {
104
- type: data.activityType as ContentBlock['type'],
105
- content: data.content,
106
- tool: data.tool,
107
- args: data.args,
108
- ts: Date.now(),
109
- }
110
- setState(prev => ({
111
- ...prev,
112
- activityBlocks: [...prev.activityBlocks, block],
113
- }))
114
- })
115
-
116
- useIPCOn('rune:streamChunk', (data: { text: string }) => {
117
- const clean = data.text
118
- .replace(/[\x00-\x08\x0e-\x1f\x7f]/g, '')
119
- .replace(/\^[A-Z@\[\\\]\^_]/g, '')
120
- appendChunk(clean)
121
- })
122
-
123
- useIPCOn('rune:streamEnd', () => {
124
- finishStream()
125
- })
126
-
127
- useIPCOn('rune:streamError', (data: { error: string }) => {
128
- if (typeTimerRef.current) {
129
- clearInterval(typeTimerRef.current)
130
- typeTimerRef.current = null
131
- }
132
- setState(prev => {
133
- const messages = [...prev.messages]
134
- if (messages.length > 0 && messages[messages.length - 1].role === 'assistant' && !messages[messages.length - 1].text) {
135
- messages[messages.length - 1] = { role: 'assistant', text: data.error }
136
- } else {
137
- messages.push({ role: 'assistant', text: data.error })
138
- }
139
- return { ...prev, messages, isStreaming: false, shownText: '', typeQueue: '' }
140
- })
141
- })
142
-
143
- useIPCOn('rune:pushMessage', (data: { text: string }) => {
144
- setState(prev => ({
145
- ...prev,
146
- messages: [...prev.messages, { role: 'assistant', text: data.text }],
147
- }))
148
- })
149
-
150
- // Handle Claude Code hook events
151
- useIPCOn('rune:hook', (data: { event: string; tool_name?: string; tool_input?: Record<string, unknown>; tool_output?: string; prompt?: string; permission_prompt_reason?: string; notification_type?: string; message?: string }) => {
152
- let block: ContentBlock | null = null
153
-
154
- if (data.event === 'PreToolUse' && data.tool_name) {
155
- block = {
156
- type: 'tool_use',
157
- tool: data.tool_name,
158
- args: data.tool_input,
159
- ts: Date.now(),
160
- }
161
- } else if (data.event === 'PostToolUse' && data.tool_name) {
162
- block = {
163
- type: 'tool_result',
164
- tool: data.tool_name,
165
- content: data.tool_output ? (data.tool_output.length > 500 ? data.tool_output.slice(0, 500) + '…' : data.tool_output) : 'done',
166
- ts: Date.now(),
167
- }
168
- } else if (data.event === 'PermissionRequest') {
169
- block = {
170
- type: 'thinking',
171
- content: `⚠️ Permission needed: ${data.tool_name} — ${data.permission_prompt_reason || ''}`,
172
- tool: data.tool_name,
173
- ts: Date.now(),
174
- }
175
- } else if (data.event === 'Notification') {
176
- block = {
177
- type: 'thinking',
178
- content: `📢 ${data.message || data.notification_type || 'notification'}`,
179
- ts: Date.now(),
180
- }
181
- }
182
-
183
- if (block) {
184
- setState(prev => ({
185
- ...prev,
186
- activityBlocks: [...prev.activityBlocks, block!],
187
- }))
188
- }
189
- })
190
-
191
- // Handle file rename
192
- useIPCOn('rune:fileRenamed', (data: { name: string; newPath: string }) => {
193
- setRuneInfo(prev => prev ? { ...prev, name: data.name, filePath: data.newPath } : prev)
194
- })
195
-
196
- // Actions
197
- const sendMessage = useCallback((content: string, files?: string[]) => {
198
- if (!runeInfo || (!content.trim() && (!files || files.length === 0))) return
199
-
200
- // If streaming, cancel current response and finalize partial text
201
- if (state.isStreaming) {
202
- send('rune:cancelStream', { port: runeInfo.port })
203
- finishStream()
204
- }
205
-
206
- // Build the actual content sent to channel: prepend file paths
207
- let channelContent = content
208
- if (files && files.length > 0) {
209
- const fileSection = files.map(f => `[Attached file: ${f}]`).join('\n')
210
- channelContent = fileSection + (content ? '\n\n' + content : '')
211
- }
212
-
213
- setState(prev => ({
214
- ...prev,
215
- messages: [...prev.messages, { role: 'user', text: content, files }],
216
- }))
217
- send('rune:sendMessage', { content: channelContent, port: runeInfo.port })
218
- }, [runeInfo, state.isStreaming, send, finishStream])
219
-
220
- const cancelStream = useCallback(() => {
221
- send('rune:cancelStream', { port: runeInfo?.port })
222
- }, [send, runeInfo?.port])
223
-
224
- const clearHistory = useCallback(() => {
225
- if (!runeInfo) return
226
- setState(prev => ({ ...prev, messages: [], shownText: '', typeQueue: '' }))
227
- send('rune:clearHistory', { port: runeInfo.port })
228
- }, [runeInfo, send])
229
-
230
- // Connect channel on mount
231
- useEffect(() => {
232
- if (runeInfo) {
233
- send('rune:connectChannel', { port: runeInfo.port })
234
- }
235
- }, [runeInfo?.port]) // eslint-disable-line react-hooks/exhaustive-deps
236
-
237
- // Cleanup
238
- useEffect(() => {
239
- return () => {
240
- if (typeTimerRef.current) clearInterval(typeTimerRef.current)
241
- }
242
- }, [])
243
-
244
- const getDisplayText = useCallback((): string => {
245
- if (!state.isStreaming || !state.shownText) return ''
246
- return state.shownText
247
- }, [state.isStreaming, state.shownText])
248
-
249
- return {
250
- runeInfo,
251
- connected,
252
- messages: state.messages,
253
- isStreaming: state.isStreaming,
254
- streamingDisplayText: getDisplayText(),
255
- streamingActivities: state.activityBlocks,
256
- sendMessage,
257
- cancelStream,
258
- clearHistory,
259
- }
260
- }
@@ -1,155 +0,0 @@
1
- import { useEffect, useRef } from 'react'
2
- import { Terminal } from '@xterm/xterm'
3
- import { FitAddon } from '@xterm/addon-fit'
4
- import '@xterm/xterm/css/xterm.css'
5
-
6
- interface TerminalPanelProps {
7
- cwd?: string
8
- autoCommand?: string
9
- visible?: boolean
10
- }
11
-
12
- const THEME = {
13
- background: '#18222d',
14
- foreground: '#d4d8de',
15
- cursor: '#d4d8de',
16
- cursorAccent: '#18222d',
17
- selectionBackground: '#2B5278',
18
- black: '#1a2634',
19
- red: '#ef5350',
20
- green: '#26a69a',
21
- yellow: '#ffb74d',
22
- blue: '#42a5f5',
23
- magenta: '#ab47bc',
24
- cyan: '#26c6da',
25
- white: '#d4d8de',
26
- brightBlack: '#546e7a',
27
- brightRed: '#ef5350',
28
- brightGreen: '#26a69a',
29
- brightYellow: '#ffb74d',
30
- brightBlue: '#64b5f6',
31
- brightMagenta: '#ab47bc',
32
- brightCyan: '#26c6da',
33
- brightWhite: '#ffffff',
34
- }
35
-
36
- export function TerminalPanel({ cwd, autoCommand, visible }: TerminalPanelProps) {
37
- const containerRef = useRef<HTMLDivElement>(null)
38
- const termRef = useRef<Terminal | null>(null)
39
- const fitRef = useRef<FitAddon | null>(null)
40
- const ptyIdRef = useRef<string | null>(null)
41
- const fittedRef = useRef(false)
42
-
43
- useEffect(() => {
44
- if (!containerRef.current) return
45
-
46
- const term = new Terminal({
47
- theme: THEME,
48
- fontSize: 13,
49
- fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace",
50
- cursorBlink: true,
51
- allowProposedApi: true,
52
- scrollback: 5000,
53
- })
54
- const fit = new FitAddon()
55
- term.loadAddon(fit)
56
- term.open(containerRef.current)
57
-
58
- termRef.current = term
59
- fitRef.current = fit
60
-
61
- const doFit = () => {
62
- const el = containerRef.current
63
- if (!el || el.clientWidth === 0 || el.clientHeight === 0) return
64
- fit.fit()
65
- if (!fittedRef.current) {
66
- fittedRef.current = true
67
- term.focus()
68
- }
69
- if (ptyIdRef.current) {
70
- window.rune.send('terminal:resize', {
71
- id: ptyIdRef.current,
72
- cols: term.cols,
73
- rows: term.rows,
74
- })
75
- }
76
- }
77
-
78
- // ResizeObserver fires on initial observe + any resize
79
- const ro = new ResizeObserver(() => requestAnimationFrame(doFit))
80
- ro.observe(containerRef.current)
81
-
82
- window.rune.invoke('terminal:spawn', { cwd: cwd || undefined }).then(({ id }: { id: string }) => {
83
- ptyIdRef.current = id
84
- // Sync size now that pty is ready
85
- window.rune.send('terminal:resize', { id, cols: term.cols, rows: term.rows })
86
-
87
- const onOutput = (msg: { id: string; data: string }) => {
88
- if (msg.id !== id) return
89
- term.write(msg.data)
90
- // Auto-select "I am using this for local development" when prompted
91
- if (msg.data.includes('I am using this for local development')) {
92
- setTimeout(() => {
93
- window.rune.send('terminal:input', { id, data: '\r' })
94
- }, 300)
95
- }
96
- }
97
- window.rune.on('terminal:output', onOutput)
98
-
99
- const onExit = (msg: { id: string; exitCode: number }) => {
100
- if (msg.id === id) {
101
- term.write(`\r\n\x1b[90m[Process exited with code ${msg.exitCode}]\x1b[0m\r\n`)
102
- ptyIdRef.current = null
103
- }
104
- }
105
- window.rune.on('terminal:exit', onExit)
106
-
107
- term.onData((data: string) => {
108
- if (ptyIdRef.current) {
109
- window.rune.send('terminal:input', { id: ptyIdRef.current, data })
110
- }
111
- })
112
-
113
- if (autoCommand) {
114
- setTimeout(() => {
115
- window.rune.send('terminal:input', { id, data: autoCommand + '\n' })
116
- }, 500)
117
- }
118
-
119
- ;(term as any).__cleanupOutput = onOutput
120
- ;(term as any).__cleanupExit = onExit
121
- })
122
-
123
- return () => {
124
- ro.disconnect()
125
- if ((term as any).__cleanupOutput) window.rune.off('terminal:output', (term as any).__cleanupOutput)
126
- if ((term as any).__cleanupExit) window.rune.off('terminal:exit', (term as any).__cleanupExit)
127
- if (ptyIdRef.current) window.rune.send('terminal:kill', { id: ptyIdRef.current })
128
- term.dispose()
129
- }
130
- }, []) // eslint-disable-line react-hooks/exhaustive-deps
131
-
132
- // Re-fit terminal when visibility changes
133
- useEffect(() => {
134
- if (visible && fitRef.current && termRef.current) {
135
- requestAnimationFrame(() => {
136
- fitRef.current?.fit()
137
- if (ptyIdRef.current) {
138
- window.rune.send('terminal:resize', {
139
- id: ptyIdRef.current,
140
- cols: termRef.current!.cols,
141
- rows: termRef.current!.rows,
142
- })
143
- }
144
- })
145
- }
146
- }, [visible])
147
-
148
- return (
149
- <div
150
- ref={containerRef}
151
- className="h-full w-full"
152
- style={{ backgroundColor: '#18222d', padding: '4px 0 0 4px' }}
153
- />
154
- )
155
- }
@@ -1,29 +0,0 @@
1
- type RuneSendChannel =
2
- | 'rune:sendMessage' | 'rune:cancelStream' | 'rune:connectChannel' | 'rune:clearHistory'
3
- | 'rune:permissionRespond'
4
- | 'terminal:input' | 'terminal:resize' | 'terminal:kill'
5
-
6
- type RuneOnChannel =
7
- | 'rune:init'
8
- | 'rune:streamStart' | 'rune:streamChunk' | 'rune:streamStatus'
9
- | 'rune:streamEnd' | 'rune:streamError'
10
- | 'rune:pushMessage' | 'rune:channelStatus'
11
- | 'rune:toolActivity'
12
- | 'rune:activity'
13
- | 'rune:permissionNeeded'
14
- | 'terminal:output' | 'terminal:exit'
15
-
16
- type RuneInvokeChannel =
17
- | 'terminal:spawn'
18
- | 'rune:createFile'
19
-
20
- interface RuneAPI {
21
- send(channel: RuneSendChannel, data?: unknown): void
22
- on(channel: RuneOnChannel, callback: (data: any) => void): void
23
- off(channel: RuneOnChannel, callback: (data: any) => void): void
24
- invoke(channel: RuneInvokeChannel, data?: unknown): Promise<any>
25
- }
26
-
27
- interface Window {
28
- rune: RuneAPI
29
- }
@@ -1,92 +0,0 @@
1
- @import "tailwindcss";
2
-
3
- @theme {
4
- --color-background: oklch(0.145 0 0);
5
- --color-foreground: oklch(0.93 0.005 270);
6
- --color-card: oklch(0.205 0 0);
7
- --color-muted: oklch(0.455 0 0);
8
- --color-muted-foreground: oklch(0.556 0 0);
9
- --color-border: oklch(1 0 0 / 10%);
10
- --color-input: oklch(1 0 0 / 15%);
11
- --color-ring: oklch(0.556 0 0);
12
-
13
- --color-accent: oklch(0.777 0.152 181.912);
14
- --color-accent-foreground: oklch(0.145 0 0);
15
- --color-accent-red: oklch(0.704 0.191 22.216);
16
-
17
- --color-background-secondary: oklch(0.17 0 0);
18
- --color-background-tertiary: oklch(0.205 0 0);
19
-
20
- --color-sidebar: oklch(0.17 0 0);
21
- --color-sidebar-border: oklch(1 0 0 / 8%);
22
- --color-surface: oklch(0.17 0 0);
23
- --color-user-bg: oklch(0.19 0.02 181);
24
- }
25
-
26
- * { box-sizing: border-box; }
27
- html, body, #root { height: 100%; }
28
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
29
-
30
- ::-webkit-scrollbar { width: 6px; height: 6px; }
31
- ::-webkit-scrollbar-track { background: transparent; }
32
- ::-webkit-scrollbar-thumb { background: oklch(1 0 0 / 8%); border-radius: 3px; }
33
- ::-webkit-scrollbar-thumb:hover { background: oklch(1 0 0 / 16%); }
34
- ::-webkit-scrollbar-corner { background: transparent; }
35
-
36
- ::selection { background: oklch(0.777 0.152 181.912 / 25%); color: inherit; }
37
- :focus-visible { outline: 2px solid oklch(0.556 0 0 / 50%); outline-offset: 2px; }
38
-
39
- .msg-content.rendered { white-space: normal; }
40
- .msg-content code { background: oklch(1 0 0 / 6%); border-radius: 4px; padding: 2px 6px; font-family: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace; font-size: 0.875em; }
41
- .msg-content pre { background: oklch(0.13 0 0); border: 1px solid oklch(1 0 0 / 8%); border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 8px 0; }
42
- .msg-content pre code { background: none; padding: 0; font-size: 12.5px; line-height: 1.6; }
43
- .msg-content h3 { font-size: 15px; font-weight: 600; margin: 14px 0 6px; }
44
- .msg-content h4 { font-size: 14px; font-weight: 600; margin: 12px 0 4px; }
45
- .msg-content h5 { font-size: 13px; font-weight: 600; margin: 10px 0 4px; }
46
- .msg-content h6 { font-size: 12px; font-weight: 500; margin: 8px 0 2px; color: oklch(0.556 0 0); }
47
- .msg-content blockquote { border-left: 2px solid oklch(0.777 0.152 181.912 / 50%); padding: 4px 14px; margin: 8px 0; color: oklch(0.556 0 0); background: oklch(0.777 0.152 181.912 / 4%); border-radius: 0 6px 6px 0; }
48
- .msg-content ul { padding-left: 22px; margin: 4px 0; list-style-type: disc; }
49
- .msg-content ol { padding-left: 22px; margin: 4px 0; list-style-type: decimal; }
50
- .msg-content li { margin: 2px 0; line-height: 1.7; }
51
- .msg-content li::marker { color: oklch(0.556 0 0); }
52
- .msg-content hr { border: none; border-top: 1px solid oklch(1 0 0 / 8%); margin: 12px 0; }
53
- .msg-content strong { font-weight: 600; }
54
- .msg-content em { font-style: italic; color: oklch(0.65 0 0); }
55
- .msg-content p { margin: 4px 0; line-height: 1.7; }
56
- .msg-content br { display: block; content: ''; margin: 2px 0; }
57
- .msg-content a { color: oklch(0.777 0.152 181.912); text-decoration: none; }
58
- .msg-content a:hover { text-decoration: underline; }
59
- .msg-content table { border-collapse: collapse; width: 100%; margin: 10px 0; font-size: 12.5px; }
60
- .msg-content th, .msg-content td { border: 1px solid oklch(1 0 0 / 8%); padding: 8px 12px; text-align: left; }
61
- .msg-content th { background: oklch(0.17 0 0); font-weight: 500; }
62
-
63
- .msg-content.streaming.empty-streaming::after { content: 'Thinking...'; animation: pulse 1.5s ease-in-out infinite; color: oklch(0.455 0 0); font-style: italic; }
64
- @keyframes pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
65
- .msg-content.streaming.rendered { animation: fadeChunk 0.15s ease-out; }
66
- @keyframes fadeChunk { from { opacity: 0.7; } to { opacity: 1; } }
67
-
68
- /* ── Activity Blocks ──────────────────────────────── */
69
- .activity-block { border-radius: 8px; border: 1px solid oklch(1 0 0 / 6%); overflow: hidden; animation: activityIn 0.2s ease-out; }
70
- @keyframes activityIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
71
-
72
- .activity-header { display: flex; align-items: center; gap: 6px; width: 100%; padding: 6px 10px; background: none; border: none; cursor: pointer; text-align: left; font-size: 11px; line-height: 1.4; color: oklch(0.65 0 0); transition: background 0.1s; }
73
- .activity-header:hover { background: oklch(1 0 0 / 3%); }
74
- .activity-icon { flex-shrink: 0; }
75
- .activity-label { font-weight: 600; font-size: 11px; white-space: nowrap; }
76
- .activity-preview { color: oklch(0.5 0 0); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
77
-
78
- .activity-result-badge { font-size: 9px; font-weight: 500; padding: 1px 5px; border-radius: 4px; background: oklch(0.45 0.12 155 / 15%); color: oklch(0.65 0.15 155); white-space: nowrap; }
79
-
80
- .activity-body { padding: 0 10px 8px 28px; font-size: 11px; color: oklch(0.6 0 0); line-height: 1.5; }
81
- .activity-thinking-body { font-style: italic; white-space: pre-wrap; word-break: break-word; }
82
-
83
- .activity-args, .activity-result-content { background: oklch(0.12 0 0); border: 1px solid oklch(1 0 0 / 6%); border-radius: 6px; padding: 6px 10px; font-family: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace; font-size: 10.5px; line-height: 1.5; overflow-x: auto; white-space: pre-wrap; word-break: break-all; margin: 0; color: oklch(0.65 0 0); }
84
- .activity-result-content { max-height: 200px; overflow-y: auto; word-break: break-word; color: oklch(0.6 0 0); }
85
-
86
- .activity-thinking { border-left: 2px solid oklch(0.65 0.15 300); background: oklch(0.65 0.15 300 / 4%); }
87
- .activity-tool-use { border-left: 2px solid oklch(0.777 0.152 181.912); background: oklch(0.777 0.152 181.912 / 4%); }
88
- .activity-tool-result { border-left: 2px solid oklch(0.65 0.15 155); background: oklch(0.65 0.15 155 / 4%); }
89
-
90
- /* ── Sonner Toast ─────────────────────────────────── */
91
- [data-sonner-toaster] { pointer-events: none !important; }
92
- [data-sonner-toast] { pointer-events: auto !important; }
@@ -1,24 +0,0 @@
1
- import { useEffect, useCallback, useRef } from 'react'
2
-
3
- export function useIPCOn(channel: RuneOnChannel, callback: (data: any) => void) {
4
- const callbackRef = useRef(callback)
5
- callbackRef.current = callback
6
-
7
- useEffect(() => {
8
- const handler = (data: any) => callbackRef.current(data)
9
- window.rune.on(channel, handler)
10
- return () => window.rune.off(channel, handler)
11
- }, [channel])
12
- }
13
-
14
- export function useIPCSend() {
15
- return useCallback((channel: RuneSendChannel, data?: unknown) => {
16
- window.rune.send(channel, data)
17
- }, [])
18
- }
19
-
20
- export function useIPCInvoke() {
21
- return useCallback((channel: RuneInvokeChannel, data?: unknown) => {
22
- return window.rune.invoke(channel, data)
23
- }, [])
24
- }
@@ -1,83 +0,0 @@
1
- function escHtml(s: string): string {
2
- return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
3
- }
4
-
5
- function inlineMd(s: string): string {
6
- s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
7
- s = s.replace(/__(.+?)__/g, '<strong>$1</strong>')
8
- s = s.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>')
9
- s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" title="$1">$1</a>')
10
- return s
11
- }
12
-
13
- export function renderMarkdown(src: string): string {
14
- const blocks: string[] = []
15
- let md = src.replace(/```([\w-]*)\n([\s\S]*?)```/g, (_, lang, code) => {
16
- blocks.push('<pre><code>' + escHtml(code) + '</code></pre>')
17
- return '\x01' + (blocks.length - 1) + '\x01'
18
- })
19
- md = md.replace(/`([^`\n]+)`/g, (_, code) => {
20
- blocks.push('<code>' + escHtml(code) + '</code>')
21
- return '\x01' + (blocks.length - 1) + '\x01'
22
- })
23
-
24
- md = escHtml(md)
25
-
26
- const lines = md.split('\n')
27
- let out = '', inUl = false, inOl = false, inTable = false
28
-
29
- for (let li = 0; li < lines.length; li++) {
30
- const line = lines[li]
31
- const isUl = /^[\-\*] /.test(line)
32
- const isOl = /^\d+\. /.test(line)
33
- const isTableRow = /^\|(.+)\|$/.test(line.trim())
34
-
35
- if (inUl && !isUl) { out += '</ul>'; inUl = false }
36
- if (inOl && !isOl) { out += '</ol>'; inOl = false }
37
- if (inTable && !isTableRow) { out += '</tbody></table>'; inTable = false }
38
-
39
- if (isTableRow) {
40
- const cells = line.trim().slice(1, -1).split('|').map(c => c.trim())
41
- if (cells.every(c => /^[-:]+$/.test(c))) continue
42
- if (!inTable) {
43
- inTable = true
44
- out += '<table><thead><tr>' + cells.map(c => '<th>' + inlineMd(c) + '</th>').join('') + '</tr></thead><tbody>'
45
- } else {
46
- out += '<tr>' + cells.map(c => '<td>' + inlineMd(c) + '</td>').join('') + '</tr>'
47
- }
48
- continue
49
- }
50
-
51
- const hm = line.match(/^(#{1,4}) (.+)$/)
52
- if (hm) {
53
- const lv = Math.min(hm[1].length + 2, 6)
54
- out += '<h' + lv + '>' + inlineMd(hm[2]) + '</h' + lv + '>'
55
- continue
56
- }
57
- if (/^[-*_]{3,}$/.test(line.trim())) { out += '<hr>'; continue }
58
- if (/^&gt; /.test(line)) {
59
- out += '<blockquote>' + inlineMd(line.slice(5)) + '</blockquote>'
60
- continue
61
- }
62
- if (isUl) {
63
- if (!inUl) { out += '<ul>'; inUl = true }
64
- out += '<li>' + inlineMd(line.replace(/^[\-\*] /, '')) + '</li>'
65
- continue
66
- }
67
- if (isOl) {
68
- if (!inOl) { out += '<ol>'; inOl = true }
69
- out += '<li>' + inlineMd(line.replace(/^\d+\. /, '')) + '</li>'
70
- continue
71
- }
72
- if (line.trim() === '') { out += '<br>'; continue }
73
- out += '<p>' + inlineMd(line) + '</p>'
74
- }
75
- if (inUl) out += '</ul>'
76
- if (inOl) out += '</ol>'
77
- if (inTable) out += '</tbody></table>'
78
-
79
- blocks.forEach((html, i) => {
80
- out = out.replace('\x01' + i + '\x01', html)
81
- })
82
- return out
83
- }
@@ -1,6 +0,0 @@
1
- import { type ClassValue, clsx } from 'clsx'
2
- import { twMerge } from 'tailwind-merge'
3
-
4
- export function cn(...inputs: ClassValue[]) {
5
- return twMerge(clsx(inputs))
6
- }
@@ -1,10 +0,0 @@
1
- import React from 'react'
2
- import ReactDOM from 'react-dom/client'
3
- import { App } from './App'
4
- import './globals.css'
5
-
6
- ReactDOM.createRoot(document.getElementById('root')!).render(
7
- <React.StrictMode>
8
- <App />
9
- </React.StrictMode>
10
- )
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "jsx": "react-jsx",
7
- "strict": true,
8
- "esModuleInterop": true,
9
- "resolveJsonModule": true,
10
- "skipLibCheck": true,
11
- "paths": {
12
- "@/*": ["./src/*"]
13
- }
14
- },
15
- "include": ["src/**/*"]
16
- }