Compare commits

..

9 Commits

Author SHA1 Message Date
Dax Raad
c4ff8dd205 revert ctrl+d - conflicts with page down 2025-06-14 21:29:02 -04:00
Dax Raad
0e035b3115 fix aborting issue 2025-06-14 21:23:57 -04:00
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
10 changed files with 462 additions and 165 deletions

View File

@@ -116,7 +116,7 @@ $ bun run src/index.ts
### FAQ
#### How do I use this with OpenRou?ter
#### How do I use this with OpenRouter
OpenRouter is not yet in the models.dev database but you can configure it manually.

View File

@@ -7,9 +7,9 @@
"baseURL": "http://localhost:11434/v1"
},
"models": {
"llama2": {
"name": "llama2"
}
"qwen3": {},
"deepseek-r1": {},
"llama2": {}
}
}
}

View File

@@ -16,9 +16,19 @@
- **Naming**: camelCase for variables/functions, PascalCase for classes/namespaces
- **Error handling**: Use Result patterns, avoid throwing exceptions in tools
- **File structure**: Namespace-based organization (e.g., `Tool.define()`, `Session.create()`)
## IMPORTANT
- Try to keep things in one function unless composable or reusable
- DO NOT do unnecessary destructuring of variables
- DO NOT use else statements unless necessary
- DO NOT use try catch if it can be avoided
- AVOID try catch where possible
- AVOID else statements
- AVOID using `any` type
- AVOID let statements
- PREFER single word variable names where possible
- Use as many bun apis as possible like Bun.file()
## Architecture
@@ -27,4 +37,3 @@
- **Validation**: All inputs validated with Zod schemas
- **Logging**: Use `Log.create({ service: "name" })` pattern
- **Storage**: Use `Storage` namespace for persistence

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:")

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({

View File

@@ -115,6 +115,9 @@ export const RunCommand = {
})
const { providerID, modelID } = await Provider.defaultModel()
setTimeout(() => {
Session.abort(session.id)
}, 8000)
await Session.chat({
sessionID: session.id,
providerID,

View File

@@ -3,11 +3,8 @@ import { App } from "./app/app"
import { Server } from "./server/server"
import fs from "fs/promises"
import path from "path"
import { Share } from "./share/share"
import { Global } from "./global"
import yargs from "yargs"
import { hideBin } from "yargs/helpers"
import { RunCommand } from "./cli/cmd/run"

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,
})
@@ -402,7 +405,7 @@ export namespace Session {
await updateMessage(next)
},
onError(err) {
log.error("error", err)
log.error("callback error", err)
switch (true) {
case LoadAPIKeyError.isInstance(err.error):
next.metadata.error = new Provider.AuthError(
@@ -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: {
@@ -474,108 +460,132 @@ export namespace Session {
},
model: model.language,
})
for await (const value of result.fullStream) {
l.info("part", {
type: value.type,
})
switch (value.type) {
case "step-start":
next.parts.push({
type: "step-start",
})
break
case "text-delta":
if (!text) {
text = {
type: "text",
text: value.textDelta,
}
next.parts.push(text)
try {
for await (const value of result.fullStream) {
l.info("part", {
type: value.type,
})
switch (value.type) {
case "step-start":
next.parts.push({
type: "step-start",
})
break
case "text-delta":
if (!text) {
text = {
type: "text",
text: value.textDelta,
}
next.parts.push(text)
break
} else text.text += value.textDelta
break
} else text.text += value.textDelta
break
case "tool-call": {
const [match] = next.parts.flatMap((p) =>
p.type === "tool-invocation" &&
p.toolInvocation.toolCallId === value.toolCallId
? [p]
: [],
)
if (!match) break
match.toolInvocation.args = value.args
match.toolInvocation.state = "call"
Bus.publish(Message.Event.PartUpdated, {
part: match,
messageID: next.id,
sessionID: next.metadata.sessionID,
})
break
}
case "tool-call-streaming-start":
next.parts.push({
type: "tool-invocation",
toolInvocation: {
state: "partial-call",
toolName: value.toolName,
toolCallId: value.toolCallId,
args: {},
},
})
Bus.publish(Message.Event.PartUpdated, {
part: next.parts[next.parts.length - 1],
messageID: next.id,
sessionID: next.metadata.sessionID,
})
break
case "tool-call-delta":
break
// for some reason ai sdk claims to not send this part but it does
// @ts-expect-error
case "tool-result":
const match = next.parts.find(
(p) =>
case "tool-call": {
const [match] = next.parts.flatMap((p) =>
p.type === "tool-invocation" &&
// @ts-expect-error
p.toolInvocation.toolCallId === value.toolCallId,
)
if (match && match.type === "tool-invocation") {
match.toolInvocation = {
// @ts-expect-error
args: value.args,
// @ts-expect-error
toolCallId: value.toolCallId,
// @ts-expect-error
toolName: value.toolName,
state: "result",
// @ts-expect-error
result: value.result as string,
}
p.toolInvocation.toolCallId === value.toolCallId
? [p]
: [],
)
if (!match) break
match.toolInvocation.args = value.args
match.toolInvocation.state = "call"
Bus.publish(Message.Event.PartUpdated, {
part: match,
messageID: next.id,
sessionID: next.metadata.sessionID,
})
break
}
break
default:
l.info("unhandled", {
type: value.type,
})
case "tool-call-streaming-start":
next.parts.push({
type: "tool-invocation",
toolInvocation: {
state: "partial-call",
toolName: value.toolName,
toolCallId: value.toolCallId,
args: {},
},
})
Bus.publish(Message.Event.PartUpdated, {
part: next.parts[next.parts.length - 1],
messageID: next.id,
sessionID: next.metadata.sessionID,
})
break
case "tool-call-delta":
break
// for some reason ai sdk claims to not send this part but it does
// @ts-expect-error
case "tool-result":
const match = next.parts.find(
(p) =>
p.type === "tool-invocation" &&
// @ts-expect-error
p.toolInvocation.toolCallId === value.toolCallId,
)
if (match && match.type === "tool-invocation") {
match.toolInvocation = {
// @ts-expect-error
args: value.args,
// @ts-expect-error
toolCallId: value.toolCallId,
// @ts-expect-error
toolName: value.toolName,
state: "result",
// @ts-expect-error
result: value.result as string,
}
Bus.publish(Message.Event.PartUpdated, {
part: match,
messageID: next.id,
sessionID: next.metadata.sessionID,
})
}
break
default:
l.info("unhandled", {
type: value.type,
})
}
await updateMessage(next)
}
await updateMessage(next)
} catch (e: any) {
log.error("stream error", {
error: e,
})
switch (true) {
case LoadAPIKeyError.isInstance(e):
next.metadata.error = new Provider.AuthError(
{
providerID: input.providerID,
message: e.message,
},
{ cause: e },
).toObject()
break
case e instanceof Error:
next.metadata.error = new NamedError.Unknown(
{ message: e.toString() },
{ cause: e },
).toObject()
break
default:
next.metadata.error = new NamedError.Unknown(
{ message: JSON.stringify(e) },
{ cause: e },
)
}
Bus.publish(Event.Error, {
error: next.metadata.error,
})
}
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 +657,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 +737,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 +769,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 +790,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
}

View File

@@ -124,11 +124,6 @@ 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")

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())
}