Compare commits

...

14 Commits

Author SHA1 Message Date
Dax Raad
2239228062 rename package 2025-06-18 14:57:51 -04:00
Dax Raad
d4157d9a96 ctrl+c should gracefully clean up pending sessions 2025-06-18 14:11:49 -04:00
Jay V
6e4ef585d8 ignore: share error styles 2025-06-18 14:10:14 -04:00
Dax Raad
e05c3b7a76 fix panic when invalid config 2025-06-18 14:03:16 -04:00
Dax Raad
f99904bc1c track version on session info 2025-06-18 13:40:36 -04:00
Jay V
b796d6763f ignore: share page styles 2025-06-18 12:53:48 -04:00
Dax Raad
c1250abdf8 implemented diff trimming 2025-06-18 11:20:40 -04:00
Dax Raad
ebe51534a1 allow setting options in global provider store 2025-06-18 11:06:16 -04:00
Dax Raad
b8bbee4718 fix issue with provider cache 2025-06-18 10:56:23 -04:00
Dax Raad
8f852b396f fix deploys 2025-06-18 10:47:07 -04:00
Dax Raad
ae4d089c06 remove call to npm causing noticible delay when starting chat 2025-06-18 10:35:41 -04:00
Dax Raad
5110fbdaf9 fix issue when running opencode in empty directory 2025-06-18 10:29:09 -04:00
Dax Raad
e6ddb474fc ignore: sync 2025-06-18 08:36:25 -04:00
SBSTN
0dc71774ce Add Everforest Theme (#170) 2025-06-18 05:55:38 -05:00
19 changed files with 613 additions and 129 deletions

View File

@@ -3,7 +3,7 @@ name: deploy
on:
push:
branches:
- dontlook
- dev
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}

View File

@@ -8,7 +8,7 @@
</a>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://www.npmjs.com/package/opencode.ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
@@ -27,7 +27,7 @@ AI coding agent, built for the terminal.
curl -fsSL https://opencode.ai/install | bash
# Package managers
npm i -g opencode-ai@latest # or bun/pnpm/yarn
npm i -g opencode.ai@latest # or bun/pnpm/yarn
brew install sst/tap/opencode # macOS
paru -S opencode-bin # Arch Linux
```

View File

@@ -18,7 +18,7 @@
},
},
"packages/opencode": {
"name": "opencode",
"name": "opencode.ai",
"version": "0.0.0",
"dependencies": {
"@clack/prompts": "0.11.0",
@@ -1200,7 +1200,7 @@
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"opencode": ["opencode@workspace:packages/opencode"],
"opencode.ai": ["opencode.ai@workspace:packages/opencode"],
"opencontrol": ["opencontrol@0.0.6", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.6.1", "@tsconfig/bun": "1.0.7", "hono": "4.7.4", "zod": "3.24.2", "zod-to-json-schema": "3.24.3" }, "bin": { "opencontrol": "bin/index.mjs" } }, "sha512-QeCrpOK5D15QV8kjnGVeD/BHFLwcVr+sn4T6KKmP0WAMs2pww56e4h+eOGHb5iPOufUQXbdbBKi6WV2kk7tefQ=="],

View File

@@ -1,6 +1,5 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode",
"private": true,
"type": "module",
"packageManager": "bun@1.2.14",

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.0.0",
"name": "opencode",
"name": "opencode.ai",
"type": "module",
"private": true,
"scripts": {

View File

@@ -68,7 +68,7 @@ await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
await Bun.file(`./dist/${pkg.name}/package.json`).write(
JSON.stringify(
{
name: pkg.name + "-ai",
name: pkg.name,
bin: {
[pkg.name]: `./bin/${pkg.name}`,
},

View File

@@ -101,7 +101,7 @@ const cli = yargs(hideBin(process.argv))
})()
await proc.exited
await server.stop()
server.stop()
return "done"
})
@@ -120,21 +120,20 @@ const cli = yargs(hideBin(process.argv))
.command(ScrapCommand)
.command(AuthCommand)
.command(UpgradeCommand)
.fail((msg, err) => {
.fail((msg) => {
if (
msg.startsWith("Unknown argument") ||
msg.startsWith("Not enough non-option arguments")
) {
cli.showHelp("log")
}
Log.Default.error(msg, {
err,
})
})
.strict()
try {
await cli.parse()
} catch (e) {
Log.Default.error(e)
Log.Default.error(e, {
stack: e instanceof Error ? e.stack : undefined,
})
}

View File

@@ -74,7 +74,7 @@ export namespace Installation {
for (const check of checks) {
const output = await check.command()
if (output.includes("opencode-ai")) {
if (output.includes("opencode-ai") || output.includes("opencode.ai")) {
return check.name
}
}
@@ -95,11 +95,13 @@ export namespace Installation {
case "curl":
return $`curl -fsSL https://opencode.ai/install | bash`
case "npm":
return $`npm install -g opencode-ai@${target}`
return $`npm remove -g opencode-ai && npm install -g opencode.ai@${target}`
case "pnpm":
return $`pnpm install -g opencode-ai@${target}`
return $`pnpm remove -g opencode-ai && pnpm install -g opencode.ai@${target}`
case "bun":
return $`bun install -g opencode-ai@${target}`
return $`bun remove -g opencode-ai && bun install -g opencode.ai@${target}`
case "yarn":
return $`yarn remove -g opencode-ai && yarn add -g opencode.ai@${target}`
default:
throw new Error(`Unknown method: ${method}`)
}

View File

@@ -1,5 +1,4 @@
import { Global } from "../global"
import { lazy } from "../util/lazy"
import { Log } from "../util/log"
import path from "path"
import { z } from "zod"
@@ -64,30 +63,4 @@ export namespace ModelsDev {
throw new Error(`Failed to fetch models.dev: ${result.statusText}`)
await Bun.write(file, result)
}
const aisdk = lazy(async () => {
log.info("fetching ai-sdk")
const response = await fetch(
"https://registry.npmjs.org/-/v1/search?text=scope:@ai-sdk",
)
if (!response.ok)
throw new Error(
`Failed to fetch ai-sdk information: ${response.statusText}`,
)
const result = await response.json()
log.info("found ai-sdk", result.objects.length)
return result.objects
.filter((obj: any) => obj.package.name.startsWith("@ai-sdk/"))
.reduce((acc: any, obj: any) => {
acc[obj.package.name] = obj
return acc
}, {})
})
export async function pkg(providerID: string): Promise<[string, string]> {
const packages = await aisdk()
const match = packages[`@ai-sdk/${providerID}`]
if (match) return [match.package.name, "latest"]
return [providerID, "latest"]
}
}

View File

@@ -43,9 +43,7 @@ export namespace Provider {
for (const model of Object.values(provider.models)) {
model.cost = {
input: 0,
inputCached: 0,
output: 0,
outputCached: 0,
}
}
return {
@@ -103,9 +101,11 @@ export namespace Provider {
) {
const provider = providers[id]
if (!provider) {
const info = database[id]
if (!info) return
providers[id] = {
source,
info: database[id],
info,
options,
}
return
@@ -205,9 +205,7 @@ export namespace Provider {
}
// load config
for (const [providerID, provider] of Object.entries(
config.provider ?? {},
)) {
for (const [providerID, provider] of configProviders) {
mergeProvider(providerID, provider.options ?? {}, "config")
}
@@ -234,8 +232,8 @@ export namespace Provider {
const s = await state()
const existing = s.sdk.get(provider.id)
if (existing) return existing
const [pkg, version] = await ModelsDev.pkg(provider.npm ?? provider.id)
const mod = await import(await BunProc.install(pkg, version))
const pkg = provider.npm ?? provider.id
const mod = await import(await BunProc.install(pkg, "latest"))
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
const loaded = fn(s.providers[provider.id]?.options)
s.sdk.set(provider.id, loaded)

View File

@@ -31,6 +31,7 @@ import { SystemPrompt } from "./system"
import { Flag } from "../flag/flag"
import type { ModelsDev } from "../provider/models"
import { GlobalConfig } from "../global/config"
import { Installation } from "../installation"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -46,6 +47,7 @@ export namespace Session {
})
.optional(),
title: z.string(),
version: z.string(),
time: z.object({
created: z.number(),
updated: z.number(),
@@ -71,19 +73,30 @@ export namespace Session {
),
}
const state = App.state("session", () => {
const sessions = new Map<string, Info>()
const messages = new Map<string, Message.Info[]>()
const state = App.state(
"session",
() => {
const sessions = new Map<string, Info>()
const messages = new Map<string, Message.Info[]>()
const pending = new Map<string, AbortController>()
return {
sessions,
messages,
}
})
return {
sessions,
messages,
pending,
}
},
async (state) => {
for (const [_, controller] of state.pending) {
controller.abort()
}
},
)
export async function create(parentID?: string) {
const result: Info = {
id: Identifier.descending("session"),
version: Installation.VERSION,
parentID,
title:
(parentID ? "Child session - " : "New Session - ") +
@@ -171,10 +184,10 @@ export namespace Session {
}
export function abort(sessionID: string) {
const controller = pending.get(sessionID)
const controller = state().pending.get(sessionID)
if (!controller) return false
controller.abort()
pending.delete(sessionID)
state().pending.delete(sessionID)
return true
}
@@ -331,6 +344,16 @@ export namespace Session {
sessionID: input.sessionID,
abort: abort.signal,
messageID: next.id,
metadata: async (val) => {
next.metadata.tool[opts.toolCallId] = {
...val,
time: {
start: 0,
end: 0,
},
}
await updateMessage(next)
},
})
next.metadata!.tool![opts.toolCallId] = {
...result.metadata,
@@ -724,17 +747,16 @@ export namespace Session {
await updateMessage(next)
}
const pending = new Map<string, AbortController>()
function lock(sessionID: string) {
log.info("locking", { sessionID })
if (pending.has(sessionID)) throw new BusyError(sessionID)
if (state().pending.has(sessionID)) throw new BusyError(sessionID)
const controller = new AbortController()
pending.set(sessionID, controller)
state().pending.set(sessionID, controller)
return {
signal: controller.signal,
[Symbol.dispose]() {
log.info("unlocking", { sessionID })
pending.delete(sessionID)
state().pending.delete(sessionID)
},
}
}

View File

@@ -1,4 +1,4 @@
you will generate a short title based on the first message a user begins a conversation with
You will generate a short title based on the first message a user begins a conversation with
- ensure it is not more than 50 characters long
- the title should be a summary of the user's message
- it should be one line long

View File

@@ -87,7 +87,9 @@ export const EditTool = Tool.define({
await file.write(contentNew)
})()
const diff = createTwoFilesPatch(filepath, filepath, contentOld, contentNew)
const diff = trimDiff(
createTwoFilesPatch(filepath, filepath, contentOld, contentNew),
)
FileTimes.read(ctx.sessionID, filepath)
@@ -113,3 +115,39 @@ export const EditTool = Tool.define({
}
},
})
function trimDiff(diff: string): string {
const lines = diff.split("\n")
const contentLines = lines.filter(
(line) =>
(line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
!line.startsWith("---") &&
!line.startsWith("+++"),
)
if (contentLines.length === 0) return diff
let min = Infinity
for (const line of contentLines) {
const content = line.slice(1)
if (content.trim().length > 0) {
const match = content.match(/^(\s*)/)
if (match) min = Math.min(min, match[1].length)
}
}
if (min === Infinity || min === 0) return diff
const trimmedLines = lines.map((line) => {
if (
(line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
!line.startsWith("---") &&
!line.startsWith("+++")
) {
const prefix = line[0]
const content = line.slice(1)
return prefix + content.slice(min)
}
return line
})
return trimmedLines.join("\n")
}

View File

@@ -2,6 +2,8 @@ import { Tool } from "./tool"
import DESCRIPTION from "./task.txt"
import { z } from "zod"
import { Session } from "../session"
import { Bus } from "../bus"
import { Message } from "../session/message"
export const TaskTool = Tool.define({
id: "opencode.task",
@@ -17,6 +19,28 @@ export const TaskTool = Tool.define({
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
const metadata = msg.metadata.assistant!
function summary(input: Message.Info) {
const result = []
for (const part of input.parts) {
if (part.type === "tool-invocation") {
result.push({
toolInvocation: part.toolInvocation,
metadata: input.metadata.tool[part.toolInvocation.toolCallId],
})
}
}
return result
}
const unsub = Bus.subscribe(Message.Event.Updated, async (evt) => {
if (evt.properties.info.metadata.sessionID !== ctx.sessionID) return
ctx.metadata({
title: params.description,
summary: summary(evt.properties.info),
})
})
const result = await Session.chat({
sessionID: session.id,
modelID: metadata.modelID,
@@ -28,10 +52,11 @@ export const TaskTool = Tool.define({
},
],
})
unsub()
return {
metadata: {
title: params.description,
summary: summary(result),
},
output: result.parts.findLast((x) => x.type === "text")!.text,
}

View File

@@ -5,10 +5,11 @@ export namespace Tool {
title: string
[key: string]: any
}
export type Context = {
export type Context<M extends Metadata = Metadata> = {
sessionID: string
messageID: string
abort: AbortSignal
metadata(meta: M): void
}
export interface Info<
Parameters extends StandardSchemaV1 = StandardSchemaV1,

View File

@@ -3,6 +3,12 @@ import { App } from "../../src/app/app"
import { GlobTool } from "../../src/tool/glob"
import { ListTool } from "../../src/tool/ls"
const ctx = {
sessionID: "test",
messageID: "",
abort: AbortSignal.any([]),
metadata: () => {},
}
describe("tool.glob", () => {
test("truncate", async () => {
await App.provide({ cwd: process.cwd() }, async () => {
@@ -11,11 +17,7 @@ describe("tool.glob", () => {
pattern: "./node_modules/**/*",
path: undefined,
},
{
sessionID: "test",
messageID: "",
abort: AbortSignal.any([]),
},
ctx,
)
expect(result.metadata.truncated).toBe(true)
})
@@ -27,11 +29,7 @@ describe("tool.glob", () => {
pattern: "*.json",
path: undefined,
},
{
sessionID: "test",
messageID: "",
abort: AbortSignal.any([]),
},
ctx,
)
expect(result.metadata).toMatchObject({
truncated: false,
@@ -46,11 +44,7 @@ describe("tool.ls", () => {
const result = await App.provide({ cwd: process.cwd() }, async () => {
return await ListTool.execute(
{ path: "./example", ignore: [".git"] },
{
sessionID: "test",
messageID: "",
abort: AbortSignal.any([]),
},
ctx,
)
})
expect(result.output).toMatchSnapshot()

View File

@@ -0,0 +1,298 @@
package theme
import (
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
// EverforestTheme implements the Theme interface with Everforest colors.
// It provides both dark and light variants with Medium (default) contrast.
type EverforestTheme struct {
BaseTheme
}
// NewEverforestTheme creates a new instance of the Everforest Medium theme.
func NewEverforestTheme() *EverforestTheme {
// Everforest color palette - Medium variant
// Official colors from https://github.com/sainnhe/everforest/wiki
// Dark mode colors - using Everforest:Dark Medium contrast palette
darkStep1 := "#2d353b" // App background
darkStep2 := "#333c43" // Subtle background
darkStep3 := "#343f44" // UI element background
darkStep4 := "#3d484d" // Hovered UI element background
darkStep5 := "#475258" // Active/Selected UI element background
darkStep6 := "#7a8478" // Subtle borders and separators
darkStep7 := "#859289" // UI element border and focus rings
darkStep8 := "#9da9a0" // Hovered UI element border
darkStep9 := "#a7c080" // Solid backgrounds
darkStep10 := "#83c092" // Hovered solid backgrounds
darkStep11 := "#7a8478" // Low-contrast text
darkStep12 := "#d3c6aa" // High-contrast text
// Dark mode accent colors
darkPrimary := darkStep9 // Primary uses step 9 (green)
darkSecondary := "#7fbbb3" // Secondary (blue)
darkAccent := "#d699b6" // Accent (purple)
darkRed := "#e67e80" // Error (red)
darkOrange := "#e69875" // Warning (orange)
darkGreen := "#a7c080" // Success (green)
darkCyan := "#83c092" // Info (aqua)
darkYellow := "#dbbc7f" // Emphasized text
// Light mode colors for the Everforest:Light Medium contrast palette
lightStep1 := "#fdf6e3" // App background
lightStep2 := "#efebd4" // Subtle background
lightStep3 := "#f4f0d9" // UI element background
lightStep4 := "#efebd4" // Hovered UI element background
lightStep5 := "#e6e2cc" // Active/Selected UI element background
lightStep6 := "#a6b0a0" // Subtle borders and separators
lightStep7 := "#939f91" // UI element border and focus rings
lightStep8 := "#829181" // Hovered UI element border
lightStep9 := "#8da101" // Solid backgrounds
lightStep10 := "#35a77c" // Hovered solid backgrounds
lightStep11 := "#a6b0a0" // Low-contrast text
lightStep12 := "#5c6a72" // High-contrast text
// Light mode accent colors
lightPrimary := lightStep9 // Primary uses step 9 (green)
lightSecondary := "#3a94c5" // Secondary blue
lightAccent := "#df69ba" // Accent purple
lightRed := "#f85552" // Error red
lightOrange := "#f57d26" // Warning orange
lightGreen := "#8da101" // Success green
lightCyan := "#35a77c" // Info aqua
lightYellow := "#dfa000" // Emphasized text
// Unused variables. These could be used for hover states
_ = darkStep4
_ = darkStep5
_ = darkStep10
_ = lightStep4
_ = lightStep5
_ = lightStep10
theme := &EverforestTheme{}
// Base colors
theme.PrimaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.SecondaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSecondary),
Light: lipgloss.Color(lightSecondary),
}
theme.AccentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
// Status colors
theme.ErrorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRed),
Light: lipgloss.Color(lightRed),
}
theme.WarningColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOrange),
Light: lipgloss.Color(lightOrange),
}
theme.SuccessColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.InfoColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
// Text colors
theme.TextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
theme.TextMutedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
// Background colors
theme.BackgroundColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep1),
Light: lipgloss.Color(lightStep1),
}
theme.BackgroundSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.BackgroundElementColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3),
Light: lipgloss.Color(lightStep3),
}
// Border colors
theme.BorderColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep7),
Light: lipgloss.Color(lightStep7),
}
theme.BorderActiveColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep8),
Light: lipgloss.Color(lightStep8),
}
theme.BorderSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep6),
Light: lipgloss.Color(lightStep6),
}
// Diff view colors
theme.DiffAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#A7C080"),
Light: lipgloss.Color("#8DA101"),
}
theme.DiffRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#E67E80"),
Light: lipgloss.Color("#F85552"),
}
theme.DiffContextColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#7A8478"),
Light: lipgloss.Color("#A6B0A0"),
}
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#859289"),
Light: lipgloss.Color("#939F91"),
}
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#A7C080"),
Light: lipgloss.Color("#8DA101"),
}
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#E67E80"),
Light: lipgloss.Color("#F85552"),
}
theme.DiffAddedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#425047"),
Light: lipgloss.Color("#F0F1D2"),
}
theme.DiffRemovedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#543A48"),
Light: lipgloss.Color("#FBE3DA"),
}
theme.DiffContextBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.DiffLineNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3),
Light: lipgloss.Color(lightStep3),
}
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#3A4A3F"),
Light: lipgloss.Color("#E8F2D1"),
}
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#4A3A40"),
Light: lipgloss.Color("#FBDAD2"),
}
// Markdown colors
theme.MarkdownTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
theme.MarkdownHeadingColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSecondary),
Light: lipgloss.Color(lightSecondary),
}
theme.MarkdownLinkColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownEmphColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownStrongColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.MarkdownListItemColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownImageColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownImageTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
// Syntax highlighting colors
theme.SyntaxCommentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.SyntaxKeywordColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.SyntaxFunctionColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSecondary),
Light: lipgloss.Color(lightSecondary),
}
theme.SyntaxVariableColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRed),
Light: lipgloss.Color(lightRed),
}
theme.SyntaxStringColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.SyntaxNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
theme.SyntaxTypeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.SyntaxOperatorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
return theme
}
func init() {
// Register the Everforest theme with the theme manager
RegisterTheme("everforest", NewEverforestTheme())
}

View File

@@ -172,14 +172,33 @@ function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> {
return entries
}
export function getDiagnostics(
diagnosticsByFile: Record<string, Diagnostic[]>,
): string[] {
// Return a flat array of error diagnostics, in the format:
// "ERROR [65:20] Property 'x' does not exist on type 'Y'"
const result: string[] = []
function formatErrorString(error: string): JSX.Element {
const errorMarker = "Error: "
const startsWithError = error.startsWith(errorMarker)
if (diagnosticsByFile === undefined) return result
return startsWithError ? (
<p>
<span data-color="red" data-marker="label" data-separator>
Error
</span>
<span>{error.slice(errorMarker.length)}</span>
</p>
) : (
<p><span data-color="dimmed">{error}</span></p>
)
}
function getDiagnostics(
diagnosticsByFile: Record<string, Diagnostic[]>,
currentFile: string
): JSX.Element[] {
// Return a flat array of error diagnostics, in the format:
// "Error [65:20] Property 'x' does not exist on type 'Y'"
const result: JSX.Element[] = []
if (
diagnosticsByFile === undefined || diagnosticsByFile[currentFile] === undefined
) return result
for (const diags of Object.values(diagnosticsByFile)) {
for (const d of diags) {
@@ -189,7 +208,15 @@ export function getDiagnostics(
const line = d.range.start.line + 1 // 1-based
const column = d.range.start.character + 1 // 1-based
result.push(`ERROR [${line}:${column}] ${d.message}`)
result.push(
<p>
<span data-color="red" data-marker="label">Error</span>
<span data-color="dimmed" data-separator>
[{line}:{column}]
</span>
<span>{d.message}</span>
</p>
)
}
}
@@ -321,6 +348,57 @@ function TextPart(props: TextPartProps) {
)
}
interface ErrorPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
expand?: boolean
}
function ErrorPart(props: ErrorPartProps) {
const [local, rest] = splitProps(props, ["expand", "children"])
const [expanded, setExpanded] = createSignal(false)
const [overflowed, setOverflowed] = createSignal(false)
let preEl: HTMLElement | undefined
function checkOverflow() {
if (preEl && !local.expand) {
setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1)
}
}
onMount(() => {
checkOverflow()
window.addEventListener("resize", checkOverflow)
})
createEffect(() => {
local.children
setTimeout(checkOverflow, 0)
})
onCleanup(() => {
window.removeEventListener("resize", checkOverflow)
})
return (
<div
class={styles["message-error"]}
data-expanded={expanded() || local.expand === true}
{...rest}
>
<div data-section="content" ref={(el) => (preEl = el)}>
{local.children}
</div>
{((!local.expand && overflowed()) || expanded()) && (
<button
type="button"
data-element-button-text
onClick={() => setExpanded((e) => !e)}
>
{expanded() ? "Show less" : "Show more"}
</button>
)}
</div>
)
}
interface MarkdownPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
text: string
expand?: boolean
@@ -456,6 +534,7 @@ export default function Share(props: {
info: SessionInfo
messages: Record<string, SessionMessage>
}) {
let hasScrolled = false
const id = props.id
const anchorId = createMemo<string | null>(() => {
@@ -530,8 +609,9 @@ export default function Share(props: {
const [, messageID] = splits
setStore("messages", messageID, reconcile(d.content))
if (messageID === anchorId()) {
if (!hasScrolled && messageID === anchorId()) {
scrollToAnchor(window.location.hash.slice(1))
hasScrolled = true
}
}
} catch (error) {
@@ -1185,12 +1265,9 @@ export default function Share(props: {
<Switch>
<Match when={hasError()}>
<div data-part-tool-result>
<TextPart
expand
text={toolData()?.result}
data-size="sm"
data-color="dimmed"
/>
<ErrorPart>
{formatErrorString(toolData()?.result)}
</ErrorPart>
</div>
</Match>
<Match when={preview()}>
@@ -1258,7 +1335,10 @@ export default function Share(props: {
const hasError = () => toolData()?.metadata?.error
const content = () => toolData()?.args?.content
const diagnostics = createMemo(() =>
getDiagnostics(toolData()?.metadata?.diagnostics)
getDiagnostics(
toolData()?.metadata?.diagnostics,
toolData()?.args.filePath
)
)
return (
@@ -1280,20 +1360,14 @@ export default function Share(props: {
<b>{filePath()}</b>
</div>
<Show when={diagnostics().length > 0}>
<TextPart
data-size="sm"
text={diagnostics().join("\n\n")}
/>
<ErrorPart>{diagnostics()}</ErrorPart>
</Show>
<Switch>
<Match when={hasError()}>
<div data-part-tool-result>
<TextPart
expand
text={toolData()?.result}
data-size="sm"
data-color="dimmed"
/>
<ErrorPart>
{formatErrorString(toolData()?.result)}
</ErrorPart>
</div>
</Match>
<Match when={content()}>
@@ -1344,7 +1418,10 @@ export default function Share(props: {
)
)
const diagnostics = createMemo(() =>
getDiagnostics(toolData()?.metadata?.diagnostics)
getDiagnostics(
toolData()?.metadata?.diagnostics,
toolData()?.args.filePath
)
)
return (
@@ -1368,12 +1445,9 @@ export default function Share(props: {
<Switch>
<Match when={hasError()}>
<div data-part-tool-result>
<TextPart
expand
data-size="sm"
data-color="dimmed"
text={message()}
/>
<ErrorPart>
{formatErrorString(message())}
</ErrorPart>
</div>
</Match>
<Match when={diff()}>
@@ -1387,10 +1461,7 @@ export default function Share(props: {
</Match>
</Switch>
<Show when={diagnostics().length > 0}>
<TextPart
data-size="sm"
text={diagnostics().join("\n\n")}
/>
<ErrorPart>{diagnostics()}</ErrorPart>
</Show>
</div>
<ToolFooter time={toolData()?.duration || 0} />
@@ -1541,12 +1612,9 @@ export default function Share(props: {
<Switch>
<Match when={hasError()}>
<div data-part-tool-result>
<TextPart
expand
text={toolData()?.result}
data-size="sm"
data-color="dimmed"
/>
<ErrorPart>
{formatErrorString(toolData()?.result)}
</ErrorPart>
</div>
</Match>
<Match when={toolData()?.result}>

View File

@@ -421,6 +421,73 @@
}
}
.message-error {
background-color: var(--sl-color-bg-surface);
padding: 0.5rem calc(0.5rem + 3px);
border-radius: 0.25rem;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1rem;
align-self: flex-start;
max-width: var(--md-tool-width);
[data-section="content"] {
p {
margin-bottom: 0.5rem;
line-height: 1.5;
font-size: 0.75rem;
white-space: pre-wrap;
word-break: break-word;
&:last-child {
margin-bottom: 0;
}
span {
margin-right: 0.25rem;
&:last-child {
margin-right: 0;
}
}
span[data-color="red"] {
color: var(--sl-color-red);
}
span[data-color="dimmed"] {
color: var(--sl-color-text-dimmed);
}
span[data-marker="label"] {
text-transform: uppercase;
letter-spacing: -0.5px;
}
span[data-separator] {
margin-right: 0.375rem;
}
}
}
&[data-expanded="true"] {
[data-section="content"] {
display: block;
}
}
&[data-expanded="false"] {
[data-section="content"] {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 7;
overflow: hidden;
}
}
button {
flex: 0 0 auto;
padding: 2px 0;
font-size: 0.75rem;
}
}
.message-terminal {
display: flex;
flex-direction: column;