summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/context
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2025-10-30 22:53:00 +0000
committerDavid Hill <[email protected]>2025-10-30 22:53:00 +0000
commit2f9f189f390401b3314c74bd9f83e64b2420cd65 (patch)
treeb275ca785719a54115752a79d4b66984ba125670 /packages/desktop/src/context
parentf3c70f4ea8818bde5b1228482f72af9b10c71796 (diff)
parenta3ba740de41eb1e4825a99dc8f519e1225357e55 (diff)
downloadopencode-2f9f189f390401b3314c74bd9f83e64b2420cd65.tar.gz
opencode-2f9f189f390401b3314c74bd9f83e64b2420cd65.zip
Merge branch 'dev' of https://github.com/sst/opencode into dev
Diffstat (limited to 'packages/desktop/src/context')
-rw-r--r--packages/desktop/src/context/local.tsx89
-rw-r--r--packages/desktop/src/context/marked.tsx30
-rw-r--r--packages/desktop/src/context/shiki.tsx573
-rw-r--r--packages/desktop/src/context/sync.tsx35
4 files changed, 98 insertions, 629 deletions
diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx
index 9c4d70fc5..4607a184c 100644
--- a/packages/desktop/src/context/local.tsx
+++ b/packages/desktop/src/context/local.tsx
@@ -45,6 +45,37 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const sdk = useSDK()
const sync = useSync()
+ function isModelValid(model: ModelKey) {
+ const provider = sync.data.provider.find((x) => x.id === model.providerID)
+ return !!provider?.models[model.modelID]
+ }
+
+ function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) {
+ for (const modelFn of modelFns) {
+ const model = modelFn()
+ if (!model) continue
+ if (isModelValid(model)) return model
+ }
+ }
+
+ // Automatically update model when agent changes
+ createEffect(() => {
+ const value = agent.current()
+ if (value.model) {
+ if (isModelValid(value.model))
+ model.set({
+ providerID: value.model.providerID,
+ modelID: value.model.modelID,
+ })
+ // else
+ // toast.show({
+ // type: "warning",
+ // message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
+ // duration: 3000,
+ // })
+ }
+ })
+
const agent = (() => {
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent"))
const [store, setStore] = createStore<{
@@ -76,11 +107,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})()
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[]
@@ -95,27 +121,54 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
localStorage.setItem("model", JSON.stringify(store.recent))
})
- const fallback = createMemo(() => {
- if (store.recent.length) return store.recent[0]
+ 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 fallbackModel = createMemo(() => {
+ if (sync.data.config.model) {
+ const [providerID, modelID] = sync.data.config.model.split("/")
+ if (isModelValid({ providerID, modelID })) {
+ return {
+ providerID,
+ modelID,
+ }
+ }
+ }
+
+ for (const item of store.recent) {
+ if (isModelValid(item)) {
+ return item
+ }
+ }
const provider = sync.data.provider[0]
const model = Object.values(provider.models)[0]
- return { modelID: model.id, providerID: provider.id }
+ return {
+ providerID: provider.id,
+ modelID: model.id,
+ }
})
- const current = createMemo(() => {
+ const currentModel = createMemo(() => {
const a = agent.current()
- return find(store.model[agent.current().name]) ?? find(a.model ?? fallback())
+ const key = getFirstValidModel(
+ () => store.model[a.name],
+ () => a.model,
+ fallbackModel,
+ )!
+ return find(key)
})
const recent = createMemo(() => store.recent.map(find).filter(Boolean))
return {
- list,
- current,
+ current: currentModel,
recent,
+ list,
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
batch(() => {
- setStore("model", agent.current().name, model ?? fallback())
+ setStore("model", agent.current().name, model ?? fallbackModel())
if (options?.recent && model) {
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
if (uniq.length > 5) uniq.pop()
@@ -279,10 +332,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
break
case "file.watcher.updated":
- setTimeout(sync.load.changes, 1000)
- const relativePath = relative(event.properties.file)
- if (relativePath.startsWith(".git/")) return
- load(relativePath)
+ // setTimeout(sync.load.changes, 1000)
+ // const relativePath = relative(event.properties.file)
+ // if (relativePath.startsWith(".git/")) return
+ // load(relativePath)
break
}
})
@@ -480,8 +533,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const getMessageText = (message: Message | Message[] | undefined): string => {
if (!message) return ""
if (Array.isArray(message)) return message.map((m) => getMessageText(m)).join(" ")
- const fileParts = sync.data.part[message.id]?.filter((p) => p.type === "file")
-
return sync.data.part[message.id]
?.filter((p) => p.type === "text")
?.filter((p) => !p.synthetic)
diff --git a/packages/desktop/src/context/marked.tsx b/packages/desktop/src/context/marked.tsx
deleted file mode 100644
index 18ce4280a..000000000
--- a/packages/desktop/src/context/marked.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { marked } from "marked"
-import markedShiki from "marked-shiki"
-import { bundledLanguages, type BundledLanguage } from "shiki"
-
-import { createSimpleContext } from "./helper"
-import { useShiki } from "./shiki"
-
-export const { use: useMarked, provider: MarkedProvider } = createSimpleContext({
- name: "Marked",
- init: () => {
- const highlighter = 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,
- })
- },
- }),
- )
- },
-})
diff --git a/packages/desktop/src/context/shiki.tsx b/packages/desktop/src/context/shiki.tsx
deleted file mode 100644
index b6c278bfe..000000000
--- a/packages/desktop/src/context/shiki.tsx
+++ /dev/null
@@ -1,573 +0,0 @@
-import { createSimpleContext } from "./helper"
-import { createHighlighter, type ThemeInput } from "shiki"
-
-const theme: ThemeInput = {
- colors: {
- "actionBar.toggledBackground": "var(--surface-raised-base)",
- "activityBarBadge.background": "var(--surface-brand-base)",
- "checkbox.border": "var(--border-base)",
- "editor.background": "transparent",
- "editor.foreground": "var(--text-base)",
- "editor.inactiveSelectionBackground": "var(--surface-raised-base)",
- "editor.selectionHighlightBackground": "var(--border-active)",
- "editorIndentGuide.activeBackground1": "var(--border-weak-base)",
- "editorIndentGuide.background1": "var(--border-weak-base)",
- "input.placeholderForeground": "var(--text-weak)",
- "list.activeSelectionIconForeground": "var(--text-base)",
- "list.dropBackground": "var(--surface-raised-base)",
- "menu.background": "var(--surface-base)",
- "menu.border": "var(--border-base)",
- "menu.foreground": "var(--text-base)",
- "menu.selectionBackground": "var(--surface-interactive-base)",
- "menu.separatorBackground": "var(--border-base)",
- "ports.iconRunningProcessForeground": "var(--icon-success-base)",
- "sideBarSectionHeader.background": "transparent",
- "sideBarSectionHeader.border": "var(--border-weak-base)",
- "sideBarTitle.foreground": "var(--text-weak)",
- "statusBarItem.remoteBackground": "var(--surface-success-base)",
- "statusBarItem.remoteForeground": "var(--text-base)",
- "tab.lastPinnedBorder": "var(--border-weak-base)",
- "tab.selectedBackground": "var(--surface-raised-base)",
- "tab.selectedForeground": "var(--text-weak)",
- "terminal.inactiveSelectionBackground": "var(--surface-raised-base)",
- "widget.border": "var(--border-base)",
- },
- displayName: "opencode",
- name: "opencode",
- semanticHighlighting: true,
- semanticTokenColors: {
- customLiteral: "var(--syntax-function)",
- newOperator: "var(--syntax-operator)",
- numberLiteral: "var(--syntax-number)",
- stringLiteral: "var(--syntax-string)",
- },
- tokenColors: [
- {
- scope: [
- "meta.embedded",
- "source.groovy.embedded",
- "string meta.image.inline.markdown",
- "variable.legacy.builtin.python",
- ],
- settings: {
- foreground: "var(--text-base)",
- },
- },
- {
- scope: "emphasis",
- settings: {
- fontStyle: "italic",
- },
- },
- {
- scope: "strong",
- settings: {
- fontStyle: "bold",
- },
- },
- {
- scope: "header",
- settings: {
- foreground: "var(--markdown-heading)",
- },
- },
- {
- scope: "comment",
- settings: {
- foreground: "var(--syntax-comment)",
- },
- },
- {
- scope: "constant.language",
- settings: {
- foreground: "var(--syntax-keyword)",
- },
- },
- {
- scope: [
- "constant.numeric",
- "variable.other.enummember",
- "keyword.operator.plus.exponent",
- "keyword.operator.minus.exponent",
- ],
- settings: {
- foreground: "var(--syntax-number)",
- },
- },
- {
- scope: "constant.regexp",
- settings: {
- foreground: "var(--syntax-operator)",
- },
- },
- {
- scope: "entity.name.tag",
- settings: {
- foreground: "var(--syntax-keyword)",
- },
- },
- {
- scope: ["entity.name.tag.css", "entity.name.tag.less"],
- settings: {
- foreground: "var(--syntax-operator)",
- },
- },
- {
- scope: "entity.other.attribute-name",
- settings: {
- foreground: "var(--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(--syntax-operator)",
- },
- },
- {
- scope: "invalid",
- settings: {
- foreground: "var(--syntax-critical)",
- },
- },
- {
- scope: "markup.underline",
- settings: {
- fontStyle: "underline",
- },
- },
- {
- scope: "markup.bold",
- settings: {
- fontStyle: "bold",
- foreground: "var(--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(--text-diff-add-base)",
- },
- },
- {
- scope: "markup.deleted",
- settings: {
- foreground: "var(--text-diff-delete-base)",
- },
- },
- {
- scope: "markup.changed",
- settings: {
- foreground: "var(--text-base)",
- },
- },
- {
- scope: "punctuation.definition.quote.begin.markdown",
- settings: {
- foreground: "var(--markdown-block-quote)",
- },
- },
- {
- scope: "punctuation.definition.list.begin.markdown",
- settings: {
- foreground: "var(--markdown-list-enumeration)",
- },
- },
- {
- scope: "markup.inline.raw",
- settings: {
- foreground: "var(--markdown-code)",
- },
- },
- {
- scope: "punctuation.definition.tag",
- settings: {
- foreground: "var(--syntax-punctuation)",
- },
- },
- {
- scope: ["meta.preprocessor", "entity.name.function.preprocessor"],
- settings: {
- foreground: "var(--syntax-keyword)",
- },
- },
- {
- scope: "meta.preprocessor.string",
- settings: {
- foreground: "var(--syntax-string)",
- },
- },
- {
- scope: "meta.preprocessor.numeric",
- settings: {
- foreground: "var(--syntax-number)",
- },
- },
- {
- scope: "meta.structure.dictionary.key.python",
- settings: {
- foreground: "var(--syntax-variable)",
- },
- },
- {
- scope: "meta.diff.header",
- settings: {
- foreground: "var(--text-weak)",
- },
- },
- {
- scope: "storage",
- settings: {
- foreground: "var(--syntax-keyword)",
- },
- },
- {
- scope: "storage.type",
- settings: {
- foreground: "var(--syntax-keyword)",
- },
- },
- {
- scope: ["storage.modifier", "keyword.operator.noexcept"],
- settings: {
- foreground: "var(--syntax-keyword)",
- },
- },
- {
- scope: ["string", "meta.embedded.assembly"],
- settings: {
- foreground: "var(--syntax-string)",
- },
- },
- {
- scope: "string.tag",
- settings: {
- foreground: "var(--syntax-string)",
- },
- },
- {
- scope: "string.value",
- settings: {
- foreground: "var(--syntax-string)",
- },
- },
- {
- scope: "string.regexp",
- settings: {
- foreground: "var(--syntax-operator)",
- },
- },
- {
- scope: [
- "punctuation.definition.template-expression.begin",
- "punctuation.definition.template-expression.end",
- "punctuation.section.embedded",
- ],
- settings: {
- foreground: "var(--syntax-keyword)",
- },
- },
- {
- scope: ["meta.template.expression"],
- settings: {
- foreground: "var(--text-base)",
- },
- },
- {
- scope: [
- "support.type.vendored.property-name",
- "support.type.property-name",
- "source.css variable",
- "source.coffee.embedded",
- ],
- settings: {
- foreground: "var(--syntax-variable)",
- },
- },
- {
- scope: "keyword",
- settings: {
- foreground: "var(--syntax-keyword)",
- },
- },
- {
- scope: "keyword.control",
- settings: {
- foreground: "var(--syntax-keyword)",
- },
- },
- {
- scope: "keyword.operator",
- settings: {
- foreground: "var(--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(--syntax-keyword)",
- },
- },
- {
- scope: "keyword.other.unit",
- settings: {
- foreground: "var(--syntax-number)",
- },
- },
- {
- scope: ["punctuation.section.embedded.begin.php", "punctuation.section.embedded.end.php"],
- settings: {
- foreground: "var(--syntax-keyword)",
- },
- },
- {
- scope: "support.function.git-rebase",
- settings: {
- foreground: "var(--syntax-variable)",
- },
- },
- {
- scope: "constant.sha.git-rebase",
- settings: {
- foreground: "var(--syntax-number)",
- },
- },
- {
- scope: ["storage.modifier.import.java", "variable.language.wildcard.java", "storage.modifier.package.java"],
- settings: {
- foreground: "var(--text-base)",
- },
- },
- {
- scope: "variable.language",
- settings: {
- foreground: "var(--syntax-keyword)",
- },
- },
- {
- scope: [
- "entity.name.function",
- "support.function",
- "support.constant.handlebars",
- "source.powershell variable.other.member",
- "entity.name.operator.custom-literal",
- ],
- settings: {
- foreground: "var(--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(--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(--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(--syntax-operator)",
- },
- },
- {
- scope: [
- "variable",
- "meta.definition.variable.name",
- "support.variable",
- "entity.name.variable",
- "constant.other.placeholder",
- ],
- settings: {
- foreground: "var(--syntax-variable)",
- },
- },
- {
- scope: ["variable.other.constant", "variable.other.enummember"],
- settings: {
- foreground: "var(--syntax-variable)",
- },
- },
- {
- scope: ["meta.object-literal.key"],
- settings: {
- foreground: "var(--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(--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(--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(--syntax-operator)",
- },
- },
- {
- scope: ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"],
- settings: {
- foreground: "var(--syntax-operator)",
- },
- },
- {
- scope: "keyword.operator.quantifier.regexp",
- settings: {
- foreground: "var(--syntax-operator)",
- },
- },
- {
- scope: ["constant.character", "constant.other.option"],
- settings: {
- foreground: "var(--syntax-keyword)",
- },
- },
- {
- scope: "constant.character.escape",
- settings: {
- foreground: "var(--syntax-operator)",
- },
- },
- {
- scope: "entity.name.label",
- settings: {
- foreground: "var(--text-weak)",
- },
- },
- ],
- type: "dark",
-}
-
-const highlighter = await createHighlighter({
- themes: [theme],
- langs: [],
-})
-
-export const { use: useShiki, provider: ShikiProvider } = createSimpleContext({
- name: "Shiki",
- init: () => {
- return highlighter
- },
-})
diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx
index 12aa95878..ce2fa1ea4 100644
--- a/packages/desktop/src/context/sync.tsx
+++ b/packages/desktop/src/context/sync.tsx
@@ -77,21 +77,22 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
break
}
case "message.part.updated": {
- const parts = store.part[event.properties.part.messageID]
+ const part = sanitizePart(event.properties.part)
+ const parts = store.part[part.messageID]
if (!parts) {
- setStore("part", event.properties.part.messageID, [event.properties.part])
+ setStore("part", part.messageID, [part])
break
}
- const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
+ const result = Binary.search(parts, part.id, (p) => p.id)
if (result.found) {
- setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
+ setStore("part", part.messageID, result.index, reconcile(part))
break
}
setStore(
"part",
- event.properties.part.messageID,
+ part.messageID,
produce((draft) => {
- draft.splice(result.index, 0, event.properties.part)
+ draft.splice(result.index, 0, part)
}),
)
break
@@ -121,6 +122,23 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
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("//", "/")
+ const sanitizePart = (part: Part) => {
+ if (part.type === "tool") {
+ if (part.state.status === "completed") {
+ for (const key in part.state.metadata) {
+ if (typeof part.state.metadata[key] === "string") {
+ part.state.metadata[key] = sanitize(part.state.metadata[key] as string)
+ }
+ }
+ for (const key in part.state.input) {
+ if (typeof part.state.input[key] === "string") {
+ part.state.input[key] = sanitize(part.state.input[key] as string)
+ }
+ }
+ }
+ }
+ return part
+ }
return {
data: store,
@@ -155,7 +173,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
.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))
+ draft.part[message.info.id] = message.parts
+ .slice()
+ .map(sanitizePart)
+ .sort((a, b) => a.id.localeCompare(b.id))
}
}),
)