Compare commits

...

11 Commits

Author SHA1 Message Date
Dax Raad b855511d9a fix issue with follow up tool calls and cancelation 2025-06-14 21:03:44 -04:00
Dax Raad 783faf554d fix issue continuing session after aborted 2025-06-14 20:24:50 -04:00
nitishxyz bfd4269d7d Add Ayu dark theme (#109) 2025-06-14 20:08:31 -04:00
Berr 25f78b053b fix: improve browser opening error handling in AuthLoginCommand (#111) 2025-06-14 20:07:41 -04:00
Dax Raad 87f260ee17 sync 2025-06-14 20:04:41 -04:00
Dax Raad 12931a869d ci: ignore commits 2025-06-14 18:59:05 -04:00
Dax Raad f759e1804d docs: typo 2025-06-14 18:58:27 -04:00
Rohan Godha c9b4564d36 tui: fix help dialog background (#110) 2025-06-14 18:57:15 -04:00
Conor O'Brien d097c546db nit: update commands displayed on home to match commands available (#108) 2025-06-14 18:56:44 -04:00
Gal Schlezinger adb54521b4 make ctrl+d quit too, just like shells (#105) 2025-06-14 18:56:34 -04:00
Dax Raad 2ea0399aa7 docs: use ollama example 2025-06-14 18:55:39 -04:00
9 changed files with 351 additions and 85 deletions
+6 -9
View File
@@ -82,17 +82,16 @@ You can use opencode with any provider listed at [here](https://ai-sdk.dev/provi
```json title="opencode.json"
{
"$schema": "http://opencode.ai/config.json",
"$schema": "https://opencode.ai/config.json",
"provider": {
"@ai-sdk/openai-compatible": {
"name": "MySpecialProvider",
"name": "ollama",
"options": {
"apiKey": "xxx",
"baseURL": "https://api.provider.com/v1"
"baseURL": "http://localhost:11434/v1"
},
"models": {
"my-model-name": {
"name": "My Model Name"
"llama2": {
"name": "llama2"
}
}
}
@@ -119,7 +118,7 @@ $ bun run src/index.ts
#### How do I use this with OpenRouter
Theoretically you can use this with OpenRouter with config like this
OpenRouter is not yet in the models.dev database but you can configure it manually.
```json title="opencode.json"
{
@@ -139,5 +138,3 @@ Theoretically you can use this with OpenRouter with config like this
}
}
```
However we are using [ai-sdk v5](https://ai-sdk.dev) which OpenRouter does not support yet. The moment they do this will work
+6 -6
View File
@@ -1,15 +1,15 @@
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"@openrouter/ai-sdk-provider": {
"name": "OpenRouter",
"@ai-sdk/openai-compatible": {
"name": "ollama",
"options": {
"apiKey": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
"baseURL": "http://localhost:11434/v1"
},
"models": {
"anthropic/claude-3.5-sonnet": {
"name": "claude-3.5-sonnet"
}
"qwen3": {},
"deepseek-r1": {},
"llama2": {}
}
}
}
+1 -1
View File
@@ -108,7 +108,7 @@ if (!snapshot) {
.filter((x: string) => {
const lower = x.toLowerCase()
return (
!lower.includes("chore:") &&
!lower.includes("ignore:") &&
!lower.includes("ci:") &&
!lower.includes("docs:") &&
!lower.includes("doc:")
+6 -2
View File
@@ -111,8 +111,12 @@ export const AuthLoginCommand = cmd({
// some weird bug where program exits without this
await new Promise((resolve) => setTimeout(resolve, 10))
const { url, verifier } = await AuthAnthropic.authorize()
prompts.note("Opening browser...")
await open(url)
prompts.note("Trying to open browser...")
try {
await open(url)
} catch (e) {
prompts.log.error("Failed to open browser perhaps you are running without a display or X server, please open the following URL in your browser:")
}
prompts.log.info(url)
const code = await prompts.text({
+47 -65
View File
@@ -6,13 +6,13 @@ import { Log } from "../util/log"
import {
generateText,
LoadAPIKeyError,
convertToCoreMessages,
streamText,
tool,
type Tool as AITool,
type LanguageModelUsage,
type CoreMessage,
type UserContent,
type AssistantContent,
type UIMessage,
} from "ai"
import { z, ZodSchema } from "zod"
import { Decimal } from "decimal.js"
@@ -236,10 +236,13 @@ export namespace Session {
content: x,
}),
),
{
role: "user",
content: toUserContent(input.parts),
},
...convertToCoreMessages([
{
role: "user",
content: "",
parts: toParts(input.parts),
},
]),
],
model: model.language,
})
@@ -446,26 +449,9 @@ export namespace Session {
content: x,
}),
),
...msgs.flatMap((msg): CoreMessage[] => {
switch (msg.role) {
case "user":
return [
{
role: "user",
content: toUserContent(msg.parts),
},
]
case "assistant":
return [
{
role: "assistant",
content: toAssistantContent(msg.parts),
},
]
default:
return []
}
}),
...convertToCoreMessages(
msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
),
],
temperature: model.info.id === "codex-mini-latest" ? undefined : 0,
tools: {
@@ -569,13 +555,6 @@ export namespace Session {
}
await updateMessage(next)
}
await result.consumeStream({
onError: (err) => {
log.error("stream error", {
err,
})
},
})
next.metadata!.time.completed = Date.now()
for (const part of next.parts) {
if (
@@ -647,14 +626,15 @@ export namespace Session {
content: x,
}),
),
...convertToCoreMessages(filtered.map(toUIMessage)),
{
role: "user",
content: toUserContent([
content: [
{
type: "text",
text: "Provide a detailed but concise summary of our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next.",
},
]),
],
},
],
})
@@ -726,8 +706,30 @@ export namespace Session {
}
}
function toAssistantContent(parts: Message.Part[]): AssistantContent {
const result: AssistantContent = []
function toUIMessage(msg: Message.Info): UIMessage {
if (msg.role === "assistant") {
return {
id: msg.id,
role: "assistant",
content: "",
parts: toParts(msg.parts),
}
}
if (msg.role === "user") {
return {
id: msg.id,
role: "user",
content: "",
parts: toParts(msg.parts),
}
}
throw new Error("not implemented")
}
function toParts(parts: Message.Part[]): UIMessage["parts"] {
const result: UIMessage["parts"] = []
for (const part of parts) {
switch (part.type) {
case "text":
@@ -736,17 +738,19 @@ function toAssistantContent(parts: Message.Part[]): AssistantContent {
case "file":
result.push({
type: "file",
data: new URL(part.url),
data: part.url,
mimeType: part.mediaType,
filename: part.filename,
})
break
case "tool-invocation":
result.push({
type: "tool-call",
args: part.toolInvocation.args,
toolName: part.toolInvocation.toolName,
toolCallId: part.toolInvocation.toolCallId,
type: "tool-invocation",
toolInvocation: part.toolInvocation,
})
break
case "step-start":
result.push({
type: "step-start",
})
break
default:
@@ -755,25 +759,3 @@ function toAssistantContent(parts: Message.Part[]): AssistantContent {
}
return result
}
function toUserContent(parts: Message.Part[]): UserContent {
const result: UserContent = []
for (const part of parts) {
switch (part.type) {
case "text":
return [{ type: "text", text: part.text }]
case "file":
return [
{
type: "file",
filename: part.filename,
data: new URL(part.url),
mimeType: part.mediaType,
},
]
default:
return []
}
}
return result
}
@@ -124,6 +124,11 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return nil
}
}
case "ctrl+d":
if m.textarea.Value() != "" {
return m, nil
}
return m, tea.Quit
case "shift+enter":
value := m.textarea.Value()
m.textarea.SetValue(value + "\n")
@@ -315,7 +315,7 @@ func (m *messagesComponent) home() string {
{"/sessions", "list sessions"},
{"/new", "start a new session"},
{"/model", "switch model"},
{"/share", "share the current session"},
{"/theme", "switch theme"},
{"/quit", "exit the app"},
}
@@ -59,6 +59,8 @@ func (h *helpDialog) View() string {
descStyle := lipgloss.NewStyle().
Background(t.BackgroundElement()).
Foreground(t.TextMuted())
contentStyle := lipgloss.NewStyle().
PaddingLeft(1).Background(t.BackgroundElement())
lines := []string{}
for _, b := range h.bindings {
@@ -74,7 +76,7 @@ func (h *helpDialog) View() string {
}
}
lines = append(lines, content)
lines = append(lines, contentStyle.Render(content))
}
return strings.Join(lines, "\n")
+276
View File
@@ -0,0 +1,276 @@
package theme
import (
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
// AyuTheme implements the Theme interface with Ayu Dark colors.
// It provides a modern dark theme inspired by the Ayu color scheme.
type AyuTheme struct {
BaseTheme
}
// NewAyuTheme creates a new instance of the Ayu Dark theme.
func NewAyuTheme() *AyuTheme {
// Ayu Dark color palette
// Base background colors
darkBg := "#0B0E14" // App background
darkBgAlt := "#0D1017" // Editor background
darkLine := "#11151C" // UI line separators
darkPanel := "#0F131A" // UI panel background
// Text colors
darkFg := "#BFBDB6" // Primary text
darkFgMuted := "#565B66" // Muted text
darkGutter := "#6C7380" // Gutter text
// Syntax highlighting colors
darkTag := "#39BAE6" // Tags and attributes
darkFunc := "#FFB454" // Functions
darkEntity := "#59C2FF" // Entities and variables
darkString := "#AAD94C" // Strings
darkRegexp := "#95E6CB" // Regular expressions
darkMarkup := "#F07178" // Markup elements
darkKeyword := "#FF8F40" // Keywords
darkSpecial := "#E6B673" // Special characters
darkComment := "#ACB6BF" // Comments
darkConstant := "#D2A6FF" // Constants
darkOperator := "#F29668" // Operators
// Version control colors
darkAdded := "#7FD962" // Added lines
darkRemoved := "#F26D78" // Removed lines
// Accent colors
darkAccent := "#E6B450" // Primary accent
darkError := "#D95757" // Error color
// Active state colors
darkIndentActive := "#6C7380" // Active indent guides
theme := &AyuTheme{}
// Base colors
theme.PrimaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkEntity),
Light: lipgloss.Color(darkEntity),
}
theme.SecondaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkConstant),
Light: lipgloss.Color(darkConstant),
}
theme.AccentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(darkAccent),
}
// Status colors
theme.ErrorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkError),
Light: lipgloss.Color(darkError),
}
theme.WarningColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSpecial),
Light: lipgloss.Color(darkSpecial),
}
theme.SuccessColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAdded),
Light: lipgloss.Color(darkAdded),
}
theme.InfoColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkTag),
Light: lipgloss.Color(darkTag),
}
// Text colors
theme.TextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFg),
Light: lipgloss.Color(darkFg),
}
theme.TextMutedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFgMuted),
Light: lipgloss.Color(darkFgMuted),
}
// Background colors
theme.BackgroundColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBg),
Light: lipgloss.Color(darkBg),
}
theme.BackgroundSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBgAlt),
Light: lipgloss.Color(darkBgAlt),
}
theme.BackgroundElementColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPanel),
Light: lipgloss.Color(darkPanel),
}
// Border colors
theme.BorderColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGutter),
Light: lipgloss.Color(darkGutter),
}
theme.BorderActiveColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkIndentActive),
Light: lipgloss.Color(darkIndentActive),
}
theme.BorderSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkLine),
Light: lipgloss.Color(darkLine),
}
// Diff view colors
theme.DiffAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAdded),
Light: lipgloss.Color(darkAdded),
}
theme.DiffRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRemoved),
Light: lipgloss.Color(darkRemoved),
}
theme.DiffContextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFgMuted),
Light: lipgloss.Color(darkFgMuted),
}
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGutter),
Light: lipgloss.Color(darkGutter),
}
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAdded),
Light: lipgloss.Color(darkAdded),
}
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRemoved),
Light: lipgloss.Color(darkRemoved),
}
theme.DiffAddedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#1a2b1a"),
Light: lipgloss.Color("#1a2b1a"),
}
theme.DiffRemovedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#2b1a1a"),
Light: lipgloss.Color("#2b1a1a"),
}
theme.DiffContextBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBgAlt),
Light: lipgloss.Color(darkBgAlt),
}
theme.DiffLineNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGutter),
Light: lipgloss.Color(darkGutter),
}
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#152b15"),
Light: lipgloss.Color("#152b15"),
}
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#2b1515"),
Light: lipgloss.Color("#2b1515"),
}
// Markdown colors
theme.MarkdownTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFg),
Light: lipgloss.Color(darkFg),
}
theme.MarkdownHeadingColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFunc),
Light: lipgloss.Color(darkFunc),
}
theme.MarkdownLinkColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkTag),
Light: lipgloss.Color(darkTag),
}
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkEntity),
Light: lipgloss.Color(darkEntity),
}
theme.MarkdownCodeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkString),
Light: lipgloss.Color(darkString),
}
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSpecial),
Light: lipgloss.Color(darkSpecial),
}
theme.MarkdownEmphColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkKeyword),
Light: lipgloss.Color(darkKeyword),
}
theme.MarkdownStrongColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkMarkup),
Light: lipgloss.Color(darkMarkup),
}
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGutter),
Light: lipgloss.Color(darkGutter),
}
theme.MarkdownListItemColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOperator),
Light: lipgloss.Color(darkOperator),
}
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkConstant),
Light: lipgloss.Color(darkConstant),
}
theme.MarkdownImageColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRegexp),
Light: lipgloss.Color(darkRegexp),
}
theme.MarkdownImageTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkEntity),
Light: lipgloss.Color(darkEntity),
}
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkString),
Light: lipgloss.Color(darkString),
}
// Syntax highlighting colors
theme.SyntaxCommentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkComment),
Light: lipgloss.Color(darkComment),
}
theme.SyntaxKeywordColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkKeyword),
Light: lipgloss.Color(darkKeyword),
}
theme.SyntaxFunctionColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFunc),
Light: lipgloss.Color(darkFunc),
}
theme.SyntaxVariableColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkEntity),
Light: lipgloss.Color(darkEntity),
}
theme.SyntaxStringColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkString),
Light: lipgloss.Color(darkString),
}
theme.SyntaxNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkConstant),
Light: lipgloss.Color(darkConstant),
}
theme.SyntaxTypeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSpecial),
Light: lipgloss.Color(darkSpecial),
}
theme.SyntaxOperatorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOperator),
Light: lipgloss.Color(darkOperator),
}
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFg),
Light: lipgloss.Color(darkFg),
}
return theme
}
func init() {
// Register the Ayu theme with the theme manager
RegisterTheme("ayu", NewAyuTheme())
}