Compare commits

..

7 Commits

Author SHA1 Message Date
Dax Raad f99904bc1c track version on session info 2025-06-18 13:40:36 -04:00
Jay V b796d6763f ignore: share page styles 2025-06-18 12:53:48 -04:00
Dax Raad c1250abdf8 implemented diff trimming 2025-06-18 11:20:40 -04:00
Dax Raad ebe51534a1 allow setting options in global provider store 2025-06-18 11:06:16 -04:00
Dax Raad b8bbee4718 fix issue with provider cache 2025-06-18 10:56:23 -04:00
Dax Raad 8f852b396f fix deploys 2025-06-18 10:47:07 -04:00
Dax Raad ae4d089c06 remove call to npm causing noticible delay when starting chat 2025-06-18 10:35:41 -04:00
12 changed files with 209 additions and 62 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ name: deploy
on:
push:
branches:
- dontlook
- dev
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}
+3
View File
@@ -83,6 +83,9 @@ Start with a `provider.toml` file in `~/.config/opencode/providers`
name = "OpenRouter"
env = ["OPENROUTER_API_KEY"]
npm = "@openrouter/ai-sdk-provider"
[options]
baseURL = "https://api.openrouter.ai" # optional settings
```
And models in `~/.config/opencode/providers/openrouter/models/[model-id]`
+1 -1
View File
@@ -120,7 +120,7 @@ const cli = yargs(hideBin(process.argv))
.command(ScrapCommand)
.command(AuthCommand)
.command(UpgradeCommand)
.fail((msg, err) => {
.fail((msg) => {
if (
msg.startsWith("Unknown argument") ||
msg.startsWith("Not enough non-option arguments")
-27
View File
@@ -1,5 +1,4 @@
import { Global } from "../global"
import { lazy } from "../util/lazy"
import { Log } from "../util/log"
import path from "path"
import { z } from "zod"
@@ -64,30 +63,4 @@ export namespace ModelsDev {
throw new Error(`Failed to fetch models.dev: ${result.statusText}`)
await Bun.write(file, result)
}
const aisdk = lazy(async () => {
log.info("fetching ai-sdk")
const response = await fetch(
"https://registry.npmjs.org/-/v1/search?text=scope:@ai-sdk",
)
if (!response.ok)
throw new Error(
`Failed to fetch ai-sdk information: ${response.statusText}`,
)
const result = await response.json()
log.info("found ai-sdk", result.objects.length)
return result.objects
.filter((obj: any) => obj.package.name.startsWith("@ai-sdk/"))
.reduce((acc: any, obj: any) => {
acc[obj.package.name] = obj
return acc
}, {})
})
export async function pkg(providerID: string): Promise<[string, string]> {
const packages = await aisdk()
const match = packages[`@ai-sdk/${providerID}`]
if (match) return [match.package.name, "latest"]
return [providerID, "latest"]
}
}
+3 -7
View File
@@ -43,9 +43,7 @@ export namespace Provider {
for (const model of Object.values(provider.models)) {
model.cost = {
input: 0,
inputCached: 0,
output: 0,
outputCached: 0,
}
}
return {
@@ -205,9 +203,7 @@ export namespace Provider {
}
// load config
for (const [providerID, provider] of Object.entries(
config.provider ?? {},
)) {
for (const [providerID, provider] of configProviders) {
mergeProvider(providerID, provider.options ?? {}, "config")
}
@@ -234,8 +230,8 @@ export namespace Provider {
const s = await state()
const existing = s.sdk.get(provider.id)
if (existing) return existing
const [pkg, version] = await ModelsDev.pkg(provider.npm ?? provider.id)
const mod = await import(await BunProc.install(pkg, version))
const pkg = provider.npm ?? provider.id
const mod = await import(await BunProc.install(pkg, "latest"))
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
const loaded = fn(s.providers[provider.id]?.options)
s.sdk.set(provider.id, loaded)
+13
View File
@@ -31,6 +31,7 @@ import { SystemPrompt } from "./system"
import { Flag } from "../flag/flag"
import type { ModelsDev } from "../provider/models"
import { GlobalConfig } from "../global/config"
import { Installation } from "../installation"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -46,6 +47,7 @@ export namespace Session {
})
.optional(),
title: z.string(),
version: z.string(),
time: z.object({
created: z.number(),
updated: z.number(),
@@ -84,6 +86,7 @@ export namespace Session {
export async function create(parentID?: string) {
const result: Info = {
id: Identifier.descending("session"),
version: Installation.VERSION,
parentID,
title:
(parentID ? "Child session - " : "New Session - ") +
@@ -331,6 +334,16 @@ export namespace Session {
sessionID: input.sessionID,
abort: abort.signal,
messageID: next.id,
metadata: async (val) => {
next.metadata.tool[opts.toolCallId] = {
...val,
time: {
start: 0,
end: 0,
},
}
await updateMessage(next)
},
})
next.metadata!.tool![opts.toolCallId] = {
...result.metadata,
+39 -1
View File
@@ -87,7 +87,9 @@ export const EditTool = Tool.define({
await file.write(contentNew)
})()
const diff = createTwoFilesPatch(filepath, filepath, contentOld, contentNew)
const diff = trimDiff(
createTwoFilesPatch(filepath, filepath, contentOld, contentNew),
)
FileTimes.read(ctx.sessionID, filepath)
@@ -113,3 +115,39 @@ export const EditTool = Tool.define({
}
},
})
function trimDiff(diff: string): string {
const lines = diff.split("\n")
const contentLines = lines.filter(
(line) =>
(line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
!line.startsWith("---") &&
!line.startsWith("+++"),
)
if (contentLines.length === 0) return diff
let min = Infinity
for (const line of contentLines) {
const content = line.slice(1)
if (content.trim().length > 0) {
const match = content.match(/^(\s*)/)
if (match) min = Math.min(min, match[1].length)
}
}
if (min === Infinity || min === 0) return diff
const trimmedLines = lines.map((line) => {
if (
(line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
!line.startsWith("---") &&
!line.startsWith("+++")
) {
const prefix = line[0]
const content = line.slice(1)
return prefix + content.slice(min)
}
return line
})
return trimmedLines.join("\n")
}
+26 -1
View File
@@ -2,6 +2,8 @@ import { Tool } from "./tool"
import DESCRIPTION from "./task.txt"
import { z } from "zod"
import { Session } from "../session"
import { Bus } from "../bus"
import { Message } from "../session/message"
export const TaskTool = Tool.define({
id: "opencode.task",
@@ -17,6 +19,28 @@ export const TaskTool = Tool.define({
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
const metadata = msg.metadata.assistant!
function summary(input: Message.Info) {
const result = []
for (const part of input.parts) {
if (part.type === "tool-invocation") {
result.push({
toolInvocation: part.toolInvocation,
metadata: input.metadata.tool[part.toolInvocation.toolCallId],
})
}
}
return result
}
const unsub = Bus.subscribe(Message.Event.Updated, async (evt) => {
if (evt.properties.info.metadata.sessionID !== ctx.sessionID) return
ctx.metadata({
title: params.description,
summary: summary(evt.properties.info),
})
})
const result = await Session.chat({
sessionID: session.id,
modelID: metadata.modelID,
@@ -28,10 +52,11 @@ export const TaskTool = Tool.define({
},
],
})
unsub()
return {
metadata: {
title: params.description,
summary: summary(result),
},
output: result.parts.findLast((x) => x.type === "text")!.text,
}
+2 -1
View File
@@ -5,10 +5,11 @@ export namespace Tool {
title: string
[key: string]: any
}
export type Context = {
export type Context<M extends Metadata = Metadata> = {
sessionID: string
messageID: string
abort: AbortSignal
metadata(meta: M): void
}
export interface Info<
Parameters extends StandardSchemaV1 = StandardSchemaV1,
+9 -15
View File
@@ -3,6 +3,12 @@ import { App } from "../../src/app/app"
import { GlobTool } from "../../src/tool/glob"
import { ListTool } from "../../src/tool/ls"
const ctx = {
sessionID: "test",
messageID: "",
abort: AbortSignal.any([]),
metadata: () => {},
}
describe("tool.glob", () => {
test("truncate", async () => {
await App.provide({ cwd: process.cwd() }, async () => {
@@ -11,11 +17,7 @@ describe("tool.glob", () => {
pattern: "./node_modules/**/*",
path: undefined,
},
{
sessionID: "test",
messageID: "",
abort: AbortSignal.any([]),
},
ctx,
)
expect(result.metadata.truncated).toBe(true)
})
@@ -27,11 +29,7 @@ describe("tool.glob", () => {
pattern: "*.json",
path: undefined,
},
{
sessionID: "test",
messageID: "",
abort: AbortSignal.any([]),
},
ctx,
)
expect(result.metadata).toMatchObject({
truncated: false,
@@ -46,11 +44,7 @@ describe("tool.ls", () => {
const result = await App.provide({ cwd: process.cwd() }, async () => {
return await ListTool.execute(
{ path: "./example", ignore: [".git"] },
{
sessionID: "test",
messageID: "",
abort: AbortSignal.any([]),
},
ctx,
)
})
expect(result.output).toMatchSnapshot()
+68 -8
View File
@@ -174,12 +174,15 @@ function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> {
export function getDiagnostics(
diagnosticsByFile: Record<string, Diagnostic[]>,
currentFile: string
): string[] {
// Return a flat array of error diagnostics, in the format:
// "ERROR [65:20] Property 'x' does not exist on type 'Y'"
const result: string[] = []
if (diagnosticsByFile === undefined) return result
if (
diagnosticsByFile === undefined || diagnosticsByFile[currentFile] === undefined
) return result
for (const diags of Object.values(diagnosticsByFile)) {
for (const d of diags) {
@@ -189,7 +192,7 @@ export function getDiagnostics(
const line = d.range.start.line + 1 // 1-based
const column = d.range.start.character + 1 // 1-based
result.push(`ERROR [${line}:${column}] ${d.message}`)
result.push(`\x1b[31mERROR\x1b[0m \x1b[2m[${line}:${column}]\x1b[0m ${d.message}`)
}
}
@@ -321,6 +324,59 @@ function TextPart(props: TextPartProps) {
)
}
interface LspPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
text: string
expand?: boolean
}
function LspPart(props: LspPartProps) {
const [local, rest] = splitProps(props, ["text", "expand"])
const [expanded, setExpanded] = createSignal(false)
const [overflowed, setOverflowed] = createSignal(false)
let preEl: HTMLElement | undefined
function checkOverflow() {
if (!preEl) return
const code = preEl.getElementsByTagName("code")[0]
if (code && !local.expand) {
setOverflowed(preEl.clientHeight < code.offsetHeight)
}
}
onMount(() => {
window.addEventListener("resize", checkOverflow)
})
onCleanup(() => {
window.removeEventListener("resize", checkOverflow)
})
return (
<div
class={styles["message-lsp"]}
data-expanded={expanded() || local.expand === true}
{...rest}
>
<CodeBlock
lang="ansi"
code={local.text}
onRendered={checkOverflow}
ref={(el) => (preEl = el)}
/>
{((!local.expand && overflowed()) || expanded()) && (
<button
type="button"
data-element-button-text
onClick={() => setExpanded((e) => !e)}
>
{expanded() ? "Show less" : "Show more"}
</button>
)}
</div>
)
}
interface MarkdownPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
text: string
expand?: boolean
@@ -1258,7 +1314,10 @@ export default function Share(props: {
const hasError = () => toolData()?.metadata?.error
const content = () => toolData()?.args?.content
const diagnostics = createMemo(() =>
getDiagnostics(toolData()?.metadata?.diagnostics)
getDiagnostics(
toolData()?.metadata?.diagnostics,
toolData()?.args.filePath
)
)
return (
@@ -1280,8 +1339,7 @@ export default function Share(props: {
<b>{filePath()}</b>
</div>
<Show when={diagnostics().length > 0}>
<TextPart
data-size="sm"
<LspPart
text={diagnostics().join("\n\n")}
/>
</Show>
@@ -1344,7 +1402,10 @@ export default function Share(props: {
)
)
const diagnostics = createMemo(() =>
getDiagnostics(toolData()?.metadata?.diagnostics)
getDiagnostics(
toolData()?.metadata?.diagnostics,
toolData()?.args.filePath
)
)
return (
@@ -1387,8 +1448,7 @@ export default function Share(props: {
</Match>
</Switch>
<Show when={diagnostics().length > 0}>
<TextPart
data-size="sm"
<LspPart
text={diagnostics().join("\n\n")}
/>
</Show>
@@ -421,6 +421,50 @@
}
}
.message-lsp {
background-color: var(--sl-color-bg-surface);
padding: 0.5rem calc(0.5rem + 3px);
border-radius: 0.25rem;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1rem;
align-self: flex-start;
max-width: var(--md-tool-width);
padding: 0.5rem calc(0.5rem + 3px);
pre {
--shiki-dark-bg: var(--sl-color-bg-surface) !important;
background-color: var(--sl-color-bg-surface) !important;
line-height: 1.4;
font-size: 0.75rem;
white-space: pre-wrap;
word-break: break-word;
}
&[data-expanded="true"] {
pre {
display: block;
}
}
&[data-expanded="false"] {
pre {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 7;
overflow: hidden;
}
}
button {
flex: 0 0 auto;
padding: 2px 0;
font-size: 0.75rem;
}
}
.message-terminal {
display: flex;
flex-direction: column;