Merge branch 'dev' of https://github.com/sst/opencode into dev

This commit is contained in:
David Hill
2025-09-23 18:36:27 +01:00
21 changed files with 1129 additions and 3980 deletions

View File

@@ -87,3 +87,4 @@
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) | | 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) | | 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) | | 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |

View File

@@ -19,6 +19,7 @@
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@shikijs/transformers": "3.9.2", "@shikijs/transformers": "3.9.2",
"@solid-primitives/event-bus": "1.1.2",
"@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3", "@solid-primitives/scroll": "2.1.3",
"@solidjs/router": "0.15.3", "@solidjs/router": "0.15.3",
@@ -996,6 +997,8 @@
"@smithy/util-utf8": ["@smithy/util-utf8@4.1.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ=="], "@smithy/util-utf8": ["@smithy/util-utf8@4.1.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ=="],
"@solid-primitives/event-bus": ["@solid-primitives/event-bus@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="],
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="], "@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="],
"@solid-primitives/keyed": ["@solid-primitives/keyed@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-BgoEdqPw48URnI+L5sZIHdF4ua4Las1eWEBBPaoSFs42kkhnHue+rwCBPL2Z9ebOyQ75sUhUfOETdJfmv0D6Kg=="], "@solid-primitives/keyed": ["@solid-primitives/keyed@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-BgoEdqPw48URnI+L5sZIHdF4ua4Las1eWEBBPaoSFs42kkhnHue+rwCBPL2Z9ebOyQ75sUhUfOETdJfmv0D6Kg=="],

1
packages/app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
src/assets/theme.css

View File

@@ -24,6 +24,7 @@
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@shikijs/transformers": "3.9.2", "@shikijs/transformers": "3.9.2",
"@solid-primitives/event-bus": "1.1.2",
"@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3", "@solid-primitives/scroll": "2.1.3",
"@solidjs/router": "0.15.3", "@solidjs/router": "0.15.3",

View File

@@ -37,7 +37,7 @@ class ColorResolver {
if (typeof value === "string") { if (typeof value === "string") {
if (value === "none") return { dark: value, light: value } if (value === "none") return { dark: value, light: value }
if (value.startsWith("#")) { if (value.startsWith("#")) {
return { dark: value.toUpperCase(), light: value.toUpperCase() } return { dark: value.toLowerCase(), light: value.toLowerCase() }
} }
const resolved = this.resolveReference(value) const resolved = this.resolveReference(value)
return { dark: resolved, light: resolved } return { dark: resolved, light: resolved }
@@ -57,7 +57,7 @@ class ColorResolver {
if (typeof value === "string") { if (typeof value === "string") {
if (value === "none") return value if (value === "none") return value
if (value.startsWith("#")) { if (value.startsWith("#")) {
return value.toUpperCase() return value.toLowerCase()
} }
return this.resolveReference(value) return this.resolveReference(value)
} }
@@ -72,7 +72,7 @@ class ColorResolver {
if (typeof colorValue === "string") { if (typeof colorValue === "string") {
if (colorValue === "none") return colorValue if (colorValue === "none") return colorValue
if (colorValue.startsWith("#")) { if (colorValue.startsWith("#")) {
return colorValue.toUpperCase() return colorValue.toLowerCase()
} }
return this.resolveReference(colorValue) return this.resolveReference(colorValue)
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,584 +1,6 @@
import { transformerNotationDiff } from "@shikijs/transformers" import { useMarked } from "@/context"
import { marked } from "marked"
import markedShiki from "marked-shiki"
import { codeToHtml } from "shiki"
import { createResource } from "solid-js" import { createResource } from "solid-js"
const markedWithShiki = marked.use(
markedShiki({
highlight(code, lang) {
return codeToHtml(code, {
// structure: "inline",
lang: lang || "text",
tabindex: false,
theme: {
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",
},
transformers: [transformerNotationDiff()],
})
},
}),
)
function strip(text: string): string { function strip(text: string): string {
const wrappedRe = /^\s*<([A-Za-z]\w*)>\s*([\s\S]*?)\s*<\/\1>\s*$/ const wrappedRe = /^\s*<([A-Za-z]\w*)>\s*([\s\S]*?)\s*<\/\1>\s*$/
const match = text.match(wrappedRe) const match = text.match(wrappedRe)
@@ -586,10 +8,11 @@ function strip(text: string): string {
} }
export default function Markdown(props: { text: string; class?: string }) { export default function Markdown(props: { text: string; class?: string }) {
const marked = useMarked()
const [html] = createResource( const [html] = createResource(
() => strip(props.text), () => strip(props.text),
async (markdown) => { async (markdown) => {
return markedWithShiki.parse(markdown) return marked.parse(markdown)
}, },
) )
return ( return (

View File

@@ -0,0 +1,34 @@
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
}

View File

@@ -1,4 +1,7 @@
export { EventProvider, useEvent } from "./event"
export { LocalProvider, useLocal } from "./local" export { LocalProvider, useLocal } from "./local"
export { MarkedProvider, useMarked } from "./marked"
export { SDKProvider, useSDK } from "./sdk" export { SDKProvider, useSDK } from "./sdk"
export { ShikiProvider, useShiki } from "./shiki"
export { SyncProvider, useSync } from "./sync" export { SyncProvider, useSync } from "./sync"
export { ThemeProvider, useTheme } from "./theme" export { ThemeProvider, useTheme } from "./theme"

View File

@@ -1,9 +1,8 @@
import { createStore, produce, reconcile } from "solid-js/store" import { createStore, produce, reconcile } from "solid-js/store"
import { batch, createContext, createEffect, createMemo, useContext, type ParentProps } from "solid-js" import { batch, createContext, createEffect, createMemo, useContext, type ParentProps } from "solid-js"
import { useSync } from "./sync"
import { uniqueBy } from "remeda" import { uniqueBy } from "remeda"
import type { FileContent, FileNode } from "@opencode-ai/sdk" import type { FileContent, FileNode } from "@opencode-ai/sdk"
import { useSDK } from "./sdk" import { useSDK, useEvent, useSync } from "@/context"
export type LocalFile = FileNode & export type LocalFile = FileNode &
Partial<{ Partial<{
@@ -165,17 +164,19 @@ function init() {
}) })
} }
const load = async (path: string) => const load = async (path: string) => {
sdk.file.read({ query: { path } }).then((x) => { const relative = path.replace(sync.data.path.directory + "/", "")
sdk.file.read({ query: { path: relative } }).then((x) => {
setStore( setStore(
"node", "node",
path, relative,
produce((draft) => { produce((draft) => {
draft.loaded = true draft.loaded = true
draft.content = x.data draft.content = x.data
}), }),
) )
}) })
}
const open = async (path: string) => { const open = async (path: string) => {
const relative = path.replace(sync.data.path.directory + "/", "") const relative = path.replace(sync.data.path.directory + "/", "")
@@ -213,8 +214,8 @@ function init() {
}) })
} }
sdk.event.subscribe().then(async (events) => { const bus = useEvent()
for await (const event of events.stream) { bus.listen((event) => {
switch (event.type) { switch (event.type) {
case "message.part.updated": case "message.part.updated":
const part = event.properties.part const part = event.properties.part
@@ -224,16 +225,16 @@ function init() {
console.log("read", part.state.input) console.log("read", part.state.input)
break break
case "edit": case "edit":
const absolute = part.state.input["filePath"] as string load(part.state.input["filePath"] as string)
const path = absolute.replace(sync.data.path.directory + "/", "")
load(path)
break break
default: default:
break break
} }
} }
break break
} case "file.watcher.updated":
load(event.properties.file)
break
} }
}) })

View File

@@ -0,0 +1,40 @@
import { createContext, useContext, type ParentProps } from "solid-js"
import { useShiki } from "@/context"
import { marked } from "marked"
import markedShiki from "marked-shiki"
import type { BundledLanguage } from "shiki"
function init(highlighter: ReturnType<typeof useShiki>) {
return marked.use(
markedShiki({
async highlight(code, lang) {
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
}

View File

@@ -0,0 +1,582 @@
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
}

View File

@@ -1,7 +1,7 @@
import type { Message, Agent, Provider, Session, Part, Config, Path, File, FileNode } from "@opencode-ai/sdk" import type { Message, Agent, Provider, Session, Part, Config, Path, File, FileNode } from "@opencode-ai/sdk"
import { createStore, produce, reconcile } from "solid-js/store" import { createStore, produce, reconcile } from "solid-js/store"
import { useSDK } from "./sdk"
import { createContext, Show, useContext, type ParentProps } from "solid-js" import { createContext, Show, useContext, type ParentProps } from "solid-js"
import { useSDK, useEvent } from "@/context"
import { Binary } from "@/utils/binary" import { Binary } from "@/utils/binary"
function init() { function init() {
@@ -33,10 +33,8 @@ function init() {
changes: [], changes: [],
}) })
const sdk = useSDK() const bus = useEvent()
bus.listen((event) => {
sdk.event.subscribe().then(async (events) => {
for await (const event of events.stream) {
switch (event.type) { switch (event.type) {
case "session.updated": { case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
@@ -93,9 +91,9 @@ function init() {
break break
} }
} }
}
}) })
const sdk = useSDK()
Promise.all([ Promise.all([
sdk.config.providers().then((x) => setStore("provider", x.data!.providers)), sdk.config.providers().then((x) => setStore("provider", x.data!.providers)),
sdk.path.get().then((x) => setStore("path", x.data!)), sdk.path.get().then((x) => setStore("path", x.data!)),

View File

@@ -4,7 +4,15 @@ import { Router, Route } from "@solidjs/router"
import "@/index.css" import "@/index.css"
import Layout from "@/pages/layout" import Layout from "@/pages/layout"
import Home from "@/pages" import Home from "@/pages"
import { SDKProvider, SyncProvider, LocalProvider, ThemeProvider } from "@/context" import {
EventProvider,
SDKProvider,
SyncProvider,
LocalProvider,
ThemeProvider,
ShikiProvider,
MarkedProvider,
} from "@/context"
const root = document.getElementById("root") const root = document.getElementById("root")
@@ -18,7 +26,10 @@ render(
() => ( () => (
<div class="h-full bg-background text-text-muted"> <div class="h-full bg-background text-text-muted">
<ThemeProvider defaultTheme="opencode" defaultDarkMode={true}> <ThemeProvider defaultTheme="opencode" defaultDarkMode={true}>
<ShikiProvider>
<MarkedProvider>
<SDKProvider> <SDKProvider>
<EventProvider>
<SyncProvider> <SyncProvider>
<LocalProvider> <LocalProvider>
<Router root={Layout}> <Router root={Layout}>
@@ -26,7 +37,10 @@ render(
</Router> </Router>
</LocalProvider> </LocalProvider>
</SyncProvider> </SyncProvider>
</EventProvider>
</SDKProvider> </SDKProvider>
</MarkedProvider>
</ShikiProvider>
</ThemeProvider> </ThemeProvider>
</div> </div>
), ),

View File

@@ -2,6 +2,7 @@ import { LSP } from "../../../lsp"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd" import { cmd } from "../cmd"
import { Log } from "../../../util/log" import { Log } from "../../../util/log"
import { UI } from "../../ui"
export const LSPCommand = cmd({ export const LSPCommand = cmd({
command: "lsp", command: "lsp",
@@ -15,6 +16,10 @@ const DiagnosticsCommand = cmd({
builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }), builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
async handler(args) { async handler(args) {
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
if (!(await Bun.file(args.file).exists())) {
UI.error(`File ${args.file} does not exist`)
return
}
await LSP.touchFile(args.file, true) await LSP.touchFile(args.file, true)
console.log(JSON.stringify(await LSP.diagnostics(), null, 2)) console.log(JSON.stringify(await LSP.diagnostics(), null, 2))
}) })

View File

@@ -28,7 +28,6 @@ export namespace FileWatcher {
const ignore = (cfg.watcher?.ignore ?? []).map((v) => new Bun.Glob(v)) const ignore = (cfg.watcher?.ignore ?? []).map((v) => new Bun.Glob(v))
const watcher = chokidar.watch(Instance.directory, { const watcher = chokidar.watch(Instance.directory, {
ignoreInitial: true, ignoreInitial: true,
awaitWriteFinish: true,
ignored: (filepath) => { ignored: (filepath) => {
return FileIgnore.match(filepath, { return FileIgnore.match(filepath, {
extra: ignore, extra: ignore,

View File

@@ -72,7 +72,7 @@ export namespace LSP {
...existing, ...existing,
id: name, id: name,
root: existing?.root ?? (async () => Instance.directory), root: existing?.root ?? (async () => Instance.directory),
extensions: item.extensions ?? existing.extensions, extensions: item.extensions ?? existing?.extensions ?? [],
spawn: async (root) => { spawn: async (root) => {
return { return {
process: spawn(item.command[0], item.command.slice(1), { process: spawn(item.command[0], item.command.slice(1), {

View File

@@ -495,9 +495,7 @@ For quick reference, here are common setups.
} }
``` ```
See the full permissions guide for more patterns. See the full [permissions guide](/docs/permissions) for more patterns.
- /docs/permissions
--- ---

View File

@@ -3,7 +3,7 @@ title: LSP Servers
description: opencode integrates with your LSP servers. description: opencode integrates with your LSP servers.
--- ---
opencode integrates with your Language Server Protocol (LSP) to help the LLM interacts with your codebase. It uses diagnostics to provide feedback to the LLM. opencode integrates with your Language Server Protocol (LSP) to help the LLM interact with your codebase. It uses diagnostics to provide feedback to the LLM.
--- ---

View File

@@ -356,6 +356,8 @@ To use your GitHub Copilot subscription with opencode:
:::note :::note
Some models might need a [Pro+ Some models might need a [Pro+
subscription](https://github.com/features/copilot/plans) to use. subscription](https://github.com/features/copilot/plans) to use.
Some models need to be manually enabled in your [GitHub Copilot settings](https://docs.github.com/en/copilot/how-tos/use-ai-models/configure-access-to-ai-models#setup-for-individual-use).
::: :::
1. Run `opencode auth login` and select GitHub Copilot. 1. Run `opencode auth login` and select GitHub Copilot.