mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-18 10:07:58 +00:00
Merge branch 'dev' into feat/canceled-prompts-in-history
This commit is contained in:
9
bun.lock
9
bun.lock
@@ -497,6 +497,9 @@
|
||||
"web-tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
@@ -516,7 +519,7 @@
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "1.3.5",
|
||||
"@types/bun": "1.3.8",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/semver": "7.7.1",
|
||||
@@ -1825,7 +1828,7 @@
|
||||
|
||||
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
||||
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||
|
||||
@@ -2135,7 +2138,7 @@
|
||||
|
||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
||||
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1768393167,
|
||||
"narHash": "sha256-n2063BRjHde6DqAz2zavhOOiLUwA3qXt7jQYHyETjX8=",
|
||||
"lastModified": 1770073757,
|
||||
"narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2f594d5af95d4fdac67fba60376ec11e482041cb",
|
||||
"rev": "47472570b1e607482890801aeaf29bfb749884f6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-FMrW0aXYOgRe3ginr4l1LwCszsD/r5CQkvRU6HHA7iw=",
|
||||
"aarch64-linux": "sha256-NZTtIsFZshWOp5mVFvrcVeHUlx62QcsSJKPYjwPhmYk=",
|
||||
"aarch64-darwin": "sha256-6cWt8KaqojTJ/b3WSYb3dDPTNuKBDt9Fxx6p/WGBnik=",
|
||||
"x86_64-darwin": "sha256-F6zuxV34RQ9RTjH0c22rGZaPrhemhRUPi+OkF+Y0ytM="
|
||||
"x86_64-linux": "sha256-UBz5qXhO+Xy6XptVdbo9V0wKsvZgItmHkWDm6I5VRCk=",
|
||||
"aarch64-linux": "sha256-G2ezu/ThZR3kYfHnbD0EOcLoAa6hwtICpmo9r+bqibE=",
|
||||
"aarch64-darwin": "sha256-PhSE23OzNlyfNFP5LffA3AtyN+hsyCeGInmDBBRjr0g=",
|
||||
"x86_64-darwin": "sha256-vWusYJD+7ClDLUFy1wEqRLf9hY8V43iqdqnZ6YWkh1Q="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "AI-powered development tool",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.5",
|
||||
"packageManager": "bun@1.3.8",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"dev:desktop": "bun --cwd packages/desktop tauri dev",
|
||||
@@ -23,7 +23,7 @@
|
||||
"packages/slack"
|
||||
],
|
||||
"catalog": {
|
||||
"@types/bun": "1.3.5",
|
||||
"@types/bun": "1.3.8",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"ulid": "3.0.1",
|
||||
@@ -100,5 +100,7 @@
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:"
|
||||
},
|
||||
"patchedDependencies": {}
|
||||
"patchedDependencies": {
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
value={store.startup}
|
||||
onChange={(v) => setStore("startup", v)}
|
||||
spellcheck={false}
|
||||
class="max-h-40 w-full font-mono text-xs no-scrollbar"
|
||||
class="max-h-14 w-full overflow-y-auto font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -19,6 +19,14 @@ import {
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import type { FileNode } from "@opencode-ai/sdk/v2"
|
||||
|
||||
function pathToFileUrl(filepath: string): string {
|
||||
const encodedPath = filepath
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/")
|
||||
return `file://${encodedPath}`
|
||||
}
|
||||
|
||||
type Kind = "add" | "del" | "mix"
|
||||
|
||||
type Filter = {
|
||||
@@ -247,7 +255,7 @@ export default function FileTree(props: {
|
||||
onDragStart={(e: DragEvent) => {
|
||||
if (!draggable()) return
|
||||
e.dataTransfer?.setData("text/plain", `file:${local.node.path}`)
|
||||
e.dataTransfer?.setData("text/uri-list", `file://${local.node.path}`)
|
||||
e.dataTransfer?.setData("text/uri-list", pathToFileUrl(local.node.path))
|
||||
if (e.dataTransfer) e.dataTransfer.effectAllowed = "copy"
|
||||
|
||||
const dragImage = document.createElement("div")
|
||||
|
||||
@@ -1023,7 +1023,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
options={local.agent.list().map((agent) => agent.name)}
|
||||
current={local.agent.current()?.name ?? ""}
|
||||
onSelect={local.agent.set}
|
||||
class={`capitalize ${local.model.variant.list().length > 0 ? "max-w-[80px]" : "max-w-[120px]"}`}
|
||||
class={`capitalize ${local.model.variant.list().length > 0 ? "max-w-full" : "max-w-[120px]"}`}
|
||||
valueClass="truncate"
|
||||
variant="ghost"
|
||||
/>
|
||||
|
||||
@@ -30,6 +30,12 @@ type BuildRequestPartsInput = {
|
||||
const absolute = (directory: string, path: string) =>
|
||||
path.startsWith("/") ? path : (directory + "/" + path).replace("//", "/")
|
||||
|
||||
const encodeFilePath = (filepath: string): string =>
|
||||
filepath
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/")
|
||||
|
||||
const fileQuery = (selection: FileSelection | undefined) =>
|
||||
selection ? `?start=${selection.startLine}&end=${selection.endLine}` : ""
|
||||
|
||||
@@ -99,7 +105,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
|
||||
id: Identifier.ascending("part"),
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
url: `file://${path}${fileQuery(attachment.selection)}`,
|
||||
url: `file://${encodeFilePath(path)}${fileQuery(attachment.selection)}`,
|
||||
filename: getFilename(attachment.path),
|
||||
source: {
|
||||
type: "file",
|
||||
@@ -129,7 +135,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
|
||||
const used = new Set(files.map((part) => part.url))
|
||||
const context = input.context.flatMap((item) => {
|
||||
const path = absolute(input.sessionDirectory, item.path)
|
||||
const url = `file://${path}${fileQuery(item.selection)}`
|
||||
const url = `file://${encodeFilePath(path)}${fileQuery(item.selection)}`
|
||||
const comment = item.comment?.trim()
|
||||
if (!comment && used.has(url)) return []
|
||||
used.add(url)
|
||||
|
||||
@@ -544,11 +544,7 @@ export function SessionHeader() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/file-tree-toggle size-6 p-0"
|
||||
onClick={() => {
|
||||
const opening = !layout.fileTree.opened()
|
||||
if (opening && !view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.toggle()
|
||||
}}
|
||||
onClick={() => layout.fileTree.toggle()}
|
||||
aria-label={language.t("command.fileTree.toggle")}
|
||||
aria-expanded={layout.fileTree.opened()}
|
||||
aria-controls="file-tree-panel"
|
||||
|
||||
@@ -44,7 +44,7 @@ function groupFor(id: string): KeybindGroup {
|
||||
if (id === PALETTE_ID) return "General"
|
||||
if (id.startsWith("terminal.")) return "Terminal"
|
||||
if (id.startsWith("model.") || id.startsWith("agent.") || id.startsWith("mcp.")) return "Model and agent"
|
||||
if (id.startsWith("file.")) return "Navigation"
|
||||
if (id.startsWith("file.") || id.startsWith("fileTree.")) return "Navigation"
|
||||
if (id.startsWith("prompt.")) return "Prompt"
|
||||
if (
|
||||
id.startsWith("session.") ||
|
||||
|
||||
@@ -72,12 +72,27 @@ export function unquoteGitPath(input: string) {
|
||||
return new TextDecoder().decode(new Uint8Array(bytes))
|
||||
}
|
||||
|
||||
export function decodeFilePath(input: string) {
|
||||
try {
|
||||
return decodeURIComponent(input)
|
||||
} catch {
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
export function encodeFilePath(filepath: string): string {
|
||||
return filepath
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/")
|
||||
}
|
||||
|
||||
export function createPathHelpers(scope: () => string) {
|
||||
const normalize = (input: string) => {
|
||||
const root = scope()
|
||||
const prefix = root.endsWith("/") ? root : root + "/"
|
||||
|
||||
let path = unquoteGitPath(stripQueryAndHash(stripFileProtocol(input)))
|
||||
let path = unquoteGitPath(decodeFilePath(stripQueryAndHash(stripFileProtocol(input))))
|
||||
|
||||
if (path.startsWith(prefix)) {
|
||||
path = path.slice(prefix.length)
|
||||
@@ -100,7 +115,7 @@ export function createPathHelpers(scope: () => string) {
|
||||
|
||||
const tab = (input: string) => {
|
||||
const path = normalize(input)
|
||||
return `file://${path}`
|
||||
return `file://${encodeFilePath(path)}`
|
||||
}
|
||||
|
||||
const pathFromTab = (tabValue: string) => {
|
||||
|
||||
@@ -233,7 +233,15 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||
const centered = createMemo(() => isDesktop() && !view().reviewPanel.opened())
|
||||
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
||||
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened())
|
||||
const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
|
||||
const sessionPanelWidth = createMemo(() => {
|
||||
if (!desktopSidePanelOpen()) return "100%"
|
||||
if (desktopReviewOpen()) return `${layout.session.width()}px`
|
||||
return `calc(100% - ${layout.fileTree.width()}px)`
|
||||
})
|
||||
const centered = createMemo(() => isDesktop() && !desktopSidePanelOpen())
|
||||
|
||||
function normalizeTab(tab: string) {
|
||||
if (!tab.startsWith("file://")) return tab
|
||||
@@ -252,12 +260,18 @@ export default function Page() {
|
||||
return next
|
||||
}
|
||||
|
||||
const openReviewPanel = () => {
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
}
|
||||
|
||||
const openTab = (value: string) => {
|
||||
const next = normalizeTab(value)
|
||||
tabs().open(next)
|
||||
|
||||
const path = file.pathFromTab(next)
|
||||
if (path) file.load(path)
|
||||
if (!path) return
|
||||
file.load(path)
|
||||
openReviewPanel()
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
@@ -1085,6 +1099,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const focusReviewDiff = (path: string) => {
|
||||
openReviewPanel()
|
||||
const current = view().review.open() ?? []
|
||||
if (!current.includes(path)) view().review.setOpen([...current, path])
|
||||
setTree({ activeDiff: path, pendingDiff: path })
|
||||
@@ -1203,7 +1218,7 @@ export default function Page() {
|
||||
if (!id) return
|
||||
|
||||
const wants = isDesktop()
|
||||
? view().reviewPanel.opened() && (layout.fileTree.opened() || activeTab() === "review")
|
||||
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
|
||||
: store.mobileTab === "changes"
|
||||
if (!wants) return
|
||||
if (sync.data.session_diff[id] !== undefined) return
|
||||
@@ -1216,7 +1231,6 @@ export default function Page() {
|
||||
createEffect(() => {
|
||||
const dir = sdk.directory
|
||||
if (!isDesktop()) return
|
||||
if (!view().reviewPanel.opened()) return
|
||||
if (!layout.fileTree.opened()) return
|
||||
if (sync.status === "loading") return
|
||||
|
||||
@@ -1533,10 +1547,10 @@ export default function Page() {
|
||||
classList={{
|
||||
"@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger": true,
|
||||
"flex-1 pt-2 md:pt-3": true,
|
||||
"md:flex-none": view().reviewPanel.opened(),
|
||||
"md:flex-none": desktopSidePanelOpen(),
|
||||
}}
|
||||
style={{
|
||||
width: isDesktop() && view().reviewPanel.opened() ? `${layout.session.width()}px` : "100%",
|
||||
width: sessionPanelWidth(),
|
||||
"--prompt-height": store.promptHeight ? `${store.promptHeight}px` : undefined,
|
||||
}}
|
||||
>
|
||||
@@ -1663,7 +1677,7 @@ export default function Page() {
|
||||
setPromptDockRef={(el) => (promptDock = el)}
|
||||
/>
|
||||
|
||||
<Show when={isDesktop() && view().reviewPanel.opened()}>
|
||||
<Show when={desktopReviewOpen()}>
|
||||
<ResizeHandle
|
||||
direction="horizontal"
|
||||
size={layout.session.width()}
|
||||
@@ -1675,7 +1689,8 @@ export default function Page() {
|
||||
</div>
|
||||
|
||||
<SessionSidePanel
|
||||
open={isDesktop() && view().reviewPanel.opened()}
|
||||
open={desktopSidePanelOpen()}
|
||||
reviewOpen={desktopReviewOpen()}
|
||||
language={language}
|
||||
layout={layout}
|
||||
command={command}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useSync } from "@/context/sync"
|
||||
|
||||
export function SessionSidePanel(props: {
|
||||
open: boolean
|
||||
reviewOpen: boolean
|
||||
language: ReturnType<typeof useLanguage>
|
||||
layout: ReturnType<typeof useLayout>
|
||||
command: ReturnType<typeof useCommand>
|
||||
@@ -72,157 +73,166 @@ export function SessionSidePanel(props: {
|
||||
<aside
|
||||
id="review-panel"
|
||||
aria-label={props.language.t("session.panel.reviewAndFiles")}
|
||||
class="relative flex-1 min-w-0 h-full border-l border-border-weak-base flex"
|
||||
class="relative min-w-0 h-full border-l border-border-weak-base flex"
|
||||
classList={{
|
||||
"flex-1": props.reviewOpen,
|
||||
"shrink-0": !props.reviewOpen,
|
||||
}}
|
||||
style={{ width: props.reviewOpen ? undefined : `${props.layout.fileTree.width()}px` }}
|
||||
>
|
||||
<div class="flex-1 min-w-0 h-full">
|
||||
<Show
|
||||
when={props.layout.fileTree.opened() && props.fileTreeTab() === "changes"}
|
||||
fallback={
|
||||
<DragDropProvider
|
||||
onDragStart={props.onDragStart}
|
||||
onDragEnd={props.onDragEnd}
|
||||
onDragOver={props.onDragOver}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragYAxis />
|
||||
<Tabs value={props.activeTab()} onChange={props.openTab}>
|
||||
<div class="sticky top-0 shrink-0 flex">
|
||||
<Tabs.List
|
||||
ref={(el: HTMLDivElement) => {
|
||||
const stop = createFileTabListSync({ el, contextOpen: props.contextOpen })
|
||||
onCleanup(stop)
|
||||
}}
|
||||
>
|
||||
<Show when={props.reviewTab}>
|
||||
<Tabs.Trigger value="review" classes={{ button: "!pl-6" }}>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div>{props.language.t("session.tab.review")}</div>
|
||||
<Show when={props.hasReview}>
|
||||
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
|
||||
{props.reviewCount}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<Show when={props.contextOpen()}>
|
||||
<Tabs.Trigger
|
||||
value="context"
|
||||
closeButton={
|
||||
<Tooltip value={props.language.t("common.closeTab")} placement="bottom">
|
||||
<IconButton
|
||||
icon="close-small"
|
||||
variant="ghost"
|
||||
class="h-5 w-5"
|
||||
onClick={() => props.tabs().close("context")}
|
||||
aria-label={props.language.t("common.closeTab")}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
hideCloseButton
|
||||
onMiddleClick={() => props.tabs().close("context")}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<SessionContextUsage variant="indicator" />
|
||||
<div>{props.language.t("session.tab.context")}</div>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<SortableProvider ids={props.openedTabs()}>
|
||||
<For each={props.openedTabs()}>
|
||||
{(tab) => <SortableTab tab={tab} onTabClose={props.tabs().close} />}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
<StickyAddButton>
|
||||
<TooltipKeybind
|
||||
title={props.language.t("command.file.open")}
|
||||
keybind={props.command.keybind("file.open")}
|
||||
class="flex items-center"
|
||||
>
|
||||
<IconButton
|
||||
icon="plus-small"
|
||||
variant="ghost"
|
||||
iconSize="large"
|
||||
onClick={() =>
|
||||
props.dialog.show(() => <DialogSelectFile mode="files" onOpenFile={props.showAllFiles} />)
|
||||
<Show when={props.reviewOpen}>
|
||||
<div class="flex-1 min-w-0 h-full">
|
||||
<Show
|
||||
when={props.layout.fileTree.opened() && props.fileTreeTab() === "changes"}
|
||||
fallback={
|
||||
<DragDropProvider
|
||||
onDragStart={props.onDragStart}
|
||||
onDragEnd={props.onDragEnd}
|
||||
onDragOver={props.onDragOver}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragYAxis />
|
||||
<Tabs value={props.activeTab()} onChange={props.openTab}>
|
||||
<div class="sticky top-0 shrink-0 flex">
|
||||
<Tabs.List
|
||||
ref={(el: HTMLDivElement) => {
|
||||
const stop = createFileTabListSync({ el, contextOpen: props.contextOpen })
|
||||
onCleanup(stop)
|
||||
}}
|
||||
>
|
||||
<Show when={props.reviewTab}>
|
||||
<Tabs.Trigger value="review" classes={{ button: "!pl-6" }}>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div>{props.language.t("session.tab.review")}</div>
|
||||
<Show when={props.hasReview}>
|
||||
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
|
||||
{props.reviewCount}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<Show when={props.contextOpen()}>
|
||||
<Tabs.Trigger
|
||||
value="context"
|
||||
closeButton={
|
||||
<Tooltip value={props.language.t("common.closeTab")} placement="bottom">
|
||||
<IconButton
|
||||
icon="close-small"
|
||||
variant="ghost"
|
||||
class="h-5 w-5"
|
||||
onClick={() => props.tabs().close("context")}
|
||||
aria-label={props.language.t("common.closeTab")}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
aria-label={props.language.t("command.file.open")}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</StickyAddButton>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
hideCloseButton
|
||||
onMiddleClick={() => props.tabs().close("context")}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<SessionContextUsage variant="indicator" />
|
||||
<div>{props.language.t("session.tab.context")}</div>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<SortableProvider ids={props.openedTabs()}>
|
||||
<For each={props.openedTabs()}>
|
||||
{(tab) => <SortableTab tab={tab} onTabClose={props.tabs().close} />}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
<StickyAddButton>
|
||||
<TooltipKeybind
|
||||
title={props.language.t("command.file.open")}
|
||||
keybind={props.command.keybind("file.open")}
|
||||
class="flex items-center"
|
||||
>
|
||||
<IconButton
|
||||
icon="plus-small"
|
||||
variant="ghost"
|
||||
iconSize="large"
|
||||
onClick={() =>
|
||||
props.dialog.show(() => (
|
||||
<DialogSelectFile mode="files" onOpenFile={props.showAllFiles} />
|
||||
))
|
||||
}
|
||||
aria-label={props.language.t("command.file.open")}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</StickyAddButton>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
|
||||
<Show when={props.reviewTab}>
|
||||
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||
<Show when={props.activeTab() === "review"}>{props.reviewPanel()}</Show>
|
||||
</Tabs.Content>
|
||||
</Show>
|
||||
|
||||
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||
<Show when={props.activeTab() === "empty"}>
|
||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||
<div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
|
||||
<Mark class="w-14 opacity-10" />
|
||||
<div class="text-14-regular text-text-weak max-w-56">
|
||||
{props.language.t("session.files.selectToOpen")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={props.reviewTab}>
|
||||
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||
<Show when={props.activeTab() === "review"}>{props.reviewPanel()}</Show>
|
||||
</Tabs.Content>
|
||||
</Show>
|
||||
</Tabs.Content>
|
||||
|
||||
<Show when={props.contextOpen()}>
|
||||
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||
<Show when={props.activeTab() === "context"}>
|
||||
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||
<Show when={props.activeTab() === "empty"}>
|
||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||
<SessionContextTab
|
||||
messages={props.messages as never}
|
||||
visibleUserMessages={props.visibleUserMessages as never}
|
||||
view={props.view as never}
|
||||
info={props.info as never}
|
||||
/>
|
||||
<div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
|
||||
<Mark class="w-14 opacity-10" />
|
||||
<div class="text-14-regular text-text-weak max-w-56">
|
||||
{props.language.t("session.files.selectToOpen")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Tabs.Content>
|
||||
</Show>
|
||||
|
||||
<Show when={props.activeFileTab()} keyed>
|
||||
{(tab) => (
|
||||
<FileTabContent
|
||||
tab={tab}
|
||||
activeTab={props.activeTab}
|
||||
tabs={props.tabs}
|
||||
view={props.view}
|
||||
handoffFiles={props.handoffFiles}
|
||||
file={props.file}
|
||||
comments={props.comments}
|
||||
language={props.language}
|
||||
codeComponent={props.codeComponent}
|
||||
addCommentToContext={props.addCommentToContext}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</Tabs>
|
||||
<DragOverlay>
|
||||
<Show when={props.activeDraggable()}>
|
||||
{(tab) => {
|
||||
const path = createMemo(() => props.file.pathFromTab(tab()))
|
||||
return (
|
||||
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
|
||||
<Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
}
|
||||
>
|
||||
{props.reviewPanel()}
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={props.contextOpen()}>
|
||||
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
|
||||
<Show when={props.activeTab() === "context"}>
|
||||
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||
<SessionContextTab
|
||||
messages={props.messages as never}
|
||||
visibleUserMessages={props.visibleUserMessages as never}
|
||||
view={props.view as never}
|
||||
info={props.info as never}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</Tabs.Content>
|
||||
</Show>
|
||||
|
||||
<Show when={props.activeFileTab()} keyed>
|
||||
{(tab) => (
|
||||
<FileTabContent
|
||||
tab={tab}
|
||||
activeTab={props.activeTab}
|
||||
tabs={props.tabs}
|
||||
view={props.view}
|
||||
handoffFiles={props.handoffFiles}
|
||||
file={props.file}
|
||||
comments={props.comments}
|
||||
language={props.language}
|
||||
codeComponent={props.codeComponent}
|
||||
addCommentToContext={props.addCommentToContext}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</Tabs>
|
||||
<DragOverlay>
|
||||
<Show when={props.activeDraggable()}>
|
||||
{(tab) => {
|
||||
const path = createMemo(() => props.file.pathFromTab(tab()))
|
||||
return (
|
||||
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
|
||||
<Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
}
|
||||
>
|
||||
{props.reviewPanel()}
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.layout.fileTree.opened()}>
|
||||
<div
|
||||
@@ -230,7 +240,10 @@ export function SessionSidePanel(props: {
|
||||
class="relative shrink-0 h-full"
|
||||
style={{ width: `${props.layout.fileTree.width()}px` }}
|
||||
>
|
||||
<div class="h-full border-l border-border-weak-base flex flex-col overflow-hidden group/filetree">
|
||||
<div
|
||||
class="h-full flex flex-col overflow-hidden group/filetree"
|
||||
classList={{ "border-l border-border-weak-base": props.reviewOpen }}
|
||||
>
|
||||
<Tabs
|
||||
variant="pill"
|
||||
value={props.fileTreeTab()}
|
||||
|
||||
@@ -139,11 +139,8 @@ export const useSessionCommands = (input: {
|
||||
title: input.language.t("command.fileTree.toggle"),
|
||||
description: "",
|
||||
category: input.language.t("command.category.view"),
|
||||
onSelect: () => {
|
||||
const opening = !input.layout.fileTree.opened()
|
||||
if (opening && !input.view().reviewPanel.opened()) input.view().reviewPanel.open()
|
||||
input.layout.fileTree.toggle()
|
||||
},
|
||||
keybind: "mod+\\",
|
||||
onSelect: () => input.layout.fileTree.toggle(),
|
||||
},
|
||||
{
|
||||
id: "terminal.new",
|
||||
|
||||
@@ -20,7 +20,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) =>
|
||||
const isBedrockModelArn = providerModel.startsWith("arn:aws:bedrock:")
|
||||
const isBedrockModelID = providerModel.startsWith("global.anthropic.")
|
||||
const isBedrock = isBedrockModelArn || isBedrockModelID
|
||||
const isSonnet = reqModel.includes("sonnet")
|
||||
const supports1m = reqModel.includes("sonnet") || reqModel.includes("opus-4-6")
|
||||
return {
|
||||
format: "anthropic",
|
||||
modifyUrl: (providerApi: string, isStream?: boolean) =>
|
||||
@@ -33,7 +33,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) =>
|
||||
} else {
|
||||
headers.set("x-api-key", apiKey)
|
||||
headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01")
|
||||
if (body.model.startsWith("claude-sonnet-")) {
|
||||
if (supports1m) {
|
||||
headers.set("anthropic-beta", "context-1m-2025-08-07")
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) =>
|
||||
...(isBedrock
|
||||
? {
|
||||
anthropic_version: "bedrock-2023-05-31",
|
||||
anthropic_beta: isSonnet ? "context-1m-2025-08-07" : undefined,
|
||||
anthropic_beta: supports1m ? "context-1m-2025-08-07" : undefined,
|
||||
model: undefined,
|
||||
stream: undefined,
|
||||
}
|
||||
|
||||
34
packages/console/core/script/disable-reload.ts
Normal file
34
packages/console/core/script/disable-reload.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Database, eq } from "../src/drizzle/index.js"
|
||||
import { BillingTable } from "../src/schema/billing.sql.js"
|
||||
import { WorkspaceTable } from "../src/schema/workspace.sql.js"
|
||||
|
||||
const workspaceID = process.argv[2]
|
||||
|
||||
if (!workspaceID) {
|
||||
console.error("Usage: bun disable-reload.ts <workspaceID>")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const billing = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ reload: BillingTable.reload })
|
||||
.from(BillingTable)
|
||||
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, BillingTable.workspaceID))
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
if (!billing) {
|
||||
console.error("Error: Workspace or billing record not found")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!billing.reload) {
|
||||
console.log(`Reload is already disabled for workspace ${workspaceID}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
await Database.use((tx) =>
|
||||
tx.update(BillingTable).set({ reload: false }).where(eq(BillingTable.workspaceID, workspaceID)),
|
||||
)
|
||||
|
||||
console.log(`Disabled reload for workspace ${workspaceID}`)
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from "@agentclientprotocol/sdk"
|
||||
|
||||
import { Log } from "../util/log"
|
||||
import { pathToFileURL } from "bun"
|
||||
import { ACPSessionManager } from "./session"
|
||||
import type { ACPConfig } from "./types"
|
||||
import { Provider } from "../provider/provider"
|
||||
@@ -986,7 +987,7 @@ export namespace ACP {
|
||||
type: "image",
|
||||
mimeType: effectiveMime,
|
||||
data: base64Data,
|
||||
uri: `file://${filename}`,
|
||||
uri: pathToFileURL(filename).href,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -996,13 +997,14 @@ export namespace ACP {
|
||||
} else {
|
||||
// Non-image: text types get decoded, binary types stay as blob
|
||||
const isText = effectiveMime.startsWith("text/") || effectiveMime === "application/json"
|
||||
const fileUri = pathToFileURL(filename).href
|
||||
const resource = isText
|
||||
? {
|
||||
uri: `file://${filename}`,
|
||||
uri: fileUri,
|
||||
mimeType: effectiveMime,
|
||||
text: Buffer.from(base64Data, "base64").toString("utf-8"),
|
||||
}
|
||||
: { uri: `file://${filename}`, mimeType: effectiveMime, blob: base64Data }
|
||||
: { uri: fileUri, mimeType: effectiveMime, blob: base64Data }
|
||||
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
@@ -1544,7 +1546,7 @@ export namespace ACP {
|
||||
const name = path.split("/").pop() || path
|
||||
return {
|
||||
type: "file",
|
||||
url: `file://${path}`,
|
||||
url: pathToFileURL(path).href,
|
||||
filename: name,
|
||||
mime: "text/plain",
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Argv } from "yargs"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "bun"
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import { Flag } from "../../flag/flag"
|
||||
@@ -236,6 +237,10 @@ export const RunCommand = cmd({
|
||||
describe: "session id to continue",
|
||||
type: "string",
|
||||
})
|
||||
.option("fork", {
|
||||
describe: "fork the session before continuing (requires --continue or --session)",
|
||||
type: "boolean",
|
||||
})
|
||||
.option("share", {
|
||||
type: "boolean",
|
||||
describe: "share the session",
|
||||
@@ -310,7 +315,7 @@ export const RunCommand = cmd({
|
||||
|
||||
files.push({
|
||||
type: "file",
|
||||
url: `file://${resolvedPath}`,
|
||||
url: pathToFileURL(resolvedPath).href,
|
||||
filename: path.basename(resolvedPath),
|
||||
mime,
|
||||
})
|
||||
@@ -324,6 +329,11 @@ export const RunCommand = cmd({
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (args.fork && !args.continue && !args.session) {
|
||||
UI.error("--fork requires --continue or --session")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const rules: PermissionNext.Ruleset = [
|
||||
{
|
||||
permission: "question",
|
||||
@@ -349,11 +359,15 @@ export const RunCommand = cmd({
|
||||
}
|
||||
|
||||
async function session(sdk: OpencodeClient) {
|
||||
if (args.continue) {
|
||||
const result = await sdk.session.list()
|
||||
return result.data?.find((s) => !s.parentID)?.id
|
||||
const baseID = args.continue ? (await sdk.session.list()).data?.find((s) => !s.parentID)?.id : args.session
|
||||
|
||||
if (baseID && args.fork) {
|
||||
const forked = await sdk.session.fork({ sessionID: baseID })
|
||||
return forked.data?.id
|
||||
}
|
||||
if (args.session) return args.session
|
||||
|
||||
if (baseID) return baseID
|
||||
|
||||
const name = title()
|
||||
const result = await sdk.session.create({ title: name, permission: rules })
|
||||
return result.data?.id
|
||||
|
||||
@@ -250,7 +250,8 @@ function App() {
|
||||
})
|
||||
local.model.set({ providerID, modelID }, { recent: true })
|
||||
}
|
||||
if (args.sessionID) {
|
||||
// Handle --session without --fork immediately (fork is handled in createEffect below)
|
||||
if (args.sessionID && !args.fork) {
|
||||
route.navigate({
|
||||
type: "session",
|
||||
sessionID: args.sessionID,
|
||||
@@ -268,10 +269,36 @@ function App() {
|
||||
.find((x) => x.parentID === undefined)?.id
|
||||
if (match) {
|
||||
continued = true
|
||||
route.navigate({ type: "session", sessionID: match })
|
||||
if (args.fork) {
|
||||
sdk.client.session.fork({ sessionID: match }).then((result) => {
|
||||
if (result.data?.id) {
|
||||
route.navigate({ type: "session", sessionID: result.data.id })
|
||||
} else {
|
||||
toast.show({ message: "Failed to fork session", variant: "error" })
|
||||
}
|
||||
})
|
||||
} else {
|
||||
route.navigate({ type: "session", sessionID: match })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Handle --session with --fork: wait for sync to be fully complete before forking
|
||||
// (session list loads in non-blocking phase for --session, so we must wait for "complete"
|
||||
// to avoid a race where reconcile overwrites the newly forked session)
|
||||
let forked = false
|
||||
createEffect(() => {
|
||||
if (forked || sync.status !== "complete" || !args.sessionID || !args.fork) return
|
||||
forked = true
|
||||
sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => {
|
||||
if (result.data?.id) {
|
||||
route.navigate({ type: "session", sessionID: result.data.id })
|
||||
} else {
|
||||
toast.show({ message: "Failed to fork session", variant: "error" })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => sync.status === "complete" && sync.data.provider.length === 0,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { fileURLToPath } from "bun"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
@@ -19,7 +20,7 @@ export function DialogStatus() {
|
||||
const list = sync.data.config.plugin ?? []
|
||||
const result = list.map((value) => {
|
||||
if (value.startsWith("file://")) {
|
||||
const path = value.substring("file://".length)
|
||||
const path = fileURLToPath(value)
|
||||
const parts = path.split("/")
|
||||
const filename = parts.pop() || path
|
||||
if (!filename.includes(".")) return { name: filename }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BoxRenderable, TextareaRenderable, KeyEvent, ScrollBoxRenderable } from "@opentui/core"
|
||||
import { pathToFileURL } from "bun"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { firstBy } from "remeda"
|
||||
import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Show, createSignal } from "solid-js"
|
||||
@@ -246,17 +247,17 @@ export function Autocomplete(props: {
|
||||
const width = props.anchor().width - 4
|
||||
options.push(
|
||||
...sortedFiles.map((item): AutocompleteOption => {
|
||||
let url = `file://${process.cwd()}/${item}`
|
||||
const fullPath = `${process.cwd()}/${item}`
|
||||
const urlObj = pathToFileURL(fullPath)
|
||||
let filename = item
|
||||
if (lineRange && !item.endsWith("/")) {
|
||||
filename = `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}`
|
||||
const urlObj = new URL(url)
|
||||
urlObj.searchParams.set("start", String(lineRange.startLine))
|
||||
if (lineRange.endLine !== undefined) {
|
||||
urlObj.searchParams.set("end", String(lineRange.endLine))
|
||||
}
|
||||
url = urlObj.toString()
|
||||
}
|
||||
const url = urlObj.href
|
||||
|
||||
const isDir = item.endsWith("/")
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface Args {
|
||||
prompt?: string
|
||||
continue?: boolean
|
||||
sessionID?: string
|
||||
fork?: boolean
|
||||
}
|
||||
|
||||
export const { use: useArgs, provider: ArgsProvider } = createSimpleContext({
|
||||
|
||||
@@ -64,6 +64,10 @@ export const TuiThreadCommand = cmd({
|
||||
type: "string",
|
||||
describe: "session id to continue",
|
||||
})
|
||||
.option("fork", {
|
||||
type: "boolean",
|
||||
describe: "fork the session when continuing (use with --continue or --session)",
|
||||
})
|
||||
.option("prompt", {
|
||||
type: "string",
|
||||
describe: "prompt to use",
|
||||
@@ -73,6 +77,11 @@ export const TuiThreadCommand = cmd({
|
||||
describe: "agent to use",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
if (args.fork && !args.continue && !args.session) {
|
||||
UI.error("--fork requires --continue or --session")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
|
||||
const baseCwd = process.env.PWD ?? process.cwd()
|
||||
const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
|
||||
@@ -150,6 +159,7 @@ export const TuiThreadCommand = cmd({
|
||||
agent: args.agent,
|
||||
model: args.model,
|
||||
prompt,
|
||||
fork: args.fork,
|
||||
},
|
||||
onExit: async () => {
|
||||
await client.call("shutdown", undefined)
|
||||
|
||||
@@ -33,6 +33,8 @@ import { proxied } from "@/util/proxied"
|
||||
import { iife } from "@/util/iife"
|
||||
|
||||
export namespace Config {
|
||||
const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
|
||||
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
// Managed settings directory for enterprise deployments (highest priority, admin-controlled)
|
||||
@@ -653,7 +655,7 @@ export namespace Config {
|
||||
template: z.string(),
|
||||
description: z.string().optional(),
|
||||
agent: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
model: ModelId.optional(),
|
||||
subtask: z.boolean().optional(),
|
||||
})
|
||||
export type Command = z.infer<typeof Command>
|
||||
@@ -669,7 +671,7 @@ export namespace Config {
|
||||
|
||||
export const Agent = z
|
||||
.object({
|
||||
model: z.string().optional(),
|
||||
model: ModelId.optional(),
|
||||
variant: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -1040,11 +1042,10 @@ export namespace Config {
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("When set, ONLY these providers will be enabled. All other providers will be ignored"),
|
||||
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
|
||||
small_model: z
|
||||
.string()
|
||||
.describe("Small model to use for tasks like title generation in the format of provider/model")
|
||||
.optional(),
|
||||
model: ModelId.describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
|
||||
small_model: ModelId.describe(
|
||||
"Small model to use for tasks like title generation in the format of provider/model",
|
||||
).optional(),
|
||||
default_agent: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Bus } from "@/bus"
|
||||
import { Log } from "../util/log"
|
||||
import { LSPClient } from "./client"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { pathToFileURL, fileURLToPath } from "url"
|
||||
import { LSPServer } from "./server"
|
||||
import z from "zod"
|
||||
import { Config } from "../config/config"
|
||||
@@ -369,7 +369,7 @@ export namespace LSP {
|
||||
}
|
||||
|
||||
export async function documentSymbol(uri: string) {
|
||||
const file = new URL(uri).pathname
|
||||
const file = fileURLToPath(uri)
|
||||
return run(file, (client) =>
|
||||
client.connection
|
||||
.sendRequest("textDocument/documentSymbol", {
|
||||
|
||||
@@ -646,6 +646,7 @@ export namespace ProviderTransform {
|
||||
if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) {
|
||||
if (!input.model.api.id.includes("gpt-5-pro")) {
|
||||
result["reasoningEffort"] = "medium"
|
||||
result["reasoningSummary"] = "auto"
|
||||
}
|
||||
|
||||
// Only set textVerbosity for non-chat gpt-5.x models
|
||||
|
||||
@@ -539,7 +539,7 @@ export const SessionRoutes = lazy(() =>
|
||||
},
|
||||
auto: body.auto,
|
||||
})
|
||||
await SessionPrompt.loop(sessionID)
|
||||
await SessionPrompt.loop({ sessionID })
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -108,6 +108,7 @@ export namespace SessionCompaction {
|
||||
sessionID: input.sessionID,
|
||||
mode: "compaction",
|
||||
agent: "compaction",
|
||||
variant: userMessage.variant,
|
||||
summary: true,
|
||||
path: {
|
||||
cwd: Instance.directory,
|
||||
|
||||
@@ -387,6 +387,7 @@ export namespace MessageV2 {
|
||||
write: z.number(),
|
||||
}),
|
||||
}),
|
||||
variant: z.string().optional(),
|
||||
finish: z.string().optional(),
|
||||
}).meta({
|
||||
ref: "AssistantMessage",
|
||||
|
||||
@@ -32,7 +32,7 @@ import { Flag } from "../flag/flag"
|
||||
import { ulid } from "ulid"
|
||||
import { spawn } from "child_process"
|
||||
import { Command } from "../command"
|
||||
import { $, fileURLToPath } from "bun"
|
||||
import { $, fileURLToPath, pathToFileURL } from "bun"
|
||||
import { ConfigMarkdown } from "../config/markdown"
|
||||
import { SessionSummary } from "./summary"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
@@ -174,7 +174,7 @@ export namespace SessionPrompt {
|
||||
return message
|
||||
}
|
||||
|
||||
return loop(input.sessionID)
|
||||
return loop({ sessionID: input.sessionID })
|
||||
})
|
||||
|
||||
export async function resolvePromptParts(template: string): Promise<PromptInput["parts"]> {
|
||||
@@ -210,7 +210,7 @@ export namespace SessionPrompt {
|
||||
if (stats.isDirectory()) {
|
||||
parts.push({
|
||||
type: "file",
|
||||
url: `file://${filepath}`,
|
||||
url: pathToFileURL(filepath).href,
|
||||
filename: name,
|
||||
mime: "application/x-directory",
|
||||
})
|
||||
@@ -219,7 +219,7 @@ export namespace SessionPrompt {
|
||||
|
||||
parts.push({
|
||||
type: "file",
|
||||
url: `file://${filepath}`,
|
||||
url: pathToFileURL(filepath).href,
|
||||
filename: name,
|
||||
mime: "text/plain",
|
||||
})
|
||||
@@ -239,6 +239,13 @@ export namespace SessionPrompt {
|
||||
return controller.signal
|
||||
}
|
||||
|
||||
function resume(sessionID: string) {
|
||||
const s = state()
|
||||
if (!s[sessionID]) return
|
||||
|
||||
return s[sessionID].abort.signal
|
||||
}
|
||||
|
||||
export function cancel(sessionID: string) {
|
||||
log.info("cancel", { sessionID })
|
||||
const s = state()
|
||||
@@ -253,8 +260,14 @@ export namespace SessionPrompt {
|
||||
return
|
||||
}
|
||||
|
||||
export const loop = fn(Identifier.schema("session"), async (sessionID) => {
|
||||
const abort = start(sessionID)
|
||||
export const LoopInput = z.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
resume_existing: z.boolean().optional(),
|
||||
})
|
||||
export const loop = fn(LoopInput, async (input) => {
|
||||
const { sessionID, resume_existing } = input
|
||||
|
||||
const abort = resume_existing ? resume(sessionID) : start(sessionID)
|
||||
if (!abort) {
|
||||
return new Promise<MessageV2.WithParts>((resolve, reject) => {
|
||||
const callbacks = state()[sessionID].callbacks
|
||||
@@ -323,6 +336,7 @@ export namespace SessionPrompt {
|
||||
sessionID,
|
||||
mode: task.agent,
|
||||
agent: task.agent,
|
||||
variant: lastUser.variant,
|
||||
path: {
|
||||
cwd: Instance.directory,
|
||||
root: Instance.worktree,
|
||||
@@ -526,6 +540,7 @@ export namespace SessionPrompt {
|
||||
role: "assistant",
|
||||
mode: agent.name,
|
||||
agent: agent.name,
|
||||
variant: lastUser.variant,
|
||||
path: {
|
||||
cwd: Instance.directory,
|
||||
root: Instance.worktree,
|
||||
@@ -1366,7 +1381,19 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
if (!abort) {
|
||||
throw new Session.BusyError(input.sessionID)
|
||||
}
|
||||
using _ = defer(() => cancel(input.sessionID))
|
||||
|
||||
using _ = defer(() => {
|
||||
// If no queued callbacks, cancel (the default)
|
||||
const callbacks = state()[input.sessionID]?.callbacks ?? []
|
||||
if (callbacks.length === 0) {
|
||||
cancel(input.sessionID)
|
||||
} else {
|
||||
// Otherwise, trigger the session loop to process queued items
|
||||
loop({ sessionID: input.sessionID, resume_existing: true }).catch((error) => {
|
||||
log.error("session loop failed to resume after shell command", { sessionID: input.sessionID, error })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const session = await Session.get(input.sessionID)
|
||||
if (session.revert) {
|
||||
|
||||
@@ -411,8 +411,13 @@ export namespace Worktree {
|
||||
if (key === directory) return item
|
||||
}
|
||||
})()
|
||||
|
||||
if (!entry?.path) {
|
||||
throw new RemoveFailedError({ message: "Worktree not found" })
|
||||
const directoryExists = await exists(directory)
|
||||
if (directoryExists) {
|
||||
await fs.rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const removed = await $`git worktree remove --force ${entry.path}`.quiet().nothrow().cwd(Instance.worktree)
|
||||
|
||||
56
packages/opencode/test/session/prompt-special-chars.test.ts
Normal file
56
packages/opencode/test/session/prompt-special-chars.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import path from "path"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { fileURLToPath } from "url"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Log } from "../../src/util/log"
|
||||
import { Session } from "../../src/session"
|
||||
import { SessionPrompt } from "../../src/session/prompt"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
Log.init({ print: false })
|
||||
|
||||
describe("session.prompt special characters", () => {
|
||||
test("handles filenames with # character", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "file#name.txt"), "special content\n")
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const template = "Read @file#name.txt"
|
||||
const parts = await SessionPrompt.resolvePromptParts(template)
|
||||
const fileParts = parts.filter((part) => part.type === "file")
|
||||
|
||||
expect(fileParts.length).toBe(1)
|
||||
expect(fileParts[0].filename).toBe("file#name.txt")
|
||||
|
||||
// Verify the URL is properly encoded (# should be %23)
|
||||
expect(fileParts[0].url).toContain("%23")
|
||||
|
||||
// Verify the URL can be correctly converted back to a file path
|
||||
const decodedPath = fileURLToPath(fileParts[0].url)
|
||||
expect(decodedPath).toBe(path.join(tmp.path, "file#name.txt"))
|
||||
|
||||
const message = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
parts,
|
||||
noReply: true,
|
||||
})
|
||||
const stored = await MessageV2.get({ sessionID: session.id, messageID: message.info.id })
|
||||
|
||||
// Verify the file content was read correctly
|
||||
const textParts = stored.parts.filter((part) => part.type === "text")
|
||||
const hasContent = textParts.some((part) => part.text.includes("special content"))
|
||||
expect(hasContent).toBe(true)
|
||||
|
||||
await Session.remove(session.id)
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
60
packages/opencode/test/skill/discovery.test.ts
Normal file
60
packages/opencode/test/skill/discovery.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { Discovery } from "../../src/skill/discovery"
|
||||
import path from "path"
|
||||
|
||||
const CLOUDFLARE_SKILLS_URL = "https://developers.cloudflare.com/.well-known/skills/"
|
||||
|
||||
describe("Discovery.pull", () => {
|
||||
test("downloads skills from cloudflare url", async () => {
|
||||
const dirs = await Discovery.pull(CLOUDFLARE_SKILLS_URL)
|
||||
expect(dirs.length).toBeGreaterThan(0)
|
||||
for (const dir of dirs) {
|
||||
expect(dir).toStartWith(Discovery.dir())
|
||||
const md = path.join(dir, "SKILL.md")
|
||||
expect(await Bun.file(md).exists()).toBe(true)
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
test("url without trailing slash works", async () => {
|
||||
const dirs = await Discovery.pull(CLOUDFLARE_SKILLS_URL.replace(/\/$/, ""))
|
||||
expect(dirs.length).toBeGreaterThan(0)
|
||||
for (const dir of dirs) {
|
||||
const md = path.join(dir, "SKILL.md")
|
||||
expect(await Bun.file(md).exists()).toBe(true)
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
test("returns empty array for invalid url", async () => {
|
||||
const dirs = await Discovery.pull("https://example.invalid/.well-known/skills/")
|
||||
expect(dirs).toEqual([])
|
||||
})
|
||||
|
||||
test("returns empty array for non-json response", async () => {
|
||||
const dirs = await Discovery.pull("https://example.com/")
|
||||
expect(dirs).toEqual([])
|
||||
})
|
||||
|
||||
test("downloads reference files alongside SKILL.md", async () => {
|
||||
const dirs = await Discovery.pull(CLOUDFLARE_SKILLS_URL)
|
||||
// find a skill dir that should have reference files (e.g. agents-sdk)
|
||||
const agentsSdk = dirs.find((d) => d.endsWith("/agents-sdk"))
|
||||
if (agentsSdk) {
|
||||
const refs = path.join(agentsSdk, "references")
|
||||
expect(await Bun.file(path.join(agentsSdk, "SKILL.md")).exists()).toBe(true)
|
||||
// agents-sdk has reference files per the index
|
||||
const refDir = await Array.fromAsync(new Bun.Glob("**/*.md").scan({ cwd: refs, onlyFiles: true }))
|
||||
expect(refDir.length).toBeGreaterThan(0)
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
test("caches downloaded files on second pull", async () => {
|
||||
// first pull to populate cache
|
||||
const first = await Discovery.pull(CLOUDFLARE_SKILLS_URL)
|
||||
expect(first.length).toBeGreaterThan(0)
|
||||
|
||||
// second pull should return same results from cache
|
||||
const second = await Discovery.pull(CLOUDFLARE_SKILLS_URL)
|
||||
expect(second.length).toBe(first.length)
|
||||
expect(second.sort()).toEqual(first.sort())
|
||||
}, 60_000)
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createOpencodeClient, createOpencodeServer } from "@opencode-ai/sdk"
|
||||
import { pathToFileURL } from "bun"
|
||||
|
||||
const server = await createOpencodeServer()
|
||||
const client = createOpencodeClient({ baseUrl: server.url })
|
||||
@@ -17,7 +18,7 @@ for await (const file of input) {
|
||||
{
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
url: `file://${file}`,
|
||||
url: pathToFileURL(file).href,
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
@@ -41,7 +42,7 @@ await Promise.all(
|
||||
{
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
url: `file://${file}`,
|
||||
url: pathToFileURL(file).href,
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
|
||||
@@ -197,6 +197,7 @@ export type AssistantMessage = {
|
||||
write: number
|
||||
}
|
||||
}
|
||||
variant?: string
|
||||
finish?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -6392,6 +6392,9 @@
|
||||
},
|
||||
"required": ["input", "output", "reasoning", "cache"]
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"finish": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -181,6 +181,8 @@
|
||||
border-collapse: collapse;
|
||||
margin: 1.5rem 0;
|
||||
font-size: var(--font-size-base);
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
th,
|
||||
|
||||
@@ -166,6 +166,10 @@
|
||||
color: var(--icon-diff-delete-base);
|
||||
}
|
||||
|
||||
[data-slot="session-review-change"][data-type="modified"] {
|
||||
color: var(--icon-diff-modified-base);
|
||||
}
|
||||
|
||||
[data-slot="session-review-file-container"] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -332,8 +332,9 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
const beforeText = () => (typeof diff.before === "string" ? diff.before : "")
|
||||
const afterText = () => (typeof diff.after === "string" ? diff.after : "")
|
||||
|
||||
const isAdded = () => beforeText().length === 0 && afterText().length > 0
|
||||
const isDeleted = () => afterText().length === 0 && beforeText().length > 0
|
||||
const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0)
|
||||
const isDeleted = () =>
|
||||
diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
|
||||
const isImage = () => isImageFile(diff.file)
|
||||
const isAudio = () => isAudioFile(diff.file)
|
||||
|
||||
@@ -422,6 +423,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
if (!isImage()) return
|
||||
if (imageSrc()) return
|
||||
if (imageStatus() !== "idle") return
|
||||
if (isDeleted()) return
|
||||
|
||||
const reader = props.readFile
|
||||
if (!reader) return
|
||||
@@ -546,6 +548,11 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
{i18n.t("ui.sessionReview.change.removed")}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={isImage()}>
|
||||
<span data-slot="session-review-change" data-type="modified">
|
||||
{i18n.t("ui.sessionReview.change.modified")}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<DiffChanges changes={diff} />
|
||||
</Match>
|
||||
@@ -564,28 +571,51 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
scheduleAnchors()
|
||||
}}
|
||||
>
|
||||
<Dynamic
|
||||
component={diffComponent}
|
||||
preloadedDiff={diff.preloaded}
|
||||
diffStyle={diffStyle()}
|
||||
onRendered={() => {
|
||||
props.onDiffRendered?.()
|
||||
scheduleAnchors()
|
||||
}}
|
||||
enableLineSelection={props.onLineComment != null}
|
||||
onLineSelected={handleLineSelected}
|
||||
onLineSelectionEnd={handleLineSelectionEnd}
|
||||
selectedLines={selectedLines()}
|
||||
commentedLines={commentedLines()}
|
||||
before={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.before === "string" ? diff.before : "",
|
||||
}}
|
||||
after={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.after === "string" ? diff.after : "",
|
||||
}}
|
||||
/>
|
||||
<Switch>
|
||||
<Match when={isImage() && imageSrc()}>
|
||||
<div data-slot="session-review-image-container">
|
||||
<img data-slot="session-review-image" src={imageSrc()} alt={diff.file} />
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isImage() && isDeleted()}>
|
||||
<div data-slot="session-review-image-container" data-removed>
|
||||
<span data-slot="session-review-image-placeholder">
|
||||
{i18n.t("ui.sessionReview.change.removed")}
|
||||
</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isImage() && !imageSrc()}>
|
||||
<div data-slot="session-review-image-container">
|
||||
<span data-slot="session-review-image-placeholder">
|
||||
{imageStatus() === "loading" ? "Loading..." : "Image"}
|
||||
</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={!isImage()}>
|
||||
<Dynamic
|
||||
component={diffComponent}
|
||||
preloadedDiff={diff.preloaded}
|
||||
diffStyle={diffStyle()}
|
||||
onRendered={() => {
|
||||
props.onDiffRendered?.()
|
||||
scheduleAnchors()
|
||||
}}
|
||||
enableLineSelection={props.onLineComment != null}
|
||||
onLineSelected={handleLineSelected}
|
||||
onLineSelectionEnd={handleLineSelectionEnd}
|
||||
selectedLines={selectedLines()}
|
||||
commentedLines={commentedLines()}
|
||||
before={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.before === "string" ? diff.before : "",
|
||||
}}
|
||||
after={{
|
||||
name: diff.file!,
|
||||
contents: typeof diff.after === "string" ? diff.after : "",
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
<For each={comments()}>
|
||||
{(comment) => (
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
|
||||
"ui.sessionReview.change.added": "مضاف",
|
||||
"ui.sessionReview.change.removed": "محذوف",
|
||||
"ui.sessionReview.change.modified": "معدل",
|
||||
"ui.lineComment.label.prefix": "تعليق على ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "جارٍ التعليق على ",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
|
||||
"ui.sessionReview.change.added": "Adicionado",
|
||||
"ui.sessionReview.change.removed": "Removido",
|
||||
"ui.sessionReview.change.modified": "Modificado",
|
||||
"ui.lineComment.label.prefix": "Comentar em ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "Comentando em ",
|
||||
|
||||
@@ -11,6 +11,7 @@ export const dict = {
|
||||
"ui.sessionReview.collapseAll": "Sažmi sve",
|
||||
"ui.sessionReview.change.added": "Dodano",
|
||||
"ui.sessionReview.change.removed": "Uklonjeno",
|
||||
"ui.sessionReview.change.modified": "Izmijenjeno",
|
||||
|
||||
"ui.lineComment.label.prefix": "Komentar na ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
|
||||
"ui.sessionReview.change.added": "Tilføjet",
|
||||
"ui.sessionReview.change.removed": "Fjernet",
|
||||
"ui.sessionReview.change.modified": "Ændret",
|
||||
"ui.lineComment.label.prefix": "Kommenter på ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "Kommenterer på ",
|
||||
|
||||
@@ -12,6 +12,7 @@ export const dict = {
|
||||
|
||||
"ui.sessionReview.change.added": "Hinzugefügt",
|
||||
"ui.sessionReview.change.removed": "Entfernt",
|
||||
"ui.sessionReview.change.modified": "Geändert",
|
||||
"ui.lineComment.label.prefix": "Kommentar zu ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "Kommentiere ",
|
||||
|
||||
@@ -7,6 +7,7 @@ export const dict = {
|
||||
"ui.sessionReview.collapseAll": "Collapse all",
|
||||
"ui.sessionReview.change.added": "Added",
|
||||
"ui.sessionReview.change.removed": "Removed",
|
||||
"ui.sessionReview.change.modified": "Modified",
|
||||
|
||||
"ui.lineComment.label.prefix": "Comment on ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
|
||||
"ui.sessionReview.change.added": "Añadido",
|
||||
"ui.sessionReview.change.removed": "Eliminado",
|
||||
"ui.sessionReview.change.modified": "Modificado",
|
||||
"ui.lineComment.label.prefix": "Comentar en ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "Comentando en ",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
|
||||
"ui.sessionReview.change.added": "Ajouté",
|
||||
"ui.sessionReview.change.removed": "Supprimé",
|
||||
"ui.sessionReview.change.modified": "Modifié",
|
||||
"ui.lineComment.label.prefix": "Commenter sur ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "Commentaire sur ",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
|
||||
"ui.sessionReview.change.added": "追加",
|
||||
"ui.sessionReview.change.removed": "削除",
|
||||
"ui.sessionReview.change.modified": "変更",
|
||||
"ui.lineComment.label.prefix": "",
|
||||
"ui.lineComment.label.suffix": "へのコメント",
|
||||
"ui.lineComment.editorLabel.prefix": "",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
|
||||
"ui.sessionReview.change.added": "추가됨",
|
||||
"ui.sessionReview.change.removed": "삭제됨",
|
||||
"ui.sessionReview.change.modified": "수정됨",
|
||||
"ui.lineComment.label.prefix": "",
|
||||
"ui.lineComment.label.suffix": "에 댓글 달기",
|
||||
"ui.lineComment.editorLabel.prefix": "",
|
||||
|
||||
@@ -11,6 +11,7 @@ export const dict: Record<Keys, string> = {
|
||||
|
||||
"ui.sessionReview.change.added": "Lagt til",
|
||||
"ui.sessionReview.change.removed": "Fjernet",
|
||||
"ui.sessionReview.change.modified": "Endret",
|
||||
"ui.lineComment.label.prefix": "Kommenter på ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "Kommenterer på ",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
|
||||
"ui.sessionReview.change.added": "Dodano",
|
||||
"ui.sessionReview.change.removed": "Usunięto",
|
||||
"ui.sessionReview.change.modified": "Zmodyfikowano",
|
||||
"ui.lineComment.label.prefix": "Komentarz do ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "Komentowanie: ",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const dict = {
|
||||
|
||||
"ui.sessionReview.change.added": "Добавлено",
|
||||
"ui.sessionReview.change.removed": "Удалено",
|
||||
"ui.sessionReview.change.modified": "Изменено",
|
||||
"ui.lineComment.label.prefix": "Комментарий к ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "Комментирование: ",
|
||||
|
||||
@@ -7,6 +7,7 @@ export const dict = {
|
||||
"ui.sessionReview.collapseAll": "ย่อทั้งหมด",
|
||||
"ui.sessionReview.change.added": "เพิ่ม",
|
||||
"ui.sessionReview.change.removed": "ลบ",
|
||||
"ui.sessionReview.change.modified": "แก้ไข",
|
||||
|
||||
"ui.lineComment.label.prefix": "แสดงความคิดเห็นบน ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
|
||||
@@ -12,6 +12,7 @@ export const dict = {
|
||||
|
||||
"ui.sessionReview.change.added": "已添加",
|
||||
"ui.sessionReview.change.removed": "已移除",
|
||||
"ui.sessionReview.change.modified": "已修改",
|
||||
"ui.lineComment.label.prefix": "评论 ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "正在评论 ",
|
||||
|
||||
@@ -12,6 +12,7 @@ export const dict = {
|
||||
|
||||
"ui.sessionReview.change.added": "已新增",
|
||||
"ui.sessionReview.change.removed": "已移除",
|
||||
"ui.sessionReview.change.modified": "已修改",
|
||||
"ui.lineComment.label.prefix": "評論 ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "正在評論 ",
|
||||
|
||||
@@ -29,15 +29,16 @@ opencode [project]
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Short | Description |
|
||||
| ------------ | ----- | ------------------------------------------ |
|
||||
| `--continue` | `-c` | Continue the last session |
|
||||
| `--session` | `-s` | Session ID to continue |
|
||||
| `--prompt` | | Prompt to use |
|
||||
| `--model` | `-m` | Model to use in the form of provider/model |
|
||||
| `--agent` | | Agent to use |
|
||||
| `--port` | | Port to listen on |
|
||||
| `--hostname` | | Hostname to listen on |
|
||||
| Flag | Short | Description |
|
||||
| ------------ | ----- | ----------------------------------------------------------------------- |
|
||||
| `--continue` | `-c` | Continue the last session |
|
||||
| `--session` | `-s` | Session ID to continue |
|
||||
| `--fork` | | Fork the session when continuing (use with `--continue` or `--session`) |
|
||||
| `--prompt` | | Prompt to use |
|
||||
| `--model` | `-m` | Model to use in the form of provider/model |
|
||||
| `--agent` | | Agent to use |
|
||||
| `--port` | | Port to listen on |
|
||||
| `--hostname` | | Hostname to listen on |
|
||||
|
||||
---
|
||||
|
||||
@@ -334,19 +335,20 @@ opencode run --attach http://localhost:4096 "Explain async/await in JavaScript"
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Short | Description |
|
||||
| ------------ | ----- | ------------------------------------------------------------------ |
|
||||
| `--command` | | The command to run, use message for args |
|
||||
| `--continue` | `-c` | Continue the last session |
|
||||
| `--session` | `-s` | Session ID to continue |
|
||||
| `--share` | | Share the session |
|
||||
| `--model` | `-m` | Model to use in the form of provider/model |
|
||||
| `--agent` | | Agent to use |
|
||||
| `--file` | `-f` | File(s) to attach to message |
|
||||
| `--format` | | Format: default (formatted) or json (raw JSON events) |
|
||||
| `--title` | | Title for the session (uses truncated prompt if no value provided) |
|
||||
| `--attach` | | Attach to a running opencode server (e.g., http://localhost:4096) |
|
||||
| `--port` | | Port for the local server (defaults to random port) |
|
||||
| Flag | Short | Description |
|
||||
| ------------ | ----- | ----------------------------------------------------------------------- |
|
||||
| `--command` | | The command to run, use message for args |
|
||||
| `--continue` | `-c` | Continue the last session |
|
||||
| `--session` | `-s` | Session ID to continue |
|
||||
| `--fork` | | Fork the session when continuing (use with `--continue` or `--session`) |
|
||||
| `--share` | | Share the session |
|
||||
| `--model` | `-m` | Model to use in the form of provider/model |
|
||||
| `--agent` | | Agent to use |
|
||||
| `--file` | `-f` | File(s) to attach to message |
|
||||
| `--format` | | Format: default (formatted) or json (raw JSON events) |
|
||||
| `--title` | | Title for the session (uses truncated prompt if no value provided) |
|
||||
| `--attach` | | Attach to a running opencode server (e.g., http://localhost:4096) |
|
||||
| `--port` | | Port for the local server (defaults to random port) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -60,13 +60,12 @@ If `localhost` does not work in your setup, connect using the WSL IP address ins
|
||||
|
||||
:::caution
|
||||
When using `--hostname 0.0.0.0`, set `OPENCODE_SERVER_PASSWORD` to secure the server.
|
||||
:::
|
||||
|
||||
```bash
|
||||
OPENCODE_SERVER_PASSWORD=your-password opencode serve --hostname 0.0.0.0
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Web Client + WSL
|
||||
|
||||
16
patches/@standard-community%2Fstandard-openapi@0.2.9.patch
Normal file
16
patches/@standard-community%2Fstandard-openapi@0.2.9.patch
Normal file
@@ -0,0 +1,16 @@
|
||||
diff --git a/dist/vendors/convert.js b/dist/vendors/convert.js
|
||||
index 0d615eebfd7cd646937ec1b29f8332db73586ec1..7b146f903c07a9377d676753691cba67781879be 100644
|
||||
--- a/dist/vendors/convert.js
|
||||
+++ b/dist/vendors/convert.js
|
||||
@@ -74,7 +74,10 @@ function convertToOpenAPISchema(jsonSchema, context) {
|
||||
$ref: `#/components/schemas/${id}`
|
||||
};
|
||||
} else if (_jsonSchema.$ref) {
|
||||
- const { $ref, $defs } = _jsonSchema;
|
||||
+ const { $ref, $defs, ...rest } = _jsonSchema;
|
||||
+ if ($ref.includes("://")) {
|
||||
+ return Object.keys(rest).length > 0 ? rest : { type: "string" };
|
||||
+ }
|
||||
const ref = $ref.split("/").pop();
|
||||
context.components.schemas = {
|
||||
...context.components.schemas,
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "bun"
|
||||
import { createOpencode } from "@opencode-ai/sdk"
|
||||
import { parseArgs } from "util"
|
||||
|
||||
@@ -49,7 +50,7 @@ Examples:
|
||||
}
|
||||
parts.push({
|
||||
type: "file",
|
||||
url: `file://${resolved}`,
|
||||
url: pathToFileURL(resolved).href,
|
||||
filename: path.basename(resolved),
|
||||
mime: "text/plain",
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user