Add first version of chat
This commit is contained in:
committed by
Mark IJbema
parent
a151a9339b
commit
e4a01805a7
@@ -106,7 +106,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; style-src 'unsafe-inline' ${webview.cspSource}; script-src 'nonce-${nonce}' ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https: data:; connect-src http://localhost:* https://localhost:*;"
|
||||
content="default-src 'none'; style-src 'unsafe-inline' ${webview.cspSource}; script-src 'nonce-${nonce}' ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https: data:; connect-src http://localhost:* https://localhost:* http://127.0.0.1:* https://127.0.0.1:*;"
|
||||
/>
|
||||
<link rel="stylesheet" href="${styleUri}?v=${bust}" />
|
||||
<title>Kilo</title>
|
||||
|
||||
@@ -8,6 +8,7 @@ export function MessageList() {
|
||||
|
||||
createEffect(() => {
|
||||
const msgs = messages()
|
||||
console.log("[MessageList] messages updated:", msgs.length, msgs)
|
||||
if (msgs.length > 0 && container) {
|
||||
container.scrollTop = container.scrollHeight
|
||||
}
|
||||
|
||||
@@ -39,12 +39,14 @@ export function ServerProvider(props: ParentProps) {
|
||||
|
||||
function handleMessage(event: MessageEvent<ExtensionMessage>) {
|
||||
const message = event.data
|
||||
console.log("[server] handleMessage", message)
|
||||
switch (message.type) {
|
||||
case "server":
|
||||
setServer(message.server ?? null)
|
||||
vscode.setState({ server: message.server ?? null, directory: directory() })
|
||||
break
|
||||
case "workspace":
|
||||
console.log("[server] setting directory to:", message.directory)
|
||||
setDirectory(message.directory ?? null)
|
||||
vscode.setState({ server: server(), directory: message.directory ?? null })
|
||||
break
|
||||
@@ -64,6 +66,8 @@ export function ServerProvider(props: ParentProps) {
|
||||
const info = server()
|
||||
const dir = directory()
|
||||
|
||||
console.log("[server] createEffect - server:", info, "directory:", dir)
|
||||
|
||||
if (!info) {
|
||||
setClient(null)
|
||||
setStatus("disconnected")
|
||||
@@ -71,12 +75,14 @@ export function ServerProvider(props: ParentProps) {
|
||||
}
|
||||
|
||||
setStatus("connecting")
|
||||
console.log("[server] creating client with baseUrl:", info.url, "directory:", dir)
|
||||
const newClient = createOpencodeClient({
|
||||
baseUrl: info.url,
|
||||
directory: dir ?? undefined,
|
||||
})
|
||||
setClient(newClient)
|
||||
setStatus("connected")
|
||||
console.log("[server] client created, status: connected")
|
||||
})
|
||||
|
||||
return <ServerContext.Provider value={{ server, directory, client, status }}>{props.children}</ServerContext.Provider>
|
||||
|
||||
@@ -60,9 +60,23 @@ export function SessionProvider(props: ParentProps) {
|
||||
const parts = () => (messageID: string) => state.parts[messageID] ?? []
|
||||
const status = () => (state.current ? (state.status[state.current] ?? "idle") : "idle")
|
||||
|
||||
// Track if we've already subscribed to prevent re-subscription
|
||||
const [subscribed, setSubscribed] = createSignal(false)
|
||||
|
||||
createEffect(() => {
|
||||
const c = client()
|
||||
if (!c || serverStatus() !== "connected") return
|
||||
const connected = serverStatus() === "connected"
|
||||
|
||||
if (!c || !connected) {
|
||||
// Reset subscription state when disconnected
|
||||
setSubscribed(false)
|
||||
eventAbort()?.abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Only subscribe once per connection
|
||||
if (subscribed()) return
|
||||
setSubscribed(true)
|
||||
|
||||
loadSessions()
|
||||
subscribeToEvents()
|
||||
@@ -88,24 +102,38 @@ export function SessionProvider(props: ParentProps) {
|
||||
|
||||
async function subscribeToEvents() {
|
||||
const c = client()
|
||||
if (!c) return
|
||||
if (!c) {
|
||||
console.log("[session] subscribeToEvents: no client")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("[session] subscribing to events...")
|
||||
|
||||
eventAbort()?.abort()
|
||||
const abort = new AbortController()
|
||||
setEventAbort(abort)
|
||||
|
||||
try {
|
||||
console.log("[session] calling c.global.event()")
|
||||
const result = await c.global.event({ signal: abort.signal })
|
||||
if (!result.stream) return
|
||||
console.log("[session] event stream result:", result, "hasStream:", !!result.stream)
|
||||
if (!result.stream) {
|
||||
console.log("[session] no stream in result!")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("[session] starting to iterate over stream...")
|
||||
for await (const event of result.stream) {
|
||||
if (abort.signal.aborted) break
|
||||
console.log("[session] received raw event:", JSON.stringify(event))
|
||||
if (abort.signal.aborted) {
|
||||
console.log("[session] abort signal received, breaking")
|
||||
break
|
||||
}
|
||||
handleEvent(event as { directory?: string; payload: Event })
|
||||
}
|
||||
console.log("[session] stream iteration ended")
|
||||
} catch (err) {
|
||||
if (!abort.signal.aborted) {
|
||||
console.error("Event stream error:", err)
|
||||
}
|
||||
console.error("[session] Event stream error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +141,15 @@ export function SessionProvider(props: ParentProps) {
|
||||
const payload = event.payload
|
||||
const dir = directory()
|
||||
|
||||
console.log("[session] handleEvent", {
|
||||
eventType: payload.type,
|
||||
eventDirectory: event.directory,
|
||||
localDirectory: dir,
|
||||
willFilter: dir && event.directory && event.directory !== dir && event.directory !== "global",
|
||||
})
|
||||
|
||||
if (dir && event.directory && event.directory !== dir && event.directory !== "global") {
|
||||
console.log("[session] filtering out event due to directory mismatch")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -152,6 +188,7 @@ export function SessionProvider(props: ParentProps) {
|
||||
break
|
||||
|
||||
case "message.updated":
|
||||
console.log("[session] message.updated", payload.properties.info)
|
||||
setState(
|
||||
produce((s) => {
|
||||
const msg = payload.properties.info
|
||||
@@ -180,6 +217,7 @@ export function SessionProvider(props: ParentProps) {
|
||||
break
|
||||
|
||||
case "message.part.updated":
|
||||
console.log("[session] message.part.updated", payload.properties.part)
|
||||
setState(
|
||||
produce((s) => {
|
||||
const part = payload.properties.part
|
||||
@@ -210,10 +248,9 @@ export function SessionProvider(props: ParentProps) {
|
||||
case "session.status":
|
||||
setState(
|
||||
produce((s) => {
|
||||
for (const [sessionID, sessionStatus] of Object.entries(payload.properties.status)) {
|
||||
const st = sessionStatus as { status: string }
|
||||
s.status[sessionID] = st.status === "running" ? "running" : "idle"
|
||||
}
|
||||
// Event payload is { sessionID: string, status: { type: "idle" | "busy" | "retry", ... } }
|
||||
const { sessionID, status } = payload.properties as { sessionID: string; status: { type: string } }
|
||||
s.status[sessionID] = status.type === "busy" ? "running" : "idle"
|
||||
}),
|
||||
)
|
||||
break
|
||||
@@ -222,16 +259,21 @@ export function SessionProvider(props: ParentProps) {
|
||||
|
||||
async function createSession(): Promise<Session | null> {
|
||||
const c = client()
|
||||
console.log("[session] createSession: client exists:", !!c)
|
||||
if (!c) return null
|
||||
|
||||
try {
|
||||
console.log("[session] createSession: calling c.session.create()...")
|
||||
const result = await c.session.create()
|
||||
console.log("[session] createSession: result:", result)
|
||||
if (result.data) {
|
||||
console.log("[session] createSession: setting current to", result.data.id)
|
||||
setState("current", result.data.id)
|
||||
return result.data
|
||||
}
|
||||
console.log("[session] createSession: no data in result, error:", result.error)
|
||||
} catch (err) {
|
||||
console.error("Failed to create session:", err)
|
||||
console.error("[session] createSession: Failed to create session:", err)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -248,9 +290,11 @@ export function SessionProvider(props: ParentProps) {
|
||||
try {
|
||||
const result = await c.session.messages({ sessionID })
|
||||
if (result.data) {
|
||||
setState("messages", sessionID, result.data.messages)
|
||||
for (const msg of result.data.messages) {
|
||||
setState("parts", msg.id, result.data.parts[msg.id] ?? [])
|
||||
// result.data is Array<{ info: Message, parts: Part[] }>
|
||||
const messages = result.data.map((m) => m.info)
|
||||
setState("messages", sessionID, messages)
|
||||
for (const msg of result.data) {
|
||||
setState("parts", msg.info.id, msg.parts)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -260,32 +304,48 @@ export function SessionProvider(props: ParentProps) {
|
||||
|
||||
async function sendMessage(text: string): Promise<void> {
|
||||
const c = client()
|
||||
if (!c) return
|
||||
if (!c) {
|
||||
console.log("[session] sendMessage: no client")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("[session] sendMessage: getting or creating session, current:", state.current)
|
||||
const sessionID = state.current ?? (await getOrCreateSession())
|
||||
if (!sessionID) return
|
||||
if (!sessionID) {
|
||||
console.log("[session] sendMessage: no session ID")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("[session] sendMessage: sending prompt to session", sessionID)
|
||||
console.log("[session] sendMessage: current state.messages:", JSON.stringify(state.messages))
|
||||
setState("status", sessionID, "running")
|
||||
|
||||
try {
|
||||
await c.session.prompt({
|
||||
console.log("[session] sendMessage: calling c.session.prompt...")
|
||||
const result = await c.session.prompt({
|
||||
sessionID,
|
||||
parts: [{ type: "text", text }],
|
||||
})
|
||||
console.log("[session] sendMessage: prompt result:", result)
|
||||
} catch (err) {
|
||||
console.error("Failed to send message:", err)
|
||||
console.error("[session] sendMessage: Failed to send message:", err)
|
||||
setState("status", sessionID, "error")
|
||||
}
|
||||
}
|
||||
|
||||
async function getOrCreateSession(): Promise<string | null> {
|
||||
console.log("[session] getOrCreateSession: creating:", creating(), "current:", state.current)
|
||||
if (creating()) {
|
||||
console.log("[session] getOrCreateSession: already creating, waiting...")
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
console.log("[session] getOrCreateSession: done waiting, current:", state.current)
|
||||
return state.current
|
||||
}
|
||||
|
||||
setCreating(true)
|
||||
console.log("[session] getOrCreateSession: calling createSession...")
|
||||
const session = await createSession()
|
||||
console.log("[session] getOrCreateSession: createSession returned:", session)
|
||||
setCreating(false)
|
||||
return session?.id ?? null
|
||||
}
|
||||
|
||||
@@ -110,6 +110,9 @@ export namespace Server {
|
||||
if (input.startsWith("http://127.0.0.1:")) return input
|
||||
if (input === "tauri://localhost" || input === "http://tauri.localhost") return input
|
||||
|
||||
// VS Code webview origins
|
||||
if (input.startsWith("vscode-webview://")) return input
|
||||
|
||||
// *.opencode.ai (https only, adjust if needed)
|
||||
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) {
|
||||
return input
|
||||
@@ -120,6 +123,10 @@ export namespace Server {
|
||||
|
||||
return
|
||||
},
|
||||
allowHeaders: ["Content-Type", "x-opencode-directory"],
|
||||
allowMethods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
||||
exposeHeaders: ["Content-Type"],
|
||||
maxAge: 86400,
|
||||
}),
|
||||
)
|
||||
.route("/global", GlobalRoutes())
|
||||
|
||||
Reference in New Issue
Block a user