mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-23 22:34:53 +00:00
Compare commits
11 Commits
v1.0.16
...
v0.0.2-fea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5dcccfd7e | ||
|
|
1f7086fe03 | ||
|
|
5f1417f1a1 | ||
|
|
740f9dadef | ||
|
|
4b66048e2b | ||
|
|
e019b097ff | ||
|
|
7409236838 | ||
|
|
7caf149e46 | ||
|
|
f1db3a2d29 | ||
|
|
c454918144 | ||
|
|
c5153772c6 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -11,3 +11,9 @@ playground
|
||||
tmp
|
||||
dist
|
||||
.turbo
|
||||
|
||||
# Test suite artifacts
|
||||
opencode/.bun/
|
||||
opencode/.local/
|
||||
opencode/.cache/
|
||||
opencode/node_modules
|
||||
|
||||
9
STATS.md
9
STATS.md
@@ -129,3 +129,12 @@
|
||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
||||
| 2025-11-04 | 663,907 (+10,777) | 608,056 (+10,917) | 1,271,963 (+21,694) |
|
||||
| 2025-11-05 | 675,059 (+11,152) | 619,690 (+11,634) | 1,294,749 (+22,786) |
|
||||
| 2025-11-06 | 686,249 (+11,190) | 630,885 (+11,195) | 1,317,134 (+22,385) |
|
||||
| 2025-11-07 | 696,626 (+10,377) | 642,146 (+11,261) | 1,338,772 (+21,638) |
|
||||
| 2025-11-08 | 706,032 (+9,406) | 653,489 (+11,343) | 1,359,521 (+20,749) |
|
||||
| 2025-11-09 | 713,462 (+7,430) | 660,459 (+6,970) | 1,373,921 (+14,400) |
|
||||
| 2025-11-10 | 722,280 (+8,818) | 668,225 (+7,766) | 1,390,505 (+16,584) |
|
||||
| 2025-11-11 | 729,769 (+7,489) | 677,501 (+9,276) | 1,407,270 (+16,765) |
|
||||
| 2025-11-12 | 740,168 (+10,399) | 686,454 (+8,953) | 1,426,622 (+19,352) |
|
||||
|
||||
22
bun.lock
22
bun.lock
@@ -39,7 +39,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -66,7 +66,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -90,7 +90,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -111,7 +111,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -150,7 +150,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
@@ -166,7 +166,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -243,7 +243,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -263,7 +263,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.81.0",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -274,7 +274,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -287,7 +287,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -317,7 +317,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
|
||||
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
||||
"start": "vinxi start",
|
||||
"version": "1.0.16"
|
||||
"version": "1.0.15"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { For, JSXElement, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
||||
import { Part } from "@opencode-ai/ui"
|
||||
import { Markdown, Part } from "@opencode-ai/ui"
|
||||
import { useSync } from "@/context/sync"
|
||||
import type { AssistantMessage as AssistantMessageType, ToolPart } from "@opencode-ai/sdk"
|
||||
import type { AssistantMessage as AssistantMessageType, Part as PartType, ToolPart } from "@opencode-ai/sdk"
|
||||
import { Spinner } from "./spinner"
|
||||
|
||||
export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) {
|
||||
@@ -22,6 +22,7 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa
|
||||
p.state.status === "running",
|
||||
) as ToolPart,
|
||||
)
|
||||
|
||||
const resolvedParts = createMemo(() => {
|
||||
let resolved = parts()
|
||||
const task = currentTask()
|
||||
@@ -31,18 +32,20 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa
|
||||
}
|
||||
return resolved
|
||||
})
|
||||
// const currentText = createMemo(
|
||||
// () =>
|
||||
// resolvedParts().findLast((p) => p?.type === "text")?.text ||
|
||||
// resolvedParts().findLast((p) => p?.type === "reasoning")?.text,
|
||||
// )
|
||||
const currentText = createMemo(
|
||||
() =>
|
||||
resolvedParts().findLast((p) => p?.type === "text")?.text ||
|
||||
resolvedParts().findLast((p) => p?.type === "reasoning")?.text,
|
||||
)
|
||||
const eligibleItems = createMemo(() => {
|
||||
return resolvedParts().filter((p) => p?.type === "tool" && p?.state.status === "completed") as ToolPart[]
|
||||
return resolvedParts().filter((p) => p?.type === "tool" && p.state.status === "completed")
|
||||
})
|
||||
const finishedItems = createMemo<(JSXElement | ToolPart)[]>(() => [
|
||||
<div class="h-8 w-full" />,
|
||||
const finishedItems = createMemo<(JSXElement | PartType)[]>(() => [
|
||||
<div class="h-8 w-full" />,
|
||||
<div class="h-8 w-full" />,
|
||||
<div class="flex items-center gap-x-5 pl-3 text-text-base">
|
||||
<Spinner /> <span class="text-12-medium">Thinking...</span>
|
||||
</div>,
|
||||
...eligibleItems(),
|
||||
...(done() ? [<div class="h-8 w-full" />, <div class="h-8 w-full" />, <div class="h-8 w-full" />] : []),
|
||||
])
|
||||
@@ -68,120 +71,57 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa
|
||||
return `-${(total - 2) * 40 - 8}px`
|
||||
})
|
||||
|
||||
const lastPart = createMemo(() => resolvedParts().slice(-1)?.at(0))
|
||||
const rawStatus = createMemo(() => {
|
||||
const defaultStatus = "Working..."
|
||||
const last = lastPart()
|
||||
if (!last) return defaultStatus
|
||||
|
||||
if (last.type === "tool") {
|
||||
switch (last.tool) {
|
||||
case "task":
|
||||
return "Delegating work..."
|
||||
case "todowrite":
|
||||
case "todoread":
|
||||
return "Planning next steps..."
|
||||
case "read":
|
||||
return "Gathering context..."
|
||||
case "list":
|
||||
case "grep":
|
||||
case "glob":
|
||||
return "Searching the codebase..."
|
||||
case "webfetch":
|
||||
return "Searching the web..."
|
||||
case "edit":
|
||||
case "write":
|
||||
return "Making edits..."
|
||||
case "bash":
|
||||
return "Running commands..."
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else if (last.type === "reasoning") {
|
||||
return "Thinking..."
|
||||
} else if (last.type === "text") {
|
||||
return "Gathering thoughts..."
|
||||
}
|
||||
return defaultStatus
|
||||
})
|
||||
|
||||
const [status, setStatus] = createSignal(rawStatus())
|
||||
let lastStatusChange = Date.now()
|
||||
let statusTimeout: number | undefined
|
||||
|
||||
createEffect(() => {
|
||||
const newStatus = rawStatus()
|
||||
if (newStatus === status()) return
|
||||
|
||||
const timeSinceLastChange = Date.now() - lastStatusChange
|
||||
|
||||
if (timeSinceLastChange >= 1000) {
|
||||
setStatus(newStatus)
|
||||
lastStatusChange = Date.now()
|
||||
if (statusTimeout) {
|
||||
clearTimeout(statusTimeout)
|
||||
statusTimeout = undefined
|
||||
}
|
||||
} else {
|
||||
if (statusTimeout) clearTimeout(statusTimeout)
|
||||
statusTimeout = setTimeout(() => {
|
||||
setStatus(rawStatus())
|
||||
lastStatusChange = Date.now()
|
||||
statusTimeout = undefined
|
||||
}, 1000 - timeSinceLastChange) as unknown as number
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-3">
|
||||
{/* <Show when={currentText()}> */}
|
||||
{/* {(text) => ( */}
|
||||
{/* <div */}
|
||||
{/* class="h-20 flex flex-col justify-end overflow-hidden py-3 */}
|
||||
{/* mask-alpha mask-t-from-80% mask-t-from-background-base mask-t-to-transparent" */}
|
||||
{/* > */}
|
||||
{/* <Markdown text={text()} class="w-full shrink-0 overflow-visible" /> */}
|
||||
{/* </div> */}
|
||||
{/* )} */}
|
||||
{/* </Show> */}
|
||||
<div class="flex items-center gap-x-5 pl-3 border border-transparent text-text-base">
|
||||
<Spinner /> <span class="text-12-medium">{status()}</span>
|
||||
</div>
|
||||
<Show when={eligibleItems().length > 0}>
|
||||
<div
|
||||
class="h-30 overflow-hidden pointer-events-none pb-1
|
||||
<div
|
||||
class="h-30 overflow-hidden pointer-events-none pb-1
|
||||
mask-alpha mask-t-from-33% mask-t-from-background-base mask-t-to-transparent
|
||||
mask-b-from-95% mask-b-from-background-base mask-b-to-transparent"
|
||||
>
|
||||
<div
|
||||
class="w-full flex flex-col items-start self-stretch gap-2 py-8
|
||||
>
|
||||
<div
|
||||
class="w-full flex flex-col items-start self-stretch gap-2 py-8
|
||||
transform transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
||||
style={{ transform: `translateY(${translateY()})` }}
|
||||
>
|
||||
<For each={finishedItems()}>
|
||||
{(part) => (
|
||||
<Switch>
|
||||
<Match when={part && typeof part === "object" && "type" in part && part}>
|
||||
{(p) => {
|
||||
const part = p() as ToolPart
|
||||
const message = createMemo(() =>
|
||||
sync.data.message[part.sessionID].find((m) => m.id === part.messageID),
|
||||
)
|
||||
return (
|
||||
<div class="h-8 flex items-center w-full">
|
||||
<Part message={message()!} part={part} />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="h-8 flex items-center w-full">{part as JSXElement}</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
style={{ transform: `translateY(${translateY()})` }}
|
||||
>
|
||||
<For each={finishedItems()}>
|
||||
{(part) => {
|
||||
if (part && typeof part === "object" && "type" in part) {
|
||||
const message = createMemo(() => sync.data.message[part.sessionID].find((m) => m.id === part.messageID))
|
||||
return (
|
||||
<div class="h-8 flex items-center w-full">
|
||||
<Switch>
|
||||
<Match when={part.type === "text" && part}>
|
||||
{(p) => (
|
||||
<div
|
||||
textContent={p().text}
|
||||
class="text-12-regular text-text-base whitespace-nowrap truncate w-full"
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={part.type === "reasoning" && part}>
|
||||
{(p) => <Part message={message()!} part={p()} />}
|
||||
</Match>
|
||||
<Match when={part.type === "tool" && part}>
|
||||
{(p) => <Part message={message()!} part={p()} />}
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div class="h-8 flex items-center w-full">{part}</div>
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={currentText()}>
|
||||
{(text) => (
|
||||
<div
|
||||
class="max-h-36 flex flex-col justify-end overflow-hidden py-3
|
||||
mask-alpha mask-t-from-80% mask-t-from-background-base mask-t-to-transparent"
|
||||
>
|
||||
<Markdown text={text()} class="w-full shrink-0 overflow-visible" />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
ProgressCircle,
|
||||
Message,
|
||||
Typewriter,
|
||||
Card,
|
||||
} from "@opencode-ai/ui"
|
||||
import { FileIcon } from "@/ui"
|
||||
import FileTree from "@/components/file-tree"
|
||||
@@ -548,13 +547,78 @@ export default function Page() {
|
||||
<For each={local.session.userMessages()}>
|
||||
{(message) => {
|
||||
const diffs = createMemo(() => message.summary?.diffs ?? [])
|
||||
const working = createMemo(() => !message.summary?.body)
|
||||
const assistantMessages = createMemo(() => {
|
||||
return sync.data.message[activeSession().id]?.filter(
|
||||
(m) => m.role === "assistant" && m.parentID == message.id,
|
||||
) as AssistantMessageType[]
|
||||
})
|
||||
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
|
||||
const working = createMemo(() => !message.summary?.body && !error())
|
||||
const parts = createMemo(() =>
|
||||
assistantMessages().flatMap((m) => sync.data.part[m.id]),
|
||||
)
|
||||
const lastPart = createMemo(() => parts().slice(-1)?.at(0))
|
||||
const rawStatus = createMemo(() => {
|
||||
const defaultStatus = "Working..."
|
||||
const last = lastPart()
|
||||
if (!last) return defaultStatus
|
||||
|
||||
if (last.type === "tool") {
|
||||
switch (last.tool) {
|
||||
case "task":
|
||||
return "Delegating work..."
|
||||
case "todowrite":
|
||||
case "todoread":
|
||||
return "Planning next steps..."
|
||||
case "read":
|
||||
return "Gathering context..."
|
||||
case "list":
|
||||
case "grep":
|
||||
case "glob":
|
||||
return "Searching the codebase..."
|
||||
case "webfetch":
|
||||
return "Searching the web..."
|
||||
case "edit":
|
||||
case "write":
|
||||
return "Making edits..."
|
||||
case "bash":
|
||||
return "Running commands..."
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else if (last.type === "reasoning") {
|
||||
return "Thinking..."
|
||||
} else if (last.type === "text") {
|
||||
return "Gathering thoughts..."
|
||||
}
|
||||
return defaultStatus
|
||||
})
|
||||
|
||||
const [status, setStatus] = createSignal(rawStatus())
|
||||
let lastStatusChange = Date.now()
|
||||
let statusTimeout: number | undefined
|
||||
|
||||
createEffect(() => {
|
||||
const newStatus = rawStatus()
|
||||
if (newStatus === status()) return
|
||||
|
||||
const timeSinceLastChange = Date.now() - lastStatusChange
|
||||
|
||||
if (timeSinceLastChange >= 1000) {
|
||||
setStatus(newStatus)
|
||||
lastStatusChange = Date.now()
|
||||
if (statusTimeout) {
|
||||
clearTimeout(statusTimeout)
|
||||
statusTimeout = undefined
|
||||
}
|
||||
} else {
|
||||
if (statusTimeout) clearTimeout(statusTimeout)
|
||||
statusTimeout = setTimeout(() => {
|
||||
setStatus(rawStatus())
|
||||
lastStatusChange = Date.now()
|
||||
statusTimeout = undefined
|
||||
}, 1000 - timeSinceLastChange) as unknown as number
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<li class="group/li flex items-center self-stretch">
|
||||
@@ -577,9 +641,10 @@ export default function Page() {
|
||||
"text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
|
||||
}}
|
||||
>
|
||||
<Show when={message.summary?.title} fallback="New message">
|
||||
{message.summary?.title}
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={working()}>{status()}</Match>
|
||||
<Match when={true}>{message.summary?.title}</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
@@ -593,24 +658,23 @@ export default function Page() {
|
||||
{(message) => {
|
||||
const isActive = createMemo(() => local.session.activeMessage()?.id === message.id)
|
||||
const [titled, setTitled] = createSignal(!!message.summary?.title)
|
||||
const assistantMessages = createMemo(() => {
|
||||
return sync.data.message[activeSession().id]?.filter(
|
||||
(m) => m.role === "assistant" && m.parentID == message.id,
|
||||
) as AssistantMessageType[]
|
||||
})
|
||||
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
|
||||
const [completed, setCompleted] = createSignal(!!message.summary?.body || !!error())
|
||||
const [completed, setCompleted] = createSignal(!!message.summary?.body)
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
const parts = createMemo(() => sync.data.part[message.id])
|
||||
const title = createMemo(() => message.summary?.title)
|
||||
const summary = createMemo(() => message.summary?.body)
|
||||
const diffs = createMemo(() => message.summary?.diffs ?? [])
|
||||
const assistantMessages = createMemo(() => {
|
||||
return sync.data.message[activeSession().id]?.filter(
|
||||
(m) => m.role === "assistant" && m.parentID == message.id,
|
||||
) as AssistantMessageType[]
|
||||
})
|
||||
const hasToolPart = createMemo(() =>
|
||||
assistantMessages()
|
||||
?.flatMap((m) => sync.data.part[m.id])
|
||||
.some((p) => p?.type === "tool"),
|
||||
)
|
||||
const working = createMemo(() => !summary() && !error())
|
||||
const working = createMemo(() => !summary())
|
||||
|
||||
// allowing time for the animations to finish
|
||||
createEffect(() => {
|
||||
@@ -618,8 +682,8 @@ export default function Page() {
|
||||
setTimeout(() => setTitled(!!title()), 10_000)
|
||||
})
|
||||
createEffect(() => {
|
||||
const complete = !!summary() || !!error()
|
||||
setTimeout(() => setCompleted(complete), 1200)
|
||||
summary()
|
||||
setTimeout(() => setCompleted(!!summary()), 1200)
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -715,11 +779,6 @@ export default function Page() {
|
||||
</Accordion>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={error() && !expanded()}>
|
||||
<Card variant="error" class="text-text-on-critical-base">
|
||||
{error()?.data?.message as string}
|
||||
</Card>
|
||||
</Show>
|
||||
{/* Response */}
|
||||
<div class="w-full">
|
||||
<Switch>
|
||||
@@ -749,11 +808,6 @@ export default function Page() {
|
||||
return <Message message={assistantMessage} parts={parts()} />
|
||||
}}
|
||||
</For>
|
||||
<Show when={error()}>
|
||||
<Card variant="error" class="text-text-on-critical-base">
|
||||
{error()?.data?.message as string}
|
||||
</Card>
|
||||
</Show>
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -165,11 +165,7 @@ export namespace File {
|
||||
const project = Instance.project
|
||||
if (project.vcs !== "git") return []
|
||||
|
||||
const diffOutput = await $`git diff --numstat HEAD`
|
||||
.cwd(Instance.directory)
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
const diffOutput = await $`git diff --numstat HEAD`.cwd(Instance.directory).quiet().nothrow().text()
|
||||
|
||||
const changedFiles: Info[] = []
|
||||
|
||||
@@ -261,14 +257,9 @@ export namespace File {
|
||||
|
||||
if (project.vcs === "git") {
|
||||
let diff = await $`git diff ${file}`.cwd(Instance.directory).quiet().nothrow().text()
|
||||
if (!diff.trim())
|
||||
diff = await $`git diff --staged ${file}`.cwd(Instance.directory).quiet().nothrow().text()
|
||||
if (!diff.trim()) diff = await $`git diff --staged ${file}`.cwd(Instance.directory).quiet().nothrow().text()
|
||||
if (diff.trim()) {
|
||||
const original = await $`git show HEAD:${file}`
|
||||
.cwd(Instance.directory)
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.text()
|
||||
const original = await $`git show HEAD:${file}`.cwd(Instance.directory).quiet().nothrow().text()
|
||||
const patch = structuredPatch(file, file, original, content, "old", "new", {
|
||||
context: Infinity,
|
||||
ignoreWhitespace: true,
|
||||
@@ -316,12 +307,12 @@ export namespace File {
|
||||
})
|
||||
}
|
||||
|
||||
export async function search(input: { query: string; limit?: number; dirs?: boolean }) {
|
||||
export async function search(input: { query: string; limit?: number }) {
|
||||
log.info("search", { query: input.query })
|
||||
const limit = input.limit ?? 100
|
||||
const result = await state().then((x) => x.files())
|
||||
if (!input.query) return input.dirs !== false ? result.dirs.toSorted().slice(0, limit) : []
|
||||
const items = input.dirs !== false ? [...result.files, ...result.dirs] : result.files
|
||||
if (!input.query) return result.dirs.toSorted().slice(0, limit)
|
||||
const items = [...result.files, ...result.dirs]
|
||||
const sorted = fuzzysort.go(input.query, items, { limit: limit }).map((r) => r.target)
|
||||
log.info("search", { query: input.query, results: sorted.length })
|
||||
return sorted
|
||||
|
||||
@@ -1106,16 +1106,13 @@ export namespace Server {
|
||||
"query",
|
||||
z.object({
|
||||
query: z.string(),
|
||||
dirs: z.boolean().optional(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const query = c.req.valid("query").query
|
||||
const dirs = c.req.valid("query").dirs
|
||||
const results = await File.search({
|
||||
query,
|
||||
limit: 10,
|
||||
dirs,
|
||||
})
|
||||
return c.json(results)
|
||||
},
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { iife } from "../../src/util/iife"
|
||||
|
||||
describe("util.iife", () => {
|
||||
test("should execute function immediately and return result", () => {
|
||||
let called = false
|
||||
const result = iife(() => {
|
||||
called = true
|
||||
return 42
|
||||
})
|
||||
|
||||
expect(called).toBe(true)
|
||||
expect(result).toBe(42)
|
||||
})
|
||||
|
||||
test("should work with async functions", async () => {
|
||||
let called = false
|
||||
const result = await iife(async () => {
|
||||
called = true
|
||||
return "async result"
|
||||
})
|
||||
|
||||
expect(called).toBe(true)
|
||||
expect(result).toBe("async result")
|
||||
})
|
||||
|
||||
test("should handle functions with no return value", () => {
|
||||
let called = false
|
||||
const result = iife(() => {
|
||||
called = true
|
||||
})
|
||||
|
||||
expect(called).toBe(true)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -1,50 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { lazy } from "../../src/util/lazy"
|
||||
|
||||
describe("util.lazy", () => {
|
||||
test("should call function only once", () => {
|
||||
let callCount = 0
|
||||
const getValue = () => {
|
||||
callCount++
|
||||
return "expensive value"
|
||||
}
|
||||
|
||||
const lazyValue = lazy(getValue)
|
||||
|
||||
expect(callCount).toBe(0)
|
||||
|
||||
const result1 = lazyValue()
|
||||
expect(result1).toBe("expensive value")
|
||||
expect(callCount).toBe(1)
|
||||
|
||||
const result2 = lazyValue()
|
||||
expect(result2).toBe("expensive value")
|
||||
expect(callCount).toBe(1)
|
||||
})
|
||||
|
||||
test("should preserve the same reference", () => {
|
||||
const obj = { value: 42 }
|
||||
const lazyObj = lazy(() => obj)
|
||||
|
||||
const result1 = lazyObj()
|
||||
const result2 = lazyObj()
|
||||
|
||||
expect(result1).toBe(obj)
|
||||
expect(result2).toBe(obj)
|
||||
expect(result1).toBe(result2)
|
||||
})
|
||||
|
||||
test("should work with different return types", () => {
|
||||
const lazyString = lazy(() => "string")
|
||||
const lazyNumber = lazy(() => 123)
|
||||
const lazyBoolean = lazy(() => true)
|
||||
const lazyNull = lazy(() => null)
|
||||
const lazyUndefined = lazy(() => undefined)
|
||||
|
||||
expect(lazyString()).toBe("string")
|
||||
expect(lazyNumber()).toBe(123)
|
||||
expect(lazyBoolean()).toBe(true)
|
||||
expect(lazyNull()).toBe(null)
|
||||
expect(lazyUndefined()).toBe(undefined)
|
||||
})
|
||||
})
|
||||
@@ -1,21 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { withTimeout } from "../../src/util/timeout"
|
||||
|
||||
describe("util.timeout", () => {
|
||||
test("should resolve when promise completes before timeout", async () => {
|
||||
const fastPromise = new Promise<string>((resolve) => {
|
||||
setTimeout(() => resolve("fast"), 10)
|
||||
})
|
||||
|
||||
const result = await withTimeout(fastPromise, 100)
|
||||
expect(result).toBe("fast")
|
||||
})
|
||||
|
||||
test("should reject when promise exceeds timeout", async () => {
|
||||
const slowPromise = new Promise<string>((resolve) => {
|
||||
setTimeout(() => resolve("slow"), 200)
|
||||
})
|
||||
|
||||
await expect(withTimeout(slowPromise, 50)).rejects.toThrow("Operation timed out after 50ms")
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -2346,7 +2346,6 @@ export type FindFilesData = {
|
||||
query: {
|
||||
directory?: string
|
||||
query: string
|
||||
dirs?: boolean
|
||||
}
|
||||
url: "/find/file"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/components/index.ts",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -114,7 +114,7 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
|
||||
|
||||
| Model | Input | Output | Cached Read | Cached Write |
|
||||
| --------------------------------- | ------ | ------ | ----------- | ------------ |
|
||||
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
|
||||
| GLM 4.6 | $0.60 | $1.90 | $0.11 | - |
|
||||
| Kimi K2 | $0.60 | $2.50 | $0.36 | - |
|
||||
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
|
||||
| Grok Code Fast 1 | Free | Free | - | - |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.15",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
101
sprints/maxsteps/opencode-02-maxsteps.md
Normal file
101
sprints/maxsteps/opencode-02-maxsteps.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Sprint: Agent MaxSteps Feature
|
||||
|
||||
## Problem Statement
|
||||
Users need the ability to limit the number of steps that agents (especially subagents) can take during execution, as outlined in [Issue #3631](https://github.com/sst/opencode/issues/3631). This feature will prevent runaway agent loops and give users better control over agent execution limits.
|
||||
|
||||
## Context
|
||||
### Existing System
|
||||
OpenCode has an agentic loop system controlled in `packages/opencode/src/session/prompt.ts`. Currently, agents can run indefinitely without any step limits, which can lead to:
|
||||
- Excessive resource consumption
|
||||
- Runaway loops in faulty agent logic
|
||||
- Unpredictable costs when using paid APIs
|
||||
- Difficulty in debugging agent behavior
|
||||
|
||||
### MaxSteps Feature Requirements
|
||||
The maxSteps feature must:
|
||||
- Allow optional step limits for agent definitions
|
||||
- Work for both primary agents and subagents (primary use case is subagents)
|
||||
- Stop execution when the limit is reached
|
||||
- On the n-1 step, remove all tools from the request to force a text-only response
|
||||
- Be configurable per agent definition
|
||||
- Default to unlimited if not specified (backward compatibility)
|
||||
|
||||
## Success Criteria
|
||||
- [ ] Agent definitions accept optional `maxSteps` parameter
|
||||
- [ ] Step counter tracks execution steps accurately
|
||||
- [ ] Agent stops at maxSteps limit
|
||||
- [ ] On step n-1, tools are removed from the request
|
||||
- [ ] Final step produces a text-only response summarizing status
|
||||
- [ ] Feature works for both primary and subagents
|
||||
- [ ] No breaking changes to existing agent definitions
|
||||
- [ ] Step limit is logged for debugging purposes
|
||||
- [ ] Clear error/status message when limit is reached
|
||||
- [ ] Tests cover various step limit scenarios
|
||||
- [ ] Documentation updated with maxSteps usage
|
||||
|
||||
## Technical Requirements
|
||||
|
||||
### Agent Definition Schema
|
||||
```typescript
|
||||
interface AgentDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
tools?: Tool[];
|
||||
maxSteps?: number; // New optional field
|
||||
// ... other existing fields
|
||||
}
|
||||
```
|
||||
|
||||
### Implementation Location
|
||||
- Primary implementation in: `packages/opencode/src/session/prompt.ts`
|
||||
- Agent definition types updated
|
||||
- Step counter logic added to agentic loop
|
||||
|
||||
### Step Counting Logic
|
||||
1. Initialize step counter when agent starts
|
||||
2. Increment counter after each tool execution
|
||||
3. Check if current step === maxSteps - 1
|
||||
- If true: Remove tools from next request
|
||||
4. Check if current step === maxSteps
|
||||
- If true: Stop execution and return final response
|
||||
|
||||
### Edge Cases to Handle
|
||||
- maxSteps = 0 (should be invalid)
|
||||
- maxSteps = 1 (immediate text response)
|
||||
- maxSteps = 2 (one tool call, then text)
|
||||
- Nested subagent calls (each has own counter)
|
||||
- Error handling when limit reached mid-operation
|
||||
|
||||
## Testing Strategy
|
||||
1. Unit tests for step counter logic
|
||||
2. Integration tests with mock agents
|
||||
3. Test various maxSteps values (1, 2, 10, undefined)
|
||||
4. Test tool removal on n-1 step
|
||||
5. Test nested agent scenarios
|
||||
6. Test error cases and boundary conditions
|
||||
7. Performance tests to ensure no overhead when maxSteps not used
|
||||
|
||||
## Validation Checklist
|
||||
- [ ] maxSteps parameter accepted in agent definitions
|
||||
- [ ] Step counter increments correctly
|
||||
- [ ] Execution stops at limit
|
||||
- [ ] Tools removed on penultimate step
|
||||
- [ ] Final response is text-only
|
||||
- [ ] No regression in unlimited agents
|
||||
- [ ] Logs show step count and limit
|
||||
- [ ] Clear status message on limit reached
|
||||
- [ ] Tests pass for all scenarios
|
||||
- [ ] Documentation includes examples
|
||||
|
||||
## Example Usage
|
||||
```typescript
|
||||
const limitedAgent = {
|
||||
name: "limited-helper",
|
||||
description: "An agent with step limits",
|
||||
maxSteps: 5, // Will stop after 5 steps
|
||||
tools: [/* ... tools ... */]
|
||||
};
|
||||
|
||||
// On step 4, tools will be removed
|
||||
// On step 5, execution stops with text response
|
||||
```
|
||||
79
sprints/uninstaller/opencode-01-uninstaller.md
Normal file
79
sprints/uninstaller/opencode-01-uninstaller.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Sprint: OpenCode Uninstaller Feature
|
||||
|
||||
## Problem Statement
|
||||
Users need a reliable way to completely remove OpenCode from their system, including all configuration files, cached data, and installed components, as outlined in [Issue #3900](https://github.com/sst/opencode/issues/3900).
|
||||
|
||||
## Context
|
||||
### Existing System
|
||||
OpenCode is a CLI tool that installs various components and configurations across the system. Currently, there is no comprehensive uninstall command that cleanly removes all traces of the installation.
|
||||
|
||||
### Uninstaller Feature Requirements
|
||||
The uninstaller must provide a complete removal process that:
|
||||
- Removes the OpenCode binary/executable
|
||||
- Cleans up configuration files from user directories
|
||||
- Removes cached data and temporary files
|
||||
- Provides options for selective removal (keep configs, etc.)
|
||||
- Confirms actions before destructive operations
|
||||
|
||||
## Success Criteria
|
||||
- [ ] Uninstall command (`opencode uninstall`) is available in the CLI
|
||||
- [ ] Command removes OpenCode executable from installation path
|
||||
- [ ] Configuration files in ~/.opencode are removed (with --all flag)
|
||||
- [ ] Cache directories are cleaned up
|
||||
- [ ] User is prompted for confirmation before removal
|
||||
- [ ] Option to keep configuration files (--keep-config)
|
||||
- [ ] Uninstaller provides clear feedback on what was removed
|
||||
- [ ] Process is reversible by reinstalling OpenCode
|
||||
- [ ] Exit code 0 on successful uninstallation
|
||||
- [ ] Comprehensive error handling for permission issues
|
||||
- [ ] Documentation updated with uninstall instructions
|
||||
|
||||
## Technical Requirements
|
||||
|
||||
### CLI Interface
|
||||
```bash
|
||||
opencode uninstall [options]
|
||||
--all Remove everything including configs
|
||||
--keep-config Keep configuration files
|
||||
--force Skip confirmation prompts
|
||||
--dry-run Show what would be removed without doing it
|
||||
```
|
||||
|
||||
### File Locations to Handle
|
||||
- **Binary**: `/usr/local/bin/opencode` or installation path
|
||||
- **Config**: `~/.opencode/` directory
|
||||
- **Cache**: `~/.cache/opencode/` directory
|
||||
- **Temp**: `/tmp/opencode-*` files
|
||||
- **Logs**: `~/.opencode/logs/` directory
|
||||
|
||||
### Implementation Steps
|
||||
1. Parse command arguments and flags
|
||||
2. Identify all OpenCode-related files and directories
|
||||
3. Show user what will be removed (with confirmation)
|
||||
4. Remove files in correct order (temp -> cache -> config -> binary)
|
||||
5. Verify removal and report status
|
||||
6. Clean up any remaining symlinks or PATH entries
|
||||
|
||||
### Error Handling
|
||||
- Permission denied errors should suggest using sudo
|
||||
- Missing files should not cause failure
|
||||
- Partial uninstall should be reported clearly
|
||||
- Network operations should have timeout handling
|
||||
|
||||
## Testing Strategy
|
||||
1. Install OpenCode in a test container
|
||||
2. Create configuration and cache files
|
||||
3. Run uninstall command with various flags
|
||||
4. Verify complete removal of all components
|
||||
5. Test edge cases (permissions, missing files, etc.)
|
||||
6. Validate that reinstallation works after uninstall
|
||||
|
||||
## Validation Checklist
|
||||
- [ ] Binary file removed from system
|
||||
- [ ] Config directory removed (when using --all)
|
||||
- [ ] Cache directory cleaned up
|
||||
- [ ] No orphaned files remain
|
||||
- [ ] PATH environment cleaned if modified
|
||||
- [ ] Exit codes match expected values
|
||||
- [ ] Help text includes uninstall command
|
||||
- [ ] Man page or docs updated
|
||||
Reference in New Issue
Block a user