Split workspace into electron and ui packages

This commit is contained in:
Shantur Rathore
2025-11-17 12:06:58 +00:00
parent aa77ca2931
commit 89bd32814f
137 changed files with 407 additions and 1371 deletions

View File

@@ -0,0 +1,63 @@
import { createMemo, type Component } from "solid-js"
import { getSessionInfo } from "../../stores/sessions"
import { formatTokenTotal } from "../../lib/formatters"
interface ContextUsagePanelProps {
instanceId: string
sessionId: string
}
const ContextUsagePanel: Component<ContextUsagePanelProps> = (props) => {
const info = createMemo(
() =>
getSessionInfo(props.instanceId, props.sessionId) ?? {
tokens: 0,
cost: 0,
contextWindow: 0,
isSubscriptionModel: false,
contextUsageTokens: 0,
contextUsagePercent: null,
},
)
const tokens = createMemo(() => info().tokens)
const contextUsageTokens = createMemo(() => info().contextUsageTokens ?? 0)
const contextWindow = createMemo(() => info().contextWindow)
const contextUsagePercent = createMemo(() => info().contextUsagePercent)
const costLabel = createMemo(() => {
if (info().isSubscriptionModel || info().cost <= 0) return "Included in plan"
return `$${info().cost.toFixed(2)} spent`
})
return (
<div class="session-context-panel border-r border-base border-b px-3 py-3">
<div class="flex items-center justify-between gap-4">
<div>
<div class="text-xs font-semibold text-primary/70 uppercase tracking-wide">Tokens (last call)</div>
<div class="text-lg font-semibold text-primary">{formatTokenTotal(tokens())}</div>
</div>
<div class="text-xs text-primary/70 text-right leading-tight">{costLabel()}</div>
</div>
<div class="mt-4">
<div class="flex items-center justify-between mb-1">
<div class="text-xs font-semibold text-primary/70 uppercase tracking-wide">Context window usage</div>
<div class="text-sm font-medium text-primary">{contextUsagePercent() !== null ? `${contextUsagePercent()}%` : "--"}</div>
</div>
<div class="text-sm text-primary/90">
{contextWindow()
? `${formatTokenTotal(contextUsageTokens())} of ${formatTokenTotal(contextWindow())}`
: "Window size unavailable"}
</div>
</div>
<div class="mt-3 h-1.5 rounded-full bg-base relative overflow-hidden">
<div
class="absolute inset-y-0 left-0 rounded-full bg-accent-primary transition-[width]"
style={{ width: contextUsagePercent() === null ? "0%" : `${contextUsagePercent()}%` }}
/>
</div>
</div>
)
}
export default ContextUsagePanel

View File

@@ -0,0 +1,150 @@
import { Show, createMemo, createEffect, onCleanup, type Component } from "solid-js"
import type { Session } from "../../types/session"
import type { Attachment } from "../../types/attachment"
import type { ClientPart } from "../../types/message"
import MessageStream from "../message-stream"
import PromptInput from "../prompt-input"
import { instances } from "../../stores/instances"
import { loadMessages, sendMessage, forkSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand } from "../../stores/sessions"
interface SessionViewProps {
sessionId: string
activeSessions: Map<string, Session>
instanceId: string
instanceFolder: string
escapeInDebounce: boolean
}
export const SessionView: Component<SessionViewProps> = (props) => {
const session = () => props.activeSessions.get(props.sessionId)
const messagesLoading = createMemo(() => isSessionMessagesLoading(props.instanceId, props.sessionId))
createEffect(() => {
const currentSession = session()
if (currentSession) {
loadMessages(props.instanceId, currentSession.id).catch(console.error)
}
})
async function handleSendMessage(prompt: string, attachments: Attachment[]) {
await sendMessage(props.instanceId, props.sessionId, prompt, attachments)
}
async function handleRunShell(command: string) {
await runShellCommand(props.instanceId, props.sessionId, command)
}
function getUserMessageText(messageId: string): string | null {
const currentSession = session()
if (!currentSession) return null
const targetMessage = currentSession.messages.find((m) => m.id === messageId)
const targetInfo = currentSession.messagesInfo.get(messageId)
if (!targetMessage || targetInfo?.role !== "user") {
return null
}
const textParts = targetMessage.parts.filter((p): p is ClientPart & { type: "text"; text: string } => p.type === "text")
if (textParts.length === 0) {
return null
}
return textParts.map((p) => p.text).join("\n")
}
async function handleRevert(messageId: string) {
const instance = instances().get(props.instanceId)
if (!instance || !instance.client) return
try {
await instance.client.session.revert({
path: { id: props.sessionId },
body: { messageID: messageId },
})
const restoredText = getUserMessageText(messageId)
if (restoredText) {
const textarea = document.querySelector(".prompt-input") as HTMLTextAreaElement
if (textarea) {
textarea.value = restoredText
textarea.dispatchEvent(new Event("input", { bubbles: true }))
textarea.focus()
}
}
} catch (error) {
console.error("Failed to revert:", error)
alert("Failed to revert to message")
}
}
async function handleFork(messageId?: string) {
if (!messageId) {
console.warn("Fork requires a user message id")
return
}
const restoredText = getUserMessageText(messageId)
try {
const forkedSession = await forkSession(props.instanceId, props.sessionId, { messageId })
const parentToActivate = forkedSession.parentId ?? forkedSession.id
setActiveParentSession(props.instanceId, parentToActivate)
if (forkedSession.parentId) {
setActiveSession(props.instanceId, forkedSession.id)
}
await loadMessages(props.instanceId, forkedSession.id).catch(console.error)
if (restoredText) {
const textarea = document.querySelector(".prompt-input") as HTMLTextAreaElement
if (textarea) {
textarea.value = restoredText
textarea.dispatchEvent(new Event("input", { bubbles: true }))
textarea.focus()
}
}
} catch (error) {
console.error("Failed to fork session:", error)
alert("Failed to fork session")
}
}
return (
<Show
when={session()}
fallback={
<div class="flex items-center justify-center h-full">
<div class="text-center text-gray-500">Session not found</div>
</div>
}
>
{(s) => (
<div class="session-view">
<MessageStream
instanceId={props.instanceId}
sessionId={s().id}
messages={s().messages || []}
messagesInfo={s().messagesInfo}
revert={s().revert}
loading={messagesLoading()}
onRevert={handleRevert}
onFork={handleFork}
/>
<PromptInput
instanceId={props.instanceId}
instanceFolder={props.instanceFolder}
sessionId={s().id}
onSend={handleSendMessage}
onRunShell={handleRunShell}
escapeInDebounce={props.escapeInDebounce}
/>
</div>
)}
</Show>
)
}
export default SessionView