Merge branch 'dev' into llm-centralization
commit
a5914f4d7c
|
|
@ -64,6 +64,12 @@ jobs:
|
|||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
|
|
|
|||
2
STATS.md
2
STATS.md
|
|
@ -168,3 +168,5 @@
|
|||
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
||||
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
||||
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
||||
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
|
||||
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
|
||||
|
|
|
|||
37
bun.lock
37
bun.lock
|
|
@ -20,7 +20,7 @@
|
|||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
|
|
@ -75,7 +75,7 @@
|
|||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
|
|
@ -99,7 +99,7 @@
|
|||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
|
|
@ -133,6 +133,7 @@
|
|||
"@solid-primitives/active-element": "2.1.3",
|
||||
"@solid-primitives/audio": "1.4.2",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/media": "2.3.3",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solid-primitives/scroll": "2.1.3",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
|
|
@ -169,7 +170,7 @@
|
|||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
|
|
@ -198,7 +199,7 @@
|
|||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
|
|
@ -214,7 +215,7 @@
|
|||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
|
|
@ -306,7 +307,7 @@
|
|||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
|
|
@ -326,7 +327,7 @@
|
|||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.88.1",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
|
|
@ -337,7 +338,7 @@
|
|||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
|
|
@ -350,7 +351,7 @@
|
|||
},
|
||||
"packages/tauri": {
|
||||
"name": "@opencode-ai/tauri",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"dependencies": {
|
||||
"@opencode-ai/desktop": "workspace:*",
|
||||
"@tauri-apps/api": "^2",
|
||||
|
|
@ -375,7 +376,7 @@
|
|||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
|
|
@ -410,7 +411,7 @@
|
|||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
|
|
@ -421,7 +422,7 @@
|
|||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
|
@ -477,7 +478,7 @@
|
|||
"@tailwindcss/vite": "4.1.11",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "1.3.3",
|
||||
"@types/bun": "1.3.4",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "22.13.9",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
|
|
@ -1703,7 +1704,7 @@
|
|||
|
||||
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
|
||||
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||
|
||||
|
|
@ -2009,7 +2010,7 @@
|
|||
|
||||
"bun-pty": ["bun-pty@0.4.2", "", {}, "sha512-sHImDz6pJDsHAroYpC9ouKVgOyqZ7FP3N+stX5IdMddHve3rf9LIZBDomQcXrACQ7sQDNuwZQHG8BKR7w8krkQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
|
||||
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
|
||||
|
||||
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ inputs:
|
|||
description: "Custom prompt to override the default prompt"
|
||||
required: false
|
||||
|
||||
use_github_token:
|
||||
description: "Use GITHUB_TOKEN directly instead of OpenCode App token exchange. When true, skips OIDC and uses the GITHUB_TOKEN env var."
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
|
|
@ -51,3 +56,4 @@ runs:
|
|||
MODEL: ${{ inputs.model }}
|
||||
SHARE: ${{ inputs.share }}
|
||||
PROMPT: ${{ inputs.prompt }}
|
||||
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"nodeModules": "sha256-nWSAnQEm/t1ESZe23dr4JnIOJQ0JLN0w4NVoMJajbVQ="
|
||||
"nodeModules": "sha256-lgPsYtNJT7a+mDk5cTiEJLlBnTMTjxZCl8bw5WxcuaM="
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"description": "AI-powered development tool",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.3",
|
||||
"packageManager": "bun@1.3.4",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
"packages/slack"
|
||||
],
|
||||
"catalog": {
|
||||
"@types/bun": "1.3.3",
|
||||
"@types/bun": "1.3.4",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
|
@ -37,6 +37,7 @@
|
|||
"@solid-primitives/active-element": "2.1.3",
|
||||
"@solid-primitives/audio": "1.4.2",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/media": "2.3.3",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solid-primitives/scroll": "2.1.3",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
|
|
|
|||
|
|
@ -1,18 +1,7 @@
|
|||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||
import {
|
||||
createEffect,
|
||||
on,
|
||||
Component,
|
||||
Show,
|
||||
For,
|
||||
onMount,
|
||||
onCleanup,
|
||||
Switch,
|
||||
Match,
|
||||
createSignal,
|
||||
createMemo,
|
||||
} from "solid-js"
|
||||
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createMemo } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { makePersisted } from "@solid-primitives/storage"
|
||||
import { createFocusSignal } from "@solid-primitives/active-element"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, useSession } from "@/context/session"
|
||||
|
|
@ -81,22 +70,85 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
|
||||
const [store, setStore] = createStore<{
|
||||
popoverIsOpen: boolean
|
||||
historyIndex: number
|
||||
savedPrompt: Prompt | null
|
||||
placeholder: number
|
||||
}>({
|
||||
popoverIsOpen: false,
|
||||
historyIndex: -1,
|
||||
savedPrompt: null,
|
||||
placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
|
||||
})
|
||||
|
||||
const [placeholder, setPlaceholder] = createSignal(Math.floor(Math.random() * PLACEHOLDERS.length))
|
||||
const MAX_HISTORY = 100
|
||||
const [history, setHistory] = makePersisted(
|
||||
createStore<{
|
||||
entries: Prompt[]
|
||||
}>({
|
||||
entries: [],
|
||||
}),
|
||||
{
|
||||
name: "prompt-history.v1",
|
||||
},
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
setPlaceholder((prev) => (prev + 1) % PLACEHOLDERS.length)
|
||||
}, 6500)
|
||||
onCleanup(() => clearInterval(interval))
|
||||
})
|
||||
const clonePromptParts = (prompt: Prompt): Prompt =>
|
||||
prompt.map((part) =>
|
||||
part.type === "text"
|
||||
? { ...part }
|
||||
: {
|
||||
...part,
|
||||
selection: part.selection ? { ...part.selection } : undefined,
|
||||
},
|
||||
)
|
||||
|
||||
const promptLength = (prompt: Prompt) => prompt.reduce((len, part) => len + part.content.length, 0)
|
||||
|
||||
const applyHistoryPrompt = (prompt: Prompt, position: "start" | "end") => {
|
||||
const length = position === "start" ? 0 : promptLength(prompt)
|
||||
session.prompt.set(prompt, length)
|
||||
requestAnimationFrame(() => {
|
||||
editorRef.focus()
|
||||
setCursorPosition(editorRef, length)
|
||||
})
|
||||
}
|
||||
|
||||
const getCaretLineState = () => {
|
||||
const selection = window.getSelection()
|
||||
if (!selection || selection.rangeCount === 0) return { collapsed: false, onFirstLine: false, onLastLine: false }
|
||||
const range = selection.getRangeAt(0)
|
||||
const rect = range.getBoundingClientRect()
|
||||
const editorRect = editorRef.getBoundingClientRect()
|
||||
const style = window.getComputedStyle(editorRef)
|
||||
const paddingTop = parseFloat(style.paddingTop) || 0
|
||||
const paddingBottom = parseFloat(style.paddingBottom) || 0
|
||||
let lineHeight = parseFloat(style.lineHeight)
|
||||
if (!Number.isFinite(lineHeight)) lineHeight = parseFloat(style.fontSize) || 16
|
||||
const scrollTop = editorRef.scrollTop
|
||||
let relativeTop = rect.top - editorRect.top - paddingTop + scrollTop
|
||||
if (!Number.isFinite(relativeTop)) relativeTop = scrollTop
|
||||
relativeTop = Math.max(0, relativeTop)
|
||||
let caretHeight = rect.height
|
||||
if (!caretHeight || !Number.isFinite(caretHeight)) caretHeight = lineHeight
|
||||
const relativeBottom = relativeTop + caretHeight
|
||||
const contentHeight = Math.max(caretHeight, editorRef.scrollHeight - paddingTop - paddingBottom)
|
||||
const threshold = Math.max(2, lineHeight / 2)
|
||||
|
||||
return {
|
||||
collapsed: selection.isCollapsed,
|
||||
onFirstLine: relativeTop <= threshold,
|
||||
onLastLine: contentHeight - relativeBottom <= threshold,
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
session.id
|
||||
editorRef.focus()
|
||||
if (session.id) return
|
||||
const interval = setInterval(() => {
|
||||
setStore("placeholder", (prev) => (prev + 1) % PLACEHOLDERS.length)
|
||||
}, 6500)
|
||||
onCleanup(() => clearInterval(interval))
|
||||
})
|
||||
|
||||
const isFocused = createFocusSignal(() => editorRef)
|
||||
|
|
@ -129,17 +181,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
addPart({ type: "file", path, content: "@" + path, start: 0, end: 0 })
|
||||
}
|
||||
|
||||
const { flat, active, onInput, onKeyDown, refetch } = useFilteredList<string>({
|
||||
const { flat, active, onInput, onKeyDown } = useFilteredList<string>({
|
||||
items: local.file.searchFilesAndDirectories,
|
||||
key: (x) => x,
|
||||
onSelect: handleFileSelect,
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
local.model.recent()
|
||||
refetch()
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => session.prompt.current(),
|
||||
|
|
@ -221,6 +268,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
setStore("popoverIsOpen", false)
|
||||
}
|
||||
|
||||
if (store.historyIndex >= 0) {
|
||||
setStore("historyIndex", -1)
|
||||
setStore("savedPrompt", null)
|
||||
}
|
||||
|
||||
session.prompt.set(rawParts, cursorPosition)
|
||||
}
|
||||
|
||||
|
|
@ -296,12 +348,100 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
sessionID: session.id!,
|
||||
})
|
||||
|
||||
const addToHistory = (prompt: Prompt) => {
|
||||
const text = prompt
|
||||
.map((p) => p.content)
|
||||
.join("")
|
||||
.trim()
|
||||
if (!text) return
|
||||
|
||||
const entry = clonePromptParts(prompt)
|
||||
const lastEntry = history.entries[0]
|
||||
if (lastEntry) {
|
||||
const lastText = lastEntry.map((p) => p.content).join("")
|
||||
if (lastText === text) return
|
||||
}
|
||||
|
||||
setHistory("entries", (entries) => [entry, ...entries].slice(0, MAX_HISTORY))
|
||||
}
|
||||
|
||||
const navigateHistory = (direction: "up" | "down") => {
|
||||
const entries = history.entries
|
||||
const current = store.historyIndex
|
||||
|
||||
if (direction === "up") {
|
||||
if (entries.length === 0) return false
|
||||
if (current === -1) {
|
||||
setStore("savedPrompt", clonePromptParts(session.prompt.current()))
|
||||
setStore("historyIndex", 0)
|
||||
applyHistoryPrompt(entries[0], "start")
|
||||
return true
|
||||
}
|
||||
if (current < entries.length - 1) {
|
||||
const next = current + 1
|
||||
setStore("historyIndex", next)
|
||||
applyHistoryPrompt(entries[next], "start")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (current > 0) {
|
||||
const next = current - 1
|
||||
setStore("historyIndex", next)
|
||||
applyHistoryPrompt(entries[next], "end")
|
||||
return true
|
||||
}
|
||||
if (current === 0) {
|
||||
setStore("historyIndex", -1)
|
||||
const saved = store.savedPrompt
|
||||
if (saved) {
|
||||
applyHistoryPrompt(saved, "end")
|
||||
setStore("savedPrompt", null)
|
||||
return true
|
||||
}
|
||||
applyHistoryPrompt(DEFAULT_PROMPT, "end")
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (store.popoverIsOpen && (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter")) {
|
||||
onKeyDown(event)
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||
const { collapsed, onFirstLine, onLastLine } = getCaretLineState()
|
||||
if (!collapsed) return
|
||||
const cursorPos = getCursorPosition(editorRef)
|
||||
const textLength = promptLength(session.prompt.current())
|
||||
const inHistory = store.historyIndex >= 0
|
||||
const isStart = cursorPos === 0
|
||||
const isEnd = cursorPos === textLength
|
||||
const atAbsoluteStart = onFirstLine && isStart
|
||||
const atAbsoluteEnd = onLastLine && isEnd
|
||||
const allowUp = (inHistory && isEnd) || atAbsoluteStart
|
||||
const allowDown = (inHistory && isStart) || atAbsoluteEnd
|
||||
|
||||
if (event.key === "ArrowUp") {
|
||||
if (!allowUp) return
|
||||
if (navigateHistory("up")) {
|
||||
event.preventDefault()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!allowDown) return
|
||||
if (navigateHistory("down")) {
|
||||
event.preventDefault()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
handleSubmit(event)
|
||||
}
|
||||
|
|
@ -323,6 +463,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
return
|
||||
}
|
||||
|
||||
addToHistory(prompt)
|
||||
setStore("historyIndex", -1)
|
||||
setStore("savedPrompt", null)
|
||||
|
||||
let existing = session.info()
|
||||
if (!existing) {
|
||||
const created = await sdk.client.session.create()
|
||||
|
|
@ -461,7 +605,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
/>
|
||||
<Show when={!session.prompt.dirty()}>
|
||||
<div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none">
|
||||
Ask anything... "{PLACEHOLDERS[placeholder()]}"
|
||||
Ask anything... "{PLACEHOLDERS[store.placeholder]}"
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
@ -507,6 +651,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
items={models}
|
||||
current={local.model.current()}
|
||||
filterKeys={["provider.name", "name", "id"]}
|
||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||
// groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
|
||||
groupBy={(x) => x.provider.name}
|
||||
sortGroupsBy={(a, b) => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
|
||||
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
|
||||
import { ComponentProps, createEffect, onCleanup, onMount, splitProps } from "solid-js"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { SerializeAddon } from "@/addons/serialize"
|
||||
import { LocalPTY } from "@/context/session"
|
||||
import { usePrefersDark } from "@solid-primitives/media"
|
||||
|
||||
export interface TerminalProps extends ComponentProps<"div"> {
|
||||
pty: LocalPTY
|
||||
|
|
@ -21,6 +22,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||
let serializeAddon: SerializeAddon
|
||||
let fitAddon: FitAddon
|
||||
let handleResize: () => void
|
||||
const prefersDark = usePrefersDark()
|
||||
|
||||
onMount(async () => {
|
||||
ghostty = await Ghostty.load()
|
||||
|
|
@ -31,10 +33,17 @@ export const Terminal = (props: TerminalProps) => {
|
|||
fontSize: 14,
|
||||
fontFamily: "TX-02, monospace",
|
||||
allowTransparency: true,
|
||||
theme: {
|
||||
background: "#191515",
|
||||
foreground: "#d4d4d4",
|
||||
},
|
||||
theme: prefersDark()
|
||||
? {
|
||||
background: "#191515",
|
||||
foreground: "#d4d4d4",
|
||||
cursor: "#d4d4d4",
|
||||
}
|
||||
: {
|
||||
background: "#fcfcfc",
|
||||
foreground: "#211e1e",
|
||||
cursor: "#211e1e",
|
||||
},
|
||||
scrollback: 10_000,
|
||||
ghostty,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -189,11 +189,13 @@ export default function Layout(props: ParentProps) {
|
|||
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
||||
const name = createMemo(() => getFilename(props.project.worktree))
|
||||
const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
|
||||
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||
|
||||
return (
|
||||
<div class="relative size-6 shrink-0">
|
||||
<Avatar
|
||||
fallback={name()}
|
||||
src={props.project.icon?.url}
|
||||
src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.url}
|
||||
{...getAvatarColors(props.project.icon?.color)}
|
||||
class={`size-full ${props.class ?? ""}`}
|
||||
style={
|
||||
|
|
@ -318,22 +320,20 @@ export default function Layout(props: ParentProps) {
|
|||
)
|
||||
}
|
||||
return (
|
||||
<A
|
||||
href={`${slug()}/session/${session.id}`}
|
||||
class="group/session focus:outline-none cursor-default"
|
||||
<div
|
||||
class="group/session relative w-full pl-4 pr-1 py-1 rounded-md cursor-default transition-colors
|
||||
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
|
||||
>
|
||||
<Tooltip placement="right" value={session.title}>
|
||||
<div
|
||||
class="relative w-full pl-4 pr-1 py-1 rounded-md
|
||||
group-[.active]/session:bg-surface-raised-base-hover
|
||||
group-hover/session:bg-surface-raised-base-hover
|
||||
group-focus/session:bg-surface-raised-base-hover"
|
||||
<Tooltip placement="right" value={session.title} gutter={10}>
|
||||
<A
|
||||
href={`${slug()}/session/${session.id}`}
|
||||
class="flex flex-col min-w-0 text-left w-full focus:outline-none"
|
||||
>
|
||||
<div class="flex items-center self-stretch gap-6 justify-between">
|
||||
<div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
|
||||
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
|
||||
{session.title}
|
||||
</span>
|
||||
<div class="shrink-0 group-hover/session:hidden mr-1">
|
||||
<div class="shrink-0 group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
|
||||
<Switch>
|
||||
<Match when={hasError()}>
|
||||
<div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
|
||||
|
|
@ -358,12 +358,6 @@ export default function Layout(props: ParentProps) {
|
|||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<div class="hidden group-hover/session:flex group-active/session:flex text-text-base gap-1">
|
||||
{/* <IconButton icon="dot-grid" variant="ghost" /> */}
|
||||
<Tooltip placement="right" value="Archive session">
|
||||
<IconButton icon="archive" variant="ghost" onClick={() => archive(session)} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={session.summary?.files}>
|
||||
<div class="flex justify-between items-center self-stretch">
|
||||
|
|
@ -371,29 +365,40 @@ export default function Layout(props: ParentProps) {
|
|||
<Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</A>
|
||||
</Tooltip>
|
||||
</A>
|
||||
<div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
|
||||
{/* <IconButton icon="dot-grid" variant="ghost" /> */}
|
||||
<Tooltip placement="right" value="Archive session">
|
||||
<IconButton icon="archive" variant="ghost" onClick={() => archive(session)} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<Show when={sessions().length === 0}>
|
||||
<A href={`${slug()}/session`} class="group/session focus:outline-none cursor-default">
|
||||
<Tooltip placement="right" value="New session">
|
||||
<div
|
||||
class="relative w-full pl-4 pr-1 py-1 rounded-md
|
||||
group-[.active]/session:bg-surface-raised-base-hover
|
||||
group-hover/session:bg-surface-raised-base-hover
|
||||
group-focus/session:bg-surface-raised-base-hover"
|
||||
>
|
||||
<div class="flex items-center self-stretch gap-6 justify-between">
|
||||
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
|
||||
New session
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="group/session relative w-full pl-4 pr-1 py-1 rounded-md cursor-default transition-colors
|
||||
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
|
||||
>
|
||||
<div class="flex items-center self-stretch w-full">
|
||||
<div class="flex-1 min-w-0">
|
||||
<Tooltip placement="right" value="New session">
|
||||
<A
|
||||
href={`${slug()}/session`}
|
||||
class="flex flex-col gap-1 min-w-0 text-left w-full focus:outline-none"
|
||||
>
|
||||
<div class="flex items-center self-stretch gap-6 justify-between">
|
||||
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
|
||||
New session
|
||||
</span>
|
||||
</div>
|
||||
</A>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</A>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</nav>
|
||||
</Collapsible.Content>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.0.152"
|
||||
version = "1.0.153"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/sst/opencode"
|
||||
|
|
@ -11,26 +11,26 @@ name = "OpenCode"
|
|||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
FROM alpine
|
||||
FROM alpine AS base
|
||||
|
||||
# Disable the runtime transpiler cache by default inside Docker containers.
|
||||
# On ephemeral containers, the cache is not useful
|
||||
ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
|
||||
ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH}
|
||||
RUN apk add libgcc libstdc++ ripgrep
|
||||
ADD ./dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
|
||||
|
||||
FROM base AS build-amd64
|
||||
COPY dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
|
||||
|
||||
FROM base AS build-arm64
|
||||
COPY dist/opencode-linux-arm64-musl/bin/opencode /usr/local/bin/opencode
|
||||
|
||||
ARG TARGETARCH
|
||||
FROM build-${TARGETARCH}
|
||||
RUN opencode --version
|
||||
ENTRYPOINT ["opencode"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
|
|
|||
|
|
@ -117,6 +117,9 @@ for (const item of targets) {
|
|||
compile: {
|
||||
autoloadBunfig: false,
|
||||
autoloadDotenv: false,
|
||||
//@ts-ignore (bun types aren't up to date)
|
||||
autoloadTsconfig: true,
|
||||
autoloadPackageJson: true,
|
||||
target: name.replace(pkg.name, "bun") as any,
|
||||
outfile: `dist/${name}/bin/opencode`,
|
||||
execArgv: [`--user-agent=opencode/${Script.version}`, "--"],
|
||||
|
|
|
|||
|
|
@ -244,8 +244,8 @@ if (!Script.preview) {
|
|||
await $`cd ./dist/homebrew-tap && git push`
|
||||
|
||||
const image = "ghcr.io/sst/opencode"
|
||||
await $`docker build -t ${image}:${Script.version} .`
|
||||
await $`docker push ${image}:${Script.version}`
|
||||
await $`docker tag ${image}:${Script.version} ${image}:latest`
|
||||
await $`docker push ${image}:latest`
|
||||
const platforms = "linux/amd64,linux/arm64"
|
||||
const tags = [`${image}:${Script.version}`, `${image}:latest`]
|
||||
const tagFlags = tags.flatMap((t) => ["-t", t])
|
||||
await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,47 +85,16 @@ export namespace BunProc {
|
|||
version,
|
||||
})
|
||||
|
||||
const total = 3
|
||||
const wait = 500
|
||||
|
||||
const runInstall = async (count: number = 1): Promise<void> => {
|
||||
log.info("bun install attempt", {
|
||||
pkg,
|
||||
version,
|
||||
attempt: count,
|
||||
total,
|
||||
})
|
||||
await BunProc.run(args, {
|
||||
cwd: Global.Path.cache,
|
||||
}).catch(async (error) => {
|
||||
log.warn("bun install failed", {
|
||||
pkg,
|
||||
version,
|
||||
attempt: count,
|
||||
total,
|
||||
error,
|
||||
})
|
||||
if (count >= total) {
|
||||
throw new InstallFailedError(
|
||||
{ pkg, version },
|
||||
{
|
||||
cause: error,
|
||||
},
|
||||
)
|
||||
}
|
||||
const delay = wait * count
|
||||
log.info("bun install retrying", {
|
||||
pkg,
|
||||
version,
|
||||
next: count + 1,
|
||||
delay,
|
||||
})
|
||||
await Bun.sleep(delay)
|
||||
return runInstall(count + 1)
|
||||
})
|
||||
}
|
||||
|
||||
await runInstall()
|
||||
await BunProc.run(args, {
|
||||
cwd: Global.Path.cache,
|
||||
}).catch((e) => {
|
||||
throw new InstallFailedError(
|
||||
{ pkg, version },
|
||||
{
|
||||
cause: e,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// Resolve actual version from installed package when using "latest"
|
||||
// This ensures subsequent starts use the cached version until explicitly updated
|
||||
|
|
|
|||
|
|
@ -411,17 +411,30 @@ export const GithubRunCommand = cmd({
|
|||
let exitCode = 0
|
||||
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
|
||||
const triggerCommentId = payload.comment.id
|
||||
const useGithubToken = normalizeUseGithubToken()
|
||||
|
||||
try {
|
||||
const actionToken = isMock ? args.token! : await getOidcToken()
|
||||
appToken = await exchangeForAppToken(actionToken)
|
||||
if (useGithubToken) {
|
||||
const githubToken = process.env["GITHUB_TOKEN"]
|
||||
if (!githubToken) {
|
||||
throw new Error(
|
||||
"GITHUB_TOKEN environment variable is not set. When using use_github_token, you must provide GITHUB_TOKEN.",
|
||||
)
|
||||
}
|
||||
appToken = githubToken
|
||||
} else {
|
||||
const actionToken = isMock ? args.token! : await getOidcToken()
|
||||
appToken = await exchangeForAppToken(actionToken)
|
||||
}
|
||||
octoRest = new Octokit({ auth: appToken })
|
||||
octoGraph = graphql.defaults({
|
||||
headers: { authorization: `token ${appToken}` },
|
||||
})
|
||||
|
||||
const { userPrompt, promptFiles } = await getUserPrompt()
|
||||
await configureGit(appToken)
|
||||
if (!useGithubToken) {
|
||||
await configureGit(appToken)
|
||||
}
|
||||
await assertPermissions()
|
||||
|
||||
await addReaction()
|
||||
|
|
@ -514,8 +527,10 @@ export const GithubRunCommand = cmd({
|
|||
// Also output the clean error message for the action to capture
|
||||
//core.setOutput("prepare_error", e.message);
|
||||
} finally {
|
||||
await restoreGitConfig()
|
||||
await revokeAppToken()
|
||||
if (!useGithubToken) {
|
||||
await restoreGitConfig()
|
||||
await revokeAppToken()
|
||||
}
|
||||
}
|
||||
process.exit(exitCode)
|
||||
|
||||
|
|
@ -544,6 +559,14 @@ export const GithubRunCommand = cmd({
|
|||
throw new Error(`Invalid share value: ${value}. Share must be a boolean.`)
|
||||
}
|
||||
|
||||
function normalizeUseGithubToken() {
|
||||
const value = process.env["USE_GITHUB_TOKEN"]
|
||||
if (!value) return false
|
||||
if (value === "true") return true
|
||||
if (value === "false") return false
|
||||
throw new Error(`Invalid use_github_token value: ${value}. Must be a boolean.`)
|
||||
}
|
||||
|
||||
function isIssueCommentEvent(
|
||||
event: IssueCommentEvent | PullRequestReviewCommentEvent,
|
||||
): event is IssueCommentEvent {
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ function App() {
|
|||
let continued = false
|
||||
createEffect(() => {
|
||||
if (continued || sync.status !== "complete" || !args.continue) return
|
||||
const match = sync.data.session.at(0)?.id
|
||||
const match = sync.data.session.find((x) => x.parentID === undefined)?.id
|
||||
if (match) {
|
||||
continued = true
|
||||
route.navigate({ type: "session", sessionID: match })
|
||||
|
|
|
|||
|
|
@ -14,12 +14,17 @@ export const AttachCommand = cmd({
|
|||
.option("dir", {
|
||||
type: "string",
|
||||
description: "directory to run in",
|
||||
})
|
||||
.option("session", {
|
||||
alias: ["s"],
|
||||
type: "string",
|
||||
describe: "session id to continue",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
if (args.dir) process.chdir(args.dir)
|
||||
await tui({
|
||||
url: args.url,
|
||||
args: {},
|
||||
args: { sessionID: args.session },
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -705,8 +705,8 @@ export function Prompt(props: PromptProps) {
|
|||
>
|
||||
<textarea
|
||||
placeholder={props.sessionID ? undefined : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
|
||||
textColor={theme.text}
|
||||
focusedTextColor={theme.text}
|
||||
textColor={keybind.leader ? theme.textMuted : theme.text}
|
||||
focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
|
||||
minHeight={1}
|
||||
maxHeight={6}
|
||||
onContentChange={() => {
|
||||
|
|
@ -854,7 +854,7 @@ export function Prompt(props: PromptProps) {
|
|||
</text>
|
||||
<Show when={store.mode === "normal"}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text flexShrink={0} fg={theme.text}>
|
||||
<text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
|
||||
{local.model.parsed().model}
|
||||
</text>
|
||||
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
|
||||
|
|
@ -869,25 +869,17 @@ export function Prompt(props: PromptProps) {
|
|||
borderColor={highlight()}
|
||||
customBorderChars={{
|
||||
...EmptyBorder,
|
||||
// when the background is transparent, don't draw the vertical line
|
||||
vertical: theme.background.a != 0 ? "╹" : " ",
|
||||
vertical: "╹",
|
||||
}}
|
||||
>
|
||||
<box
|
||||
height={1}
|
||||
border={["bottom"]}
|
||||
borderColor={theme.backgroundElement}
|
||||
customBorderChars={
|
||||
theme.background.a != 0
|
||||
? {
|
||||
...EmptyBorder,
|
||||
horizontal: "▀",
|
||||
}
|
||||
: {
|
||||
...EmptyBorder,
|
||||
horizontal: " ",
|
||||
}
|
||||
}
|
||||
customBorderChars={{
|
||||
...EmptyBorder,
|
||||
horizontal: "▀",
|
||||
}}
|
||||
/>
|
||||
</box>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
|
|
|
|||
|
|
@ -277,7 +277,10 @@ export function Sidebar(props: { sessionID: string }) {
|
|||
</box>
|
||||
</box>
|
||||
</Show>
|
||||
<text fg={theme.text}>{directory()}</text>
|
||||
<text>
|
||||
<span style={{ fg: theme.textMuted }}>{directory().split("/").slice(0, -1).join("/")}/</span>
|
||||
<span style={{ fg: theme.text }}>{directory().split("/").at(-1)}</span>
|
||||
</text>
|
||||
<text fg={theme.textMuted}>
|
||||
<span style={{ fg: theme.success }}>•</span> <b>Open</b>
|
||||
<span style={{ fg: theme.text }}>
|
||||
|
|
|
|||
|
|
@ -275,3 +275,21 @@ export const terraform: Info = {
|
|||
return Bun.which("terraform") !== null
|
||||
},
|
||||
}
|
||||
|
||||
export const latexindent: Info = {
|
||||
name: "latexindent",
|
||||
command: ["latexindent", "-w", "-s", "$FILE"],
|
||||
extensions: [".tex"],
|
||||
async enabled() {
|
||||
return Bun.which("latexindent") !== null
|
||||
},
|
||||
}
|
||||
|
||||
export const gleam: Info = {
|
||||
name: "gleam",
|
||||
command: ["gleam", "format", "$FILE"],
|
||||
extensions: [".gleam"],
|
||||
async enabled() {
|
||||
return Bun.which("gleam") !== null
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
|
|||
".gitrebase": "git-rebase",
|
||||
".go": "go",
|
||||
".groovy": "groovy",
|
||||
".gleam": "gleam",
|
||||
".hbs": "handlebars",
|
||||
".handlebars": "handlebars",
|
||||
".hs": "haskell",
|
||||
|
|
|
|||
|
|
@ -1386,4 +1386,145 @@ export namespace LSPServer {
|
|||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const TexLab: Info = {
|
||||
id: "texlab",
|
||||
extensions: [".tex", ".bib"],
|
||||
root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
|
||||
async spawn(root) {
|
||||
let bin = Bun.which("texlab", {
|
||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||
})
|
||||
|
||||
if (!bin) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("downloading texlab from GitHub releases")
|
||||
|
||||
const response = await fetch("https://api.github.com/repos/latex-lsp/texlab/releases/latest")
|
||||
if (!response.ok) {
|
||||
log.error("Failed to fetch texlab release info")
|
||||
return
|
||||
}
|
||||
|
||||
const release = (await response.json()) as {
|
||||
tag_name?: string
|
||||
assets?: { name?: string; browser_download_url?: string }[]
|
||||
}
|
||||
const version = release.tag_name?.replace("v", "")
|
||||
if (!version) {
|
||||
log.error("texlab release did not include a version tag")
|
||||
return
|
||||
}
|
||||
|
||||
const platform = process.platform
|
||||
const arch = process.arch
|
||||
|
||||
const texArch = arch === "arm64" ? "aarch64" : "x86_64"
|
||||
const texPlatform = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux"
|
||||
const ext = platform === "win32" ? "zip" : "tar.gz"
|
||||
const assetName = `texlab-${texArch}-${texPlatform}.${ext}`
|
||||
|
||||
const assets = release.assets ?? []
|
||||
const asset = assets.find((a) => a.name === assetName)
|
||||
if (!asset?.browser_download_url) {
|
||||
log.error(`Could not find asset ${assetName} in texlab release`)
|
||||
return
|
||||
}
|
||||
|
||||
const downloadResponse = await fetch(asset.browser_download_url)
|
||||
if (!downloadResponse.ok) {
|
||||
log.error("Failed to download texlab")
|
||||
return
|
||||
}
|
||||
|
||||
const tempPath = path.join(Global.Path.bin, assetName)
|
||||
await Bun.file(tempPath).write(downloadResponse)
|
||||
|
||||
if (ext === "zip") {
|
||||
await $`unzip -o -q ${tempPath}`.cwd(Global.Path.bin).nothrow()
|
||||
}
|
||||
if (ext === "tar.gz") {
|
||||
await $`tar -xzf ${tempPath}`.cwd(Global.Path.bin).nothrow()
|
||||
}
|
||||
|
||||
await fs.rm(tempPath, { force: true })
|
||||
|
||||
bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : ""))
|
||||
|
||||
if (!(await Bun.file(bin).exists())) {
|
||||
log.error("Failed to extract texlab binary")
|
||||
return
|
||||
}
|
||||
|
||||
if (platform !== "win32") {
|
||||
await $`chmod +x ${bin}`.nothrow()
|
||||
}
|
||||
|
||||
log.info("installed texlab", { bin })
|
||||
}
|
||||
|
||||
return {
|
||||
process: spawn(bin, {
|
||||
cwd: root,
|
||||
}),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const DockerfileLS: Info = {
|
||||
id: "dockerfile",
|
||||
extensions: [".dockerfile", "Dockerfile"],
|
||||
root: async () => Instance.directory,
|
||||
async spawn(root) {
|
||||
let binary = Bun.which("docker-langserver")
|
||||
const args: string[] = []
|
||||
if (!binary) {
|
||||
const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
|
||||
if (!(await Bun.file(js).exists())) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
|
||||
cwd: Global.Path.bin,
|
||||
env: {
|
||||
...process.env,
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
stdin: "pipe",
|
||||
}).exited
|
||||
}
|
||||
binary = BunProc.which()
|
||||
args.push("run", js)
|
||||
}
|
||||
args.push("--stdio")
|
||||
const proc = spawn(binary, args, {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
})
|
||||
return {
|
||||
process: proc,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const Gleam: Info = {
|
||||
id: "gleam",
|
||||
extensions: [".gleam"],
|
||||
root: NearestRoot(["gleam.toml"]),
|
||||
async spawn(root) {
|
||||
const gleam = Bun.which("gleam")
|
||||
if (!gleam) {
|
||||
log.info("gleam not found, please install gleam first")
|
||||
return
|
||||
}
|
||||
return {
|
||||
process: spawn(gleam, ["lsp"], {
|
||||
cwd: root,
|
||||
}),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,14 +199,29 @@ export namespace ProviderTransform {
|
|||
}
|
||||
|
||||
export function temperature(model: Provider.Model) {
|
||||
if (model.api.id.toLowerCase().includes("qwen")) return 0.55
|
||||
if (model.api.id.toLowerCase().includes("claude")) return undefined
|
||||
if (model.api.id.toLowerCase().includes("gemini-3-pro")) return 1.0
|
||||
return 0
|
||||
const id = model.id.toLowerCase()
|
||||
if (id.includes("qwen")) return 0.55
|
||||
if (id.includes("claude")) return undefined
|
||||
if (id.includes("gemini-3-pro")) return 1.0
|
||||
if (id.includes("glm-4.6")) return 1.0
|
||||
if (id.includes("minimax-m2")) return 1.0
|
||||
// if (id.includes("kimi-k2")) {
|
||||
// if (id.includes("thinking")) return 1.0
|
||||
// return 0.6
|
||||
// }
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function topP(model: Provider.Model) {
|
||||
if (model.api.id.toLowerCase().includes("qwen")) return 1
|
||||
const id = model.id.toLowerCase()
|
||||
if (id.includes("qwen")) return 1
|
||||
if (id.includes("minimax-m2")) return 0.95
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function topK(model: Provider.Model) {
|
||||
const id = model.id.toLowerCase()
|
||||
if (id.includes("minimax-m2")) return 40
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type { MessageV2 } from "./message-v2"
|
|||
import { Plugin } from "@/plugin"
|
||||
import { SystemPrompt } from "./system"
|
||||
import { ToolRegistry } from "@/tool/registry"
|
||||
import { Flag } from "@/flag/flag"
|
||||
|
||||
export namespace LLM {
|
||||
const log = Log.create({ service: "llm" })
|
||||
|
|
@ -133,6 +134,7 @@ export namespace LLM {
|
|||
"x-opencode-project": Instance.project.id,
|
||||
"x-opencode-session": input.sessionID,
|
||||
"x-opencode-request": input.user.id,
|
||||
"x-opencode-client": Flag.OPENCODE_CLIENT,
|
||||
}
|
||||
: undefined),
|
||||
...input.model.headers,
|
||||
|
|
|
|||
|
|
@ -68,6 +68,15 @@ export namespace SessionRetry {
|
|||
if (json.code === "Some resource has been exhausted") {
|
||||
return "Provider is overloaded"
|
||||
}
|
||||
if (json.type === "error" && json.error?.code?.includes("rate_limit")) {
|
||||
return "Rate Limited"
|
||||
}
|
||||
if (
|
||||
json.error?.message?.includes("no_kv_space") ||
|
||||
(json.type === "error" && json.error?.type === "server_error")
|
||||
) {
|
||||
return "Provider Server Error"
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import { Instance } from "../project/instance"
|
|||
import { Agent } from "../agent/agent"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
|
||||
const MAX_DIAGNOSTICS_PER_FILE = 20
|
||||
|
||||
function normalizeLineEndings(text: string): string {
|
||||
return text.replaceAll("\r\n", "\n")
|
||||
}
|
||||
|
|
@ -141,10 +143,11 @@ export const EditTool = Tool.define("edit", {
|
|||
for (const [file, issues] of Object.entries(diagnostics)) {
|
||||
if (issues.length === 0) continue
|
||||
if (file === filePath) {
|
||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues
|
||||
.filter((item) => item.severity === 1)
|
||||
.map(LSP.Diagnostic.pretty)
|
||||
.join("\n")}\n</file_diagnostics>\n`
|
||||
const errors = issues.filter((item) => item.severity === 1)
|
||||
const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE)
|
||||
const suffix =
|
||||
errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
|
||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</file_diagnostics>\n`
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import { Filesystem } from "../util/filesystem"
|
|||
import { Instance } from "../project/instance"
|
||||
import { Agent } from "../agent/agent"
|
||||
|
||||
const MAX_DIAGNOSTICS_PER_FILE = 20
|
||||
const MAX_PROJECT_DIAGNOSTICS_FILES = 5
|
||||
|
||||
export const WriteTool = Tool.define("write", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
|
|
@ -77,13 +80,20 @@ export const WriteTool = Tool.define("write", {
|
|||
let output = ""
|
||||
await LSP.touchFile(filepath, true)
|
||||
const diagnostics = await LSP.diagnostics()
|
||||
let projectDiagnosticsCount = 0
|
||||
for (const [file, issues] of Object.entries(diagnostics)) {
|
||||
if (issues.length === 0) continue
|
||||
const sorted = issues.toSorted((a, b) => (a.severity ?? 4) - (b.severity ?? 4))
|
||||
const limited = sorted.slice(0, MAX_DIAGNOSTICS_PER_FILE)
|
||||
const suffix =
|
||||
issues.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${issues.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
|
||||
if (file === filepath) {
|
||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
|
||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</file_diagnostics>\n`
|
||||
continue
|
||||
}
|
||||
output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`
|
||||
if (projectDiagnosticsCount >= MAX_PROJECT_DIAGNOSTICS_FILES) continue
|
||||
projectDiagnosticsCount++
|
||||
output += `\n<project_diagnostics>\n${file}\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</project_diagnostics>\n`
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
|
@ -24,4 +24,4 @@
|
|||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
|
@ -29,4 +29,4 @@
|
|||
"publishConfig": {
|
||||
"directory": "dist"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@opencode-ai/tauri",
|
||||
"private": true,
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "OpenCode",
|
||||
"mainBinaryName": "OpenCode Desktop",
|
||||
"mainBinaryName": "OpenCode",
|
||||
"version": "../package.json",
|
||||
"identifier": "ai.opencode.desktop",
|
||||
"build": {
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["deb", "rpm", "dmg", "nsis"],
|
||||
"targets": ["deb", "rpm", "dmg", "nsis", "appimage"],
|
||||
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
||||
"externalBin": ["sidecars/opencode"],
|
||||
"createUpdaterArtifacts": true,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./src/components/*.tsx",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ export type TriggerTitle = {
|
|||
}
|
||||
|
||||
const isTriggerTitle = (val: any): val is TriggerTitle => {
|
||||
return typeof val === "object" && val !== null && "title" in val && !(val instanceof Node)
|
||||
return (
|
||||
typeof val === "object" && val !== null && "title" in val && (typeof Node === "undefined" || !(val instanceof Node))
|
||||
)
|
||||
}
|
||||
|
||||
export interface BasicToolProps {
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@
|
|||
padding: 0 12px 0 8px;
|
||||
}
|
||||
|
||||
gap: 4px;
|
||||
gap: 8px;
|
||||
|
||||
/* text-14-medium */
|
||||
font-family: var(--font-family-sans);
|
||||
|
|
|
|||
|
|
@ -321,7 +321,6 @@ ToolRegistry.register({
|
|||
render(props) {
|
||||
return (
|
||||
<BasicTool
|
||||
defaultOpen
|
||||
icon="console"
|
||||
trigger={{
|
||||
title: "Shell",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
[data-component="session-turn"] {
|
||||
/* flex: 1; */
|
||||
--scroll-y: 0px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
|
|
@ -26,18 +27,27 @@
|
|||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
min-width: 0;
|
||||
gap: 32px;
|
||||
gap: clamp(8px, calc(42px - var(--scroll-y) * 0.48), 42px);
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-sticky-header"] {
|
||||
[data-slot="session-turn-sticky-title"] {
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--background-stronger);
|
||||
z-index: 21;
|
||||
/* padding-bottom: clamp(0px, calc(8px - var(--scroll-y) * 0.16), 8px); */
|
||||
}
|
||||
|
||||
[data-slot="session-turn-response-trigger"] {
|
||||
position: sticky;
|
||||
top: 32px;
|
||||
background-color: var(--background-stronger);
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: calc(100% + 9px);
|
||||
margin-left: -9px;
|
||||
padding-left: 9px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
|
|
@ -49,13 +59,8 @@
|
|||
height: 32px;
|
||||
}
|
||||
|
||||
/* [data-slot="session-turn-message-content"] { */
|
||||
/* } */
|
||||
|
||||
[data-slot="session-turn-response-trigger"] {
|
||||
width: calc(100% + 9px);
|
||||
margin-left: -9px;
|
||||
padding-left: 9px;
|
||||
[data-slot="session-turn-message-content"] {
|
||||
margin-top: -24px;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-message-title"] {
|
||||
|
|
@ -292,6 +297,7 @@
|
|||
[data-slot="session-turn-collapsible"] {
|
||||
gap: 32px;
|
||||
overflow: visible;
|
||||
/* margin-top: clamp(8px, calc(24px - var(--scroll-y) * 0.32), 24px); */
|
||||
}
|
||||
|
||||
[data-slot="session-turn-collapsible-trigger-content"] {
|
||||
|
|
|
|||
|
|
@ -3,18 +3,7 @@ import { useData } from "../context"
|
|||
import { useDiffComponent } from "../context/diff"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
onCleanup,
|
||||
onMount,
|
||||
ParentProps,
|
||||
Show,
|
||||
Switch,
|
||||
} from "solid-js"
|
||||
import { createEffect, createMemo, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
import { Typewriter } from "./typewriter"
|
||||
|
|
@ -60,45 +49,77 @@ export function SessionTurn(
|
|||
const working = createMemo(() => status()?.type !== "idle")
|
||||
|
||||
let scrollRef: HTMLDivElement | undefined
|
||||
const [contentRef, setContentRef] = createSignal<HTMLDivElement>()
|
||||
const [stickyHeaderRef, setStickyHeaderRef] = createSignal<HTMLDivElement>()
|
||||
const [userScrolled, setUserScrolled] = createSignal(false)
|
||||
const [stickyHeaderHeight, setStickyHeaderHeight] = createSignal(0)
|
||||
const [state, setState] = createStore({
|
||||
contentRef: undefined as HTMLDivElement | undefined,
|
||||
stickyTitleRef: undefined as HTMLDivElement | undefined,
|
||||
stickyTriggerRef: undefined as HTMLDivElement | undefined,
|
||||
userScrolled: false,
|
||||
stickyHeaderHeight: 0,
|
||||
scrollY: 0,
|
||||
autoScrolling: false,
|
||||
})
|
||||
|
||||
function handleScroll() {
|
||||
if (!scrollRef) return
|
||||
setState("scrollY", scrollRef.scrollTop)
|
||||
if (state.autoScrolling) return
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef
|
||||
const atBottom = scrollHeight - scrollTop - clientHeight < 50
|
||||
if (!atBottom && working()) {
|
||||
setUserScrolled(true)
|
||||
setState("userScrolled", true)
|
||||
}
|
||||
}
|
||||
|
||||
function handleInteraction() {
|
||||
if (working()) {
|
||||
setUserScrolled(true)
|
||||
setState("userScrolled", true)
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
if (!scrollRef || state.userScrolled || !working() || state.autoScrolling) return
|
||||
setState("autoScrolling", true)
|
||||
requestAnimationFrame(() => {
|
||||
scrollRef?.scrollTo({ top: scrollRef.scrollHeight, behavior: "auto" })
|
||||
requestAnimationFrame(() => {
|
||||
setState("autoScrolling", false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!working()) {
|
||||
setUserScrolled(false)
|
||||
setState("userScrolled", false)
|
||||
}
|
||||
})
|
||||
|
||||
createResizeObserver(contentRef, () => {
|
||||
if (!scrollRef || userScrolled() || !working()) return
|
||||
scrollRef.scrollTop = scrollRef.scrollHeight
|
||||
})
|
||||
createResizeObserver(
|
||||
() => state.contentRef,
|
||||
() => {
|
||||
scrollToBottom()
|
||||
},
|
||||
)
|
||||
|
||||
createResizeObserver(stickyHeaderRef, ({ height }) => {
|
||||
setStickyHeaderHeight(height + 8)
|
||||
})
|
||||
createResizeObserver(
|
||||
() => state.stickyTitleRef,
|
||||
({ height }) => {
|
||||
const triggerHeight = state.stickyTriggerRef?.offsetHeight ?? 0
|
||||
setState("stickyHeaderHeight", height + triggerHeight + 8)
|
||||
},
|
||||
)
|
||||
|
||||
createResizeObserver(
|
||||
() => state.stickyTriggerRef,
|
||||
({ height }) => {
|
||||
const titleHeight = state.stickyTitleRef?.offsetHeight ?? 0
|
||||
setState("stickyHeaderHeight", titleHeight + height + 8)
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<div data-component="session-turn" class={props.classes?.root}>
|
||||
<div data-component="session-turn" class={props.classes?.root} style={{ "--scroll-y": `${state.scrollY}px` }}>
|
||||
<div ref={scrollRef} onScroll={handleScroll} data-slot="session-turn-content" class={props.classes?.content}>
|
||||
<div ref={setContentRef} onClick={handleInteraction}>
|
||||
<div ref={(el) => setState("contentRef", el)} onClick={handleInteraction}>
|
||||
<Show when={message()}>
|
||||
{(message) => {
|
||||
const assistantMessages = createMemo(() => {
|
||||
|
|
@ -175,6 +196,9 @@ export function SessionTurn(
|
|||
break
|
||||
}
|
||||
} else if (last.type === "reasoning") {
|
||||
const text = last.text ?? ""
|
||||
const match = text.trimStart().match(/^\*\*(.+?)\*\*/)
|
||||
if (match) return `Thinking · ${match[1].trim()}`
|
||||
return "Thinking"
|
||||
} else if (last.type === "text") {
|
||||
return "Gathering thoughts"
|
||||
|
|
@ -237,7 +261,7 @@ export function SessionTurn(
|
|||
|
||||
createEffect((prev) => {
|
||||
const isWorking = working()
|
||||
if (prev && !isWorking && !userScrolled()) {
|
||||
if (prev && !isWorking && !state.userScrolled) {
|
||||
setStore("stepsExpanded", false)
|
||||
}
|
||||
return isWorking
|
||||
|
|
@ -248,10 +272,10 @@ export function SessionTurn(
|
|||
data-message={message().id}
|
||||
data-slot="session-turn-message-container"
|
||||
class={props.classes?.container}
|
||||
style={{ "--sticky-header-height": `${stickyHeaderHeight()}px` }}
|
||||
style={{ "--sticky-header-height": `${state.stickyHeaderHeight}px` }}
|
||||
>
|
||||
{/* Sticky Header */}
|
||||
<div ref={setStickyHeaderRef} data-slot="session-turn-sticky-header">
|
||||
{/* Title (sticky) */}
|
||||
<div ref={(el) => setState("stickyTitleRef", el)} data-slot="session-turn-sticky-title">
|
||||
<div data-slot="session-turn-message-header">
|
||||
<div data-slot="session-turn-message-title">
|
||||
<Switch>
|
||||
|
|
@ -264,29 +288,31 @@ export function SessionTurn(
|
|||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<div data-slot="session-turn-message-content">
|
||||
<Message message={message()} parts={parts()} />
|
||||
</div>
|
||||
<div data-slot="session-turn-response-trigger">
|
||||
<Button
|
||||
data-slot="session-turn-collapsible-trigger-content"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => setStore("stepsExpanded", !store.stepsExpanded)}
|
||||
>
|
||||
<Show when={working()}>
|
||||
<Spinner />
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={working()}>{store.status ?? "Considering next steps..."}</Match>
|
||||
<Match when={store.stepsExpanded}>Hide steps</Match>
|
||||
<Match when={!store.stepsExpanded}>Show steps</Match>
|
||||
</Switch>
|
||||
<span>·</span>
|
||||
<span>{store.duration}</span>
|
||||
<Icon name="chevron-grabber-vertical" size="small" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* User Message */}
|
||||
<div data-slot="session-turn-message-content">
|
||||
<Message message={message()} parts={parts()} />
|
||||
</div>
|
||||
{/* Trigger (sticky) */}
|
||||
<div ref={(el) => setState("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
|
||||
<Button
|
||||
data-slot="session-turn-collapsible-trigger-content"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => setStore("stepsExpanded", !store.stepsExpanded)}
|
||||
>
|
||||
<Show when={working()}>
|
||||
<Spinner />
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={working()}>{store.status ?? "Considering next steps"}</Match>
|
||||
<Match when={store.stepsExpanded}>Hide steps</Match>
|
||||
<Match when={!store.stepsExpanded}>Show steps</Match>
|
||||
</Switch>
|
||||
<span>·</span>
|
||||
<span>{store.duration}</span>
|
||||
<Icon name="chevron-grabber-vertical" size="small" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Response */}
|
||||
<Show when={store.stepsExpanded}>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
@keyframes pulse-opacity {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export default defineConfig({
|
|||
"",
|
||||
"config",
|
||||
"providers",
|
||||
"network",
|
||||
"enterprise",
|
||||
"troubleshooting",
|
||||
"1-0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
|
|
|||
|
|
@ -67,6 +67,25 @@ You can also bind a keyboard shortcut by editing your `keymap.json`:
|
|||
|
||||
---
|
||||
|
||||
### JetBrains IDEs
|
||||
|
||||
Add to your [JetBrains IDE](https://www.jetbrains.com/) acp.json according to the [documentation](https://www.jetbrains.com/help/ai-assistant/acp.html):
|
||||
|
||||
```json title="acp.json"
|
||||
{
|
||||
"agent_servers": {
|
||||
"OpenCode": {
|
||||
"command": "/absolute/path/bin/opencode",
|
||||
"args": ["acp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To open it, use the new 'OpenCode' agent in the AI Chat agent selector.
|
||||
|
||||
---
|
||||
|
||||
### Avante.nvim
|
||||
|
||||
Add to your [Avante.nvim](https://github.com/yetone/avante.nvim) configuration:
|
||||
|
|
|
|||
|
|
@ -15,17 +15,21 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
|
|||
|
||||
## Plugins
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping |
|
||||
| [opencode-skills](https://github.com/malhashemi/opencode-skills) | Manage and organize OpenCode skills and capabilities |
|
||||
| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools |
|
||||
| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits |
|
||||
| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use your existing Gemini plan instead of API billing |
|
||||
| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use Antigravity's free models instead of API billing |
|
||||
| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs |
|
||||
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Track OpenCode usage with Wakatime |
|
||||
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs |
|
||||
| Name | Description |
|
||||
| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping |
|
||||
| [opencode-skills](https://github.com/malhashemi/opencode-skills) | Manage and organize OpenCode skills and capabilities |
|
||||
| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools |
|
||||
| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits |
|
||||
| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use your existing Gemini plan instead of API billing |
|
||||
| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use Antigravity's free models instead of API billing |
|
||||
| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-goggle-antigravity-auth) | Google Antigravity OAuth Plugin, with support for Google Search, and more robust API handling |
|
||||
| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs |
|
||||
| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Add native websearch support for supported providers with Google grounded style |
|
||||
| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Enables AI agents to run background processes in a PTY, send interactive input to them. |
|
||||
| [opencode-wakatime](https://github.com/angrister/opencode-wakatime) | Track OpenCode usage with Wakatime |
|
||||
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs |
|
||||
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ OpenCode comes with several built-in formatters for popular languages and framew
|
|||
| dart | .dart | `dart` command available |
|
||||
| ocamlformat | .ml, .mli | `ocamlformat` command available and `.ocamlformat` config file |
|
||||
| terraform | .tf, .tfvars | `terraform` command available |
|
||||
| gleam | .gleam | `gleam` command available |
|
||||
|
||||
So if your project has `prettier` in your `package.json`, OpenCode will automatically use it.
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ OpenCode comes with several built-in LSP servers for popular languages:
|
|||
| ocaml-lsp | .ml, .mli | `ocamllsp` command available |
|
||||
| terraform | .tf, .tfvars | Auto-installs from GitHub releases |
|
||||
| bash | .sh, .bash, .zsh, .ksh | Auto-installs bash-language-server |
|
||||
| gleam | .gleam | `gleam` command available |
|
||||
|
||||
LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
title: Network
|
||||
description: Configure proxies and custom certificates.
|
||||
---
|
||||
|
||||
OpenCode supports standard proxy environment variables and custom certificates for enterprise network environments.
|
||||
|
||||
---
|
||||
|
||||
## Proxy
|
||||
|
||||
OpenCode respects standard proxy environment variables.
|
||||
|
||||
```bash
|
||||
# HTTPS proxy (recommended)
|
||||
export HTTPS_PROXY=https://proxy.example.com:8080
|
||||
|
||||
# HTTP proxy (if HTTPS not available)
|
||||
export HTTP_PROXY=http://proxy.example.com:8080
|
||||
|
||||
# Bypass proxy for local server (required)
|
||||
export NO_PROXY=localhost,127.0.0.1
|
||||
```
|
||||
|
||||
:::caution
|
||||
The TUI communicates with a local HTTP server. You must bypass the proxy for this connection to prevent routing loops.
|
||||
:::
|
||||
|
||||
You can configure the server's port and hostname using [CLI flags](/docs/cli#run).
|
||||
|
||||
---
|
||||
|
||||
### Authenticate
|
||||
|
||||
If your proxy requires basic authentication, include credentials in the URL.
|
||||
|
||||
```bash
|
||||
export HTTPS_PROXY=http://username:password@proxy.example.com:8080
|
||||
```
|
||||
|
||||
:::caution
|
||||
Avoid hardcoding passwords. Use environment variables or secure credential storage.
|
||||
:::
|
||||
|
||||
For proxies requiring advanced authentication like NTLM or Kerberos, consider using an LLM Gateway that supports your authentication method.
|
||||
|
||||
---
|
||||
|
||||
## Custom certificates
|
||||
|
||||
If your enterprise uses custom CAs for HTTPS connections, configure OpenCode to trust them.
|
||||
|
||||
```bash
|
||||
export NODE_EXTRA_CA_CERTS=/path/to/ca-cert.pem
|
||||
```
|
||||
|
||||
This works for both proxy connections and direct API access.
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.0.152",
|
||||
"version": "1.0.153",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
Loading…
Reference in New Issue