diff options
| author | Adam <[email protected]> | 2025-10-03 09:04:28 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-10-03 09:04:28 -0500 |
| commit | 3fa280d21878ae391674a21758199df3d2d8c3b5 (patch) | |
| tree | f70c6ecafffeecc8e7a59dc9acef66c59a9ea54a /packages/app/src/context | |
| parent | 1d58b5548287a3e32ffce3abdcf0f2db08fdb155 (diff) | |
| download | opencode-3fa280d21878ae391674a21758199df3d2d8c3b5.tar.gz opencode-3fa280d21878ae391674a21758199df3d2d8c3b5.zip | |
chore: app -> desktop
Diffstat (limited to 'packages/app/src/context')
| -rw-r--r-- | packages/app/src/context/event.tsx | 34 | ||||
| -rw-r--r-- | packages/app/src/context/index.ts | 7 | ||||
| -rw-r--r-- | packages/app/src/context/local.tsx | 537 | ||||
| -rw-r--r-- | packages/app/src/context/marked.tsx | 43 | ||||
| -rw-r--r-- | packages/app/src/context/sdk.tsx | 29 | ||||
| -rw-r--r-- | packages/app/src/context/shiki.tsx | 582 | ||||
| -rw-r--r-- | packages/app/src/context/sync.tsx | 174 | ||||
| -rw-r--r-- | packages/app/src/context/theme.tsx | 92 |
8 files changed, 0 insertions, 1498 deletions
diff --git a/packages/app/src/context/event.tsx b/packages/app/src/context/event.tsx deleted file mode 100644 index a2aa54181..000000000 --- a/packages/app/src/context/event.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { createContext, useContext, type ParentProps } from "solid-js" -import { createEventBus } from "@solid-primitives/event-bus" -import type { Event as SDKEvent } from "@opencode-ai/sdk" -import { useSDK } from "@/context" - -export type Event = SDKEvent // can extend with custom events later - -function init() { - const sdk = useSDK() - const bus = createEventBus<Event>() - sdk.event.subscribe().then(async (events) => { - for await (const event of events.stream) { - bus.emit(event) - } - }) - return bus -} - -type EventContext = ReturnType<typeof init> - -const ctx = createContext<EventContext>() - -export function EventProvider(props: ParentProps) { - const value = init() - return <ctx.Provider value={value}>{props.children}</ctx.Provider> -} - -export function useEvent() { - const value = useContext(ctx) - if (!value) { - throw new Error("useEvent must be used within a EventProvider") - } - return value -} diff --git a/packages/app/src/context/index.ts b/packages/app/src/context/index.ts deleted file mode 100644 index bc4bf3b1d..000000000 --- a/packages/app/src/context/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { EventProvider, useEvent } from "./event" -export { LocalProvider, useLocal } from "./local" -export { MarkedProvider, useMarked } from "./marked" -export { SDKProvider, useSDK } from "./sdk" -export { ShikiProvider, useShiki } from "./shiki" -export { SyncProvider, useSync } from "./sync" -export { ThemeProvider, useTheme } from "./theme" diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx deleted file mode 100644 index 4c2a3d3be..000000000 --- a/packages/app/src/context/local.tsx +++ /dev/null @@ -1,537 +0,0 @@ -import { createStore, produce, reconcile } from "solid-js/store" -import { batch, createContext, createEffect, createMemo, useContext, type ParentProps } from "solid-js" -import { uniqueBy } from "remeda" -import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk" -import { useSDK, useEvent, useSync } from "@/context" - -export type LocalFile = FileNode & - Partial<{ - loaded: boolean - pinned: boolean - expanded: boolean - content: FileContent - selection: { startLine: number; startChar: number; endLine: number; endChar: number } - scrollTop: number - view: "raw" | "diff-unified" | "diff-split" - folded: string[] - selectedChange: number - status: FileStatus - }> -export type TextSelection = LocalFile["selection"] -export type View = LocalFile["view"] - -export type LocalModel = Omit<Model, "provider"> & { - provider: Provider -} -export type ModelKey = { providerID: string; modelID: string } - -export type FileContext = { type: "file"; path: string; selection?: TextSelection } -export type ContextItem = FileContext - -function init() { - const sdk = useSDK() - const sync = useSync() - - const agent = (() => { - const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent")) - const [store, setStore] = createStore<{ - current: string - }>({ - current: list()[0].name, - }) - return { - list, - current() { - return list().find((x) => x.name === store.current)! - }, - set(name: string | undefined) { - setStore("current", name ?? list()[0].name) - }, - move(direction: 1 | -1) { - let next = list().findIndex((x) => x.name === store.current) + direction - if (next < 0) next = list().length - 1 - if (next >= list().length) next = 0 - const value = list()[next] - setStore("current", value.name) - if (value.model) - model.set({ - providerID: value.model.providerID, - modelID: value.model.modelID, - }) - }, - } - })() - - const model = (() => { - const list = createMemo(() => - sync.data.provider.flatMap((p) => Object.values(p.models).map((m) => ({ ...m, provider: p }) as LocalModel)), - ) - const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID) - - const [store, setStore] = createStore<{ - model: Record<string, ModelKey> - recent: ModelKey[] - }>({ - model: {}, - recent: [], - }) - - const value = localStorage.getItem("model") - setStore("recent", JSON.parse(value ?? "[]")) - createEffect(() => { - localStorage.setItem("model", JSON.stringify(store.recent)) - }) - - const fallback = createMemo(() => { - if (store.recent.length) return store.recent[0] - const provider = sync.data.provider[0] - const model = Object.values(provider.models)[0] - return { modelID: model.id, providerID: provider.id } - }) - - const current = createMemo(() => { - const a = agent.current() - return find(store.model[agent.current().name]) ?? find(a.model ?? fallback()) - }) - - const recent = createMemo(() => store.recent.map(find).filter(Boolean)) - - return { - list, - current, - recent, - set(model: ModelKey | undefined, options?: { recent?: boolean }) { - batch(() => { - setStore("model", agent.current().name, model ?? fallback()) - if (options?.recent && model) { - const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID) - if (uniq.length > 5) uniq.pop() - setStore("recent", uniq) - } - }) - }, - } - })() - - const file = (() => { - const [store, setStore] = createStore<{ - node: Record<string, LocalFile> - opened: string[] - active?: string - }>({ - node: Object.fromEntries(sync.data.node.map((x) => [x.path, x])), - opened: [], - }) - - const active = createMemo(() => { - if (!store.active) return undefined - return store.node[store.active] - }) - const opened = createMemo(() => store.opened.map((x) => store.node[x])) - const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path))) - const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b))) - - createEffect((prev: FileStatus[]) => { - const removed = prev.filter((p) => !sync.data.changes.find((c) => c.path === p.path)) - for (const p of removed) { - setStore( - "node", - p.path, - produce((draft) => { - draft.status = undefined - draft.view = "raw" - }), - ) - load(p.path) - } - for (const p of sync.data.changes) { - if (store.node[p.path] === undefined) { - fetch(p.path).then(() => setStore("node", p.path, "status", p)) - } else { - setStore("node", p.path, "status", p) - } - } - return sync.data.changes - }, sync.data.changes) - - const changed = (path: string) => { - const node = store.node[path] - if (node?.status) return true - const set = changeset() - if (set.has(path)) return true - for (const p of set) { - if (p.startsWith(path ? path + "/" : "")) return true - } - return false - } - - const resetNode = (path: string) => { - setStore("node", path, { - loaded: undefined, - pinned: undefined, - content: undefined, - selection: undefined, - scrollTop: undefined, - folded: undefined, - view: undefined, - selectedChange: undefined, - }) - } - - const relative = (path: string) => path.replace(sync.data.path.directory + "/", "") - - const load = async (path: string) => { - const relativePath = relative(path) - sdk.file.read({ query: { path: relativePath } }).then((x) => { - setStore( - "node", - relativePath, - produce((draft) => { - draft.loaded = true - draft.content = x.data - }), - ) - }) - } - - const fetch = async (path: string) => { - const relativePath = relative(path) - const parent = relativePath.split("/").slice(0, -1).join("/") - if (parent) { - await list(parent) - } - } - - const open = async (path: string, options?: { pinned?: boolean; view?: LocalFile["view"] }) => { - const relativePath = relative(path) - if (!store.node[relativePath]) await fetch(path) - setStore("opened", (x) => { - if (x.includes(relativePath)) return x - return [ - ...opened() - .filter((x) => x.pinned) - .map((x) => x.path), - relativePath, - ] - }) - setStore("active", relativePath) - context.addActive() - if (options?.pinned) setStore("node", path, "pinned", true) - if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view) - if (store.node[relativePath].loaded) return - return load(relativePath) - } - - const list = async (path: string) => { - return sdk.file.list({ query: { path: path + "/" } }).then((x) => { - setStore( - "node", - produce((draft) => { - x.data!.forEach((node) => { - if (node.path in draft) return - draft[node.path] = node - }) - }), - ) - }) - } - - const search = (query: string) => sdk.find.files({ query: { query } }).then((x) => x.data!) - - const bus = useEvent() - bus.listen((event) => { - switch (event.type) { - case "message.part.updated": - const part = event.properties.part - if (part.type === "tool" && part.state.status === "completed") { - switch (part.tool) { - case "read": - break - case "edit": - // load(part.state.input["filePath"] as string) - break - default: - break - } - } - break - case "file.watcher.updated": - setTimeout(sync.load.changes, 1000) - const relativePath = relative(event.properties.file) - if (relativePath.startsWith(".git/")) return - load(relativePath) - break - } - }) - - return { - active, - opened, - node: (path: string) => store.node[path], - update: (path: string, node: LocalFile) => setStore("node", path, reconcile(node)), - open, - load, - close(path: string) { - setStore("opened", (opened) => opened.filter((x) => x !== path)) - if (store.active === path) { - const index = store.opened.findIndex((f) => f === path) - const previous = store.opened[Math.max(0, index - 1)] - setStore("active", previous) - } - resetNode(path) - }, - expand(path: string) { - setStore("node", path, "expanded", true) - if (store.node[path].loaded) return - setStore("node", path, "loaded", true) - list(path) - }, - collapse(path: string) { - setStore("node", path, "expanded", false) - }, - select(path: string, selection: TextSelection | undefined) { - setStore("node", path, "selection", selection) - }, - scroll(path: string, scrollTop: number) { - setStore("node", path, "scrollTop", scrollTop) - }, - move(path: string, to: number) { - const index = store.opened.findIndex((f) => f === path) - if (index === -1) return - setStore( - "opened", - produce((opened) => { - opened.splice(to, 0, opened.splice(index, 1)[0]) - }), - ) - setStore("node", path, "pinned", true) - }, - view(path: string): View { - const n = store.node[path] - return n && n.view ? n.view : "raw" - }, - setView(path: string, view: View) { - setStore("node", path, "view", view) - }, - unfold(path: string, key: string) { - setStore("node", path, "folded", (xs) => { - const a = xs ?? [] - if (a.includes(key)) return a - return [...a, key] - }) - }, - fold(path: string, key: string) { - setStore("node", path, "folded", (xs) => (xs ?? []).filter((k) => k !== key)) - }, - folded(path: string) { - const n = store.node[path] - return n && n.folded ? n.folded : [] - }, - changeIndex(path: string) { - return store.node[path]?.selectedChange - }, - setChangeIndex(path: string, index: number | undefined) { - setStore("node", path, "selectedChange", index) - }, - changes, - changed, - children(path: string) { - return Object.values(store.node).filter( - (x) => - x.path.startsWith(path) && - x.path !== path && - !x.path.replace(new RegExp(`^${path + "/"}`), "").includes("/"), - ) - }, - search, - relative, - } - })() - - const layout = (() => { - type PaneState = { size: number; visible: boolean } - type LayoutState = { panes: Record<string, PaneState>; order: string[] } - type PaneDefault = number | { size: number; visible?: boolean } - - const [store, setStore] = createStore<Record<string, LayoutState>>({}) - - const raw = localStorage.getItem("layout") - if (raw) { - const data = JSON.parse(raw) - if (data && typeof data === "object" && !Array.isArray(data)) { - const first = Object.values(data)[0] as LayoutState - if (first && typeof first === "object" && "panes" in first) { - setStore(() => data as Record<string, LayoutState>) - } - } - } - - createEffect(() => { - localStorage.setItem("layout", JSON.stringify(store)) - }) - - const normalize = (value: PaneDefault): PaneState => { - if (typeof value === "number") return { size: value, visible: true } - return { size: value.size, visible: value.visible ?? true } - } - - const ensure = (id: string, defaults: Record<string, PaneDefault>) => { - const entries = Object.entries(defaults) - if (!entries.length) return - setStore(id, (current) => { - if (current) return current - return { - panes: Object.fromEntries(entries.map(([pane, config]) => [pane, normalize(config)])), - order: entries.map(([pane]) => pane), - } - }) - for (const [pane, config] of entries) { - if (!store[id]?.panes[pane]) { - setStore(id, "panes", pane, () => normalize(config)) - } - if (!(store[id]?.order ?? []).includes(pane)) { - setStore(id, "order", (list) => [...list, pane]) - } - } - } - - const ensurePane = (id: string, pane: string, fallback?: PaneDefault) => { - if (!store[id]) { - const value = normalize(fallback ?? { size: 0, visible: true }) - setStore(id, () => ({ - panes: { [pane]: value }, - order: [pane], - })) - return - } - if (!store[id].panes[pane]) { - const value = normalize(fallback ?? { size: 0, visible: true }) - setStore(id, "panes", pane, () => value) - } - if (!store[id].order.includes(pane)) { - setStore(id, "order", (list) => [...list, pane]) - } - } - - const size = (id: string, pane: string) => store[id]?.panes[pane]?.size ?? 0 - const visible = (id: string, pane: string) => store[id]?.panes[pane]?.visible ?? false - - const setSize = (id: string, pane: string, value: number) => { - if (!store[id]?.panes[pane]) return - const next = Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 0 - setStore(id, "panes", pane, "size", next) - } - - const setVisible = (id: string, pane: string, value: boolean) => { - if (!store[id]?.panes[pane]) return - setStore(id, "panes", pane, "visible", value) - } - - const toggle = (id: string, pane: string) => { - setVisible(id, pane, !visible(id, pane)) - } - - const show = (id: string, pane: string) => setVisible(id, pane, true) - const hide = (id: string, pane: string) => setVisible(id, pane, false) - const order = (id: string) => store[id]?.order ?? [] - - return { - ensure, - ensurePane, - size, - visible, - setSize, - setVisible, - toggle, - show, - hide, - order, - } - })() - - const session = (() => { - const [store, setStore] = createStore<{ - active?: string - }>({}) - - const active = createMemo(() => { - if (!store.active) return undefined - return sync.session.get(store.active) - }) - - return { - active, - setActive(sessionId: string | undefined) { - setStore("active", sessionId) - }, - clearActive() { - setStore("active", undefined) - }, - } - })() - - const context = (() => { - const [store, setStore] = createStore<{ - activeTab: boolean - items: (ContextItem & { key: string })[] - }>({ - activeTab: true, - items: [], - }) - - return { - all() { - return store.items - }, - active() { - return store.activeTab ? file.active() : undefined - }, - addActive() { - setStore("activeTab", true) - }, - removeActive() { - setStore("activeTab", false) - }, - add(item: ContextItem) { - let key = item.type - switch (item.type) { - case "file": - key += `${item.path}:${item.selection?.startLine}:${item.selection?.endLine}` - break - } - if (store.items.find((x) => x.key === key)) return - setStore("items", (x) => [...x, { key, ...item }]) - }, - remove(key: string) { - setStore("items", (x) => x.filter((x) => x.key !== key)) - }, - } - })() - - const result = { - model, - agent, - file, - layout, - session, - context, - } - return result -} - -type LocalContext = ReturnType<typeof init> - -const ctx = createContext<LocalContext>() - -export function LocalProvider(props: ParentProps) { - const value = init() - return <ctx.Provider value={value}>{props.children}</ctx.Provider> -} - -export function useLocal() { - const value = useContext(ctx) - if (!value) { - throw new Error("useLocal must be used within a LocalProvider") - } - return value -} diff --git a/packages/app/src/context/marked.tsx b/packages/app/src/context/marked.tsx deleted file mode 100644 index 550a0456a..000000000 --- a/packages/app/src/context/marked.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { createContext, useContext, type ParentProps } from "solid-js" -import { useShiki } from "@/context" -import { marked } from "marked" -import markedShiki from "marked-shiki" -import { bundledLanguages, type BundledLanguage } from "shiki" - -function init(highlighter: ReturnType<typeof useShiki>) { - return marked.use( - markedShiki({ - async highlight(code, lang) { - if (!(lang in bundledLanguages)) { - lang = "text" - } - if (!highlighter.getLoadedLanguages().includes(lang)) { - await highlighter.loadLanguage(lang as BundledLanguage) - } - return highlighter.codeToHtml(code, { - lang: lang || "text", - theme: "opencode", - tabindex: false, - }) - }, - }), - ) -} - -type MarkedContext = ReturnType<typeof init> - -const ctx = createContext<MarkedContext>() - -export function MarkedProvider(props: ParentProps) { - const highlighter = useShiki() - const value = init(highlighter) - return <ctx.Provider value={value}>{props.children}</ctx.Provider> -} - -export function useMarked() { - const value = useContext(ctx) - if (!value) { - throw new Error("useMarked must be used within a MarkedProvider") - } - return value -} diff --git a/packages/app/src/context/sdk.tsx b/packages/app/src/context/sdk.tsx deleted file mode 100644 index 48595cf9d..000000000 --- a/packages/app/src/context/sdk.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { createContext, useContext, type ParentProps } from "solid-js" -import { createOpencodeClient } from "@opencode-ai/sdk/client" - -const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1" -const port = import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096" - -function init() { - const client = createOpencodeClient({ - baseUrl: `http://${host}:${port}`, - }) - return client -} - -type SDKContext = ReturnType<typeof init> - -const ctx = createContext<SDKContext>() - -export function SDKProvider(props: ParentProps) { - const value = init() - return <ctx.Provider value={value}>{props.children}</ctx.Provider> -} - -export function useSDK() { - const value = useContext(ctx) - if (!value) { - throw new Error("useSDK must be used within a SDKProvider") - } - return value -} diff --git a/packages/app/src/context/shiki.tsx b/packages/app/src/context/shiki.tsx deleted file mode 100644 index 1930b907c..000000000 --- a/packages/app/src/context/shiki.tsx +++ /dev/null @@ -1,582 +0,0 @@ -import { createHighlighter, type ThemeInput } from "shiki" -import { createContext, useContext, type ParentProps } from "solid-js" - -const theme: ThemeInput = { - colors: { - "actionBar.toggledBackground": "var(--theme-background-element)", - "activityBarBadge.background": "var(--theme-accent)", - "checkbox.border": "var(--theme-border)", - "editor.background": "transparent", - "editor.foreground": "var(--theme-text)", - "editor.inactiveSelectionBackground": "var(--theme-background-element)", - "editor.selectionHighlightBackground": "var(--theme-border-active)", - "editorIndentGuide.activeBackground1": "var(--theme-border-subtle)", - "editorIndentGuide.background1": "var(--theme-border-subtle)", - "input.placeholderForeground": "var(--theme-text-muted)", - "list.activeSelectionIconForeground": "var(--theme-text)", - "list.dropBackground": "var(--theme-background-element)", - "menu.background": "var(--theme-background-panel)", - "menu.border": "var(--theme-border)", - "menu.foreground": "var(--theme-text)", - "menu.selectionBackground": "var(--theme-primary)", - "menu.separatorBackground": "var(--theme-border)", - "ports.iconRunningProcessForeground": "var(--theme-success)", - "sideBarSectionHeader.background": "transparent", - "sideBarSectionHeader.border": "var(--theme-border-subtle)", - "sideBarTitle.foreground": "var(--theme-text-muted)", - "statusBarItem.remoteBackground": "var(--theme-success)", - "statusBarItem.remoteForeground": "var(--theme-text)", - "tab.lastPinnedBorder": "var(--theme-border-subtle)", - "tab.selectedBackground": "var(--theme-background-element)", - "tab.selectedForeground": "var(--theme-text-muted)", - "terminal.inactiveSelectionBackground": "var(--theme-background-element)", - "widget.border": "var(--theme-border)", - }, - displayName: "opencode", - name: "opencode", - semanticHighlighting: true, - semanticTokenColors: { - customLiteral: "var(--theme-syntax-function)", - newOperator: "var(--theme-syntax-operator)", - numberLiteral: "var(--theme-syntax-number)", - stringLiteral: "var(--theme-syntax-string)", - }, - tokenColors: [ - { - scope: [ - "meta.embedded", - "source.groovy.embedded", - "string meta.image.inline.markdown", - "variable.legacy.builtin.python", - ], - settings: { - foreground: "var(--theme-text)", - }, - }, - { - scope: "emphasis", - settings: { - fontStyle: "italic", - }, - }, - { - scope: "strong", - settings: { - fontStyle: "bold", - }, - }, - { - scope: "header", - settings: { - foreground: "var(--theme-markdown-heading)", - }, - }, - { - scope: "comment", - settings: { - foreground: "var(--theme-syntax-comment)", - }, - }, - { - scope: "constant.language", - settings: { - foreground: "var(--theme-syntax-keyword)", - }, - }, - { - scope: [ - "constant.numeric", - "variable.other.enummember", - "keyword.operator.plus.exponent", - "keyword.operator.minus.exponent", - ], - settings: { - foreground: "var(--theme-syntax-number)", - }, - }, - { - scope: "constant.regexp", - settings: { - foreground: "var(--theme-syntax-operator)", - }, - }, - { - scope: "entity.name.tag", - settings: { - foreground: "var(--theme-syntax-keyword)", - }, - }, - { - scope: ["entity.name.tag.css", "entity.name.tag.less"], - settings: { - foreground: "var(--theme-syntax-operator)", - }, - }, - { - scope: "entity.other.attribute-name", - settings: { - foreground: "var(--theme-syntax-variable)", - }, - }, - { - scope: [ - "entity.other.attribute-name.class.css", - "source.css entity.other.attribute-name.class", - "entity.other.attribute-name.id.css", - "entity.other.attribute-name.parent-selector.css", - "entity.other.attribute-name.parent.less", - "source.css entity.other.attribute-name.pseudo-class", - "entity.other.attribute-name.pseudo-element.css", - "source.css.less entity.other.attribute-name.id", - "entity.other.attribute-name.scss", - ], - settings: { - foreground: "var(--theme-syntax-operator)", - }, - }, - { - scope: "invalid", - settings: { - foreground: "var(--theme-error)", - }, - }, - { - scope: "markup.underline", - settings: { - fontStyle: "underline", - }, - }, - { - scope: "markup.bold", - settings: { - fontStyle: "bold", - foreground: "var(--theme-markdown-strong)", - }, - }, - { - scope: "markup.heading", - settings: { - fontStyle: "bold", - foreground: "var(--theme-markdown-heading)", - }, - }, - { - scope: "markup.italic", - settings: { - fontStyle: "italic", - }, - }, - { - scope: "markup.strikethrough", - settings: { - fontStyle: "strikethrough", - }, - }, - { - scope: "markup.inserted", - settings: { - foreground: "var(--theme-diff-added)", - }, - }, - { - scope: "markup.deleted", - settings: { - foreground: "var(--theme-diff-removed)", - }, - }, - { - scope: "markup.changed", - settings: { - foreground: "var(--theme-diff-context)", - }, - }, - { - scope: "punctuation.definition.quote.begin.markdown", - settings: { - foreground: "var(--theme-markdown-block-quote)", - }, - }, - { - scope: "punctuation.definition.list.begin.markdown", - settings: { - foreground: "var(--theme-markdown-list-enumeration)", - }, - }, - { - scope: "markup.inline.raw", - settings: { - foreground: "var(--theme-markdown-code)", - }, - }, - { - scope: "punctuation.definition.tag", - settings: { - foreground: "var(--theme-syntax-punctuation)", - }, - }, - { - scope: ["meta.preprocessor", "entity.name.function.preprocessor"], - settings: { - foreground: "var(--theme-syntax-keyword)", - }, - }, - { - scope: "meta.preprocessor.string", - settings: { - foreground: "var(--theme-syntax-string)", - }, - }, - { - scope: "meta.preprocessor.numeric", - settings: { - foreground: "var(--theme-syntax-number)", - }, - }, - { - scope: "meta.structure.dictionary.key.python", - settings: { - foreground: "var(--theme-syntax-variable)", - }, - }, - { - scope: "meta.diff.header", - settings: { - foreground: "var(--theme-diff-hunk-header)", - }, - }, - { - scope: "storage", - settings: { - foreground: "var(--theme-syntax-keyword)", - }, - }, - { - scope: "storage.type", - settings: { - foreground: "var(--theme-syntax-keyword)", - }, - }, - { - scope: ["storage.modifier", "keyword.operator.noexcept"], - settings: { - foreground: "var(--theme-syntax-keyword)", - }, - }, - { - scope: ["string", "meta.embedded.assembly"], - settings: { - foreground: "var(--theme-syntax-string)", - }, - }, - { - scope: "string.tag", - settings: { - foreground: "var(--theme-syntax-string)", - }, - }, - { - scope: "string.value", - settings: { - foreground: "var(--theme-syntax-string)", - }, - }, - { - scope: "string.regexp", - settings: { - foreground: "var(--theme-syntax-operator)", - }, - }, - { - scope: [ - "punctuation.definition.template-expression.begin", - "punctuation.definition.template-expression.end", - "punctuation.section.embedded", - ], - settings: { - foreground: "var(--theme-syntax-keyword)", - }, - }, - { - scope: ["meta.template.expression"], - settings: { - foreground: "var(--theme-text)", - }, - }, - { - scope: [ - "support.type.vendored.property-name", - "support.type.property-name", - "source.css variable", - "source.coffee.embedded", - ], - settings: { - foreground: "var(--theme-syntax-variable)", - }, - }, - { - scope: "keyword", - settings: { - foreground: "var(--theme-syntax-keyword)", - }, - }, - { - scope: "keyword.control", - settings: { - foreground: "var(--theme-syntax-keyword)", - }, - }, - { - scope: "keyword.operator", - settings: { - foreground: "var(--theme-syntax-operator)", - }, - }, - { - scope: [ - "keyword.operator.new", - "keyword.operator.expression", - "keyword.operator.cast", - "keyword.operator.sizeof", - "keyword.operator.alignof", - "keyword.operator.typeid", - "keyword.operator.alignas", - "keyword.operator.instanceof", - "keyword.operator.logical.python", - "keyword.operator.wordlike", - ], - settings: { - foreground: "var(--theme-syntax-keyword)", - }, - }, - { - scope: "keyword.other.unit", - settings: { - foreground: "var(--theme-syntax-number)", - }, - }, - { - scope: ["punctuation.section.embedded.begin.php", "punctuation.section.embedded.end.php"], - settings: { - foreground: "var(--theme-syntax-keyword)", - }, - }, - { - scope: "support.function.git-rebase", - settings: { - foreground: "var(--theme-syntax-variable)", - }, - }, - { - scope: "constant.sha.git-rebase", - settings: { - foreground: "var(--theme-syntax-number)", - }, - }, - { - scope: ["storage.modifier.import.java", "variable.language.wildcard.java", "storage.modifier.package.java"], - settings: { - foreground: "var(--theme-text)", - }, - }, - { - scope: "variable.language", - settings: { - foreground: "var(--theme-syntax-keyword)", - }, - }, - { - scope: [ - "entity.name.function", - "support.function", - "support.constant.handlebars", - "source.powershell variable.other.member", - "entity.name.operator.custom-literal", - ], - settings: { - foreground: "var(--theme-syntax-function)", - }, - }, - { - scope: [ - "support.class", - "support.type", - "entity.name.type", - "entity.name.namespace", - "entity.other.attribute", - "entity.name.scope-resolution", - "entity.name.class", - "storage.type.numeric.go", - "storage.type.byte.go", - "storage.type.boolean.go", - "storage.type.string.go", - "storage.type.uintptr.go", - "storage.type.error.go", - "storage.type.rune.go", - "storage.type.cs", - "storage.type.generic.cs", - "storage.type.modifier.cs", - "storage.type.variable.cs", - "storage.type.annotation.java", - "storage.type.generic.java", - "storage.type.java", - "storage.type.object.array.java", - "storage.type.primitive.array.java", - "storage.type.primitive.java", - "storage.type.token.java", - "storage.type.groovy", - "storage.type.annotation.groovy", - "storage.type.parameters.groovy", - "storage.type.generic.groovy", - "storage.type.object.array.groovy", - "storage.type.primitive.array.groovy", - "storage.type.primitive.groovy", - ], - settings: { - foreground: "var(--theme-syntax-type)", - }, - }, - { - scope: [ - "meta.type.cast.expr", - "meta.type.new.expr", - "support.constant.math", - "support.constant.dom", - "support.constant.json", - "entity.other.inherited-class", - "punctuation.separator.namespace.ruby", - ], - settings: { - foreground: "var(--theme-syntax-type)", - }, - }, - { - scope: [ - "keyword.control", - "source.cpp keyword.operator.new", - "keyword.operator.delete", - "keyword.other.using", - "keyword.other.directive.using", - "keyword.other.operator", - "entity.name.operator", - ], - settings: { - foreground: "var(--theme-syntax-operator)", - }, - }, - { - scope: [ - "variable", - "meta.definition.variable.name", - "support.variable", - "entity.name.variable", - "constant.other.placeholder", - ], - settings: { - foreground: "var(--theme-syntax-variable)", - }, - }, - { - scope: ["variable.other.constant", "variable.other.enummember"], - settings: { - foreground: "var(--theme-syntax-variable)", - }, - }, - { - scope: ["meta.object-literal.key"], - settings: { - foreground: "var(--theme-syntax-variable)", - }, - }, - { - scope: [ - "support.constant.property-value", - "support.constant.font-name", - "support.constant.media-type", - "support.constant.media", - "constant.other.color.rgb-value", - "constant.other.rgb-value", - "support.constant.color", - ], - settings: { - foreground: "var(--theme-syntax-string)", - }, - }, - { - scope: [ - "punctuation.definition.group.regexp", - "punctuation.definition.group.assertion.regexp", - "punctuation.definition.character-class.regexp", - "punctuation.character.set.begin.regexp", - "punctuation.character.set.end.regexp", - "keyword.operator.negation.regexp", - "support.other.parenthesis.regexp", - ], - settings: { - foreground: "var(--theme-syntax-string)", - }, - }, - { - scope: [ - "constant.character.character-class.regexp", - "constant.other.character-class.set.regexp", - "constant.other.character-class.regexp", - "constant.character.set.regexp", - ], - settings: { - foreground: "var(--theme-syntax-operator)", - }, - }, - { - scope: ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"], - settings: { - foreground: "var(--theme-syntax-operator)", - }, - }, - { - scope: "keyword.operator.quantifier.regexp", - settings: { - foreground: "var(--theme-syntax-operator)", - }, - }, - { - scope: ["constant.character", "constant.other.option"], - settings: { - foreground: "var(--theme-syntax-keyword)", - }, - }, - { - scope: "constant.character.escape", - settings: { - foreground: "var(--theme-syntax-operator)", - }, - }, - { - scope: "entity.name.label", - settings: { - foreground: "var(--theme-text-muted)", - }, - }, - ], - type: "dark", -} - -const highlighter = await createHighlighter({ - themes: [theme], - langs: [], -}) - -type ShikiContext = typeof highlighter - -const ctx = createContext<ShikiContext>() - -export function ShikiProvider(props: ParentProps) { - return <ctx.Provider value={highlighter}>{props.children}</ctx.Provider> -} - -export function useShiki() { - const value = useContext(ctx) - if (!value) { - throw new Error("useShiki must be used within a ShikiProvider") - } - return value -} diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx deleted file mode 100644 index 5d64f3ee7..000000000 --- a/packages/app/src/context/sync.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import type { Message, Agent, Provider, Session, Part, Config, Path, File, FileNode } from "@opencode-ai/sdk" -import { createStore, produce, reconcile } from "solid-js/store" -import { createContext, createMemo, Show, useContext, type ParentProps } from "solid-js" -import { useSDK, useEvent } from "@/context" -import { Binary } from "@/utils/binary" - -function init() { - const [store, setStore] = createStore<{ - ready: boolean - provider: Provider[] - agent: Agent[] - config: Config - path: Path - session: Session[] - message: { - [sessionID: string]: Message[] - } - part: { - [messageID: string]: Part[] - } - node: FileNode[] - changes: File[] - }>({ - config: {}, - path: { state: "", config: "", worktree: "", directory: "" }, - ready: false, - agent: [], - provider: [], - session: [], - message: {}, - part: {}, - node: [], - changes: [], - }) - - const bus = useEvent() - bus.listen((event) => { - switch (event.type) { - case "session.updated": { - const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) - if (result.found) { - setStore("session", result.index, reconcile(event.properties.info)) - break - } - setStore( - "session", - produce((draft) => { - draft.splice(result.index, 0, event.properties.info) - }), - ) - break - } - case "message.updated": { - const messages = store.message[event.properties.info.sessionID] - if (!messages) { - setStore("message", event.properties.info.sessionID, [event.properties.info]) - break - } - const result = Binary.search(messages, event.properties.info.id, (m) => m.id) - if (result.found) { - setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info)) - break - } - setStore( - "message", - event.properties.info.sessionID, - produce((draft) => { - draft.splice(result.index, 0, event.properties.info) - }), - ) - break - } - case "message.part.updated": { - const parts = store.part[event.properties.part.messageID] - if (!parts) { - setStore("part", event.properties.part.messageID, [event.properties.part]) - break - } - const result = Binary.search(parts, event.properties.part.id, (p) => p.id) - if (result.found) { - setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part)) - break - } - setStore( - "part", - event.properties.part.messageID, - produce((draft) => { - draft.splice(result.index, 0, event.properties.part) - }), - ) - break - } - } - }) - - const sdk = useSDK() - - const load = { - provider: () => sdk.config.providers().then((x) => setStore("provider", x.data!.providers)), - path: () => sdk.path.get().then((x) => setStore("path", x.data!)), - agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])), - session: () => - sdk.session.list().then((x) => - setStore( - "session", - (x.data ?? []).slice().sort((a, b) => a.id.localeCompare(b.id)), - ), - ), - config: () => sdk.config.get().then((x) => setStore("config", x.data!)), - changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)), - node: () => sdk.file.list({ query: { path: "/" } }).then((x) => setStore("node", x.data!)), - } - - Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true)) - - const sanitizer = createMemo(() => new RegExp(`${store.path.directory}/`, "g")) - const sanitize = (text: string) => text.replace(sanitizer(), "") - const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/") - - return { - data: store, - set: setStore, - session: { - get(sessionID: string) { - const match = Binary.search(store.session, sessionID, (s) => s.id) - if (match.found) return store.session[match.index] - return undefined - }, - async sync(sessionID: string) { - const [session, messages] = await Promise.all([ - sdk.session.get({ path: { id: sessionID } }), - sdk.session.messages({ path: { id: sessionID } }), - ]) - setStore( - produce((draft) => { - const match = Binary.search(draft.session, sessionID, (s) => s.id) - draft.session[match.index] = session.data! - draft.message[sessionID] = messages - .data!.map((x) => x.info) - .slice() - .sort((a, b) => a.id.localeCompare(b.id)) - for (const message of messages.data!) { - draft.part[message.info.id] = message.parts.slice().sort((a, b) => a.id.localeCompare(b.id)) - } - }), - ) - }, - }, - load, - absolute, - sanitize, - } -} - -type SyncContext = ReturnType<typeof init> - -const ctx = createContext<SyncContext>() - -export function SyncProvider(props: ParentProps) { - const value = init() - return ( - <Show when={value.data.ready}> - <ctx.Provider value={value}>{props.children}</ctx.Provider> - </Show> - ) -} - -export function useSync() { - const value = useContext(ctx) - if (!value) { - throw new Error("useSync must be used within a SyncProvider") - } - return value -} diff --git a/packages/app/src/context/theme.tsx b/packages/app/src/context/theme.tsx deleted file mode 100644 index 0b344ea97..000000000 --- a/packages/app/src/context/theme.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { - createContext, - useContext, - createSignal, - createEffect, - onMount, - type ParentComponent, - onCleanup, -} from "solid-js" - -export interface ThemeContextValue { - theme: string | undefined - isDark: boolean - setTheme: (themeName: string) => void - setDarkMode: (isDark: boolean) => void -} - -const ThemeContext = createContext<ThemeContextValue>() - -export const useTheme = () => { - const context = useContext(ThemeContext) - if (!context) { - throw new Error("useTheme must be used within a ThemeProvider") - } - return context -} - -interface ThemeProviderProps { - defaultTheme?: string - defaultDarkMode?: boolean -} - -const themes = ["opencode", "tokyonight", "ayu", "nord", "catppuccin"] - -export const ThemeProvider: ParentComponent<ThemeProviderProps> = (props) => { - const [theme, setThemeSignal] = createSignal<string | undefined>() - const [isDark, setIsDark] = createSignal(props.defaultDarkMode ?? false) - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === "t" && event.ctrlKey) { - event.preventDefault() - const current = theme() - if (!current) return - const index = themes.indexOf(current) - const next = themes[(index + 1) % themes.length] - setTheme(next) - } - } - - onMount(() => { - window.addEventListener("keydown", handleKeyDown) - }) - - onCleanup(() => { - window.removeEventListener("keydown", handleKeyDown) - }) - - onMount(() => { - const savedTheme = localStorage.getItem("theme") ?? "opencode" - const savedDarkMode = localStorage.getItem("darkMode") ?? "true" - setIsDark(savedDarkMode === "true") - setTheme(savedTheme) - }) - - createEffect(() => { - const currentTheme = theme() - const darkMode = isDark() - if (currentTheme) { - document.documentElement.setAttribute("data-theme", currentTheme) - document.documentElement.setAttribute("data-dark", darkMode.toString()) - } - }) - - const setTheme = async (theme: string) => { - setThemeSignal(theme) - localStorage.setItem("theme", theme) - } - - const setDarkMode = (dark: boolean) => { - setIsDark(dark) - localStorage.setItem("darkMode", dark.toString()) - } - - const contextValue: ThemeContextValue = { - theme: theme(), - isDark: isDark(), - setTheme, - setDarkMode, - } - - return <ThemeContext.Provider value={contextValue}>{props.children}</ThemeContext.Provider> -} |
