From 794fe8f381c846f5241800363023d892c12cf495 Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Mon, 22 Dec 2025 19:38:50 -0600
Subject: chore: rename packages/desktop -> packages/app
---
.../src/components/dialog-connect-provider.tsx | 383 -------
.../src/components/dialog-manage-models.tsx | 57 -
.../desktop/src/components/dialog-select-file.tsx | 48 -
.../src/components/dialog-select-model-unpaid.tsx | 110 --
.../desktop/src/components/dialog-select-model.tsx | 83 --
.../src/components/dialog-select-provider.tsx | 54 -
packages/desktop/src/components/file-tree.tsx | 112 --
packages/desktop/src/components/header.tsx | 209 ----
packages/desktop/src/components/link.tsx | 17 -
packages/desktop/src/components/prompt-input.tsx | 1153 --------------------
.../src/components/session-context-usage.tsx | 64 --
packages/desktop/src/components/terminal.tsx | 160 ---
12 files changed, 2450 deletions(-)
delete mode 100644 packages/desktop/src/components/dialog-connect-provider.tsx
delete mode 100644 packages/desktop/src/components/dialog-manage-models.tsx
delete mode 100644 packages/desktop/src/components/dialog-select-file.tsx
delete mode 100644 packages/desktop/src/components/dialog-select-model-unpaid.tsx
delete mode 100644 packages/desktop/src/components/dialog-select-model.tsx
delete mode 100644 packages/desktop/src/components/dialog-select-provider.tsx
delete mode 100644 packages/desktop/src/components/file-tree.tsx
delete mode 100644 packages/desktop/src/components/header.tsx
delete mode 100644 packages/desktop/src/components/link.tsx
delete mode 100644 packages/desktop/src/components/prompt-input.tsx
delete mode 100644 packages/desktop/src/components/session-context-usage.tsx
delete mode 100644 packages/desktop/src/components/terminal.tsx
(limited to 'packages/desktop/src/components')
diff --git a/packages/desktop/src/components/dialog-connect-provider.tsx b/packages/desktop/src/components/dialog-connect-provider.tsx
deleted file mode 100644
index 789a5d3b7..000000000
--- a/packages/desktop/src/components/dialog-connect-provider.tsx
+++ /dev/null
@@ -1,383 +0,0 @@
-import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
-import { Button } from "@opencode-ai/ui/button"
-import { useDialog } from "@opencode-ai/ui/context/dialog"
-import { Dialog } from "@opencode-ai/ui/dialog"
-import { Icon } from "@opencode-ai/ui/icon"
-import { IconButton } from "@opencode-ai/ui/icon-button"
-import type { IconName } from "@opencode-ai/ui/icons/provider"
-import { List, type ListRef } from "@opencode-ai/ui/list"
-import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
-import { Spinner } from "@opencode-ai/ui/spinner"
-import { TextField } from "@opencode-ai/ui/text-field"
-import { showToast } from "@opencode-ai/ui/toast"
-import { iife } from "@opencode-ai/util/iife"
-import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
-import { createStore, produce } from "solid-js/store"
-import { Link } from "@/components/link"
-import { useGlobalSDK } from "@/context/global-sdk"
-import { useGlobalSync } from "@/context/global-sync"
-import { usePlatform } from "@/context/platform"
-import { DialogSelectModel } from "./dialog-select-model"
-import { DialogSelectProvider } from "./dialog-select-provider"
-
-export function DialogConnectProvider(props: { provider: string }) {
- const dialog = useDialog()
- const globalSync = useGlobalSync()
- const globalSDK = useGlobalSDK()
- const platform = usePlatform()
- const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
- const methods = createMemo(
- () =>
- globalSync.data.provider_auth[props.provider] ?? [
- {
- type: "api",
- label: "API key",
- },
- ],
- )
- const [store, setStore] = createStore({
- methodIndex: undefined as undefined | number,
- authorization: undefined as undefined | ProviderAuthAuthorization,
- state: "pending" as undefined | "pending" | "complete" | "error",
- error: undefined as string | undefined,
- })
-
- const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined))
-
- async function selectMethod(index: number) {
- const method = methods()[index]
- setStore(
- produce((draft) => {
- draft.methodIndex = index
- draft.authorization = undefined
- draft.state = undefined
- draft.error = undefined
- }),
- )
-
- if (method.type === "oauth") {
- setStore("state", "pending")
- const start = Date.now()
- await globalSDK.client.provider.oauth
- .authorize(
- {
- providerID: props.provider,
- method: index,
- },
- { throwOnError: true },
- )
- .then((x) => {
- const elapsed = Date.now() - start
- const delay = 1000 - elapsed
-
- if (delay > 0) {
- setTimeout(() => {
- setStore("state", "complete")
- setStore("authorization", x.data!)
- }, delay)
- return
- }
- setStore("state", "complete")
- setStore("authorization", x.data!)
- })
- .catch((e) => {
- setStore("state", "error")
- setStore("error", String(e))
- })
- }
- }
-
- let listRef: ListRef | undefined
- function handleKey(e: KeyboardEvent) {
- if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
- return
- }
- if (e.key === "Escape") return
- listRef?.onKeyDown(e)
- }
-
- onMount(() => {
- if (methods().length === 1) {
- selectMethod(0)
- }
- document.addEventListener("keydown", handleKey)
- onCleanup(() => {
- document.removeEventListener("keydown", handleKey)
- })
- })
-
- async function complete() {
- await globalSDK.client.global.dispose()
- dialog.close()
- showToast({
- variant: "success",
- icon: "circle-check",
- title: `${provider().name} connected`,
- description: `${provider().name} models are now available to use.`,
- })
- }
-
- function goBack() {
- if (methods().length === 1) {
- dialog.show(() => )
- return
- }
- if (store.authorization) {
- setStore("authorization", undefined)
- setStore("methodIndex", undefined)
- return
- }
- if (store.methodIndex) {
- setStore("methodIndex", undefined)
- return
- }
- dialog.show(() => )
- }
-
- return (
- }>
-
-
-
-
-
-
- Login with Claude Pro/Max
-
- Connect {provider().name}
-
-
-
-
-
-
- Select login method for {provider().name}.
-
-
{
- listRef = ref
- }}
- items={methods}
- key={(m) => m?.label}
- onSelect={async (method, index) => {
- if (!method) return
- selectMethod(index)
- }}
- >
- {(i) => (
-
- )}
-
-
-
-
-
-
-
- Authorization in progress...
-
-
-
-
-
-
-
- Authorization failed: {store.error}
-
-
-
-
- {iife(() => {
- const [formStore, setFormStore] = createStore({
- value: "",
- error: undefined as string | undefined,
- })
-
- async function handleSubmit(e: SubmitEvent) {
- e.preventDefault()
-
- const form = e.currentTarget as HTMLFormElement
- const formData = new FormData(form)
- const apiKey = formData.get("apiKey") as string
-
- if (!apiKey?.trim()) {
- setFormStore("error", "API key is required")
- return
- }
-
- setFormStore("error", undefined)
- await globalSDK.client.auth.set({
- providerID: props.provider,
- auth: {
- type: "api",
- key: apiKey,
- },
- })
- await complete()
- }
-
- return (
-
-
-
-
-
- OpenCode Zen gives you access to a curated set of reliable optimized models for coding
- agents.
-
-
- With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more.
-
-
- Visit{" "}
-
- opencode.ai/zen
- {" "}
- to collect your API key.
-
-
-
-
-
- Enter your {provider().name} API key to connect your account and use {provider().name} models
- in OpenCode.
-
-
-
-
-
- )
- })}
-
-
-
-
- {iife(() => {
- const [formStore, setFormStore] = createStore({
- value: "",
- error: undefined as string | undefined,
- })
-
- onMount(() => {
- if (store.authorization?.method === "code" && store.authorization?.url) {
- platform.openLink(store.authorization.url)
- }
- })
-
- async function handleSubmit(e: SubmitEvent) {
- e.preventDefault()
-
- const form = e.currentTarget as HTMLFormElement
- const formData = new FormData(form)
- const code = formData.get("code") as string
-
- if (!code?.trim()) {
- setFormStore("error", "Authorization code is required")
- return
- }
-
- setFormStore("error", undefined)
- const { error } = await globalSDK.client.provider.oauth.callback({
- providerID: props.provider,
- method: store.methodIndex,
- code,
- })
- if (!error) {
- await complete()
- return
- }
- setFormStore("error", "Invalid authorization code")
- }
-
- return (
-
-
- Visit this link to collect your authorization
- code to connect your account and use {provider().name} models in OpenCode.
-
-
-
- )
- })}
-
-
- {iife(() => {
- const code = createMemo(() => {
- const instructions = store.authorization?.instructions
- if (instructions?.includes(":")) {
- return instructions?.split(":")[1]?.trim()
- }
- return instructions
- })
-
- onMount(async () => {
- const result = await globalSDK.client.provider.oauth.callback({
- providerID: props.provider,
- method: store.methodIndex,
- })
- if (result.error) {
- // TODO: show error
- dialog.close()
- return
- }
- await complete()
- })
-
- return (
-
-
- Visit this link and enter the code below to
- connect your account and use {provider().name} models in OpenCode.
-
-
-
-
- Waiting for authorization...
-
-
- )
- })}
-
-
-
-
-
-
-
- )
-}
diff --git a/packages/desktop/src/components/dialog-manage-models.tsx b/packages/desktop/src/components/dialog-manage-models.tsx
deleted file mode 100644
index 66d125288..000000000
--- a/packages/desktop/src/components/dialog-manage-models.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Dialog } from "@opencode-ai/ui/dialog"
-import { List } from "@opencode-ai/ui/list"
-import { Switch } from "@opencode-ai/ui/switch"
-import type { Component } from "solid-js"
-import { useLocal } from "@/context/local"
-import { popularProviders } from "@/hooks/use-providers"
-
-export const DialogManageModels: Component = () => {
- const local = useLocal()
- return (
-
- )
-}
diff --git a/packages/desktop/src/components/dialog-select-file.tsx b/packages/desktop/src/components/dialog-select-file.tsx
deleted file mode 100644
index b27afdc8b..000000000
--- a/packages/desktop/src/components/dialog-select-file.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { useDialog } from "@opencode-ai/ui/context/dialog"
-import { Dialog } from "@opencode-ai/ui/dialog"
-import { FileIcon } from "@opencode-ai/ui/file-icon"
-import { List } from "@opencode-ai/ui/list"
-import { getDirectory, getFilename } from "@opencode-ai/util/path"
-import { useParams } from "@solidjs/router"
-import { createMemo } from "solid-js"
-import { useLayout } from "@/context/layout"
-import { useLocal } from "@/context/local"
-
-export function DialogSelectFile() {
- const layout = useLayout()
- const local = useLocal()
- const dialog = useDialog()
- const params = useParams()
- const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
- const tabs = createMemo(() => layout.tabs(sessionKey()))
- return (
-
- )
-}
diff --git a/packages/desktop/src/components/dialog-select-model-unpaid.tsx b/packages/desktop/src/components/dialog-select-model-unpaid.tsx
deleted file mode 100644
index 24ec8092d..000000000
--- a/packages/desktop/src/components/dialog-select-model-unpaid.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import { Button } from "@opencode-ai/ui/button"
-import { useDialog } from "@opencode-ai/ui/context/dialog"
-import { Dialog } from "@opencode-ai/ui/dialog"
-import type { IconName } from "@opencode-ai/ui/icons/provider"
-import { List, type ListRef } from "@opencode-ai/ui/list"
-import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
-import { Tag } from "@opencode-ai/ui/tag"
-import { type Component, onCleanup, onMount, Show } from "solid-js"
-import { useLocal } from "@/context/local"
-import { popularProviders, useProviders } from "@/hooks/use-providers"
-import { DialogConnectProvider } from "./dialog-connect-provider"
-import { DialogSelectProvider } from "./dialog-select-provider"
-
-export const DialogSelectModelUnpaid: Component = () => {
- const local = useLocal()
- const dialog = useDialog()
- const providers = useProviders()
-
- let listRef: ListRef | undefined
- const handleKey = (e: KeyboardEvent) => {
- if (e.key === "Escape") return
- listRef?.onKeyDown(e)
- }
-
- onMount(() => {
- document.addEventListener("keydown", handleKey)
- onCleanup(() => {
- document.removeEventListener("keydown", handleKey)
- })
- })
-
- return (
-
- )
-}
diff --git a/packages/desktop/src/components/dialog-select-model.tsx b/packages/desktop/src/components/dialog-select-model.tsx
deleted file mode 100644
index 54783386a..000000000
--- a/packages/desktop/src/components/dialog-select-model.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import { Component, createMemo, Show } from "solid-js"
-import { useLocal } from "@/context/local"
-import { useDialog } from "@opencode-ai/ui/context/dialog"
-import { popularProviders } from "@/hooks/use-providers"
-import { Button } from "@opencode-ai/ui/button"
-import { Tag } from "@opencode-ai/ui/tag"
-import { Dialog } from "@opencode-ai/ui/dialog"
-import { List } from "@opencode-ai/ui/list"
-import { DialogSelectProvider } from "./dialog-select-provider"
-import { DialogManageModels } from "./dialog-manage-models"
-
-export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
- const local = useLocal()
- const dialog = useDialog()
-
- const models = createMemo(() =>
- local.model
- .list()
- .filter((m) => local.model.visible({ modelID: m.id, providerID: m.provider.id }))
- .filter((m) => (props.provider ? m.provider.id === props.provider : true)),
- )
-
- return (
-
- )
-}
diff --git a/packages/desktop/src/components/dialog-select-provider.tsx b/packages/desktop/src/components/dialog-select-provider.tsx
deleted file mode 100644
index 5bbde5d41..000000000
--- a/packages/desktop/src/components/dialog-select-provider.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Component, Show } from "solid-js"
-import { useDialog } from "@opencode-ai/ui/context/dialog"
-import { popularProviders, useProviders } from "@/hooks/use-providers"
-import { Dialog } from "@opencode-ai/ui/dialog"
-import { List } from "@opencode-ai/ui/list"
-import { Tag } from "@opencode-ai/ui/tag"
-import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
-import { IconName } from "@opencode-ai/ui/icons/provider"
-import { DialogConnectProvider } from "./dialog-connect-provider"
-
-export const DialogSelectProvider: Component = () => {
- const dialog = useDialog()
- const providers = useProviders()
-
- return (
-
- )
-}
diff --git a/packages/desktop/src/components/file-tree.tsx b/packages/desktop/src/components/file-tree.tsx
deleted file mode 100644
index 0841c71d1..000000000
--- a/packages/desktop/src/components/file-tree.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import { useLocal, type LocalFile } from "@/context/local"
-import { Collapsible } from "@opencode-ai/ui/collapsible"
-import { FileIcon } from "@opencode-ai/ui/file-icon"
-import { Tooltip } from "@opencode-ai/ui/tooltip"
-import { For, Match, Switch, Show, type ComponentProps, type ParentProps } from "solid-js"
-import { Dynamic } from "solid-js/web"
-
-export default function FileTree(props: {
- path: string
- class?: string
- nodeClass?: string
- level?: number
- onFileClick?: (file: LocalFile) => void
-}) {
- const local = useLocal()
- const level = props.level ?? 0
-
- const Node = (p: ParentProps & ComponentProps<"div"> & { node: LocalFile; as?: "div" | "button" }) => (
- {
- const evt = e as globalThis.DragEvent
- evt.dataTransfer!.effectAllowed = "copy"
- evt.dataTransfer!.setData("text/plain", `file:${p.node.path}`)
-
- // Create custom drag image without margins
- const dragImage = document.createElement("div")
- dragImage.className =
- "flex items-center gap-x-2 px-2 py-1 bg-background-element rounded-md border border-border-1"
- dragImage.style.position = "absolute"
- dragImage.style.top = "-1000px"
-
- // Copy only the icon and text content without padding
- const icon = e.currentTarget.querySelector("svg")
- const text = e.currentTarget.querySelector("span")
- if (icon && text) {
- dragImage.innerHTML = icon.outerHTML + text.outerHTML
- }
-
- document.body.appendChild(dragImage)
- evt.dataTransfer!.setDragImage(dragImage, 0, 12)
- setTimeout(() => document.body.removeChild(dragImage), 0)
- }}
- {...p}
- >
- {p.children}
-
- {p.node.name}
-
-
-
-
-
- )
-
- return (
-
-
- {(node) => (
-
-
-
- (open ? local.file.expand(node.path) : local.file.collapse(node.path))}
- >
-
-
-
-
-
-
-
-
-
-
-
-
- props.onFileClick?.(node)}>
-
-
-
-
-
-
- )}
-
-
- )
-}
diff --git a/packages/desktop/src/components/header.tsx b/packages/desktop/src/components/header.tsx
deleted file mode 100644
index ec7cdfa25..000000000
--- a/packages/desktop/src/components/header.tsx
+++ /dev/null
@@ -1,209 +0,0 @@
-import { useGlobalSync } from "@/context/global-sync"
-import { useGlobalSDK } from "@/context/global-sdk"
-import { useLayout } from "@/context/layout"
-import { Session } from "@opencode-ai/sdk/v2/client"
-import { Button } from "@opencode-ai/ui/button"
-import { Icon } from "@opencode-ai/ui/icon"
-import { Mark } from "@opencode-ai/ui/logo"
-import { Popover } from "@opencode-ai/ui/popover"
-import { Select } from "@opencode-ai/ui/select"
-import { TextField } from "@opencode-ai/ui/text-field"
-import { Tooltip } from "@opencode-ai/ui/tooltip"
-import { base64Decode } from "@opencode-ai/util/encode"
-import { useCommand } from "@/context/command"
-import { getFilename } from "@opencode-ai/util/path"
-import { A, useParams } from "@solidjs/router"
-import { createMemo, createResource, Show } from "solid-js"
-import { IconButton } from "@opencode-ai/ui/icon-button"
-import { iife } from "@opencode-ai/util/iife"
-
-export function Header(props: {
- navigateToProject: (directory: string) => void
- navigateToSession: (session: Session | undefined) => void
- onMobileMenuToggle?: () => void
-}) {
- const globalSync = useGlobalSync()
- const globalSDK = useGlobalSDK()
- const layout = useLayout()
- const params = useParams()
- const command = useCommand()
-
- return (
-
- )
-}
diff --git a/packages/desktop/src/components/link.tsx b/packages/desktop/src/components/link.tsx
deleted file mode 100644
index e13c31330..000000000
--- a/packages/desktop/src/components/link.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { ComponentProps, splitProps } from "solid-js"
-import { usePlatform } from "@/context/platform"
-
-export interface LinkProps extends ComponentProps<"button"> {
- href: string
-}
-
-export function Link(props: LinkProps) {
- const platform = usePlatform()
- const [local, rest] = splitProps(props, ["href", "children"])
-
- return (
-
- )
-}
diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx
deleted file mode 100644
index 94d4ae97e..000000000
--- a/packages/desktop/src/components/prompt-input.tsx
+++ /dev/null
@@ -1,1153 +0,0 @@
-import { useFilteredList } from "@opencode-ai/ui/hooks"
-import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createMemo } from "solid-js"
-import { createStore, produce } from "solid-js/store"
-import { createFocusSignal } from "@solid-primitives/active-element"
-import { useLocal } from "@/context/local"
-import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, usePrompt, ImageAttachmentPart } from "@/context/prompt"
-import { useLayout } from "@/context/layout"
-import { useSDK } from "@/context/sdk"
-import { useNavigate, useParams } from "@solidjs/router"
-import { useSync } from "@/context/sync"
-import { FileIcon } from "@opencode-ai/ui/file-icon"
-import { Button } from "@opencode-ai/ui/button"
-import { Icon } from "@opencode-ai/ui/icon"
-import { Tooltip } from "@opencode-ai/ui/tooltip"
-import { IconButton } from "@opencode-ai/ui/icon-button"
-import { Select } from "@opencode-ai/ui/select"
-import { getDirectory, getFilename } from "@opencode-ai/util/path"
-import { useDialog } from "@opencode-ai/ui/context/dialog"
-import { DialogSelectModel } from "@/components/dialog-select-model"
-import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
-import { useProviders } from "@/hooks/use-providers"
-import { useCommand } from "@/context/command"
-import { persisted } from "@/utils/persist"
-import { Identifier } from "@/utils/id"
-import { SessionContextUsage } from "@/components/session-context-usage"
-
-const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
-const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
-
-interface PromptInputProps {
- class?: string
- ref?: (el: HTMLDivElement) => void
-}
-
-const PLACEHOLDERS = [
- "Fix a TODO in the codebase",
- "What is the tech stack of this project?",
- "Fix broken tests",
- "Explain how authentication works",
- "Find and fix security vulnerabilities",
- "Add unit tests for the user service",
- "Refactor this function to be more readable",
- "What does this error mean?",
- "Help me debug this issue",
- "Generate API documentation",
- "Optimize database queries",
- "Add input validation",
- "Create a new component for...",
- "How do I deploy this project?",
- "Review my code for best practices",
- "Add error handling to this function",
- "Explain this regex pattern",
- "Convert this to TypeScript",
- "Add logging throughout the codebase",
- "What dependencies are outdated?",
- "Help me write a migration script",
- "Implement caching for this endpoint",
- "Add pagination to this list",
- "Create a CLI command for...",
- "How do environment variables work here?",
-]
-
-interface SlashCommand {
- id: string
- trigger: string
- title: string
- description?: string
- keybind?: string
- type: "builtin" | "custom"
-}
-
-export const PromptInput: Component = (props) => {
- const navigate = useNavigate()
- const sdk = useSDK()
- const sync = useSync()
- const local = useLocal()
- const prompt = usePrompt()
- const layout = useLayout()
- const params = useParams()
- const dialog = useDialog()
- const providers = useProviders()
- const command = useCommand()
- let editorRef!: HTMLDivElement
- let fileInputRef!: HTMLInputElement
-
- const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
- const tabs = createMemo(() => layout.tabs(sessionKey()))
- const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
- const status = createMemo(
- () =>
- sync.data.session_status[params.id ?? ""] ?? {
- type: "idle",
- },
- )
- const working = createMemo(() => status()?.type !== "idle")
-
- const [store, setStore] = createStore<{
- popover: "file" | "slash" | null
- historyIndex: number
- savedPrompt: Prompt | null
- placeholder: number
- dragging: boolean
- imageAttachments: ImageAttachmentPart[]
- mode: "normal" | "shell"
- applyingHistory: boolean
- userHasEdited: boolean
- }>({
- popover: null,
- historyIndex: -1,
- savedPrompt: null,
- placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
- dragging: false,
- imageAttachments: [],
- mode: "normal",
- applyingHistory: false,
- userHasEdited: false,
- })
-
- const MAX_HISTORY = 100
- const [history, setHistory] = persisted(
- "prompt-history.v1",
- createStore<{
- entries: Prompt[]
- }>({
- entries: [],
- }),
- )
- const [shellHistory, setShellHistory] = persisted(
- "prompt-history-shell.v1",
- createStore<{
- entries: Prompt[]
- }>({
- entries: [],
- }),
- )
-
- const clonePromptParts = (prompt: Prompt): Prompt =>
- prompt.map((part) => {
- if (part.type === "text") return { ...part }
- if (part.type === "image") return { ...part }
- return {
- ...part,
- selection: part.selection ? { ...part.selection } : undefined,
- }
- })
-
- const promptLength = (prompt: Prompt) =>
- prompt.reduce((len, part) => len + ("content" in part ? part.content.length : 0), 0)
-
- const applyHistoryPrompt = (p: Prompt, position: "start" | "end") => {
- const length = position === "start" ? 0 : promptLength(p)
- setStore("applyingHistory", true)
- setStore("userHasEdited", false)
- prompt.set(p, length)
- requestAnimationFrame(() => {
- editorRef.focus()
- setCursorPosition(editorRef, length)
- setStore("applyingHistory", false)
- })
- }
-
- const getCaretState = () => {
- const selection = window.getSelection()
- const textLength = promptLength(prompt.current())
- if (!selection || selection.rangeCount === 0) {
- return { collapsed: false, cursorPosition: 0, textLength }
- }
- const anchorNode = selection.anchorNode
- if (!anchorNode || !editorRef.contains(anchorNode)) {
- return { collapsed: false, cursorPosition: 0, textLength }
- }
- return {
- collapsed: selection.isCollapsed,
- cursorPosition: getCursorPosition(editorRef),
- textLength,
- }
- }
-
- createEffect(() => {
- params.id
- editorRef.focus()
- if (params.id) return
- const interval = setInterval(() => {
- setStore("placeholder", (prev) => (prev + 1) % PLACEHOLDERS.length)
- }, 6500)
- onCleanup(() => clearInterval(interval))
- })
-
- const isFocused = createFocusSignal(() => editorRef)
-
- const addImageAttachment = async (file: File) => {
- if (!ACCEPTED_FILE_TYPES.includes(file.type)) return
-
- const reader = new FileReader()
- reader.onload = () => {
- const dataUrl = reader.result as string
- const attachment: ImageAttachmentPart = {
- type: "image",
- id: crypto.randomUUID(),
- filename: file.name,
- mime: file.type,
- dataUrl,
- }
- setStore(
- produce((draft) => {
- draft.imageAttachments.push(attachment)
- }),
- )
- }
- reader.readAsDataURL(file)
- }
-
- const removeImageAttachment = (id: string) => {
- setStore(
- produce((draft) => {
- draft.imageAttachments = draft.imageAttachments.filter((a) => a.id !== id)
- }),
- )
- }
-
- const handlePaste = async (event: ClipboardEvent) => {
- const clipboardData = event.clipboardData
- if (!clipboardData) return
-
- const items = Array.from(clipboardData.items)
- const imageItems = items.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type))
-
- if (imageItems.length > 0) {
- event.preventDefault()
- event.stopPropagation()
- for (const item of imageItems) {
- const file = item.getAsFile()
- if (file) await addImageAttachment(file)
- }
- return
- }
-
- event.preventDefault()
- event.stopPropagation()
- const plainText = clipboardData.getData("text/plain") ?? ""
- addPart({ type: "text", content: plainText, start: 0, end: 0 })
- }
-
- const handleDragOver = (event: DragEvent) => {
- event.preventDefault()
- const hasFiles = event.dataTransfer?.types.includes("Files")
- if (hasFiles) {
- setStore("dragging", true)
- }
- }
-
- const handleDragLeave = (event: DragEvent) => {
- const related = event.relatedTarget as Node | null
- const form = event.currentTarget as HTMLElement
- if (!related || !form.contains(related)) {
- setStore("dragging", false)
- }
- }
-
- const handleDrop = async (event: DragEvent) => {
- event.preventDefault()
- setStore("dragging", false)
-
- const files = event.dataTransfer?.files
- if (!files) return
-
- for (const file of Array.from(files)) {
- if (ACCEPTED_FILE_TYPES.includes(file.type)) {
- await addImageAttachment(file)
- }
- }
- }
-
- onMount(() => {
- editorRef.addEventListener("paste", handlePaste)
- })
- onCleanup(() => {
- editorRef.removeEventListener("paste", handlePaste)
- })
-
- createEffect(() => {
- if (isFocused()) {
- handleInput()
- } else {
- setStore("popover", null)
- }
- })
-
- const handleFileSelect = (path: string | undefined) => {
- if (!path) return
- addPart({ type: "file", path, content: "@" + path, start: 0, end: 0 })
- }
-
- const { flat, active, onInput, onKeyDown } = useFilteredList({
- items: local.file.searchFilesAndDirectories,
- key: (x) => x,
- onSelect: handleFileSelect,
- })
-
- const slashCommands = createMemo(() => {
- const builtin = command.options
- .filter((opt) => !opt.disabled && !opt.id.startsWith("suggested.") && opt.slash)
- .map((opt) => ({
- id: opt.id,
- trigger: opt.slash!,
- title: opt.title,
- description: opt.description,
- keybind: opt.keybind,
- type: "builtin" as const,
- }))
-
- const custom = sync.data.command.map((cmd) => ({
- id: `custom.${cmd.name}`,
- trigger: cmd.name,
- title: cmd.name,
- description: cmd.description,
- type: "custom" as const,
- }))
-
- return [...custom, ...builtin]
- })
-
- const handleSlashSelect = (cmd: SlashCommand | undefined) => {
- if (!cmd) return
- setStore("popover", null)
-
- if (cmd.type === "custom") {
- const text = `/${cmd.trigger} `
- editorRef.innerHTML = ""
- editorRef.textContent = text
- prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
- requestAnimationFrame(() => {
- editorRef.focus()
- const range = document.createRange()
- const sel = window.getSelection()
- range.selectNodeContents(editorRef)
- range.collapse(false)
- sel?.removeAllRanges()
- sel?.addRange(range)
- })
- return
- }
-
- editorRef.innerHTML = ""
- prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
- command.trigger(cmd.id, "slash")
- }
-
- const {
- flat: slashFlat,
- active: slashActive,
- onInput: slashOnInput,
- onKeyDown: slashOnKeyDown,
- } = useFilteredList({
- items: slashCommands,
- key: (x) => x?.id,
- filterKeys: ["trigger", "title", "description"],
- onSelect: handleSlashSelect,
- })
-
- createEffect(
- on(
- () => prompt.current(),
- (currentParts) => {
- const domParts = parseFromDOM()
- if (isPromptEqual(currentParts, domParts)) return
-
- const selection = window.getSelection()
- let cursorPosition: number | null = null
- if (selection && selection.rangeCount > 0 && editorRef.contains(selection.anchorNode)) {
- cursorPosition = getCursorPosition(editorRef)
- }
-
- editorRef.innerHTML = ""
- currentParts.forEach((part) => {
- if (part.type === "text") {
- editorRef.appendChild(document.createTextNode(part.content))
- } else if (part.type === "file") {
- const pill = document.createElement("span")
- pill.textContent = part.content
- pill.setAttribute("data-type", "file")
- pill.setAttribute("data-path", part.path)
- pill.setAttribute("contenteditable", "false")
- pill.style.userSelect = "text"
- pill.style.cursor = "default"
- editorRef.appendChild(pill)
- }
- })
-
- if (cursorPosition !== null) {
- setCursorPosition(editorRef, cursorPosition)
- }
- },
- ),
- )
-
- const parseFromDOM = (): Prompt => {
- const newParts: Prompt = []
- let position = 0
-
- const pushText = (content: string) => {
- if (!content) return
- newParts.push({ type: "text", content, start: position, end: position + content.length })
- position += content.length
- }
-
- const rangeText = (range: Range) => {
- const fragment = range.cloneContents()
- const container = document.createElement("div")
- container.append(fragment)
- return container.innerText
- }
-
- const files = Array.from(editorRef.querySelectorAll("[data-type=file]"))
- let last: HTMLElement | undefined
-
- files.forEach((file) => {
- const before = document.createRange()
- before.selectNodeContents(editorRef)
- if (last) before.setStartAfter(last)
- before.setEndBefore(file)
- pushText(rangeText(before))
-
- const content = file.textContent ?? ""
- newParts.push({
- type: "file",
- path: file.dataset.path!,
- content,
- start: position,
- end: position + content.length,
- })
- position += content.length
- last = file
- })
-
- const after = document.createRange()
- after.selectNodeContents(editorRef)
- if (last) after.setStartAfter(last)
- pushText(rangeText(after))
-
- if (newParts.length === 0) newParts.push(...DEFAULT_PROMPT)
- return newParts
- }
-
- const handleInput = () => {
- const rawParts = parseFromDOM()
- const cursorPosition = getCursorPosition(editorRef)
- const rawText = rawParts.map((p) => ("content" in p ? p.content : "")).join("")
- const trimmed = rawText.replace(/\u200B/g, "").trim()
- const hasNonText = rawParts.some((part) => part.type !== "text")
- const shouldReset = trimmed.length === 0 && !hasNonText
-
- if (shouldReset) {
- setStore("popover", null)
- setStore("userHasEdited", false)
- if (store.historyIndex >= 0 && !store.applyingHistory) {
- setStore("historyIndex", -1)
- setStore("savedPrompt", null)
- }
- if (prompt.dirty()) {
- prompt.set(DEFAULT_PROMPT, 0)
- }
- return
- }
-
- const shellMode = store.mode === "shell"
-
- if (!shellMode) {
- const atMatch = rawText.substring(0, cursorPosition).match(/@(\S*)$/)
- const slashMatch = rawText.match(/^\/(\S*)$/)
-
- if (atMatch) {
- onInput(atMatch[1])
- setStore("popover", "file")
- } else if (slashMatch) {
- slashOnInput(slashMatch[1])
- setStore("popover", "slash")
- } else {
- setStore("popover", null)
- }
- } else {
- setStore("popover", null)
- }
-
- if (store.historyIndex >= 0 && !store.applyingHistory) {
- setStore("historyIndex", -1)
- setStore("savedPrompt", null)
- }
-
- if (!store.applyingHistory) {
- setStore("userHasEdited", true)
- }
-
- prompt.set(rawParts, cursorPosition)
- }
-
- const addPart = (part: ContentPart) => {
- const selection = window.getSelection()
- if (!selection || selection.rangeCount === 0) return
-
- const cursorPosition = getCursorPosition(editorRef)
- const currentPrompt = prompt.current()
- const rawText = currentPrompt.map((p) => ("content" in p ? p.content : "")).join("")
- const textBeforeCursor = rawText.substring(0, cursorPosition)
- const atMatch = textBeforeCursor.match(/@(\S*)$/)
-
- if (part.type === "file") {
- const pill = document.createElement("span")
- pill.textContent = part.content
- pill.setAttribute("data-type", "file")
- pill.setAttribute("data-path", part.path)
- pill.setAttribute("contenteditable", "false")
- pill.style.userSelect = "text"
- pill.style.cursor = "default"
-
- const gap = document.createTextNode(" ")
- const range = selection.getRangeAt(0)
-
- if (atMatch) {
- let runningLength = 0
-
- const walker = document.createTreeWalker(editorRef, NodeFilter.SHOW_TEXT, null)
- let currentNode = walker.nextNode()
- while (currentNode) {
- const textContent = currentNode.textContent || ""
- if (runningLength + textContent.length >= atMatch.index!) {
- const localStart = atMatch.index! - runningLength
- const localEnd = cursorPosition - runningLength
- if (currentNode === range.startContainer || runningLength + textContent.length >= cursorPosition) {
- range.setStart(currentNode, localStart)
- range.setEnd(currentNode, Math.min(localEnd, textContent.length))
- break
- }
- }
- runningLength += textContent.length
- currentNode = walker.nextNode()
- }
- }
-
- range.deleteContents()
- range.insertNode(gap)
- range.insertNode(pill)
- range.setStartAfter(gap)
- range.collapse(true)
- selection.removeAllRanges()
- selection.addRange(range)
- } else if (part.type === "text") {
- const textNode = document.createTextNode(part.content)
- const range = selection.getRangeAt(0)
- range.deleteContents()
- range.insertNode(textNode)
- range.setStartAfter(textNode)
- range.collapse(true)
- selection.removeAllRanges()
- selection.addRange(range)
- }
-
- handleInput()
- setStore("popover", null)
- }
-
- const abort = () =>
- sdk.client.session.abort({
- sessionID: params.id!,
- })
-
- const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
- const text = prompt
- .map((p) => ("content" in p ? p.content : ""))
- .join("")
- .trim()
- if (!text) return
-
- const entry = clonePromptParts(prompt)
- const currentHistory = mode === "shell" ? shellHistory : history
- const setCurrentHistory = mode === "shell" ? setShellHistory : setHistory
- const lastEntry = currentHistory.entries[0]
- if (lastEntry) {
- const lastText = lastEntry.map((p) => ("content" in p ? p.content : "")).join("")
- if (lastText === text) return
- }
-
- setCurrentHistory("entries", (entries) => [entry, ...entries].slice(0, MAX_HISTORY))
- }
-
- const navigateHistory = (direction: "up" | "down") => {
- if (store.userHasEdited) return false
-
- const entries = store.mode === "shell" ? shellHistory.entries : history.entries
- const current = store.historyIndex
-
- if (direction === "up") {
- if (entries.length === 0) return false
- if (current === -1) {
- setStore("savedPrompt", clonePromptParts(prompt.current()))
- setStore("historyIndex", 0)
- applyHistoryPrompt(entries[0], "start")
- return true
- }
- if (current < entries.length - 1) {
- const next = current + 1
- setStore("historyIndex", next)
- applyHistoryPrompt(entries[next], "start")
- return true
- }
- return false
- }
-
- if (current > 0) {
- const next = current - 1
- setStore("historyIndex", next)
- applyHistoryPrompt(entries[next], "end")
- return true
- }
- if (current === 0) {
- setStore("historyIndex", -1)
- const saved = store.savedPrompt
- if (saved) {
- applyHistoryPrompt(saved, "end")
- setStore("savedPrompt", null)
- return true
- }
- applyHistoryPrompt(DEFAULT_PROMPT, "end")
- return true
- }
-
- return false
- }
-
- const handleKeyDown = (event: KeyboardEvent) => {
- if (event.key === "!" && store.mode === "normal") {
- const cursorPosition = getCursorPosition(editorRef)
- if (cursorPosition === 0) {
- setStore("mode", "shell")
- setStore("popover", null)
- event.preventDefault()
- return
- }
- }
- if (store.mode === "shell") {
- const { collapsed, cursorPosition, textLength } = getCaretState()
- if (event.key === "Escape") {
- setStore("mode", "normal")
- event.preventDefault()
- return
- }
- if (event.key === "Backspace" && collapsed && cursorPosition === 0 && textLength === 0) {
- setStore("mode", "normal")
- event.preventDefault()
- return
- }
- }
-
- if (store.popover && (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter")) {
- if (store.popover === "file") {
- onKeyDown(event)
- } else {
- slashOnKeyDown(event)
- }
- event.preventDefault()
- return
- }
-
- if (event.key === "ArrowUp" || event.key === "ArrowDown") {
- if (event.altKey || event.ctrlKey || event.metaKey) return
- const { collapsed } = getCaretState()
- if (!collapsed) return
-
- const cursorPosition = getCursorPosition(editorRef)
- const textLength = promptLength(prompt.current())
- const textContent = editorRef.textContent ?? ""
- const isEmpty = textContent.trim() === "" || textLength <= 1
- const hasNewlines = textContent.includes("\n")
- const inHistory = store.historyIndex >= 0
- const atStart = cursorPosition <= (isEmpty ? 1 : 0)
- const atEnd = cursorPosition >= (isEmpty ? textLength - 1 : textLength)
- const allowUp = isEmpty || atStart || (!hasNewlines && !inHistory) || (inHistory && atEnd)
- const allowDown = isEmpty || atEnd || (!hasNewlines && !inHistory) || (inHistory && atStart)
-
- if (event.key === "ArrowUp") {
- if (!allowUp) return
- if (navigateHistory("up")) {
- event.preventDefault()
- }
- return
- }
-
- if (!allowDown) return
- if (navigateHistory("down")) {
- event.preventDefault()
- }
- return
- }
-
- if (event.key === "Enter" && !event.shiftKey) {
- handleSubmit(event)
- }
- if (event.key === "Escape") {
- if (store.popover) {
- setStore("popover", null)
- } else if (working()) {
- abort()
- }
- }
- }
-
- const handleSubmit = async (event: Event) => {
- event.preventDefault()
- const currentPrompt = prompt.current()
- const text = currentPrompt.map((part) => ("content" in part ? part.content : "")).join("")
- const hasImageAttachments = store.imageAttachments.length > 0
- if (text.trim().length === 0 && !hasImageAttachments) {
- if (working()) abort()
- return
- }
-
- addToHistory(currentPrompt, store.mode)
- setStore("historyIndex", -1)
- setStore("savedPrompt", null)
- setStore("userHasEdited", false)
-
- let existing = info()
- if (!existing) {
- const created = await sdk.client.session.create()
- existing = created.data ?? undefined
- if (existing) navigate(existing.id)
- }
- if (!existing) return
-
- const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path))
- const attachments = currentPrompt.filter(
- (part) => part.type === "file",
- ) as import("@/context/prompt").FileAttachmentPart[]
-
- const fileAttachmentParts = attachments.map((attachment) => {
- const absolute = toAbsolutePath(attachment.path)
- const query = attachment.selection
- ? `?start=${attachment.selection.startLine}&end=${attachment.selection.endLine}`
- : ""
- return {
- id: Identifier.ascending("part"),
- type: "file" as const,
- mime: "text/plain",
- url: `file://${absolute}${query}`,
- filename: getFilename(attachment.path),
- source: {
- type: "file" as const,
- text: {
- value: attachment.content,
- start: attachment.start,
- end: attachment.end,
- },
- path: absolute,
- },
- }
- })
-
- const imageAttachmentParts = store.imageAttachments.map((attachment) => ({
- id: Identifier.ascending("part"),
- type: "file" as const,
- mime: attachment.mime,
- url: attachment.dataUrl,
- filename: attachment.filename,
- }))
-
- const isShellMode = store.mode === "shell"
- tabs().setActive(undefined)
- editorRef.innerHTML = ""
- prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
- setStore("imageAttachments", [])
- setStore("mode", "normal")
-
- const model = {
- modelID: local.model.current()!.id,
- providerID: local.model.current()!.provider.id,
- }
- const agent = local.agent.current()!.name
-
- if (isShellMode) {
- sdk.client.session.shell({
- sessionID: existing.id,
- agent,
- model,
- command: text,
- })
- return
- }
-
- if (text.startsWith("/")) {
- const [cmdName, ...args] = text.split(" ")
- const commandName = cmdName.slice(1)
- const customCommand = sync.data.command.find((c) => c.name === commandName)
- if (customCommand) {
- sdk.client.session.command({
- sessionID: existing.id,
- command: commandName,
- arguments: args.join(" "),
- agent,
- model: `${model.providerID}/${model.modelID}`,
- })
- return
- }
- }
-
- const messageID = Identifier.ascending("message")
- const textPart = {
- id: Identifier.ascending("part"),
- type: "text" as const,
- text,
- }
- const requestParts = [textPart, ...fileAttachmentParts, ...imageAttachmentParts]
- const optimisticParts = requestParts.map((part) => ({
- ...part,
- sessionID: existing.id,
- messageID,
- }))
-
- sync.session.addOptimisticMessage({
- sessionID: existing.id,
- messageID,
- parts: optimisticParts,
- agent,
- model,
- })
-
- sdk.client.session.prompt({
- sessionID: existing.id,
- agent,
- model,
- messageID,
- parts: requestParts,
- })
- }
-
- return (
-
-
-
-
{
- editorRef = el
- props.ref?.(el)
- }}
- contenteditable="true"
- onInput={handleInput}
- onKeyDown={handleKeyDown}
- classList={{
- "w-full px-5 py-3 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
- "[&>[data-type=file]]:text-icon-info-active": true,
- "font-mono!": store.mode === "shell",
- }}
- />
-
-
- {store.mode === "shell"
- ? "Enter shell command..."
- : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
-
-
-
-
-
-
-
-
-
- Shell
- esc to exit
-
-
-
-
- Cycle agent
- {command.keybind("agent.cycle")}
-
- }
- >
-
- }
- >
-
-
-
-
-
-
-
-
{
- const file = e.currentTarget.files?.[0]
- if (file) addImageAttachment(file)
- e.currentTarget.value = ""
- }}
- />
-
-
- fileInputRef.click()}
- />
-
-
-
-
-
- Stop
- ESC
-
-
-
-
- Send
-
-
-
-
- }
- >
-
-
-
-
-
-
- )
-}
-
-function getCursorPosition(parent: HTMLElement): number {
- const selection = window.getSelection()
- if (!selection || selection.rangeCount === 0) return 0
- const range = selection.getRangeAt(0)
- const preCaretRange = range.cloneRange()
- preCaretRange.selectNodeContents(parent)
- preCaretRange.setEnd(range.startContainer, range.startOffset)
- return preCaretRange.toString().length
-}
-
-function setCursorPosition(parent: HTMLElement, position: number) {
- let remaining = position
- let node = parent.firstChild
- while (node) {
- const length = node.textContent ? node.textContent.length : 0
- const isText = node.nodeType === Node.TEXT_NODE
- const isFile = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type === "file"
-
- if (isText && remaining <= length) {
- const range = document.createRange()
- const selection = window.getSelection()
- range.setStart(node, remaining)
- range.collapse(true)
- selection?.removeAllRanges()
- selection?.addRange(range)
- return
- }
-
- if (isFile && remaining <= length) {
- const range = document.createRange()
- const selection = window.getSelection()
- range.setStartAfter(node)
- range.collapse(true)
- selection?.removeAllRanges()
- selection?.addRange(range)
- return
- }
-
- remaining -= length
- node = node.nextSibling
- }
-
- const fallbackRange = document.createRange()
- const fallbackSelection = window.getSelection()
- const last = parent.lastChild
- if (last && last.nodeType === Node.TEXT_NODE) {
- const len = last.textContent ? last.textContent.length : 0
- fallbackRange.setStart(last, len)
- }
- if (!last || last.nodeType !== Node.TEXT_NODE) {
- fallbackRange.selectNodeContents(parent)
- }
- fallbackRange.collapse(false)
- fallbackSelection?.removeAllRanges()
- fallbackSelection?.addRange(fallbackRange)
-}
diff --git a/packages/desktop/src/components/session-context-usage.tsx b/packages/desktop/src/components/session-context-usage.tsx
deleted file mode 100644
index 5474005c7..000000000
--- a/packages/desktop/src/components/session-context-usage.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { createMemo, Show } from "solid-js"
-import { Tooltip } from "@opencode-ai/ui/tooltip"
-import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
-import { useSync } from "@/context/sync"
-import { useParams } from "@solidjs/router"
-import { AssistantMessage } from "@opencode-ai/sdk/v2"
-
-export function SessionContextUsage() {
- const sync = useSync()
- const params = useParams()
- const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
-
- const cost = createMemo(() => {
- const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
- return new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: "USD",
- }).format(total)
- })
-
- const context = createMemo(() => {
- const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage
- if (!last) return
- const total =
- last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
- const model = sync.data.provider.all.find((x) => x.id === last.providerID)?.models[last.modelID]
- return {
- tokens: total.toLocaleString(),
- percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null,
- }
- })
-
- return (
-
- {(ctx) => (
-
-
- Tokens
- {ctx().tokens}
-
-
- Usage
- {ctx().percentage ?? 0}%
-
-
- Cost
- {cost()}
-
-
- }
- placement="top"
- >
-
-
{`${ctx().percentage ?? 0}%`}
-
-
-
- )}
-
- )
-}
diff --git a/packages/desktop/src/components/terminal.tsx b/packages/desktop/src/components/terminal.tsx
deleted file mode 100644
index c05ddfbf6..000000000
--- a/packages/desktop/src/components/terminal.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
-import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
-import { useSDK } from "@/context/sdk"
-import { SerializeAddon } from "@/addons/serialize"
-import { LocalPTY } from "@/context/terminal"
-import { usePrefersDark } from "@solid-primitives/media"
-
-export interface TerminalProps extends ComponentProps<"div"> {
- pty: LocalPTY
- onSubmit?: () => void
- onCleanup?: (pty: LocalPTY) => void
- onConnectError?: (error: unknown) => void
-}
-
-export const Terminal = (props: TerminalProps) => {
- const sdk = useSDK()
- let container!: HTMLDivElement
- const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
- let ws: WebSocket
- let term: Term
- let ghostty: Ghostty
- let serializeAddon: SerializeAddon
- let fitAddon: FitAddon
- let handleResize: () => void
- const prefersDark = usePrefersDark()
-
- onMount(async () => {
- ghostty = await Ghostty.load()
-
- ws = new WebSocket(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
- term = new Term({
- cursorBlink: true,
- fontSize: 14,
- fontFamily: "IBM Plex Mono, monospace",
- allowTransparency: true,
- theme: prefersDark()
- ? {
- background: "#191515",
- foreground: "#d4d4d4",
- cursor: "#d4d4d4",
- }
- : {
- background: "#fcfcfc",
- foreground: "#211e1e",
- cursor: "#211e1e",
- },
- scrollback: 10_000,
- ghostty,
- })
- term.attachCustomKeyEventHandler((event) => {
- // allow for ctrl-` to toggle terminal in parent
- if (event.ctrlKey && event.key.toLowerCase() === "`") {
- event.preventDefault()
- return true
- }
- return false
- })
-
- fitAddon = new FitAddon()
- serializeAddon = new SerializeAddon()
- term.loadAddon(serializeAddon)
- term.loadAddon(fitAddon)
-
- term.open(container)
-
- if (local.pty.buffer) {
- if (local.pty.rows && local.pty.cols) {
- term.resize(local.pty.cols, local.pty.rows)
- }
- term.reset()
- term.write(local.pty.buffer)
- if (local.pty.scrollY) {
- term.scrollToLine(local.pty.scrollY)
- }
- fitAddon.fit()
- }
-
- container.focus()
-
- fitAddon.observeResize()
- handleResize = () => fitAddon.fit()
- window.addEventListener("resize", handleResize)
- term.onResize(async (size) => {
- if (ws && ws.readyState === WebSocket.OPEN) {
- await sdk.client.pty.update({
- ptyID: local.pty.id,
- size: {
- cols: size.cols,
- rows: size.rows,
- },
- })
- }
- })
- term.onData((data) => {
- if (ws && ws.readyState === WebSocket.OPEN) {
- ws.send(data)
- }
- })
- term.onKey((key) => {
- if (key.key == "Enter") {
- props.onSubmit?.()
- }
- })
- // term.onScroll((ydisp) => {
- // console.log("Scroll position:", ydisp)
- // })
- ws.addEventListener("open", () => {
- console.log("WebSocket connected")
- sdk.client.pty.update({
- ptyID: local.pty.id,
- size: {
- cols: term.cols,
- rows: term.rows,
- },
- })
- })
- ws.addEventListener("message", (event) => {
- term.write(event.data)
- })
- ws.addEventListener("error", (error) => {
- console.error("WebSocket error:", error)
- props.onConnectError?.(error)
- })
- ws.addEventListener("close", () => {
- console.log("WebSocket disconnected")
- })
- })
-
- onCleanup(() => {
- if (handleResize) {
- window.removeEventListener("resize", handleResize)
- }
- if (serializeAddon && props.onCleanup) {
- const buffer = serializeAddon.serialize()
- props.onCleanup({
- ...local.pty,
- buffer,
- rows: term.rows,
- cols: term.cols,
- scrollY: term.getViewportY(),
- })
- }
- ws?.close()
- term?.dispose()
- })
-
- return (
-
- )
-}
--
cgit v1.2.3