mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 16:56:33 -04:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91d01fd4cc | |||
| 9beb0f8512 | |||
| d4cb47eadc | |||
| 261ff416a9 | |||
| d0a70cb217 | |||
| 20fc56d020 | |||
| a57ae3ec93 | |||
| 30f9fa12d9 | |||
| d473d4ffc8 | |||
| af50596529 | |||
| 3823d8d50e | |||
| 7a926b32ce | |||
| a5ede68241 | |||
| 60dc38050d | |||
| 31d0caee38 | |||
| 2a7ab45605 | |||
| 019054dd1e | |||
| a018a15f32 | |||
| e630d680dd | |||
| 9e392f25a6 | |||
| 2cc4e6ad7c | |||
| 70d8d1ab1e | |||
| 342aa27e03 | |||
| e1aed0cd01 | |||
| c8ea2c5ce0 | |||
| 5e8309a353 | |||
| aae0ce9921 | |||
| 81b94d84dc | |||
| ceab70f8d9 | |||
| afe8cecc2b | |||
| 4a292bf977 | |||
| e249b41513 | |||
| 9021dd60a1 |
@@ -0,0 +1,40 @@
|
||||
name: Auto-label TUI Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
auto-label:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Add opentui label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const title = issue.title;
|
||||
const description = issue.body || '';
|
||||
|
||||
// Check for version patterns like v1.0.x or 1.0.x
|
||||
const versionPattern = /\b[v]?1\.0\.[x\d]\b/i;
|
||||
|
||||
if (versionPattern.test(title) || versionPattern.test(description)) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: ['opentui']
|
||||
});
|
||||
|
||||
await github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
assignees: ['thdxr']
|
||||
});
|
||||
}
|
||||
@@ -26,4 +26,4 @@ jobs:
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
with:
|
||||
model: opencode/kimi-k2
|
||||
model: opencode/glm-4.6
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: hello world
|
||||
description: hello world iaosd ioasjdoiasjd oisadjoisajd osiajd oisaj dosaij dsoajsajdaijdoisa jdoias jdoias jdoia jois jo jdois jdoias jdoias j djoasdj
|
||||
---
|
||||
|
||||
hey there $ARGUMENTS
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"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.0",
|
||||
"version": "1.0.5",
|
||||
"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.0",
|
||||
"version": "1.0.5",
|
||||
"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.0",
|
||||
"version": "1.0.5",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -150,7 +150,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
@@ -166,7 +166,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -243,7 +243,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -263,7 +263,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.81.0",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -274,7 +274,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -287,7 +287,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -317,7 +317,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"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.0"
|
||||
"version": "1.0.5"
|
||||
},
|
||||
"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.0",
|
||||
"version": "1.0.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,34 +1,64 @@
|
||||
import { For, Match, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
||||
import { Part } from "@opencode-ai/ui"
|
||||
import { For, JSXElement, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
||||
import { Markdown, Part } from "@opencode-ai/ui"
|
||||
import { useSync } from "@/context/sync"
|
||||
import type { AssistantMessage as AssistantMessageType } 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[] }) {
|
||||
export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) {
|
||||
const sync = useSync()
|
||||
const items = createMemo(() => props.assistantMessages().flatMap((m) => sync.data.part[m.id]))
|
||||
const parts = createMemo(() => props.assistantMessages().flatMap((m) => sync.data.part[m.id]))
|
||||
const done = createMemo(() => props.done ?? false)
|
||||
const currentTask = createMemo(
|
||||
() =>
|
||||
parts().findLast(
|
||||
(p) =>
|
||||
p &&
|
||||
p.type === "tool" &&
|
||||
p.tool === "task" &&
|
||||
p.state &&
|
||||
"metadata" in p.state &&
|
||||
p.state.metadata &&
|
||||
p.state.metadata.sessionId &&
|
||||
p.state.status === "running",
|
||||
) as ToolPart,
|
||||
)
|
||||
|
||||
const finishedItems = createMemo(() => [
|
||||
"",
|
||||
"",
|
||||
"Loading...",
|
||||
...items().filter(
|
||||
(p) =>
|
||||
p?.type === "text" ||
|
||||
(p?.type === "reasoning" && p.time?.end) ||
|
||||
(p?.type === "tool" && p.state.status === "completed"),
|
||||
),
|
||||
"",
|
||||
const resolvedParts = createMemo(() => {
|
||||
let resolved = parts()
|
||||
const task = currentTask()
|
||||
if (task && task.state && "metadata" in task.state && task.state.metadata?.sessionId) {
|
||||
const messages = sync.data.message[task.state.metadata.sessionId as string]?.filter((m) => m.role === "assistant")
|
||||
resolved = messages?.flatMap((m) => sync.data.part[m.id]) ?? parts()
|
||||
}
|
||||
return resolved
|
||||
})
|
||||
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")
|
||||
})
|
||||
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" />] : []),
|
||||
])
|
||||
|
||||
const MINIMUM_DELAY = 400
|
||||
const [visibleCount, setVisibleCount] = createSignal(1)
|
||||
const delay = createMemo(() => (done() ? 220 : 400))
|
||||
const [visibleCount, setVisibleCount] = createSignal(eligibleItems().length)
|
||||
|
||||
createEffect(() => {
|
||||
const total = finishedItems().length
|
||||
if (total > visibleCount()) {
|
||||
const timer = setTimeout(() => {
|
||||
setVisibleCount((prev) => prev + 1)
|
||||
}, MINIMUM_DELAY)
|
||||
}, delay())
|
||||
onCleanup(() => clearTimeout(timer))
|
||||
} else if (total < visibleCount()) {
|
||||
setVisibleCount(total)
|
||||
@@ -42,41 +72,57 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
class="h-30 overflow-hidden pointer-events-none
|
||||
mask-alpha mask-t-from-33% mask-t-from-background-base mask-t-to-transparent
|
||||
mask-b-from-90% mask-b-from-background-base mask-b-to-transparent"
|
||||
>
|
||||
<div class="flex flex-col gap-3">
|
||||
<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()})` }}
|
||||
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"
|
||||
>
|
||||
<For each={finishedItems()}>
|
||||
{(part) => {
|
||||
if (typeof part === "string") return <div class="h-8 flex items-center w-full">{part}</div>
|
||||
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>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<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) => {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
onSubmit={handleSubmit}
|
||||
classList={{
|
||||
"bg-surface-raised-stronger-non-alpha border border-border-strong-base": true,
|
||||
"rounded-2xl overflow-clip focus-within:shadow-xs-border-selected": true,
|
||||
"rounded-2xl overflow-clip focus-within:border-transparent focus-within:shadow-xs-border-select": true,
|
||||
[props.class ?? ""]: !!props.class,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -162,10 +162,32 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
|
||||
const recent = createMemo(() => store.recent.map(find).filter(Boolean))
|
||||
|
||||
const cycle = (direction: 1 | -1) => {
|
||||
const recentList = recent()
|
||||
const current = currentModel()
|
||||
if (!current) return
|
||||
|
||||
const index = recentList.findIndex((x) => x?.provider.id === current.provider.id && x?.id === current.id)
|
||||
if (index === -1) return
|
||||
|
||||
let next = index + direction
|
||||
if (next < 0) next = recentList.length - 1
|
||||
if (next >= recentList.length) next = 0
|
||||
|
||||
const val = recentList[next]
|
||||
if (!val) return
|
||||
|
||||
model.set({
|
||||
providerID: val.provider.id,
|
||||
modelID: val.id,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
current: currentModel,
|
||||
recent,
|
||||
list,
|
||||
cycle,
|
||||
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
|
||||
batch(() => {
|
||||
setStore("model", agent.current().name, model ?? fallbackModel())
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Accordion,
|
||||
Diff,
|
||||
Collapsible,
|
||||
Part,
|
||||
DiffChanges,
|
||||
ProgressCircle,
|
||||
Message,
|
||||
@@ -548,7 +547,79 @@ export default function Page() {
|
||||
<For each={local.session.userMessages()}>
|
||||
{(message) => {
|
||||
const diffs = createMemo(() => message.summary?.diffs ?? [])
|
||||
const working = createMemo(() => !message.summary?.title)
|
||||
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 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">
|
||||
<button
|
||||
@@ -570,7 +641,10 @@ export default function Page() {
|
||||
"text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
|
||||
}}
|
||||
>
|
||||
{message.summary?.title ?? local.session.getMessageText(message)}
|
||||
<Switch>
|
||||
<Match when={working()}>{status()}</Match>
|
||||
<Match when={true}>{message.summary?.title}</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
@@ -583,7 +657,8 @@ export default function Page() {
|
||||
<For each={local.session.userMessages()}>
|
||||
{(message) => {
|
||||
const isActive = createMemo(() => local.session.activeMessage()?.id === message.id)
|
||||
const [initialized, setInitialized] = createSignal(!!message.summary?.title)
|
||||
const [titled, setTitled] = createSignal(!!message.summary?.title)
|
||||
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)
|
||||
@@ -597,11 +672,18 @@ export default function Page() {
|
||||
const hasToolPart = createMemo(() =>
|
||||
assistantMessages()
|
||||
?.flatMap((m) => sync.data.part[m.id])
|
||||
.some((p) => p.type === "tool"),
|
||||
.some((p) => p?.type === "tool"),
|
||||
)
|
||||
const working = createMemo(() => !summary())
|
||||
|
||||
// allowing time for the animations to finish
|
||||
createEffect(() => {
|
||||
setTimeout(() => setInitialized(!!title()), 10_000)
|
||||
title()
|
||||
setTimeout(() => setTitled(!!title()), 10_000)
|
||||
})
|
||||
createEffect(() => {
|
||||
summary()
|
||||
setTimeout(() => setCompleted(!!summary()), 1200)
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -612,9 +694,18 @@ export default function Page() {
|
||||
>
|
||||
{/* Title */}
|
||||
<div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10">
|
||||
<div class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0">
|
||||
<Show when={initialized()} fallback={<Typewriter as="h1" text={title()} />}>
|
||||
<h1>{title()}</h1>
|
||||
<div class="w-full text-14-medium text-text-strong">
|
||||
<Show
|
||||
when={titled()}
|
||||
fallback={
|
||||
<Typewriter
|
||||
as="h1"
|
||||
text={title()}
|
||||
class="overflow-hidden text-ellipsis min-w-0 text-nowrap"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<h1 class="overflow-hidden text-ellipsis min-w-0 text-nowrap">{title()}</h1>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
@@ -622,7 +713,7 @@ export default function Page() {
|
||||
<Message message={message} parts={parts()} />
|
||||
</div>
|
||||
{/* Summary */}
|
||||
<Show when={!working()}>
|
||||
<Show when={completed()}>
|
||||
<div class="w-full flex flex-col gap-6 items-start self-stretch">
|
||||
<div class="flex flex-col items-start gap-1 self-stretch">
|
||||
<h2 class="text-12-medium text-text-weak">
|
||||
@@ -631,7 +722,14 @@ export default function Page() {
|
||||
<Match when={true}>Response</Match>
|
||||
</Switch>
|
||||
</h2>
|
||||
<Show when={summary()}>{(summary) => <Markdown text={summary()} />}</Show>
|
||||
<Show when={summary()}>
|
||||
{(summary) => (
|
||||
<Markdown
|
||||
classList={{ "[&>*]:fade-up-text": !diffs().length }}
|
||||
text={summary()}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<Accordion class="w-full" multiple>
|
||||
<For each={diffs()}>
|
||||
@@ -684,17 +782,17 @@ export default function Page() {
|
||||
{/* Response */}
|
||||
<div class="w-full">
|
||||
<Switch>
|
||||
<Match when={working()}>
|
||||
<MessageProgress assistantMessages={assistantMessages} />
|
||||
<Match when={!completed()}>
|
||||
<MessageProgress assistantMessages={assistantMessages} done={!working()} />
|
||||
</Match>
|
||||
<Match when={!working() && hasToolPart()}>
|
||||
<Match when={completed() && hasToolPart()}>
|
||||
<Collapsible variant="ghost" open={expanded()} onOpenChange={setExpanded}>
|
||||
<Collapsible.Trigger class="text-text-weak hover:text-text-strong">
|
||||
<div class="flex items-center gap-1 self-stretch">
|
||||
<div class="text-12-medium">
|
||||
<Switch>
|
||||
<Match when={expanded()}>Hide steps</Match>
|
||||
<Match when={!expanded()}>Show steps</Match>
|
||||
<Match when={expanded()}>Hide details</Match>
|
||||
<Match when={!expanded()}>Show details</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<Collapsible.Arrow />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"$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.0",
|
||||
"version": "1.0.5",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -12,7 +12,6 @@ export const ServeCommand = cmd({
|
||||
default: 0,
|
||||
})
|
||||
.option("hostname", {
|
||||
alias: ["h"],
|
||||
type: "string",
|
||||
describe: "hostname to listen on",
|
||||
default: "127.0.0.1",
|
||||
|
||||
@@ -65,7 +65,6 @@ export const TuiCommand = cmd({
|
||||
default: 0,
|
||||
})
|
||||
.option("hostname", {
|
||||
alias: ["h"],
|
||||
type: "string",
|
||||
describe: "hostname to listen on",
|
||||
default: "127.0.0.1",
|
||||
|
||||
@@ -27,6 +27,7 @@ import { ExitProvider } from "./context/exit"
|
||||
import type { SessionRoute } from "./context/route"
|
||||
import { Session as SessionApi } from "@/session"
|
||||
import { TuiEvent } from "./event"
|
||||
import { KVProvider, useKV } from "./context/kv"
|
||||
|
||||
export function tui(input: {
|
||||
url: string
|
||||
@@ -52,29 +53,35 @@ export function tui(input: {
|
||||
render(
|
||||
() => {
|
||||
return (
|
||||
<ErrorBoundary fallback={<text>Something went wrong</text>}>
|
||||
<ErrorBoundary
|
||||
fallback={(error, reset) => (
|
||||
<ErrorComponent error={error} reset={reset} onExit={onExit} />
|
||||
)}
|
||||
>
|
||||
<ExitProvider onExit={onExit}>
|
||||
<ToastProvider>
|
||||
<RouteProvider data={routeData}>
|
||||
<SDKProvider url={input.url}>
|
||||
<SyncProvider>
|
||||
<ThemeProvider>
|
||||
<LocalProvider initialModel={input.model} initialAgent={input.agent}>
|
||||
<KeybindProvider>
|
||||
<DialogProvider>
|
||||
<CommandProvider>
|
||||
<PromptHistoryProvider>
|
||||
<App />
|
||||
</PromptHistoryProvider>
|
||||
</CommandProvider>
|
||||
</DialogProvider>
|
||||
</KeybindProvider>
|
||||
</LocalProvider>
|
||||
</ThemeProvider>
|
||||
</SyncProvider>
|
||||
</SDKProvider>
|
||||
</RouteProvider>
|
||||
</ToastProvider>
|
||||
<KVProvider>
|
||||
<ToastProvider>
|
||||
<RouteProvider data={routeData}>
|
||||
<SDKProvider url={input.url}>
|
||||
<SyncProvider>
|
||||
<ThemeProvider>
|
||||
<LocalProvider initialModel={input.model} initialAgent={input.agent}>
|
||||
<KeybindProvider>
|
||||
<DialogProvider>
|
||||
<CommandProvider>
|
||||
<PromptHistoryProvider>
|
||||
<App />
|
||||
</PromptHistoryProvider>
|
||||
</CommandProvider>
|
||||
</DialogProvider>
|
||||
</KeybindProvider>
|
||||
</LocalProvider>
|
||||
</ThemeProvider>
|
||||
</SyncProvider>
|
||||
</SDKProvider>
|
||||
</RouteProvider>
|
||||
</ToastProvider>
|
||||
</KVProvider>
|
||||
</ExitProvider>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
@@ -95,6 +102,7 @@ function App() {
|
||||
renderer.disableStdoutInterception()
|
||||
const dialog = useDialog()
|
||||
const local = useLocal()
|
||||
const kv = useKV()
|
||||
const command = useCommandDialog()
|
||||
const { event } = useSDK()
|
||||
const sync = useSync()
|
||||
@@ -164,6 +172,24 @@ function App() {
|
||||
dialog.replace(() => <DialogModel />)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Model cycle",
|
||||
value: "model.cycle_recent",
|
||||
keybind: "model_cycle_recent",
|
||||
category: "Agent",
|
||||
onSelect: () => {
|
||||
local.model.cycle(1)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Model cycle reverse",
|
||||
value: "model.cycle_recent_reverse",
|
||||
keybind: "model_cycle_recent_reverse",
|
||||
category: "Agent",
|
||||
onSelect: () => {
|
||||
local.model.cycle(-1)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Switch agent",
|
||||
value: "agent.list",
|
||||
@@ -222,13 +248,13 @@ function App() {
|
||||
|
||||
createEffect(() => {
|
||||
const providerID = local.model.current().providerID
|
||||
if (providerID === "openrouter" && !local.kv.data.openrouter_warning) {
|
||||
if (providerID === "openrouter" && !kv.data.openrouter_warning) {
|
||||
untrack(() => {
|
||||
DialogAlert.show(
|
||||
dialog,
|
||||
"Warning",
|
||||
"While openrouter is a convenient way to access LLMs your request will often be routed to subpar providers that do not work well in our testing.\n\nFor reliable access to models check out OpenCode Zen\nhttps://opencode.ai/zen",
|
||||
).then(() => local.kv.set("openrouter_warning", true))
|
||||
).then(() => kv.set("openrouter_warning", true))
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -315,7 +341,7 @@ function App() {
|
||||
<text
|
||||
bg={local.agent.color(local.agent.current().name)}
|
||||
fg={theme.background}
|
||||
wrapMode="none"
|
||||
wrapMode={undefined}
|
||||
>
|
||||
<span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span>
|
||||
<span> AGENT </span>
|
||||
@@ -325,3 +351,57 @@ function App() {
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function ErrorComponent(props: { error: Error; reset: () => void; onExit: () => Promise<void> }) {
|
||||
const term = useTerminalDimensions()
|
||||
useKeyboard((evt) => {
|
||||
if (evt.ctrl && evt.name === "c") {
|
||||
props.onExit()
|
||||
}
|
||||
})
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
|
||||
const issueURL = new URL("https://github.com/sst/opencode/issues/new?template=bug-report.yml")
|
||||
|
||||
if (props.error.message) {
|
||||
issueURL.searchParams.set("title", `opentui: fatal: ${props.error.message}`)
|
||||
}
|
||||
|
||||
if (props.error.stack) {
|
||||
issueURL.searchParams.set(
|
||||
"description",
|
||||
"```\n" + props.error.stack.substring(0, 6000 - issueURL.toString().length) + "...\n```",
|
||||
)
|
||||
}
|
||||
|
||||
const copyIssueURL = () => {
|
||||
Clipboard.copy(issueURL.toString()).then(() => {
|
||||
setCopied(true)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<box flexDirection="column" gap={1}>
|
||||
<box flexDirection="row" gap={1} alignItems="center">
|
||||
<text attributes={TextAttributes.BOLD}>Please report an issue.</text>
|
||||
<box onMouseUp={copyIssueURL} backgroundColor="#565f89" padding={1}>
|
||||
<text attributes={TextAttributes.BOLD}>Copy issue URL (exception info pre-filled)</text>
|
||||
</box>
|
||||
{copied() && <text>Successfully copied</text>}
|
||||
</box>
|
||||
<box flexDirection="row" gap={2} alignItems="center">
|
||||
<text>A fatal error occurred!</text>
|
||||
<box onMouseUp={props.reset} backgroundColor="#565f89" padding={1}>
|
||||
<text>Reset TUI</text>
|
||||
</box>
|
||||
<box onMouseUp={props.onExit} backgroundColor="#565f89" padding={1}>
|
||||
<text>Exit</text>
|
||||
</box>
|
||||
</box>
|
||||
<scrollbox height={Math.floor(term().height * 0.7)}>
|
||||
<text>{props.error.stack}</text>
|
||||
</scrollbox>
|
||||
<text>{props.error.message}</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ function init() {
|
||||
}
|
||||
}
|
||||
},
|
||||
show() {
|
||||
dialog.replace(() => <DialogCommand options={options()} />)
|
||||
},
|
||||
register(cb: () => CommandOption[]) {
|
||||
const results = createMemo(cb)
|
||||
setRegistrations((arr) => [results, ...arr])
|
||||
@@ -75,7 +78,7 @@ export function CommandProvider(props: ParentProps) {
|
||||
const keybind = useKeybind()
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (keybind.match("command_list", evt)) {
|
||||
if (keybind.match("command_list", evt) && dialog.stack.length === 0) {
|
||||
evt.preventDefault()
|
||||
dialog.replace(() => <DialogCommand options={value.options} />)
|
||||
return
|
||||
@@ -90,7 +93,10 @@ function DialogCommand(props: { options: CommandOption[] }) {
|
||||
return (
|
||||
<DialogSelect
|
||||
title="Commands"
|
||||
options={props.options.map((x) => ({ ...x, footer: x.keybind ? keybind.print(x.keybind) : undefined }))}
|
||||
options={props.options.map((x) => ({
|
||||
...x,
|
||||
footer: x.keybind ? keybind.print(x.keybind) : undefined,
|
||||
}))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Locale } from "@/util/locale"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { DialogSessionRename } from "./dialog-session-rename"
|
||||
|
||||
export function DialogSessionList() {
|
||||
const dialog = useDialog()
|
||||
@@ -74,6 +75,13 @@ export function DialogSessionList() {
|
||||
setToDelete(option.value)
|
||||
},
|
||||
},
|
||||
{
|
||||
keybind: Keybind.parse("r")[0],
|
||||
title: "rename",
|
||||
onTrigger: async (option) => {
|
||||
dialog.replace(() => <DialogSessionRename session={option.value} />)
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { DialogPrompt } from "@tui/ui/dialog-prompt"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { createMemo } from "solid-js"
|
||||
import { useSDK } from "../context/sdk"
|
||||
|
||||
interface DialogSessionRenameProps {
|
||||
session: string
|
||||
}
|
||||
|
||||
export function DialogSessionRename(props: DialogSessionRenameProps) {
|
||||
const dialog = useDialog()
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const session = createMemo(() => sync.session.get(props.session))
|
||||
|
||||
return (
|
||||
<DialogPrompt
|
||||
title="Rename Session"
|
||||
value={session()?.title}
|
||||
onConfirm={(value) => {
|
||||
sdk.client.session.update({
|
||||
path: {
|
||||
id: props.session,
|
||||
},
|
||||
body: {
|
||||
title: value,
|
||||
},
|
||||
})
|
||||
dialog.clear()
|
||||
}}
|
||||
onCancel={() => dialog.clear()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export function DialogStatus() {
|
||||
<text attributes={TextAttributes.BOLD}>Status</text>
|
||||
<text fg={theme.textMuted}>esc</text>
|
||||
</box>
|
||||
<Show when={Object.keys(sync.data.mcp).length > 0}>
|
||||
<Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text>No MCP Servers</text>}>
|
||||
<box>
|
||||
<text>{Object.keys(sync.data.mcp).length} MCP Servers</text>
|
||||
<For each={Object.entries(sync.data.mcp)}>
|
||||
|
||||
@@ -4,23 +4,24 @@ import { useDialog } from "../ui/dialog"
|
||||
import { onCleanup, onMount } from "solid-js"
|
||||
|
||||
export function DialogThemeList() {
|
||||
const { selectedTheme, setSelectedTheme } = useTheme()
|
||||
const theme = useTheme()
|
||||
const options = Object.keys(THEMES).map((value) => ({
|
||||
title: value,
|
||||
value: value as keyof typeof THEMES,
|
||||
}))
|
||||
const initial = selectedTheme()
|
||||
const dialog = useDialog()
|
||||
let confirmed = false
|
||||
let ref: DialogSelectRef<keyof typeof THEMES>
|
||||
const initial = theme.selected
|
||||
|
||||
onMount(() => {
|
||||
// highlight the first theme in the list when we open it for UX
|
||||
setSelectedTheme(Object.keys(THEMES)[0] as keyof typeof THEMES)
|
||||
theme.set(Object.keys(THEMES)[0] as keyof typeof THEMES)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
// if we close the dialog without confirming, reset back to the initial theme
|
||||
if (!confirmed) setSelectedTheme(initial)
|
||||
if (!confirmed) theme.set(initial)
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -28,10 +29,10 @@ export function DialogThemeList() {
|
||||
title="Themes"
|
||||
options={options}
|
||||
onMove={(opt) => {
|
||||
setSelectedTheme(opt.value)
|
||||
theme.set(opt.value)
|
||||
}}
|
||||
onSelect={(opt) => {
|
||||
setSelectedTheme(opt.value)
|
||||
theme.set(opt.value)
|
||||
confirmed = true
|
||||
dialog.clear()
|
||||
}}
|
||||
@@ -40,12 +41,12 @@ export function DialogThemeList() {
|
||||
}}
|
||||
onFilter={(query) => {
|
||||
if (query.length === 0) {
|
||||
setSelectedTheme(initial)
|
||||
theme.set(initial)
|
||||
return
|
||||
}
|
||||
|
||||
const first = ref.filtered[0]
|
||||
if (first) setSelectedTheme(first.value)
|
||||
if (first) theme.set(first.value)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -222,6 +222,11 @@ export function Autocomplete(props: {
|
||||
description: "unshare a session",
|
||||
onSelect: () => command.trigger("session.unshare"),
|
||||
},
|
||||
{
|
||||
display: "/rename",
|
||||
description: "rename session",
|
||||
onSelect: () => command.trigger("session.rename"),
|
||||
},
|
||||
)
|
||||
}
|
||||
results.push(
|
||||
@@ -240,16 +245,36 @@ export function Autocomplete(props: {
|
||||
description: "list agents",
|
||||
onSelect: () => command.trigger("agent.list"),
|
||||
},
|
||||
{
|
||||
display: "/session",
|
||||
description: "list sessions",
|
||||
onSelect: () => command.trigger("session.list"),
|
||||
},
|
||||
{
|
||||
display: "/status",
|
||||
description: "show status",
|
||||
onSelect: () => command.trigger("opencode.status"),
|
||||
},
|
||||
{
|
||||
display: "/theme",
|
||||
description: "toggle theme",
|
||||
onSelect: () => command.trigger("theme.switch"),
|
||||
},
|
||||
{
|
||||
display: "/editor",
|
||||
description: "open editor",
|
||||
onSelect: () => command.trigger("prompt.editor"),
|
||||
},
|
||||
{
|
||||
display: "/help",
|
||||
description: "show help",
|
||||
onSelect: () => command.trigger("help.show"),
|
||||
},
|
||||
{
|
||||
display: "/commands",
|
||||
description: "show all commands",
|
||||
onSelect: () => command.show(),
|
||||
},
|
||||
)
|
||||
const max = firstBy(results, [(x) => x.display.length, "desc"])?.display.length
|
||||
if (!max) return results
|
||||
@@ -329,8 +354,8 @@ export function Autocomplete(props: {
|
||||
if (e.name === "up") move(-1)
|
||||
if (e.name === "down") move(1)
|
||||
if (e.name === "escape") hide()
|
||||
if (e.name === "return") select()
|
||||
if (["up", "down", "return", "escape"].includes(e.name)) e.preventDefault()
|
||||
if (e.name === "return" || e.name === "tab") select()
|
||||
if (["up", "down", "return", "tab", "escape"].includes(e.name)) e.preventDefault()
|
||||
}
|
||||
if (!store.visible) {
|
||||
if (e.name === "@") {
|
||||
@@ -386,11 +411,14 @@ export function Autocomplete(props: {
|
||||
backgroundColor={index() === store.selected ? theme.primary : undefined}
|
||||
flexDirection="row"
|
||||
>
|
||||
<text fg={index() === store.selected ? theme.background : theme.text}>
|
||||
<text fg={index() === store.selected ? theme.background : theme.text} flexShrink={0}>
|
||||
{option.display}
|
||||
</text>
|
||||
<Show when={option.description}>
|
||||
<text fg={index() === store.selected ? theme.background : theme.textMuted}>
|
||||
<text
|
||||
fg={index() === store.selected ? theme.background : theme.textMuted}
|
||||
wrapMode="none"
|
||||
>
|
||||
{option.description}
|
||||
</text>
|
||||
</Show>
|
||||
|
||||
@@ -530,6 +530,17 @@ export function Prompt(props: PromptProps) {
|
||||
setStore("extmarkToPartIndex", new Map())
|
||||
return
|
||||
}
|
||||
if (keybind.match("input_forward_delete", e) && store.prompt.input !== "") {
|
||||
const cursorOffset = input.cursorOffset
|
||||
if (cursorOffset < input.plainText.length) {
|
||||
const text = input.plainText
|
||||
const newText = text.slice(0, cursorOffset) + text.slice(cursorOffset + 1)
|
||||
input.setText(newText)
|
||||
input.cursorOffset = cursorOffset
|
||||
}
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
if (keybind.match("app_exit", e)) {
|
||||
await exit()
|
||||
return
|
||||
@@ -552,10 +563,11 @@ export function Prompt(props: PromptProps) {
|
||||
if (store.mode === "normal") autocomplete.onKeyDown(e)
|
||||
if (!autocomplete.visible) {
|
||||
if (
|
||||
(e.name === "up" && input.cursorOffset === 0) ||
|
||||
(e.name === "down" && input.cursorOffset === input.plainText.length)
|
||||
(keybind.match("history_previous", e) && input.cursorOffset === 0) ||
|
||||
(keybind.match("history_next", e) &&
|
||||
input.cursorOffset === input.plainText.length)
|
||||
) {
|
||||
const direction = e.name === "up" ? -1 : 1
|
||||
const direction = keybind.match("history_previous", e) ? -1 : 1
|
||||
const item = history.move(direction, input.plainText)
|
||||
|
||||
if (item) {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Global } from "@/global"
|
||||
import { createSignal } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import path from "path"
|
||||
|
||||
export const { use: useKV, provider: KVProvider } = createSimpleContext({
|
||||
name: "KV",
|
||||
init: () => {
|
||||
const [ready, setReady] = createSignal(false)
|
||||
const [kvStore, setKvStore] = createStore({
|
||||
openrouter_warning: false,
|
||||
theme: "opencode",
|
||||
})
|
||||
const file = Bun.file(path.join(Global.Path.state, "kv.json"))
|
||||
|
||||
file
|
||||
.json()
|
||||
.then((x) => {
|
||||
setKvStore(x)
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
setReady(true)
|
||||
})
|
||||
|
||||
return {
|
||||
get data() {
|
||||
return kvStore
|
||||
},
|
||||
get ready() {
|
||||
return ready()
|
||||
},
|
||||
set(key: string, value: any) {
|
||||
setKvStore(key as any, value)
|
||||
Bun.write(
|
||||
file,
|
||||
JSON.stringify({
|
||||
[key]: value,
|
||||
}),
|
||||
)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -15,17 +15,18 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const sync = useSync()
|
||||
const toast = useToast()
|
||||
|
||||
function isModelValid(model: { providerID: string, modelID: string }) {
|
||||
function isModelValid(model: { providerID: string; modelID: string }) {
|
||||
const provider = sync.data.provider.find((x) => x.id === model.providerID)
|
||||
return !!provider?.models[model.modelID]
|
||||
}
|
||||
|
||||
function getFirstValidModel(...modelFns: (() => { providerID: string, modelID: string } | undefined)[]) {
|
||||
function getFirstValidModel(
|
||||
...modelFns: (() => { providerID: string; modelID: string } | undefined)[]
|
||||
) {
|
||||
for (const modelFn of modelFns) {
|
||||
const model = modelFn()
|
||||
if (!model) continue
|
||||
if (isModelValid(model))
|
||||
return model
|
||||
if (isModelValid(model)) return model
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,20 +142,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
.then((x) => {
|
||||
setModelStore("recent", x.recent)
|
||||
})
|
||||
.catch(() => { })
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
setModelStore("ready", true)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
Bun.write(
|
||||
file,
|
||||
JSON.stringify({
|
||||
recent: modelStore.recent,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const fallbackModel = createMemo(() => {
|
||||
if (sync.data.config.model) {
|
||||
const [providerID, modelID] = sync.data.config.model.split("/")
|
||||
@@ -205,6 +197,21 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
model: model.name ?? value.modelID,
|
||||
}
|
||||
}),
|
||||
cycle(direction: 1 | -1) {
|
||||
const current = currentModel()
|
||||
if (!current) return
|
||||
const recent = modelStore.recent
|
||||
const index = recent.findIndex(
|
||||
(x) => x.providerID === current.providerID && x.modelID === current.modelID,
|
||||
)
|
||||
if (index === -1) return
|
||||
let next = index + direction
|
||||
if (next < 0) next = recent.length - 1
|
||||
if (next >= recent.length) next = 0
|
||||
const val = recent[next]
|
||||
if (!val) return
|
||||
setModelStore("model", agent.current().name, { ...val })
|
||||
},
|
||||
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
|
||||
batch(() => {
|
||||
if (!isModelValid(model)) {
|
||||
@@ -215,61 +222,26 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setModelStore("model", agent.current().name, model)
|
||||
if (options?.recent) {
|
||||
const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID)
|
||||
if (uniq.length > 5) uniq.pop()
|
||||
setModelStore("recent", uniq)
|
||||
Bun.write(
|
||||
file,
|
||||
JSON.stringify({
|
||||
recent: modelStore.recent,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const kv = iife(() => {
|
||||
const [ready, setReady] = createSignal(false)
|
||||
const [kvStore, setKvStore] = createStore({
|
||||
openrouter_warning: false,
|
||||
})
|
||||
const file = Bun.file(path.join(Global.Path.state, "kv.json"))
|
||||
|
||||
file
|
||||
.json()
|
||||
.then((x) => {
|
||||
setKvStore(x)
|
||||
})
|
||||
.catch(() => { })
|
||||
.finally(() => {
|
||||
setReady(true)
|
||||
})
|
||||
|
||||
return {
|
||||
get data() {
|
||||
return kvStore
|
||||
},
|
||||
get ready() {
|
||||
return ready()
|
||||
},
|
||||
set(key: string, value: any) {
|
||||
setKvStore(key as any, value)
|
||||
Bun.write(
|
||||
file,
|
||||
JSON.stringify({
|
||||
[key]: value,
|
||||
}),
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const result = {
|
||||
model,
|
||||
agent,
|
||||
kv,
|
||||
get ready() {
|
||||
return kv.ready && model.ready
|
||||
},
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SyntaxStyle, RGBA } from "@opentui/core"
|
||||
import { createMemo, createSignal, createEffect } from "solid-js"
|
||||
import { createMemo, createSignal } from "solid-js"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import aura from "../../../../../../tui/internal/theme/themes/aura.json" with { type: "json" }
|
||||
@@ -24,8 +24,7 @@ import synthwave84 from "../../../../../../tui/internal/theme/themes/synthwave84
|
||||
import tokyonight from "../../../../../../tui/internal/theme/themes/tokyonight.json" with { type: "json" }
|
||||
import vesper from "../../../../../../tui/internal/theme/themes/vesper.json" with { type: "json" }
|
||||
import zenburn from "../../../../../../tui/internal/theme/themes/zenburn.json" with { type: "json" }
|
||||
import { iife } from "@/util/iife"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { useKV } from "./kv"
|
||||
|
||||
type Theme = {
|
||||
primary: RGBA
|
||||
@@ -84,7 +83,7 @@ type ThemeJson = {
|
||||
theme: Record<keyof Theme, ColorValue>
|
||||
}
|
||||
|
||||
export const THEMES = {
|
||||
export const THEMES: Record<string, Theme> = {
|
||||
aura: resolveTheme(aura),
|
||||
ayu: resolveTheme(ayu),
|
||||
catppuccin: resolveTheme(catppuccin),
|
||||
@@ -628,28 +627,29 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
name: "Theme",
|
||||
init: () => {
|
||||
const sync = useSync()
|
||||
const [selectedTheme, setSelectedTheme] = createSignal<keyof typeof THEMES>("opencode")
|
||||
const [theme, setTheme] = createStore({} as Theme)
|
||||
createEffect(() => {
|
||||
if (!sync.ready) return
|
||||
setSelectedTheme(
|
||||
iife(() => {
|
||||
if (typeof sync.data.config.theme === "string" && sync.data.config.theme in THEMES) {
|
||||
return sync.data.config.theme as keyof typeof THEMES
|
||||
}
|
||||
return "opencode"
|
||||
}),
|
||||
)
|
||||
})
|
||||
const kv = useKV()
|
||||
|
||||
createEffect(() => {
|
||||
setTheme(reconcile(THEMES[selectedTheme()]))
|
||||
const [theme, setTheme] = createSignal(sync.data.config.theme ?? kv.data.theme)
|
||||
|
||||
const values = createMemo(() => {
|
||||
return THEMES[theme()] ?? THEMES.opencode
|
||||
})
|
||||
|
||||
return {
|
||||
theme,
|
||||
selectedTheme,
|
||||
setSelectedTheme,
|
||||
theme: new Proxy(values(), {
|
||||
get(_target, prop) {
|
||||
// @ts-expect-error
|
||||
return values()[prop]
|
||||
},
|
||||
}),
|
||||
get selected() {
|
||||
return kv.data.theme
|
||||
},
|
||||
set(theme: string) {
|
||||
if (!THEMES[theme]) return
|
||||
setTheme(theme)
|
||||
kv.set("theme", theme)
|
||||
},
|
||||
get ready() {
|
||||
return sync.ready
|
||||
},
|
||||
|
||||
@@ -63,6 +63,7 @@ import { Sidebar } from "./sidebar"
|
||||
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
||||
import parsers from "../../../../../../parsers-config.ts"
|
||||
import { Toast } from "../../ui/toast"
|
||||
import { DialogSessionRename } from "../../component/dialog-session-rename"
|
||||
|
||||
addDefaultParsers(parsers.parsers)
|
||||
|
||||
@@ -370,6 +371,15 @@ export function Session() {
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Rename session",
|
||||
value: "session.rename",
|
||||
keybind: "session_rename",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
dialog.replace(() => <DialogSessionRename session={route.sessionID} />)
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const revert = createMemo(() => {
|
||||
|
||||
@@ -18,7 +18,6 @@ export const TuiSpawnCommand = cmd({
|
||||
default: 0,
|
||||
})
|
||||
.option("hostname", {
|
||||
alias: ["h"],
|
||||
type: "string",
|
||||
describe: "hostname to listen on",
|
||||
default: "127.0.0.1",
|
||||
|
||||
@@ -42,7 +42,6 @@ export const TuiThreadCommand = cmd({
|
||||
default: 0,
|
||||
})
|
||||
.option("hostname", {
|
||||
alias: ["h"],
|
||||
type: "string",
|
||||
describe: "hostname to listen on",
|
||||
default: "127.0.0.1",
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { TextareaRenderable, TextAttributes } from "@opentui/core"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useDialog, type DialogContext } from "./dialog"
|
||||
import { onMount } from "solid-js"
|
||||
|
||||
export type DialogPromptProps = {
|
||||
title: string
|
||||
value?: string
|
||||
onConfirm?: (value: string) => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
export function DialogPrompt(props: DialogPromptProps) {
|
||||
const dialog = useDialog()
|
||||
const { theme } = useTheme()
|
||||
let textarea: TextareaRenderable
|
||||
|
||||
onMount(() => {
|
||||
dialog.setSize("large")
|
||||
setTimeout(() => {
|
||||
textarea.focus()
|
||||
}, 1)
|
||||
textarea.gotoLineEnd()
|
||||
})
|
||||
|
||||
return (
|
||||
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD}>{props.title}</text>
|
||||
<text fg={theme.textMuted}>esc</text>
|
||||
</box>
|
||||
<box>
|
||||
<textarea
|
||||
onSubmit={() => {
|
||||
props.onConfirm?.(textarea.plainText)
|
||||
dialog.clear()
|
||||
}}
|
||||
keyBindings={[{ name: "return", action: "submit" }]}
|
||||
ref={(val: TextareaRenderable) => (textarea = val)}
|
||||
initialValue={props.value}
|
||||
placeholder="Enter text"
|
||||
/>
|
||||
</box>
|
||||
<box paddingBottom={1}>
|
||||
<text fg={theme.textMuted}>Press enter to confirm, esc to cancel</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
DialogPrompt.show = (dialog: DialogContext, title: string, value?: string) => {
|
||||
return new Promise<string | null>((resolve) => {
|
||||
dialog.replace(
|
||||
() => (
|
||||
<DialogPrompt
|
||||
title={title}
|
||||
value={value}
|
||||
onConfirm={(value) => resolve(value)}
|
||||
onCancel={() => resolve(null)}
|
||||
/>
|
||||
),
|
||||
() => resolve(null),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -59,7 +59,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
props.options,
|
||||
filter((x) => x.disabled !== true),
|
||||
take(props.limit ?? Infinity),
|
||||
(x) => (!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj)),
|
||||
(x) =>
|
||||
!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj),
|
||||
)
|
||||
return result
|
||||
})
|
||||
@@ -122,20 +123,25 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
|
||||
const keybind = useKeybind()
|
||||
useKeyboard((evt) => {
|
||||
if (evt.name === "up") move(-1)
|
||||
if (evt.name === "down") move(1)
|
||||
if (evt.name === "up" || (evt.ctrl && evt.name === "p")) move(-1)
|
||||
if (evt.name === "down" || (evt.ctrl && evt.name === "n")) move(1)
|
||||
if (evt.name === "pageup") move(-10)
|
||||
if (evt.name === "pagedown") move(10)
|
||||
if (evt.name === "return") {
|
||||
const option = selected()
|
||||
if (option.onSelect) option.onSelect(dialog)
|
||||
props.onSelect?.(option)
|
||||
if (option) {
|
||||
if (option.onSelect) option.onSelect(dialog)
|
||||
props.onSelect?.(option)
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of props.keybind ?? []) {
|
||||
if (Keybind.match(item.keybind, keybind.parse(evt))) {
|
||||
const s = selected()
|
||||
if (s) item.onTrigger(s)
|
||||
if (s) {
|
||||
evt.preventDefault()
|
||||
item.onTrigger(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -206,11 +212,15 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
props.onSelect?.(option)
|
||||
}}
|
||||
onMouseOver={() => {
|
||||
const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value))
|
||||
const index = filtered().findIndex((x) =>
|
||||
isDeepEqual(x.value, option.value),
|
||||
)
|
||||
if (index === -1) return
|
||||
moveTo(index)
|
||||
}}
|
||||
backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
|
||||
backgroundColor={
|
||||
active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)
|
||||
}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
gap={1}
|
||||
@@ -218,7 +228,9 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
<Option
|
||||
title={option.title}
|
||||
footer={option.footer}
|
||||
description={option.description !== category ? option.description : undefined}
|
||||
description={
|
||||
option.description !== category ? option.description : undefined
|
||||
}
|
||||
active={active()}
|
||||
current={isDeepEqual(option.value, props.current)}
|
||||
/>
|
||||
@@ -234,7 +246,9 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
<For each={props.keybind ?? []}>
|
||||
{(item) => (
|
||||
<text>
|
||||
<span style={{ fg: theme.text, attributes: TextAttributes.BOLD }}>{Keybind.toString(item.keybind)}</span>
|
||||
<span style={{ fg: theme.text, attributes: TextAttributes.BOLD }}>
|
||||
{Keybind.toString(item.keybind)}
|
||||
</span>
|
||||
<span style={{ fg: theme.textMuted }}> {item.title}</span>
|
||||
</text>
|
||||
)}
|
||||
@@ -263,7 +277,10 @@ function Option(props: {
|
||||
wrapMode="none"
|
||||
>
|
||||
{Locale.truncate(props.title, 62)}
|
||||
<span style={{ fg: props.active ? theme.background : theme.textMuted }}> {props.description}</span>
|
||||
<span style={{ fg: props.active ? theme.background : theme.textMuted }}>
|
||||
{" "}
|
||||
{props.description}
|
||||
</span>
|
||||
</text>
|
||||
<Show when={props.footer}>
|
||||
<box flexShrink={0}>
|
||||
|
||||
@@ -385,7 +385,11 @@ export namespace Config {
|
||||
.optional()
|
||||
.default("ctrl+x")
|
||||
.describe("Leader key for keybind combinations"),
|
||||
app_exit: z.string().optional().default("ctrl+c,<leader>q").describe("Exit the application"),
|
||||
app_exit: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("ctrl+c,ctrl+d,<leader>q")
|
||||
.describe("Exit the application"),
|
||||
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
|
||||
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
|
||||
sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
|
||||
@@ -449,11 +453,18 @@ export namespace Config {
|
||||
.default("<leader>h")
|
||||
.describe("Toggle code block concealment in messages"),
|
||||
model_list: z.string().optional().default("<leader>m").describe("List available models"),
|
||||
model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
|
||||
model_cycle_recent_reverse: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("shift+f2")
|
||||
.describe("Previous recently used model"),
|
||||
command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
|
||||
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
|
||||
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
|
||||
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
|
||||
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
|
||||
input_forward_delete: z.string().optional().default("ctrl+d").describe("Forward delete"),
|
||||
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
|
||||
input_submit: z.string().optional().default("enter").describe("Submit input"),
|
||||
input_newline: z
|
||||
@@ -461,6 +472,8 @@ export namespace Config {
|
||||
.optional()
|
||||
.default("shift+enter,ctrl+j")
|
||||
.describe("Insert newline in input"),
|
||||
history_previous: z.string().optional().default("up").describe("Previous history item"),
|
||||
history_next: z.string().optional().default("down").describe("Previous history item"),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
|
||||
@@ -367,6 +367,7 @@ export namespace Ripgrep {
|
||||
args.push(`--max-count=${input.limit}`)
|
||||
}
|
||||
|
||||
args.push("--")
|
||||
args.push(input.pattern)
|
||||
|
||||
const command = args.join(" ")
|
||||
|
||||
@@ -38,6 +38,7 @@ process.on("uncaughtException", (e) => {
|
||||
const cli = yargs(hideBin(process.argv))
|
||||
.scriptName("opencode")
|
||||
.help("help", "show help")
|
||||
.alias("help", "h")
|
||||
.version("version", "show version number", Installation.VERSION)
|
||||
.alias("version", "v")
|
||||
.option("print-logs", {
|
||||
@@ -140,5 +141,5 @@ try {
|
||||
// Most notably, some docker-container-based MCP servers don't handle such signals unless
|
||||
// run using `docker run --init`.
|
||||
// Explicitly exit to avoid any hanging subprocesses.
|
||||
process.exit();
|
||||
process.exit()
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const GrepTool = Tool.define("grep", {
|
||||
const searchPath = params.path || Instance.directory
|
||||
|
||||
const rgPath = await Ripgrep.filepath()
|
||||
const args = ["-nH", "--field-match-separator=|", params.pattern]
|
||||
const args = ["-nH", "--field-match-separator=|", "--regexp", params.pattern]
|
||||
if (params.include) {
|
||||
args.push("--glob", params.include)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -114,6 +114,14 @@ export type KeybindsConfig = {
|
||||
* List available models
|
||||
*/
|
||||
model_list?: string
|
||||
/**
|
||||
* Next recently used model
|
||||
*/
|
||||
model_cycle_recent?: string
|
||||
/**
|
||||
* Previous recently used model
|
||||
*/
|
||||
model_cycle_recent_reverse?: string
|
||||
/**
|
||||
* List available commands
|
||||
*/
|
||||
@@ -134,6 +142,10 @@ export type KeybindsConfig = {
|
||||
* Clear input field
|
||||
*/
|
||||
input_clear?: string
|
||||
/**
|
||||
* Forward delete
|
||||
*/
|
||||
input_forward_delete?: string
|
||||
/**
|
||||
* Paste from clipboard
|
||||
*/
|
||||
@@ -146,6 +158,14 @@ export type KeybindsConfig = {
|
||||
* Insert newline in input
|
||||
*/
|
||||
input_newline?: string
|
||||
/**
|
||||
* Previous history item
|
||||
*/
|
||||
history_previous?: string
|
||||
/**
|
||||
* Previous history item
|
||||
*/
|
||||
history_next?: string
|
||||
}
|
||||
|
||||
export type AgentConfig = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/components/index.ts",
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
color: var(--text-base);
|
||||
text-wrap: pretty;
|
||||
|
||||
/* text-14-regular */
|
||||
/* text-12-regular */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large); /* 142.857% */
|
||||
line-height: var(--line-height-large); /* 166.667% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
|
||||
@@ -11,3 +11,110 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-up-text {
|
||||
animation: fadeUp 0.4s ease-out forwards;
|
||||
opacity: 0;
|
||||
|
||||
&:nth-child(1) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
&:nth-child(4) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
&:nth-child(5) {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
&:nth-child(6) {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
&:nth-child(7) {
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
&:nth-child(8) {
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
&:nth-child(9) {
|
||||
animation-delay: 0.9s;
|
||||
}
|
||||
&:nth-child(10) {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
&:nth-child(11) {
|
||||
animation-delay: 1.1s;
|
||||
}
|
||||
&:nth-child(12) {
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
&:nth-child(13) {
|
||||
animation-delay: 1.3s;
|
||||
}
|
||||
&:nth-child(14) {
|
||||
animation-delay: 1.4s;
|
||||
}
|
||||
&:nth-child(15) {
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
&:nth-child(16) {
|
||||
animation-delay: 1.6s;
|
||||
}
|
||||
&:nth-child(17) {
|
||||
animation-delay: 1.7s;
|
||||
}
|
||||
&:nth-child(18) {
|
||||
animation-delay: 1.8s;
|
||||
}
|
||||
&:nth-child(19) {
|
||||
animation-delay: 1.9s;
|
||||
}
|
||||
&:nth-child(20) {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
&:nth-child(21) {
|
||||
animation-delay: 2.1s;
|
||||
}
|
||||
&:nth-child(22) {
|
||||
animation-delay: 2.2s;
|
||||
}
|
||||
&:nth-child(23) {
|
||||
animation-delay: 2.3s;
|
||||
}
|
||||
&:nth-child(24) {
|
||||
animation-delay: 2.4s;
|
||||
}
|
||||
&:nth-child(25) {
|
||||
animation-delay: 2.5s;
|
||||
}
|
||||
&:nth-child(26) {
|
||||
animation-delay: 2.6s;
|
||||
}
|
||||
&:nth-child(27) {
|
||||
animation-delay: 2.7s;
|
||||
}
|
||||
&:nth-child(28) {
|
||||
animation-delay: 2.8s;
|
||||
}
|
||||
&:nth-child(29) {
|
||||
animation-delay: 2.9s;
|
||||
}
|
||||
&:nth-child(30) {
|
||||
animation-delay: 3s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
--shadow-xs: var(--shadow-xs);
|
||||
--shadow-md: var(--shadow-md);
|
||||
--shadow-xs-border-selected: var(--shadow-xs-border-selected);
|
||||
--shadow-xs-border-select: var(--shadow-xs-border-select);
|
||||
|
||||
--animate-pulse: var(--animate-pulse);
|
||||
}
|
||||
|
||||
@@ -15,3 +15,99 @@
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@utility fade-up-text {
|
||||
animation: fadeUp 0.4s ease-out forwards;
|
||||
opacity: 0;
|
||||
|
||||
&:nth-child(1) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
&:nth-child(4) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
&:nth-child(5) {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
&:nth-child(6) {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
&:nth-child(7) {
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
&:nth-child(8) {
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
&:nth-child(9) {
|
||||
animation-delay: 0.9s;
|
||||
}
|
||||
&:nth-child(10) {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
&:nth-child(11) {
|
||||
animation-delay: 1.1s;
|
||||
}
|
||||
&:nth-child(12) {
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
&:nth-child(13) {
|
||||
animation-delay: 1.3s;
|
||||
}
|
||||
&:nth-child(14) {
|
||||
animation-delay: 1.4s;
|
||||
}
|
||||
&:nth-child(15) {
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
&:nth-child(16) {
|
||||
animation-delay: 1.6s;
|
||||
}
|
||||
&:nth-child(17) {
|
||||
animation-delay: 1.7s;
|
||||
}
|
||||
&:nth-child(18) {
|
||||
animation-delay: 1.8s;
|
||||
}
|
||||
&:nth-child(19) {
|
||||
animation-delay: 1.9s;
|
||||
}
|
||||
&:nth-child(20) {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
&:nth-child(21) {
|
||||
animation-delay: 2.1s;
|
||||
}
|
||||
&:nth-child(22) {
|
||||
animation-delay: 2.2s;
|
||||
}
|
||||
&:nth-child(23) {
|
||||
animation-delay: 2.3s;
|
||||
}
|
||||
&:nth-child(24) {
|
||||
animation-delay: 2.4s;
|
||||
}
|
||||
&:nth-child(25) {
|
||||
animation-delay: 2.5s;
|
||||
}
|
||||
&:nth-child(26) {
|
||||
animation-delay: 2.6s;
|
||||
}
|
||||
&:nth-child(27) {
|
||||
animation-delay: 2.7s;
|
||||
}
|
||||
&:nth-child(28) {
|
||||
animation-delay: 2.8s;
|
||||
}
|
||||
&:nth-child(29) {
|
||||
animation-delay: 2.9s;
|
||||
}
|
||||
&:nth-child(30) {
|
||||
animation-delay: 3s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -19,6 +19,28 @@ opencode run "Explain how closures work in JavaScript"
|
||||
|
||||
---
|
||||
|
||||
### tui
|
||||
|
||||
Start the OpenCode terminal user interface.
|
||||
|
||||
```bash
|
||||
opencode [project]
|
||||
```
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Short | Description |
|
||||
| ------------ | ----- | ------------------------------------------ |
|
||||
| `--continue` | `-c` | Continue the last session |
|
||||
| `--session` | `-s` | Session ID to continue |
|
||||
| `--prompt` | `-p` | 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 |
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
The OpenCode CLI also has the following commands.
|
||||
@@ -164,13 +186,17 @@ opencode run Explain the use of context in Go
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Short | Description |
|
||||
| ------------ | ----- | ------------------------------------------ |
|
||||
| `--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 |
|
||||
| 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) |
|
||||
|
||||
---
|
||||
|
||||
@@ -189,7 +215,7 @@ This starts an HTTP server that provides API access to opencode functionality wi
|
||||
| Flag | Short | Description |
|
||||
| ------------ | ----- | --------------------- |
|
||||
| `--port` | `-p` | Port to listen on |
|
||||
| `--hostname` | `-h` | Hostname to listen on |
|
||||
| `--hostname` | | Hostname to listen on |
|
||||
|
||||
---
|
||||
|
||||
@@ -221,18 +247,13 @@ opencode upgrade v0.1.48
|
||||
|
||||
---
|
||||
|
||||
## Flags
|
||||
## Global Flags
|
||||
|
||||
The opencode CLI takes the following global flags.
|
||||
|
||||
| Flag | Short | Description |
|
||||
| -------------- | ----- | ------------------------------------------ |
|
||||
| `--help` | `-h` | Display help |
|
||||
| `--version` | | Print version number |
|
||||
| `--print-logs` | | Print logs to stderr |
|
||||
| `--log-level` | | Log level (DEBUG, INFO, WARN, ERROR) |
|
||||
| `--prompt` | `-p` | 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 |
|
||||
| -------------- | ----- | ------------------------------------ |
|
||||
| `--help` | `-h` | Display help |
|
||||
| `--version` | `-v` | Print version number |
|
||||
| `--print-logs` | | Print logs to stderr |
|
||||
| `--log-level` | | Log level (DEBUG, INFO, WARN, ERROR) |
|
||||
|
||||
@@ -11,7 +11,7 @@ OpenCode has a list of keybinds that you can customize through the OpenCode conf
|
||||
"keybinds": {
|
||||
"leader": "ctrl+x",
|
||||
"app_help": "<leader>h",
|
||||
"app_exit": "ctrl+c,<leader>q",
|
||||
"app_exit": "ctrl+c,ctrl+d,<leader>q",
|
||||
"editor_open": "<leader>e",
|
||||
"theme_list": "<leader>t",
|
||||
"project_init": "<leader>i",
|
||||
@@ -42,6 +42,7 @@ OpenCode has a list of keybinds that you can customize through the OpenCode conf
|
||||
"agent_cycle": "tab",
|
||||
"agent_cycle_reverse": "shift+tab",
|
||||
"input_clear": "ctrl+c",
|
||||
"input_forward_delete": "ctrl+d",
|
||||
"input_paste": "ctrl+v",
|
||||
"input_submit": "enter",
|
||||
"input_newline": "shift+enter,ctrl+j"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user