WIP
This commit is contained in:
Generated
+7809
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,14 @@ function createSessionsStore() {
|
|||||||
update((s) => ({ ...s, error: (e as Error).message }));
|
update((s) => ({ ...s, error: (e as Error).message }));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async create(data: Partial<Session> = {}): Promise<Session> {
|
||||||
|
const session = await api.createSession(data);
|
||||||
|
update((s) => ({
|
||||||
|
...s,
|
||||||
|
sessions: [session, ...s.sessions]
|
||||||
|
}));
|
||||||
|
return session;
|
||||||
|
},
|
||||||
updateSession(id: string, data: Partial<Session>) {
|
updateSession(id: string, data: Partial<Session>) {
|
||||||
update((s) => ({
|
update((s) => ({
|
||||||
...s,
|
...s,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import { sessions, sortedSessions, runningSessions } from '$lib/stores/sessions';
|
import { sessions, sortedSessions, runningSessions } from '$lib/stores/sessions';
|
||||||
import { api, type DiscoveredSession } from '$lib/api';
|
import { api, type DiscoveredSession } from '$lib/api';
|
||||||
import SessionCard from '$lib/components/SessionCard.svelte';
|
import SessionCard from '$lib/components/SessionCard.svelte';
|
||||||
@@ -6,11 +7,24 @@
|
|||||||
let discovering = false;
|
let discovering = false;
|
||||||
let discoveredSessions: DiscoveredSession[] = [];
|
let discoveredSessions: DiscoveredSession[] = [];
|
||||||
let showDiscovery = false;
|
let showDiscovery = false;
|
||||||
|
let creating = false;
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
await sessions.load();
|
await sessions.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createNewSession() {
|
||||||
|
creating = true;
|
||||||
|
try {
|
||||||
|
const session = await sessions.create({ provider: 'claude' });
|
||||||
|
await goto(`/session/${session.id}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to create session:', e);
|
||||||
|
} finally {
|
||||||
|
creating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function discoverSessions() {
|
async function discoverSessions() {
|
||||||
discovering = true;
|
discovering = true;
|
||||||
try {
|
try {
|
||||||
@@ -50,6 +64,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
on:click={createNewSession}
|
||||||
|
disabled={creating}
|
||||||
|
class="btn btn-primary p-2"
|
||||||
|
title="New Session"
|
||||||
|
>
|
||||||
|
{#if creating}
|
||||||
|
<svg class="h-5 w-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
on:click={refresh}
|
on:click={refresh}
|
||||||
disabled={$sessions.loading}
|
disabled={$sessions.loading}
|
||||||
@@ -75,7 +107,7 @@
|
|||||||
<button
|
<button
|
||||||
on:click={discoverSessions}
|
on:click={discoverSessions}
|
||||||
disabled={discovering}
|
disabled={discovering}
|
||||||
class="btn btn-primary"
|
class="btn btn-secondary"
|
||||||
>
|
>
|
||||||
{#if discovering}
|
{#if discovering}
|
||||||
<span class="animate-spin inline-block mr-2">
|
<span class="animate-spin inline-block mr-2">
|
||||||
@@ -158,7 +190,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<h2 class="text-lg font-medium text-zinc-300 mb-2">No sessions yet</h2>
|
<h2 class="text-lg font-medium text-zinc-300 mb-2">No sessions yet</h2>
|
||||||
<p class="text-sm mb-4">
|
<p class="text-sm mb-4">
|
||||||
Click "Discover" to find existing Claude Code or OpenCode sessions on your machine.
|
Click the + button to start a new chat, or "Discover" to find existing Claude Code sessions.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,8 +29,9 @@
|
|||||||
$: session = $activeSession.session;
|
$: session = $activeSession.session;
|
||||||
$: externalId = session?.['external-id'] || session?.externalId || '';
|
$: externalId = session?.['external-id'] || session?.externalId || '';
|
||||||
$: workingDir = session?.['working-dir'] || session?.workingDir || '';
|
$: workingDir = session?.['working-dir'] || session?.workingDir || '';
|
||||||
$: shortId = externalId.slice(0, 8);
|
$: shortId = externalId ? externalId.slice(0, 8) : session?.id?.slice(0, 8) || '';
|
||||||
$: projectName = workingDir.split('/').pop() || '';
|
$: projectName = workingDir.split('/').pop() || '';
|
||||||
|
$: isNewSession = !externalId && $activeSession.messages.length === 0;
|
||||||
|
|
||||||
const statusColors: Record<string, string> = {
|
const statusColors: Record<string, string> = {
|
||||||
idle: 'bg-zinc-600',
|
idle: 'bg-zinc-600',
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
metosin/reitit {:mvn/version "0.7.0-alpha7"}
|
metosin/reitit {:mvn/version "0.7.0-alpha7"}
|
||||||
|
|
||||||
;; WebSocket
|
;; WebSocket
|
||||||
info.sunng/ring-jetty9-adapter {:mvn/version "0.30.0"}
|
info.sunng/ring-jetty9-adapter {:mvn/version "0.14.3"}
|
||||||
|
|
||||||
;; Database
|
;; Database
|
||||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.894"}
|
com.github.seancorfield/next.jdbc {:mvn/version "1.3.894"}
|
||||||
|
|||||||
@@ -48,16 +48,28 @@
|
|||||||
(try
|
(try
|
||||||
(let [data (json/read-value line mapper)]
|
(let [data (json/read-value line mapper)]
|
||||||
(case (:type data)
|
(case (:type data)
|
||||||
|
;; System init message contains session_id
|
||||||
|
"system" (when (= (:subtype data) "init")
|
||||||
|
{:event :init
|
||||||
|
:session-id (:session_id data)
|
||||||
|
:cwd (:cwd data)})
|
||||||
|
;; User message (from history)
|
||||||
"user" {:role :user
|
"user" {:role :user
|
||||||
:content (get-in data [:message :content])
|
:content (get-in data [:message :content])
|
||||||
:timestamp (:timestamp data)
|
:timestamp (:timestamp data)
|
||||||
:uuid (:uuid data)}
|
:uuid (:uuid data)}
|
||||||
"assistant" {:role :assistant
|
;; Assistant response - extract text content
|
||||||
:content (get-in data [:message :content])
|
"assistant" (let [content-blocks (get-in data [:message :content])
|
||||||
:timestamp (:timestamp data)
|
text-content (->> content-blocks
|
||||||
:uuid (:uuid data)
|
(filter #(= (:type %) "text"))
|
||||||
:metadata {:model (get-in data [:message :model])
|
(map :text)
|
||||||
:stop-reason (get-in data [:message :stopReason])}}
|
(clojure.string/join ""))]
|
||||||
|
{:event :content-delta
|
||||||
|
:text text-content
|
||||||
|
:role :assistant
|
||||||
|
:uuid (:uuid data)
|
||||||
|
:metadata {:model (get-in data [:message :model])
|
||||||
|
:stop-reason (get-in data [:message :stop_reason])}})
|
||||||
;; Stream events from --output-format stream-json
|
;; Stream events from --output-format stream-json
|
||||||
"content_block_start" {:event :content-start
|
"content_block_start" {:event :content-start
|
||||||
:index (:index data)
|
:index (:index data)
|
||||||
@@ -72,10 +84,11 @@
|
|||||||
"message_delta" {:event :message-delta
|
"message_delta" {:event :message-delta
|
||||||
:stop-reason (get-in data [:delta :stop_reason])}
|
:stop-reason (get-in data [:delta :stop_reason])}
|
||||||
"message_stop" {:event :message-stop}
|
"message_stop" {:event :message-stop}
|
||||||
|
;; Result contains final content and session_id
|
||||||
"result" {:event :result
|
"result" {:event :result
|
||||||
:content (get-in data [:result :assistant :content])
|
:content (:result data)
|
||||||
:cost (:cost data)
|
:session-id (:session_id data)
|
||||||
:session-id (:session_id data)}
|
:cost (:total_cost_usd data)}
|
||||||
;; Unknown type
|
;; Unknown type
|
||||||
{:raw data}))
|
{:raw data}))
|
||||||
(catch Exception e
|
(catch Exception e
|
||||||
@@ -111,11 +124,14 @@
|
|||||||
|
|
||||||
(spawn-session [_ session-id opts]
|
(spawn-session [_ session-id opts]
|
||||||
(let [{:keys [working-dir model permission-mode]} opts
|
(let [{:keys [working-dir model permission-mode]} opts
|
||||||
|
;; Build base args - only include --resume if we have a session-id
|
||||||
|
;; --verbose is required when using --print with --output-format=stream-json
|
||||||
args (cond-> ["claude"
|
args (cond-> ["claude"
|
||||||
"--resume" session-id
|
|
||||||
"--output-format" "stream-json"
|
"--output-format" "stream-json"
|
||||||
"--input-format" "stream-json"
|
"--input-format" "stream-json"
|
||||||
|
"--verbose"
|
||||||
"--print"]
|
"--print"]
|
||||||
|
session-id (conj "--resume" session-id)
|
||||||
model (conj "--model" model)
|
model (conj "--model" model)
|
||||||
permission-mode (conj "--permission-mode" permission-mode))
|
permission-mode (conj "--permission-mode" permission-mode))
|
||||||
pb (ProcessBuilder. args)]
|
pb (ProcessBuilder. args)]
|
||||||
@@ -130,7 +146,10 @@
|
|||||||
|
|
||||||
(send-message [_ {:keys [stdin]} message]
|
(send-message [_ {:keys [stdin]} message]
|
||||||
(try
|
(try
|
||||||
(let [json-msg (json/write-value-as-string {:type "user" :content message})]
|
;; Claude stream-json format: {"type":"user","message":{"role":"user","content":"..."}}
|
||||||
|
(let [json-msg (json/write-value-as-string {:type "user"
|
||||||
|
:message {:role "user"
|
||||||
|
:content message}})]
|
||||||
(.write stdin json-msg)
|
(.write stdin json-msg)
|
||||||
(.newLine stdin)
|
(.newLine stdin)
|
||||||
(.flush stdin)
|
(.flush stdin)
|
||||||
@@ -143,9 +162,13 @@
|
|||||||
(try
|
(try
|
||||||
(loop []
|
(loop []
|
||||||
(when-let [line (.readLine stdout)]
|
(when-let [line (.readLine stdout)]
|
||||||
(when-let [parsed (proto/parse-output this line)]
|
(let [parsed (proto/parse-output this line)]
|
||||||
(callback parsed))
|
(when parsed
|
||||||
(recur)))
|
(callback parsed))
|
||||||
|
;; Stop reading after result event - response is complete
|
||||||
|
(if (= :result (:event parsed))
|
||||||
|
(log/debug "Received result, stopping stream read")
|
||||||
|
(recur)))))
|
||||||
(catch Exception e
|
(catch Exception e
|
||||||
(log/debug "Stream ended:" (.getMessage e)))))
|
(log/debug "Stream ended:" (.getMessage e)))))
|
||||||
|
|
||||||
|
|||||||
@@ -82,25 +82,28 @@
|
|||||||
(defn create-ws-listener
|
(defn create-ws-listener
|
||||||
"Create a WebSocket listener"
|
"Create a WebSocket listener"
|
||||||
[]
|
[]
|
||||||
(reify WebSocketListener
|
(let [session-atom (atom nil)]
|
||||||
(onWebSocketConnect [_ session]
|
(reify WebSocketListener
|
||||||
(log/debug "WebSocket connected")
|
(onWebSocketConnect [_ session]
|
||||||
(.add all-connections session)
|
(reset! session-atom session)
|
||||||
(send-to-ws session {:type "connected"}))
|
(log/debug "WebSocket connected")
|
||||||
|
(.add all-connections session)
|
||||||
|
(send-to-ws session {:type "connected"}))
|
||||||
|
|
||||||
(onWebSocketText [_ session message]
|
(onWebSocketText [_ message]
|
||||||
(handle-message session message))
|
(handle-message @session-atom message))
|
||||||
|
|
||||||
(onWebSocketBinary [_ _session _payload _offset _len]
|
(onWebSocketBinary [_ _payload _offset _len]
|
||||||
(log/debug "Binary WebSocket message ignored"))
|
(log/debug "Binary WebSocket message ignored"))
|
||||||
|
|
||||||
(onWebSocketClose [_ session status-code reason]
|
(onWebSocketClose [_ status-code reason]
|
||||||
(log/debug "WebSocket closed:" status-code reason)
|
(log/debug "WebSocket closed:" status-code reason)
|
||||||
(.remove all-connections session)
|
(when-let [session @session-atom]
|
||||||
(unsubscribe-from-all session))
|
(.remove all-connections session)
|
||||||
|
(unsubscribe-from-all session)))
|
||||||
|
|
||||||
(onWebSocketError [_ _session cause]
|
(onWebSocketError [_ cause]
|
||||||
(log/warn "WebSocket error:" (.getMessage cause)))))
|
(log/warn "WebSocket error:" (.getMessage cause))))))
|
||||||
|
|
||||||
(defn ws-handler
|
(defn ws-handler
|
||||||
"Ring handler for WebSocket upgrade"
|
"Ring handler for WebSocket upgrade"
|
||||||
|
|||||||
@@ -101,8 +101,14 @@
|
|||||||
;; Accumulate text content
|
;; Accumulate text content
|
||||||
(when-let [text (:text event)]
|
(when-let [text (:text event)]
|
||||||
(.append content-buffer text))
|
(.append content-buffer text))
|
||||||
;; On message stop, save the accumulated message
|
;; Capture external session-id from init or result event (for new sessions)
|
||||||
(when (= :message-stop (:event event))
|
(when (and (contains? #{:init :result} (:event event))
|
||||||
|
(:session-id event)
|
||||||
|
(not (:external-id session)))
|
||||||
|
(log/debug "Capturing external session-id:" (:session-id event))
|
||||||
|
(db/update-session store session-id {:external-id (:session-id event)}))
|
||||||
|
;; On result event, save the accumulated message
|
||||||
|
(when (= :result (:event event))
|
||||||
(let [content (.toString content-buffer)]
|
(let [content (.toString content-buffer)]
|
||||||
(when (seq content)
|
(when (seq content)
|
||||||
(db/save-message store {:session-id session-id
|
(db/save-message store {:session-id session-id
|
||||||
|
|||||||
Reference in New Issue
Block a user