weifuwu 0.18.8 → 0.18.9
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/opencode/ui/app.css +1 -0
- package/opencode/ui/layout.tsx +14 -0
- package/opencode/ui/page.tsx +294 -0
- package/package.json +2 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default function RootLayout({ children }: { children: any }) {
|
|
2
|
+
return (
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charSet="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<title>Opencode Chat</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="__weifuwu_root">{children}</div>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
interface ToolCallEvent { toolName: string; input: unknown }
|
|
4
|
+
interface ToolResultEvent { toolName: string; output: unknown }
|
|
5
|
+
interface SessionItem { id: number; title: string; created_at?: string }
|
|
6
|
+
interface MessageItem { role: string; content: string; toolCalls?: ToolCallEvent[]; toolResults?: ToolResultEvent[] }
|
|
7
|
+
|
|
8
|
+
function formatDate(s: string) {
|
|
9
|
+
const d = new Date(s)
|
|
10
|
+
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function firstLine(text: string) {
|
|
14
|
+
return text.split('\n')[0].slice(0, 48) || 'Empty'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function Page() {
|
|
18
|
+
const [sessions, setSessions] = useState<SessionItem[]>([])
|
|
19
|
+
const [currentId, setCurrentId] = useState<number | null>(null)
|
|
20
|
+
const [messages, setMessages] = useState<MessageItem[]>([])
|
|
21
|
+
const [input, setInput] = useState('')
|
|
22
|
+
const [streaming, setStreaming] = useState('')
|
|
23
|
+
const [loading, setLoading] = useState(false)
|
|
24
|
+
const [error, setError] = useState('')
|
|
25
|
+
const abortRef = useRef<AbortController | null>(null)
|
|
26
|
+
const textRef = useRef('')
|
|
27
|
+
const bottomRef = useRef<HTMLDivElement>(null)
|
|
28
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
29
|
+
|
|
30
|
+
useEffect(() => { fetch('/opencode/sessions').then(r => r.json()).then(setSessions).catch(() => {}) }, [])
|
|
31
|
+
useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages, streaming])
|
|
32
|
+
|
|
33
|
+
async function createSession() {
|
|
34
|
+
setError('')
|
|
35
|
+
const res = await fetch('/opencode/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) })
|
|
36
|
+
if (!res.ok) { setError('Failed'); return }
|
|
37
|
+
const s = await res.json()
|
|
38
|
+
setSessions(p => [s, ...p])
|
|
39
|
+
setCurrentId(s.id); setMessages([]); setStreaming(''); textRef.current = ''
|
|
40
|
+
setTimeout(() => inputRef.current?.focus(), 100)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function selectSession(id: number) {
|
|
44
|
+
abortRef.current?.abort()
|
|
45
|
+
setCurrentId(id); setStreaming(''); textRef.current = ''
|
|
46
|
+
const res = await fetch(`/opencode/sessions/${id}`)
|
|
47
|
+
if (!res.ok) { setError('Failed to load'); return }
|
|
48
|
+
const { messages: msgs } = await res.json() as any
|
|
49
|
+
setMessages(msgs.map((m: any) => ({
|
|
50
|
+
role: m.role, content: m.content || '',
|
|
51
|
+
toolCalls: Array.isArray(m.tool_calls) ? m.tool_calls : undefined,
|
|
52
|
+
toolResults: Array.isArray(m.tool_results) ? m.tool_results : undefined,
|
|
53
|
+
})))
|
|
54
|
+
setTimeout(() => inputRef.current?.focus(), 100)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function deleteSession(e: React.MouseEvent, id: number) {
|
|
58
|
+
e.stopPropagation()
|
|
59
|
+
fetch('/opencode/sessions/' + id, { method: 'DELETE' })
|
|
60
|
+
setSessions(p => p.filter(s => s.id !== id))
|
|
61
|
+
if (currentId === id) { setCurrentId(null); setMessages([]) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function sendMessage() {
|
|
65
|
+
const content = input.trim()
|
|
66
|
+
if (!content || !currentId || loading) return
|
|
67
|
+
setError('')
|
|
68
|
+
setMessages(p => [...p, { role: 'user', content }])
|
|
69
|
+
setInput(''); setLoading(true); setStreaming(''); textRef.current = ''
|
|
70
|
+
abortRef.current?.abort()
|
|
71
|
+
const controller = new AbortController()
|
|
72
|
+
abortRef.current = controller
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(`/opencode/sessions/${currentId}/message`, {
|
|
76
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
77
|
+
body: JSON.stringify({ content }), signal: controller.signal,
|
|
78
|
+
})
|
|
79
|
+
if (!res.ok) { setError('Request failed'); setLoading(false); return }
|
|
80
|
+
|
|
81
|
+
const reader = res.body!.getReader()
|
|
82
|
+
const decoder = new TextDecoder()
|
|
83
|
+
let buffer = ''
|
|
84
|
+
|
|
85
|
+
while (true) {
|
|
86
|
+
const { done, value } = await reader.read()
|
|
87
|
+
if (done) break
|
|
88
|
+
buffer += decoder.decode(value, { stream: true })
|
|
89
|
+
const lines = buffer.split('\n')
|
|
90
|
+
buffer = lines.pop() || ''
|
|
91
|
+
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
if (line.startsWith('event: ')) continue
|
|
94
|
+
if (!line.startsWith('data: ')) continue
|
|
95
|
+
try {
|
|
96
|
+
const d = JSON.parse(line.slice(6))
|
|
97
|
+
switch (d.type) {
|
|
98
|
+
case 'text-delta':
|
|
99
|
+
textRef.current += d.text || ''
|
|
100
|
+
setStreaming(textRef.current)
|
|
101
|
+
break
|
|
102
|
+
case 'tool-call':
|
|
103
|
+
setMessages(p => {
|
|
104
|
+
const last = p[p.length - 1]
|
|
105
|
+
if (last?.role === 'assistant') {
|
|
106
|
+
const tcs = last.toolCalls || []
|
|
107
|
+
return [...p.slice(0, -1), { ...last, toolCalls: [...tcs, { toolName: d.toolName, input: d.input }] }]
|
|
108
|
+
}
|
|
109
|
+
return [...p, { role: 'assistant', content: '', toolCalls: [{ toolName: d.toolName, input: d.input }] }]
|
|
110
|
+
})
|
|
111
|
+
break
|
|
112
|
+
case 'tool-result':
|
|
113
|
+
setMessages(p => {
|
|
114
|
+
const last = p[p.length - 1]
|
|
115
|
+
if (last?.role === 'assistant') {
|
|
116
|
+
const trs = last.toolResults || []
|
|
117
|
+
return [...p.slice(0, -1), { ...last, toolResults: [...trs, { toolName: d.toolName, output: d.output }] }]
|
|
118
|
+
}
|
|
119
|
+
return [...p, { role: 'assistant', content: '', toolResults: [{ toolName: d.toolName, output: d.output }] }]
|
|
120
|
+
})
|
|
121
|
+
break
|
|
122
|
+
case 'finish':
|
|
123
|
+
const finalContent = textRef.current
|
|
124
|
+
setMessages(p => {
|
|
125
|
+
const last = p[p.length - 1]
|
|
126
|
+
if (last?.role === 'assistant' && !last.content && finalContent) {
|
|
127
|
+
return [...p.slice(0, -1), { ...last, content: finalContent }]
|
|
128
|
+
}
|
|
129
|
+
if (last?.role === 'assistant' && last.content) return p
|
|
130
|
+
return [...p, { role: 'assistant', content: finalContent }]
|
|
131
|
+
})
|
|
132
|
+
setStreaming(''); textRef.current = ''; setLoading(false)
|
|
133
|
+
setTimeout(() => inputRef.current?.focus(), 50)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (e: any) {
|
|
140
|
+
if (e.name !== 'AbortError') setError(e.message || 'Error')
|
|
141
|
+
}
|
|
142
|
+
setLoading(false)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="flex h-screen font-sans antialiased bg-zinc-950 text-zinc-100">
|
|
147
|
+
{/* Sidebar */}
|
|
148
|
+
<aside className="w-64 flex flex-col border-r border-zinc-800 bg-zinc-900/50 shrink-0">
|
|
149
|
+
<div className="p-3 border-b border-zinc-800">
|
|
150
|
+
<button onClick={createSession}
|
|
151
|
+
className="w-full flex items-center justify-center gap-2 py-2 px-3 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded-lg text-sm transition-colors cursor-pointer">
|
|
152
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4"/></svg>
|
|
153
|
+
New Chat
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
<nav className="flex-1 overflow-y-auto p-2 space-y-0.5">
|
|
157
|
+
{sessions.length === 0 && (
|
|
158
|
+
<div className="text-xs text-zinc-600 text-center py-8">No sessions yet</div>
|
|
159
|
+
)}
|
|
160
|
+
{sessions.map(s => (
|
|
161
|
+
<div key={s.id}
|
|
162
|
+
onClick={() => selectSession(s.id)}
|
|
163
|
+
className={`group flex items-center gap-2 p-2 rounded-lg cursor-pointer text-sm transition-colors ${
|
|
164
|
+
currentId === s.id ? 'bg-zinc-700/60 text-zinc-100' : 'text-zinc-400 hover:bg-zinc-800/60 hover:text-zinc-200'
|
|
165
|
+
}`}>
|
|
166
|
+
<svg className="w-4 h-4 shrink-0 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/></svg>
|
|
167
|
+
<span className="truncate flex-1">{s.title || `Session ${s.id}`}</span>
|
|
168
|
+
<button onClick={e => deleteSession(e, s.id)}
|
|
169
|
+
className="opacity-0 group-hover:opacity-100 p-0.5 rounded hover:bg-zinc-600 transition-all cursor-pointer">
|
|
170
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
173
|
+
))}
|
|
174
|
+
</nav>
|
|
175
|
+
</aside>
|
|
176
|
+
|
|
177
|
+
{/* Main */}
|
|
178
|
+
<main className="flex-1 flex flex-col min-w-0">
|
|
179
|
+
{/* Header */}
|
|
180
|
+
{currentId && (
|
|
181
|
+
<header className="flex items-center gap-2 px-5 py-2.5 border-b border-zinc-800 bg-zinc-900/30">
|
|
182
|
+
<svg className="w-4 h-4 text-emerald-500" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
|
|
183
|
+
<span className="text-xs text-zinc-500 font-mono">opencode</span>
|
|
184
|
+
<span className="text-xs text-zinc-600 mx-1">/</span>
|
|
185
|
+
<span className="text-sm text-zinc-300 truncate">{sessions.find(s => s.id === currentId)?.title || `Session ${currentId}`}</span>
|
|
186
|
+
</header>
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
{/* Messages */}
|
|
190
|
+
<div className="flex-1 overflow-y-auto">
|
|
191
|
+
<div className="max-w-3xl mx-auto px-4 py-6 space-y-4">
|
|
192
|
+
{!currentId && (
|
|
193
|
+
<div className="flex flex-col items-center justify-center h-[70vh] text-zinc-600">
|
|
194
|
+
<svg className="w-12 h-12 mb-4 opacity-40" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/></svg>
|
|
195
|
+
<p className="text-sm mb-2">Select a session or create a new one</p>
|
|
196
|
+
<button onClick={createSession} className="mt-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded-lg text-sm transition-colors cursor-pointer">
|
|
197
|
+
+ New Chat
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{messages.map((m, i) => (
|
|
203
|
+
<div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
|
204
|
+
<div className={`max-w-[75%] rounded-2xl px-4 py-2.5 leading-relaxed whitespace-pre-wrap text-sm ${
|
|
205
|
+
m.role === 'user'
|
|
206
|
+
? 'bg-indigo-600 text-white rounded-br-md'
|
|
207
|
+
: 'bg-zinc-800/80 text-zinc-200 rounded-bl-md border border-zinc-700/50'
|
|
208
|
+
}`}>
|
|
209
|
+
{m.content || <span className="text-zinc-500 italic">No response</span>}
|
|
210
|
+
{m.toolCalls?.map((tc, j) => (
|
|
211
|
+
<details key={j} className="mt-2 rounded-lg overflow-hidden bg-black/20 border border-zinc-700/50">
|
|
212
|
+
<summary className="px-3 py-1.5 text-xs text-zinc-400 cursor-pointer hover:text-zinc-200 select-none flex items-center gap-1.5">
|
|
213
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
|
214
|
+
{tc.toolName}
|
|
215
|
+
</summary>
|
|
216
|
+
<pre className="px-3 py-2 text-xs text-zinc-400 overflow-x-auto">{JSON.stringify(tc.input, null, 2)}</pre>
|
|
217
|
+
</details>
|
|
218
|
+
))}
|
|
219
|
+
{m.toolResults?.map((tr, j) => (
|
|
220
|
+
<details key={j} className="mt-1.5 rounded-lg overflow-hidden bg-black/20 border border-zinc-700/50">
|
|
221
|
+
<summary className="px-3 py-1.5 text-xs text-zinc-400 cursor-pointer hover:text-zinc-200 select-none flex items-center gap-1.5">
|
|
222
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
223
|
+
{tr.toolName} result
|
|
224
|
+
</summary>
|
|
225
|
+
<pre className="px-3 py-2 text-xs text-zinc-400 overflow-x-auto max-h-48">{JSON.stringify(tr.output, null, 2)}</pre>
|
|
226
|
+
</details>
|
|
227
|
+
))}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
))}
|
|
231
|
+
|
|
232
|
+
{streaming && (
|
|
233
|
+
<div className="flex justify-start">
|
|
234
|
+
<div className="max-w-[75%] rounded-2xl px-4 py-2.5 bg-zinc-800/80 text-zinc-200 rounded-bl-md border border-zinc-700/50 leading-relaxed whitespace-pre-wrap text-sm">
|
|
235
|
+
{streaming}<span className="inline-block w-1.5 h-4 bg-indigo-400 ml-0.5 animate-pulse rounded-sm" />
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
{loading && !streaming && (
|
|
241
|
+
<div className="flex justify-start">
|
|
242
|
+
<div className="flex items-center gap-2 px-4 py-3 bg-zinc-800/60 rounded-2xl rounded-bl-md border border-zinc-700/40">
|
|
243
|
+
<div className="flex gap-1">
|
|
244
|
+
<span className="w-2 h-2 bg-zinc-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
|
245
|
+
<span className="w-2 h-2 bg-zinc-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
|
246
|
+
<span className="w-2 h-2 bg-zinc-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
{error && (
|
|
253
|
+
<div className="flex justify-center">
|
|
254
|
+
<div className="flex items-center gap-2 px-4 py-2 bg-red-900/30 text-red-400 rounded-lg text-xs border border-red-800/40">
|
|
255
|
+
<svg className="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
256
|
+
{error}
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
<div ref={bottomRef} />
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* Input */}
|
|
266
|
+
<div className="border-t border-zinc-800 bg-zinc-900/50">
|
|
267
|
+
<div className="max-w-3xl mx-auto px-4 py-3">
|
|
268
|
+
<div className="flex items-end gap-2 bg-zinc-800/80 rounded-xl border border-zinc-700/50 px-3 py-2 focus-within:border-zinc-500 transition-colors">
|
|
269
|
+
<input ref={inputRef}
|
|
270
|
+
value={input}
|
|
271
|
+
onChange={e => setInput(e.target.value)}
|
|
272
|
+
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage() } }}
|
|
273
|
+
placeholder={currentId ? 'Type a message...' : 'Create a session first'}
|
|
274
|
+
disabled={!currentId || loading}
|
|
275
|
+
className="flex-1 bg-transparent text-sm text-zinc-100 placeholder-zinc-500 outline-none resize-none disabled:opacity-40"
|
|
276
|
+
/>
|
|
277
|
+
<button onClick={sendMessage} disabled={!currentId || loading || !input.trim()}
|
|
278
|
+
className="flex items-center justify-center p-1.5 rounded-lg bg-indigo-600 hover:bg-indigo-500 disabled:bg-zinc-700 disabled:text-zinc-500 text-white transition-colors cursor-pointer disabled:cursor-default shrink-0">
|
|
279
|
+
{loading ? (
|
|
280
|
+
<svg className="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"/><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/></svg>
|
|
281
|
+
) : (
|
|
282
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19V5m0 0l-7 7m7-7l7 7"/></svg>
|
|
283
|
+
)}
|
|
284
|
+
</button>
|
|
285
|
+
</div>
|
|
286
|
+
{currentId && (
|
|
287
|
+
<p className="text-[10px] text-zinc-600 text-center mt-1.5">Enter to send</p>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</main>
|
|
292
|
+
</div>
|
|
293
|
+
)
|
|
294
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weifuwu",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.9",
|
|
4
4
|
"description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"dist/",
|
|
16
16
|
"cli.ts",
|
|
17
|
+
"opencode/ui/",
|
|
17
18
|
"README.md",
|
|
18
19
|
"LICENSE"
|
|
19
20
|
],
|