summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/function/package.json10
-rw-r--r--packages/function/src/api.ts167
-rw-r--r--packages/function/sst-env.d.ts25
-rw-r--r--packages/function/tsconfig.json9
-rw-r--r--packages/opencode/.gitignore3
-rw-r--r--packages/opencode/README.md15
-rw-r--r--packages/opencode/bin/opencode.mjs29
-rw-r--r--packages/opencode/package.json39
-rw-r--r--packages/opencode/scrap.ts30
-rwxr-xr-xpackages/opencode/script/release.ts68
-rw-r--r--packages/opencode/src/app/app.ts78
-rw-r--r--packages/opencode/src/app/path.ts11
-rw-r--r--packages/opencode/src/bun/index.ts28
-rw-r--r--packages/opencode/src/bus/index.ts101
-rw-r--r--packages/opencode/src/config/config.ts51
-rw-r--r--packages/opencode/src/global/index.ts20
-rw-r--r--packages/opencode/src/id/id.ts74
-rw-r--r--packages/opencode/src/index.ts85
-rw-r--r--packages/opencode/src/llm/llm.ts172
-rw-r--r--packages/opencode/src/lsp/client.ts208
-rw-r--r--packages/opencode/src/lsp/index.ts131
-rw-r--r--packages/opencode/src/lsp/language.ts89
-rw-r--r--packages/opencode/src/provider/provider.ts35
-rw-r--r--packages/opencode/src/server/server.ts309
-rw-r--r--packages/opencode/src/session/message.ts171
-rw-r--r--packages/opencode/src/session/prompt/anthropic.txt95
-rw-r--r--packages/opencode/src/session/prompt/summarize.txt10
-rw-r--r--packages/opencode/src/session/prompt/title.txt7
-rw-r--r--packages/opencode/src/session/session.ts498
-rw-r--r--packages/opencode/src/share/share.ts67
-rw-r--r--packages/opencode/src/storage/storage.ts55
-rw-r--r--packages/opencode/src/tool/bash.ts199
-rw-r--r--packages/opencode/src/tool/edit.ts136
-rw-r--r--packages/opencode/src/tool/fetch.ts137
-rw-r--r--packages/opencode/src/tool/glob.ts96
-rw-r--r--packages/opencode/src/tool/grep.ts345
-rw-r--r--packages/opencode/src/tool/index.ts9
-rw-r--r--packages/opencode/src/tool/ls.ts96
-rw-r--r--packages/opencode/src/tool/lsp-diagnostics.ts53
-rw-r--r--packages/opencode/src/tool/lsp-hover.ts38
-rw-r--r--packages/opencode/src/tool/patch.ts420
-rw-r--r--packages/opencode/src/tool/tool.ts61
-rw-r--r--packages/opencode/src/tool/util/file-times.ts20
-rw-r--r--packages/opencode/src/tool/view.ts152
-rw-r--r--packages/opencode/src/util/context.ts25
-rw-r--r--packages/opencode/src/util/event.ts0
-rw-r--r--packages/opencode/src/util/log.ts64
-rw-r--r--packages/opencode/src/util/scrap.ts5
-rw-r--r--packages/opencode/test/tool/__snapshots__/tool.test.ts.snap17
-rw-r--r--packages/opencode/test/tool/tool.test.ts55
-rw-r--r--packages/opencode/tsconfig.json5
-rw-r--r--packages/tui/.goreleaser.yml77
-rw-r--r--packages/tui/app.log8
-rw-r--r--packages/tui/cmd/root.go258
-rw-r--r--packages/tui/go.mod105
-rw-r--r--packages/tui/go.sum338
-rw-r--r--packages/tui/internal/completions/files-folders.go191
-rw-r--r--packages/tui/internal/config/config.go266
-rw-r--r--packages/tui/internal/config/init.go60
-rw-r--r--packages/tui/internal/diff/diff.go869
-rw-r--r--packages/tui/internal/diff/diff_test.go103
-rw-r--r--packages/tui/internal/diff/patch.go740
-rw-r--r--packages/tui/internal/fileutil/fileutil.go163
-rw-r--r--packages/tui/internal/format/format.go46
-rw-r--r--packages/tui/internal/format/format_test.go90
-rw-r--r--packages/tui/internal/pubsub/broker.go113
-rw-r--r--packages/tui/internal/pubsub/broker_test.go144
-rw-r--r--packages/tui/internal/pubsub/events.go24
-rw-r--r--packages/tui/internal/status/status.go142
-rw-r--r--packages/tui/internal/tui/app/app.go215
-rw-r--r--packages/tui/internal/tui/app/bridge.go42
-rw-r--r--packages/tui/internal/tui/app/interfaces.go13
-rw-r--r--packages/tui/internal/tui/components/chat/chat.go133
-rw-r--r--packages/tui/internal/tui/components/chat/editor.go406
-rw-r--r--packages/tui/internal/tui/components/chat/message.go716
-rw-r--r--packages/tui/internal/tui/components/chat/messages.go344
-rw-r--r--packages/tui/internal/tui/components/chat/sidebar.go220
-rw-r--r--packages/tui/internal/tui/components/core/status.go366
-rw-r--r--packages/tui/internal/tui/components/dialog/arguments.go257
-rw-r--r--packages/tui/internal/tui/components/dialog/commands.go180
-rw-r--r--packages/tui/internal/tui/components/dialog/complete.go263
-rw-r--r--packages/tui/internal/tui/components/dialog/custom_commands.go186
-rw-r--r--packages/tui/internal/tui/components/dialog/custom_commands_test.go106
-rw-r--r--packages/tui/internal/tui/components/dialog/filepicker.go485
-rw-r--r--packages/tui/internal/tui/components/dialog/help.go200
-rw-r--r--packages/tui/internal/tui/components/dialog/init.go189
-rw-r--r--packages/tui/internal/tui/components/dialog/models.go327
-rw-r--r--packages/tui/internal/tui/components/dialog/permission.go502
-rw-r--r--packages/tui/internal/tui/components/dialog/quit.go136
-rw-r--r--packages/tui/internal/tui/components/dialog/session.go230
-rw-r--r--packages/tui/internal/tui/components/dialog/theme.go199
-rw-r--r--packages/tui/internal/tui/components/dialog/tools.go178
-rw-r--r--packages/tui/internal/tui/components/qr/qr.go58
-rw-r--r--packages/tui/internal/tui/components/spinner/spinner.go127
-rw-r--r--packages/tui/internal/tui/components/spinner/spinner_test.go24
-rw-r--r--packages/tui/internal/tui/components/util/simple-list.go159
-rw-r--r--packages/tui/internal/tui/image/clipboard_unix.go49
-rw-r--r--packages/tui/internal/tui/image/clipboard_windows.go192
-rw-r--r--packages/tui/internal/tui/image/images.go85
-rw-r--r--packages/tui/internal/tui/layout/container.go230
-rw-r--r--packages/tui/internal/tui/layout/layout.go35
-rw-r--r--packages/tui/internal/tui/layout/overlay.go169
-rw-r--r--packages/tui/internal/tui/layout/split.go283
-rw-r--r--packages/tui/internal/tui/page/chat.go233
-rw-r--r--packages/tui/internal/tui/page/page.go8
-rw-r--r--packages/tui/internal/tui/state/state.go19
-rw-r--r--packages/tui/internal/tui/styles/background.go123
-rw-r--r--packages/tui/internal/tui/styles/icons.go12
-rw-r--r--packages/tui/internal/tui/styles/markdown.go283
-rw-r--r--packages/tui/internal/tui/styles/styles.go153
-rw-r--r--packages/tui/internal/tui/theme/ayu.go280
-rw-r--r--packages/tui/internal/tui/theme/catppuccin.go248
-rw-r--r--packages/tui/internal/tui/theme/dracula.go274
-rw-r--r--packages/tui/internal/tui/theme/flexoki.go282
-rw-r--r--packages/tui/internal/tui/theme/gruvbox.go302
-rw-r--r--packages/tui/internal/tui/theme/manager.go265
-rw-r--r--packages/tui/internal/tui/theme/monokai.go273
-rw-r--r--packages/tui/internal/tui/theme/onedark.go274
-rw-r--r--packages/tui/internal/tui/theme/opencode.go276
-rw-r--r--packages/tui/internal/tui/theme/theme.go290
-rw-r--r--packages/tui/internal/tui/theme/theme_test.go89
-rw-r--r--packages/tui/internal/tui/theme/tokyonight.go274
-rw-r--r--packages/tui/internal/tui/theme/tron.go276
-rw-r--r--packages/tui/internal/tui/tui.go988
-rw-r--r--packages/tui/internal/tui/util/util.go18
-rw-r--r--packages/tui/internal/version/version.go25
-rw-r--r--packages/tui/main.go9
-rw-r--r--packages/tui/pkg/client/.gitignore2
-rw-r--r--packages/tui/pkg/client/client.go4
-rw-r--r--packages/tui/pkg/client/event.go53
-rw-r--r--packages/tui/pkg/tui/theme/opencode.go276
-rw-r--r--packages/web/.gitignore21
-rw-r--r--packages/web/README.md54
-rw-r--r--packages/web/astro.config.mjs69
-rw-r--r--packages/web/package.json29
-rw-r--r--packages/web/public/favicon.svg3
-rw-r--r--packages/web/public/social-share.pngbin0 -> 25480 bytes
-rw-r--r--packages/web/src/assets/lander/check.svg2
-rw-r--r--packages/web/src/assets/lander/copy.svg2
-rw-r--r--packages/web/src/assets/logo-dark.svg11
-rw-r--r--packages/web/src/assets/logo-light.svg11
-rw-r--r--packages/web/src/components/CodeBlock.tsx47
-rw-r--r--packages/web/src/components/DiffView.tsx73
-rw-r--r--packages/web/src/components/Header.astro62
-rw-r--r--packages/web/src/components/Hero.astro11
-rw-r--r--packages/web/src/components/Lander.astro269
-rw-r--r--packages/web/src/components/Share.tsx772
-rw-r--r--packages/web/src/components/diffview.module.css80
-rw-r--r--packages/web/src/components/icons/custom.tsx22
-rw-r--r--packages/web/src/components/icons/index.tsx6101
-rw-r--r--packages/web/src/components/share.module.css326
-rw-r--r--packages/web/src/content.config.ts7
-rw-r--r--packages/web/src/content/docs/docs/cli.mdx89
-rw-r--r--packages/web/src/content/docs/docs/config.mdx88
-rw-r--r--packages/web/src/content/docs/docs/index.mdx58
-rw-r--r--packages/web/src/content/docs/docs/lsp-servers.mdx34
-rw-r--r--packages/web/src/content/docs/docs/mcp-servers.mdx51
-rw-r--r--packages/web/src/content/docs/docs/models.mdx34
-rw-r--r--packages/web/src/content/docs/docs/shortcuts.mdx68
-rw-r--r--packages/web/src/content/docs/docs/themes.mdx75
-rw-r--r--packages/web/src/content/docs/index.mdx12
-rw-r--r--packages/web/src/pages/s/index.astro28
-rw-r--r--packages/web/src/styles/custom.css16
-rw-r--r--packages/web/sst-env.d.ts9
-rw-r--r--packages/web/tsconfig.json9
165 files changed, 29816 insertions, 0 deletions
diff --git a/packages/function/package.json b/packages/function/package.json
new file mode 100644
index 000000000..46c83e840
--- /dev/null
+++ b/packages/function/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@opencode/function",
+ "version": "0.0.1",
+ "$schema": "https://json.schemastore.org/package.json",
+ "private": true,
+ "type": "module",
+ "devDependencies": {
+ "@cloudflare/workers-types": "^4.20250522.0"
+ }
+}
diff --git a/packages/function/src/api.ts b/packages/function/src/api.ts
new file mode 100644
index 000000000..3e2ce9eca
--- /dev/null
+++ b/packages/function/src/api.ts
@@ -0,0 +1,167 @@
+import { DurableObject } from "cloudflare:workers"
+import { randomUUID } from "node:crypto"
+
+type Env = {
+ SYNC_SERVER: DurableObjectNamespace<SyncServer>
+ Bucket: R2Bucket
+}
+
+export class SyncServer extends DurableObject<Env> {
+ constructor(ctx: DurableObjectState, env: Env) {
+ super(ctx, env)
+ }
+ async fetch() {
+ console.log("SyncServer subscribe")
+
+ const webSocketPair = new WebSocketPair()
+ const [client, server] = Object.values(webSocketPair)
+
+ this.ctx.acceptWebSocket(server)
+
+ const data = await this.ctx.storage.list()
+ for (const [key, content] of data.entries()) {
+ server.send(JSON.stringify({ key, content }))
+ }
+
+ return new Response(null, {
+ status: 101,
+ webSocket: client,
+ })
+ }
+
+ async webSocketMessage(ws, message) {}
+
+ async webSocketClose(ws, code, reason, wasClean) {
+ ws.close(code, "Durable Object is closing WebSocket")
+ }
+
+ async publish(secret: string, key: string, content: any) {
+ if (secret !== (await this.getSecret())) throw new Error("Invalid secret")
+ const sessionID = await this.getSessionID()
+ if (
+ !key.startsWith(`session/info/${sessionID}`) &&
+ !key.startsWith(`session/message/${sessionID}/`)
+ )
+ return new Response("Error: Invalid key", { status: 400 })
+
+ // store message
+ await this.env.Bucket.put(`share/${key}.json`, JSON.stringify(content), {
+ httpMetadata: {
+ contentType: "application/json",
+ },
+ })
+ await this.ctx.storage.put(key, content)
+ const clients = this.ctx.getWebSockets()
+ console.log("SyncServer publish", key, "to", clients.length, "subscribers")
+ for (const client of clients) {
+ client.send(JSON.stringify({ key, content }))
+ }
+ }
+
+ public async share(sessionID: string) {
+ let secret = await this.getSecret()
+ if (secret) return secret
+ secret = randomUUID()
+
+ await this.ctx.storage.put("secret", secret)
+ await this.ctx.storage.put("sessionID", sessionID)
+
+ return secret
+ }
+
+ private async getSecret() {
+ return this.ctx.storage.get<string>("secret")
+ }
+
+ private async getSessionID() {
+ return this.ctx.storage.get<string>("sessionID")
+ }
+
+ async clear(secret: string) {
+ await this.assertSecret(secret)
+ await this.ctx.storage.deleteAll()
+ }
+
+ private async assertSecret(secret: string) {
+ if (secret !== (await this.getSecret())) throw new Error("Invalid secret")
+ }
+
+ static shortName(id: string) {
+ return id.substring(id.length - 8)
+ }
+}
+
+export default {
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
+ const url = new URL(request.url)
+ const splits = url.pathname.split("/")
+ const method = splits[1]
+
+ if (request.method === "GET" && method === "") {
+ return new Response("Hello, world!", {
+ headers: { "Content-Type": "text/plain" },
+ })
+ }
+
+ if (request.method === "POST" && method === "share_create") {
+ const body = await request.json<any>()
+ const sessionID = body.sessionID
+ const short = SyncServer.shortName(sessionID)
+ const id = env.SYNC_SERVER.idFromName(short)
+ const stub = env.SYNC_SERVER.get(id)
+ const secret = await stub.share(sessionID)
+ return new Response(
+ JSON.stringify({
+ secret,
+ url: "https://dev.opencode.ai/s?id=" + short,
+ }),
+ {
+ headers: { "Content-Type": "application/json" },
+ },
+ )
+ }
+
+ if (request.method === "POST" && method === "share_delete") {
+ const body = await request.json<any>()
+ const sessionID = body.sessionID
+ const secret = body.secret
+ const id = env.SYNC_SERVER.idFromName(SyncServer.shortName(sessionID))
+ const stub = env.SYNC_SERVER.get(id)
+ await stub.clear(secret)
+ return new Response(JSON.stringify({}), {
+ headers: { "Content-Type": "application/json" },
+ })
+ }
+
+ if (request.method === "POST" && method === "share_sync") {
+ const body = await request.json<{
+ sessionID: string
+ secret: string
+ key: string
+ content: any
+ }>()
+ const name = SyncServer.shortName(body.sessionID)
+ const id = env.SYNC_SERVER.idFromName(name)
+ const stub = env.SYNC_SERVER.get(id)
+ await stub.publish(body.secret, body.key, body.content)
+ return new Response(JSON.stringify({}), {
+ headers: { "Content-Type": "application/json" },
+ })
+ }
+
+ if (request.method === "GET" && method === "share_poll") {
+ const upgradeHeader = request.headers.get("Upgrade")
+ if (!upgradeHeader || upgradeHeader !== "websocket") {
+ return new Response("Error: Upgrade header is required", {
+ status: 426,
+ })
+ }
+ const id = url.searchParams.get("id")
+ console.log("share_poll", id)
+ if (!id)
+ return new Response("Error: Share ID is required", { status: 400 })
+ const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
+ return stub.fetch(request)
+ }
+ },
+}
diff --git a/packages/function/sst-env.d.ts b/packages/function/sst-env.d.ts
new file mode 100644
index 000000000..41727ee9d
--- /dev/null
+++ b/packages/function/sst-env.d.ts
@@ -0,0 +1,25 @@
+/* This file is auto-generated by SST. Do not edit. */
+/* tslint:disable */
+/* eslint-disable */
+/* deno-fmt-ignore-file */
+
+import "sst"
+declare module "sst" {
+ export interface Resource {
+ "Web": {
+ "type": "sst.cloudflare.StaticSite"
+ "url": string
+ }
+ }
+}
+// cloudflare
+import * as cloudflare from "@cloudflare/workers-types";
+declare module "sst" {
+ export interface Resource {
+ "Api": cloudflare.Service
+ "Bucket": cloudflare.R2Bucket
+ }
+}
+
+import "sst"
+export {} \ No newline at end of file
diff --git a/packages/function/tsconfig.json b/packages/function/tsconfig.json
new file mode 100644
index 000000000..0faf16aab
--- /dev/null
+++ b/packages/function/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@tsconfig/node22/tsconfig.json",
+ "compilerOptions": {
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "types": ["@cloudflare/workers-types", "node"]
+ }
+}
diff --git a/packages/opencode/.gitignore b/packages/opencode/.gitignore
new file mode 100644
index 000000000..5f9a3e7af
--- /dev/null
+++ b/packages/opencode/.gitignore
@@ -0,0 +1,3 @@
+node_modules
+dist
+gen
diff --git a/packages/opencode/README.md b/packages/opencode/README.md
new file mode 100644
index 000000000..75890119c
--- /dev/null
+++ b/packages/opencode/README.md
@@ -0,0 +1,15 @@
+# js
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run index.ts
+```
+
+This project was created using `bun init` in bun v1.2.12. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
diff --git a/packages/opencode/bin/opencode.mjs b/packages/opencode/bin/opencode.mjs
new file mode 100644
index 000000000..05eac3fb7
--- /dev/null
+++ b/packages/opencode/bin/opencode.mjs
@@ -0,0 +1,29 @@
+#!/usr/bin/env node
+import { createRequire } from "node:module"
+const require = createRequire(import.meta.url)
+import path from "path"
+import { execFileSync } from "child_process"
+let resolved = process.env.SST_BIN_PATH
+if (!resolved) {
+ const name = `opencode-${process.platform}-${process.arch}`
+ const binary = process.platform === "win32" ? "opencode.exe" : "opencode"
+ try {
+ resolved = require.resolve(path.join(name, "bin", binary))
+ } catch (ex) {
+ console.error(
+ `It seems that your package manager failed to install the right version of the SST CLI for your platform. You can try manually installing the "${name}" package`,
+ )
+
+ process.exit(1)
+ }
+}
+
+process.on("SIGINT", () => {})
+
+try {
+ execFileSync(resolved, process.argv.slice(2), {
+ stdio: "inherit",
+ })
+} catch (ex) {
+ process.exit(1)
+}
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
new file mode 100644
index 000000000..6ce3cabc9
--- /dev/null
+++ b/packages/opencode/package.json
@@ -0,0 +1,39 @@
+{
+ "$schema": "https://json.schemastore.org/package.json",
+ "version": "0.0.0",
+ "name": "opencode",
+ "type": "module",
+ "private": true,
+ "scripts": {},
+ "exports": {
+ "./*": [
+ "./src/*.ts",
+ "./src/*/index.ts"
+ ]
+ },
+ "devDependencies": {
+ "@tsconfig/bun": "^1.0.7",
+ "@types/bun": "latest",
+ "@types/jsdom": "^21.1.7",
+ "@types/turndown": "^5.0.5"
+ },
+ "dependencies": {
+ "@flystorage/file-storage": "^1.1.0",
+ "@flystorage/local-fs": "^1.1.0",
+ "@hono/zod-validator": "^0.5.0",
+ "ai": "5.0.0-alpha.7",
+ "cac": "^6.7.14",
+ "decimal.js": "^10.5.0",
+ "env-paths": "^3.0.0",
+ "hono": "^4.7.10",
+ "hono-openapi": "^0.4.8",
+ "jsdom": "^26.1.0",
+ "remeda": "^2.22.3",
+ "ts-lsp-client": "^1.0.3",
+ "turndown": "^7.2.0",
+ "vscode-jsonrpc": "^8.2.1",
+ "vscode-languageclient": "8",
+ "zod": "^3.25.3",
+ "zod-openapi": "^4.2.4"
+ }
+}
diff --git a/packages/opencode/scrap.ts b/packages/opencode/scrap.ts
new file mode 100644
index 000000000..35ff3fbfb
--- /dev/null
+++ b/packages/opencode/scrap.ts
@@ -0,0 +1,30 @@
+// This is a dummy file for testing purposes
+console.log('Hello, world!');
+
+export function dummyFunction(): void {
+ console.log('This is a dummy function');
+}
+
+export function anotherDummyFunction(): string {
+ return 'This is another dummy function';
+}
+
+export function newDummyFunction(): number {
+ return 42;
+}
+
+export function extraDummyFunction(): boolean {
+ return true;
+}
+
+export function superDummyFunction(): void {
+ console.log('This is a super dummy function');
+}
+
+export function ultraDummyFunction(): object {
+ return { dummy: true };
+}
+
+export function megaDummyFunction(): Array<string> {
+ return ['dummy', 'mega', 'function'];
+} \ No newline at end of file
diff --git a/packages/opencode/script/release.ts b/packages/opencode/script/release.ts
new file mode 100755
index 000000000..af770cc33
--- /dev/null
+++ b/packages/opencode/script/release.ts
@@ -0,0 +1,68 @@
+#!/usr/bin/env bun
+
+import { $ } from "bun"
+
+import pkg from "../package.json"
+
+const version = `0.0.0-${Date.now()}`
+
+const ARCH = {
+ arm64: "arm64",
+ x64: "amd64",
+}
+
+const OS = {
+ linux: "linux",
+ darwin: "mac",
+ win32: "windows",
+}
+
+const targets = [
+ ["linux", "arm64"],
+ ["linux", "x64"],
+ ["darwin", "x64"],
+ ["darwin", "arm64"],
+ ["windows", "x64"],
+]
+
+await $`rm -rf dist`
+
+const optionalDependencies: Record<string, string> = {}
+for (const [os, arch] of targets) {
+ console.log(`building ${os}-${arch}`)
+ const name = `${pkg.name}-${os}-${arch}`
+ await $`mkdir -p dist/${name}/bin`
+ await $`bun build --compile --minify --target=bun-${os}-${arch} --outfile=dist/${name}/bin/${pkg.name} ./src/index.ts`
+ await Bun.file(`dist/${name}/package.json`).write(
+ JSON.stringify(
+ {
+ name,
+ version,
+ os: [os],
+ cpu: [arch],
+ },
+ null,
+ 2,
+ ),
+ )
+ await $`cd dist/${name} && npm publish --access public --tag latest`
+ optionalDependencies[name] = version
+}
+
+await $`mkdir -p ./dist/${pkg.name}`
+await $`cp -r ./bin ./dist/${pkg.name}/bin`
+await Bun.file(`./dist/${pkg.name}/package.json`).write(
+ JSON.stringify(
+ {
+ name: pkg.name + "-ai",
+ bin: {
+ [pkg.name]: `./bin/${pkg.name}.mjs`,
+ },
+ version,
+ optionalDependencies,
+ },
+ null,
+ 2,
+ ),
+)
+await $`cd ./dist/${pkg.name} && npm publish --access public --tag latest`
diff --git a/packages/opencode/src/app/app.ts b/packages/opencode/src/app/app.ts
new file mode 100644
index 000000000..0c6260bc7
--- /dev/null
+++ b/packages/opencode/src/app/app.ts
@@ -0,0 +1,78 @@
+import fs from "fs/promises";
+import { AppPath } from "./path";
+import { Log } from "../util/log";
+import { Context } from "../util/context";
+
+export namespace App {
+ const log = Log.create({ service: "app" });
+
+ export type Info = Awaited<ReturnType<typeof create>>;
+
+ const ctx = Context.create<Info>("app");
+
+ async function create(input: { directory: string }) {
+ const dataDir = AppPath.data(input.directory);
+ await fs.mkdir(dataDir, { recursive: true });
+ await Log.file(input.directory);
+
+ log.info("created", { path: dataDir });
+
+ const services = new Map<
+ any,
+ {
+ state: any;
+ shutdown?: (input: any) => Promise<void>;
+ }
+ >();
+
+ const result = {
+ get services() {
+ return services;
+ },
+ get root() {
+ return input.directory;
+ },
+ };
+
+ return result;
+ }
+
+ export function state<State>(
+ key: any,
+ init: (app: Info) => State,
+ shutdown?: (state: Awaited<State>) => Promise<void>,
+ ) {
+ return () => {
+ const app = ctx.use();
+ const services = app.services;
+ if (!services.has(key)) {
+ log.info("registering service", { name: key });
+ services.set(key, {
+ state: init(app),
+ shutdown: shutdown,
+ });
+ }
+ return services.get(key)?.state as State;
+ };
+ }
+
+ export async function use() {
+ return ctx.use();
+ }
+
+ export async function provide<T extends (app: Info) => any>(
+ input: { directory: string },
+ cb: T,
+ ) {
+ const app = await create(input);
+
+ return ctx.provide(app, async () => {
+ const result = await cb(app);
+ for (const [key, entry] of app.services.entries()) {
+ log.info("shutdown", { name: key });
+ await entry.shutdown?.(await entry.state);
+ }
+ return result;
+ });
+ }
+}
diff --git a/packages/opencode/src/app/path.ts b/packages/opencode/src/app/path.ts
new file mode 100644
index 000000000..972d18c41
--- /dev/null
+++ b/packages/opencode/src/app/path.ts
@@ -0,0 +1,11 @@
+import path from "path";
+
+export namespace AppPath {
+ export function data(input: string) {
+ return path.join(input, ".opencode");
+ }
+
+ export function storage(input: string) {
+ return path.join(data(input), "storage");
+ }
+}
diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts
new file mode 100644
index 000000000..35e8cbbf9
--- /dev/null
+++ b/packages/opencode/src/bun/index.ts
@@ -0,0 +1,28 @@
+import path from "path";
+import { Log } from "../util/log";
+export namespace BunProc {
+ const log = Log.create({ service: "bun" });
+
+ export function run(
+ cmd: string[],
+ options?: Bun.SpawnOptions.OptionsObject<any, any, any>,
+ ) {
+ const root =
+ process.argv0 !== "bun"
+ ? path.resolve(process.cwd(), process.argv0)
+ : process.argv0;
+ log.info("running", {
+ cmd: [root, ...cmd],
+ options,
+ });
+ const result = Bun.spawnSync([root, ...cmd], {
+ ...options,
+ argv0: "bun",
+ env: {
+ ...process.env,
+ ...options?.env,
+ },
+ });
+ return result;
+ }
+}
diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts
new file mode 100644
index 000000000..342d82b6d
--- /dev/null
+++ b/packages/opencode/src/bus/index.ts
@@ -0,0 +1,101 @@
+import { z, type ZodType } from "zod";
+import { App } from "../app/app";
+import { Log } from "../util/log";
+
+export namespace Bus {
+ const log = Log.create({ service: "bus" });
+ type Subscription = (event: any) => void;
+
+ const state = App.state("bus", () => {
+ const subscriptions = new Map<any, Subscription[]>();
+
+ return {
+ subscriptions,
+ };
+ });
+
+ export type EventDefinition = ReturnType<typeof event>;
+
+ const registry = new Map<string, EventDefinition>();
+
+ export function event<Type extends string, Properties extends ZodType>(
+ type: Type,
+ properties: Properties,
+ ) {
+ const result = {
+ type,
+ properties,
+ };
+ registry.set(type, result);
+ return result;
+ }
+
+ export function payloads() {
+ return z.discriminatedUnion(
+ "type",
+ registry
+ .entries()
+ .map(([type, def]) =>
+ z
+ .object({
+ type: z.literal(type),
+ properties: def.properties,
+ })
+ .openapi({
+ ref: "Event" + "." + def.type,
+ }),
+ )
+ .toArray() as any,
+ );
+ }
+
+ export function publish<Definition extends EventDefinition>(
+ def: Definition,
+ properties: z.output<Definition["properties"]>,
+ ) {
+ const payload = {
+ type: def.type,
+ properties,
+ };
+ log.info("publishing", {
+ type: def.type,
+ });
+ for (const key of [def.type, "*"]) {
+ const match = state().subscriptions.get(key);
+ for (const sub of match ?? []) {
+ sub(payload);
+ }
+ }
+ }
+
+ export function subscribe<Definition extends EventDefinition>(
+ def: Definition,
+ callback: (event: {
+ type: Definition["type"];
+ properties: z.infer<Definition["properties"]>;
+ }) => void,
+ ) {
+ return raw(def.type, callback);
+ }
+
+ export function subscribeAll(callback: (event: any) => void) {
+ return raw("*", callback);
+ }
+
+ function raw(type: string, callback: (event: any) => void) {
+ log.info("subscribing", { type });
+ const subscriptions = state().subscriptions;
+ let match = subscriptions.get(type) ?? [];
+ match.push(callback);
+ subscriptions.set(type, match);
+
+ return () => {
+ log.info("unsubscribing", { type });
+ const match = subscriptions.get(type);
+ if (!match) return;
+ const index = match.indexOf(callback);
+ if (index === -1) return;
+ match.splice(index, 1);
+ };
+ }
+}
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
new file mode 100644
index 000000000..8c374b3f3
--- /dev/null
+++ b/packages/opencode/src/config/config.ts
@@ -0,0 +1,51 @@
+import path from "path";
+import { Log } from "../util/log";
+import { z } from "zod";
+import { App } from "../app/app";
+import { Provider } from "../provider/provider";
+
+export namespace Config {
+ const log = Log.create({ service: "config" });
+
+ export const state = App.state("config", async (app) => {
+ const result = await load(app.root);
+ return result;
+ });
+
+ export const Info = z
+ .object({
+ providers: Provider.Info.array().optional(),
+ })
+ .strict();
+
+ export type Info = z.output<typeof Info>;
+
+ export function get() {
+ return state();
+ }
+
+ async function load(directory: string) {
+ let result: Info = {};
+ for (const file of ["opencode.jsonc", "opencode.json"]) {
+ const resolved = path.join(directory, file);
+ log.info("searching", { path: resolved });
+ try {
+ result = await import(path.join(directory, file)).then((mod) =>
+ Info.parse(mod.default),
+ );
+ log.info("found", { path: resolved });
+ break;
+ } catch (e) {
+ if (e instanceof z.ZodError) {
+ for (const issue of e.issues) {
+ log.info(issue.message);
+ }
+ throw e;
+ }
+ continue;
+ }
+ }
+ log.info("loaded", result);
+ return result;
+ }
+}
diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts
new file mode 100644
index 000000000..1e097f38c
--- /dev/null
+++ b/packages/opencode/src/global/index.ts
@@ -0,0 +1,20 @@
+import envpaths from "env-paths";
+import fs from "fs/promises";
+const paths = envpaths("opencode", {
+ suffix: "",
+});
+
+await Promise.all([
+ fs.mkdir(paths.config, { recursive: true }),
+ fs.mkdir(paths.cache, { recursive: true }),
+]);
+
+export namespace Global {
+ export function config() {
+ return paths.config;
+ }
+
+ export function cache() {
+ return paths.cache;
+ }
+}
diff --git a/packages/opencode/src/id/id.ts b/packages/opencode/src/id/id.ts
new file mode 100644
index 000000000..62c6a12bf
--- /dev/null
+++ b/packages/opencode/src/id/id.ts
@@ -0,0 +1,74 @@
+import { z } from "zod";
+import { randomBytes } from "crypto";
+
+export namespace Identifier {
+ const prefixes = {
+ session: "ses",
+ message: "msg",
+ } as const;
+
+ export function schema(prefix: keyof typeof prefixes) {
+ return z.string().startsWith(prefixes[prefix]);
+ }
+
+ const LENGTH = 26;
+
+ // State for monotonic ID generation
+ let lastTimestamp = 0;
+ let counter = 0;
+
+ export function ascending(prefix: keyof typeof prefixes, given?: string) {
+ return generateID(prefix, false, given);
+ }
+
+ export function descending(prefix: keyof typeof prefixes, given?: string) {
+ return generateID(prefix, true, given);
+ }
+
+ function generateID(
+ prefix: keyof typeof prefixes,
+ descending: boolean,
+ given?: string,
+ ): string {
+ if (!given) {
+ return generateNewID(prefix, descending);
+ }
+
+ if (!given.startsWith(prefixes[prefix])) {
+ throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`);
+ }
+ return given;
+ }
+
+ function generateNewID(
+ prefix: keyof typeof prefixes,
+ descending: boolean,
+ ): string {
+ const currentTimestamp = Date.now();
+
+ if (currentTimestamp !== lastTimestamp) {
+ lastTimestamp = currentTimestamp;
+ counter = 0;
+ }
+ counter++;
+
+ let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter);
+
+ now = descending ? ~now : now;
+
+ const timeBytes = Buffer.alloc(6);
+ for (let i = 0; i < 6; i++) {
+ timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff));
+ }
+
+ const randLength = (LENGTH - 12) / 2;
+ const random = randomBytes(randLength);
+
+ return (
+ prefixes[prefix] +
+ "_" +
+ timeBytes.toString("hex") +
+ random.toString("hex")
+ );
+ }
+}
diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts
new file mode 100644
index 000000000..26f25f783
--- /dev/null
+++ b/packages/opencode/src/index.ts
@@ -0,0 +1,85 @@
+import "zod-openapi/extend"
+import { App } from "./app/app"
+import { Server } from "./server/server"
+import fs from "fs/promises"
+import path from "path"
+import { Bus } from "./bus"
+import { Session } from "./session/session"
+import cac from "cac"
+import { Share } from "./share/share"
+import { Storage } from "./storage/storage"
+import { LLM } from "./llm/llm"
+import { Message } from "./session/message"
+
+const cli = cac("opencode")
+
+cli.command("", "Start the opencode in interactive mode").action(async () => {
+ await App.provide({ directory: process.cwd() }, async () => {
+ await Share.init()
+ Server.listen()
+ })
+})
+
+cli.command("generate", "Generate OpenAPI and event specs").action(async () => {
+ const specs = await Server.openapi()
+ const dir = "gen"
+ await fs.rmdir(dir, { recursive: true }).catch(() => {})
+ await fs.mkdir(dir, { recursive: true })
+ await Bun.write(
+ path.join(dir, "openapi.json"),
+ JSON.stringify(specs, null, 2),
+ )
+})
+
+cli
+ .command("run [...message]", "Run a chat message")
+ .option("--session <id>", "Session ID")
+ .action(async (message: string[], options) => {
+ await App.provide({ directory: process.cwd() }, async () => {
+ await Share.init()
+ const session = options.session
+ ? await Session.get(options.session)
+ : await Session.create()
+ console.log("Session:", session.id)
+
+ Bus.subscribe(Message.Event.Updated, async (message) => {
+ console.log("Thinking...")
+ })
+
+ const unsub = Bus.subscribe(Session.Event.Updated, async (message) => {
+ if (message.properties.info.share?.url)
+ console.log("Share:", message.properties.info.share.url)
+ unsub()
+ })
+
+ const providers = await LLM.providers()
+ const providerID = Object.keys(providers)[0]
+ const modelID = providers[providerID].info.models[0].id
+ console.log("using", providerID, modelID)
+ const result = await Session.chat({
+ sessionID: session.id,
+ providerID,
+ modelID,
+ parts: [
+ {
+ type: "text",
+ text: message.join(" "),
+ },
+ ],
+ })
+
+ for (const part of result.parts) {
+ if (part.type === "text") {
+ console.log("opencode:", part.text)
+ }
+ }
+ console.log({
+ cost: result.metadata.assistant?.cost,
+ tokens: result.metadata.assistant?.tokens,
+ })
+ })
+ })
+
+cli.help()
+cli.version("1.0.0")
+cli.parse()
diff --git a/packages/opencode/src/llm/llm.ts b/packages/opencode/src/llm/llm.ts
new file mode 100644
index 000000000..3fb8357f2
--- /dev/null
+++ b/packages/opencode/src/llm/llm.ts
@@ -0,0 +1,172 @@
+import { App } from "../app/app"
+import { Log } from "../util/log"
+import { concat } from "remeda"
+import path from "path"
+import { Provider } from "../provider/provider"
+
+import type { LanguageModel, Provider as ProviderInstance } from "ai"
+import { NoSuchModelError } from "ai"
+import { Config } from "../config/config"
+import { BunProc } from "../bun"
+import { Global } from "../global"
+
+export namespace LLM {
+ const log = Log.create({ service: "llm" })
+
+ export class ModelNotFoundError extends Error {
+ constructor(public readonly model: string) {
+ super()
+ }
+ }
+
+ const NATIVE_PROVIDERS: Provider.Info[] = [
+ {
+ id: "anthropic",
+ name: "Anthropic",
+ models: [
+ {
+ id: "claude-sonnet-4-20250514",
+ name: "Claude Sonnet 4",
+ cost: {
+ input: 3.0 / 1_000_000,
+ output: 15.0 / 1_000_000,
+ inputCached: 3.75 / 1_000_000,
+ outputCached: 0.3 / 1_000_000,
+ },
+ contextWindow: 200_000,
+ maxOutputTokens: 50_000,
+ attachment: true,
+ },
+ ],
+ },
+ {
+ id: "openai",
+ name: "OpenAI",
+ models: [
+ {
+ id: "codex-mini-latest",
+ name: "Codex Mini",
+ cost: {
+ input: 1.5 / 1_000_000,
+ inputCached: 0.375 / 1_000_000,
+ output: 6.0 / 1_000_000,
+ outputCached: 0.0 / 1_000_000,
+ },
+ contextWindow: 200_000,
+ maxOutputTokens: 100_000,
+ attachment: true,
+ reasoning: true,
+ },
+ ],
+ },
+ {
+ id: "google",
+ name: "Google",
+ models: [
+ {
+ id: "gemini-2.5-pro-preview-03-25",
+ name: "Gemini 2.5 Pro",
+ cost: {
+ input: 1.25 / 1_000_000,
+ inputCached: 0 / 1_000_000,
+ output: 10 / 1_000_000,
+ outputCached: 0 / 1_000_000,
+ },
+ contextWindow: 1_000_000,
+ maxOutputTokens: 50_000,
+ attachment: true,
+ },
+ ],
+ },
+ ]
+
+ const AUTODETECT: Record<string, string[]> = {
+ anthropic: ["ANTHROPIC_API_KEY"],
+ openai: ["OPENAI_API_KEY"],
+ google: ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"],
+ }
+
+ const state = App.state("llm", async () => {
+ const config = await Config.get()
+ const providers: Record<
+ string,
+ {
+ info: Provider.Info
+ instance: ProviderInstance
+ }
+ > = {}
+ const models = new Map<
+ string,
+ { info: Provider.Model; instance: LanguageModel }
+ >()
+
+ const list = concat(NATIVE_PROVIDERS, config.providers ?? [])
+
+ for (const provider of list) {
+ if (
+ !config.providers?.find((p) => p.id === provider.id) &&
+ !AUTODETECT[provider.id]?.some((env) => process.env[env])
+ )
+ continue
+ const dir = path.join(
+ Global.cache(),
+ `node_modules`,
+ `@ai-sdk`,
+ provider.id,
+ )
+ if (!(await Bun.file(path.join(dir, "package.json")).exists())) {
+ BunProc.run(["add", "--exact", `@ai-sdk/${provider.id}@alpha`], {
+ cwd: Global.cache(),
+ })
+ }
+ const mod = await import(
+ path.join(Global.cache(), `node_modules`, `@ai-sdk`, provider.id)
+ )
+ const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
+ const loaded = fn(provider.options)
+ log.info("loaded", { provider: provider.id })
+ providers[provider.id] = {
+ info: provider,
+ instance: loaded,
+ }
+ }
+
+ return {
+ models,
+ providers,
+ }
+ })
+
+ export async function providers() {
+ return state().then((state) => state.providers)
+ }
+
+ export async function findModel(providerID: string, modelID: string) {
+ const key = `${providerID}/${modelID}`
+ const s = await state()
+ if (s.models.has(key)) return s.models.get(key)!
+ const provider = s.providers[providerID]
+ if (!provider) throw new ModelNotFoundError(modelID)
+ log.info("loading", {
+ providerID,
+ modelID,
+ })
+ const info = provider.info.models.find((m) => m.id === modelID)
+ if (!info) throw new ModelNotFoundError(modelID)
+ try {
+ const match = provider.instance.languageModel(modelID)
+ log.info("found", { providerID, modelID })
+ s.models.set(key, {
+ info,
+ instance: match,
+ })
+ return {
+ info,
+ instance: match,
+ }
+ } catch (e) {
+ if (e instanceof NoSuchModelError) throw new ModelNotFoundError(modelID)
+ throw e
+ }
+ }
+}
diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts
new file mode 100644
index 000000000..82caa82a2
--- /dev/null
+++ b/packages/opencode/src/lsp/client.ts
@@ -0,0 +1,208 @@
+import { spawn } from "child_process";
+import path from "path";
+import {
+ createMessageConnection,
+ StreamMessageReader,
+ StreamMessageWriter,
+} from "vscode-jsonrpc/node";
+import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types";
+import { App } from "../app/app";
+import { Log } from "../util/log";
+import { LANGUAGE_EXTENSIONS } from "./language";
+import { Bus } from "../bus";
+import z from "zod";
+
+export namespace LSPClient {
+ const log = Log.create({ service: "lsp.client" });
+
+ export type Info = Awaited<ReturnType<typeof create>>;
+
+ export type Diagnostic = VSCodeDiagnostic;
+
+ export const Event = {
+ Diagnostics: Bus.event(
+ "lsp.client.diagnostics",
+ z.object({
+ serverID: z.string(),
+ path: z.string(),
+ }),
+ ),
+ };
+
+ export async function create(input: { cmd: string[]; serverID: string }) {
+ log.info("starting client", input);
+
+ const app = await App.use();
+ const [command, ...args] = input.cmd;
+ const server = spawn(command, args, {
+ stdio: ["pipe", "pipe", "pipe"],
+ cwd: app.root,
+ });
+
+ const connection = createMessageConnection(
+ new StreamMessageReader(server.stdout),
+ new StreamMessageWriter(server.stdin),
+ );
+
+ const diagnostics = new Map<string, Diagnostic[]>();
+ connection.onNotification("textDocument/publishDiagnostics", (params) => {
+ const path = new URL(params.uri).pathname;
+ log.info("textDocument/publishDiagnostics", {
+ path,
+ });
+ const exists = diagnostics.has(path);
+ diagnostics.set(path, params.diagnostics);
+ // servers seem to send one blank publishDiagnostics event before the first real one
+ if (!exists && !params.diagnostics.length) return;
+ Bus.publish(Event.Diagnostics, { path, serverID: input.serverID });
+ });
+ connection.listen();
+
+ await connection.sendRequest("initialize", {
+ processId: server.pid,
+ initializationOptions: {
+ workspaceFolders: [
+ {
+ name: "workspace",
+ uri: "file://" + app.root,
+ },
+ ],
+ tsserver: {
+ path: require.resolve("typescript/lib/tsserver.js"),
+ },
+ },
+ capabilities: {
+ workspace: {
+ configuration: true,
+ didChangeConfiguration: {
+ dynamicRegistration: true,
+ },
+ didChangeWatchedFiles: {
+ dynamicRegistration: true,
+ relativePatternSupport: true,
+ },
+ },
+ textDocument: {
+ synchronization: {
+ dynamicRegistration: true,
+ didSave: true,
+ },
+ completion: {
+ completionItem: {},
+ },
+ codeLens: {
+ dynamicRegistration: true,
+ },
+ documentSymbol: {},
+ codeAction: {
+ codeActionLiteralSupport: {
+ codeActionKind: {
+ valueSet: [],
+ },
+ },
+ },
+ publishDiagnostics: {
+ versionSupport: true,
+ },
+ semanticTokens: {
+ requests: {
+ range: {},
+ full: {},
+ },
+ tokenTypes: [],
+ tokenModifiers: [],
+ formats: [],
+ },
+ },
+ window: {},
+ },
+ });
+ await connection.sendNotification("initialized", {});
+ log.info("initialized");
+
+ const files = new Set<string>();
+
+ const result = {
+ get clientID() {
+ return input.serverID;
+ },
+ get connection() {
+ return connection;
+ },
+ notify: {
+ async open(input: { path: string }) {
+ const file = Bun.file(input.path);
+ const text = await file.text();
+ const opened = files.has(input.path);
+ if (!opened) {
+ log.info("textDocument/didOpen", input);
+ diagnostics.delete(input.path);
+ const extension = path.extname(input.path);
+ const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext";
+ await connection.sendNotification("textDocument/didOpen", {
+ textDocument: {
+ uri: `file://` + input.path,
+ languageId,
+ version: Date.now(),
+ text,
+ },
+ });
+ files.add(input.path);
+ return;
+ }
+
+ log.info("textDocument/didChange", input);
+ diagnostics.delete(input.path);
+ await connection.sendNotification("textDocument/didChange", {
+ textDocument: {
+ uri: `file://` + input.path,
+ version: Date.now(),
+ },
+ contentChanges: [
+ {
+ text,
+ },
+ ],
+ });
+ },
+ },
+ get diagnostics() {
+ return diagnostics;
+ },
+ async waitForDiagnostics(input: { path: string }) {
+ log.info("waiting for diagnostics", input);
+ let unsub: () => void;
+ let timeout: NodeJS.Timeout;
+ return await Promise.race([
+ new Promise<void>(async (resolve) => {
+ unsub = Bus.subscribe(Event.Diagnostics, (event) => {
+ if (
+ event.properties.path === input.path &&
+ event.properties.serverID === result.clientID
+ ) {
+ log.info("got diagnostics", input);
+ clearTimeout(timeout);
+ unsub?.();
+ resolve();
+ }
+ });
+ }),
+ new Promise<void>((resolve) => {
+ timeout = setTimeout(() => {
+ log.info("timed out refreshing diagnostics", input);
+ unsub?.();
+ resolve();
+ }, 5000);
+ }),
+ ]);
+ },
+ async shutdown() {
+ log.info("shutting down");
+ connection.end();
+ connection.dispose();
+ },
+ };
+
+ return result;
+ }
+}
diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts
new file mode 100644
index 000000000..e3344a934
--- /dev/null
+++ b/packages/opencode/src/lsp/index.ts
@@ -0,0 +1,131 @@
+import { App } from "../app/app";
+import { Log } from "../util/log";
+import { LSPClient } from "./client";
+import path from "path";
+
+export namespace LSP {
+ const log = Log.create({ service: "lsp" });
+
+ const state = App.state(
+ "lsp",
+ async () => {
+ log.info("initializing");
+ const clients = new Map<string, LSPClient.Info>();
+
+ return {
+ clients,
+ };
+ },
+ async (state) => {
+ for (const client of state.clients.values()) {
+ await client.shutdown();
+ }
+ },
+ );
+
+ export async function file(input: string) {
+ const extension = path.parse(input).ext;
+ const s = await state();
+ const matches = AUTO.filter((x) => x.extensions.includes(extension));
+ for (const match of matches) {
+ const existing = s.clients.get(match.id);
+ if (existing) continue;
+ const client = await LSPClient.create({
+ cmd: match.command,
+ serverID: match.id,
+ });
+ s.clients.set(match.id, client);
+ }
+ await run(async (client) => {
+ const wait = client.waitForDiagnostics({ path: input });
+ await client.notify.open({ path: input });
+ return wait;
+ });
+ }
+
+ export async function diagnostics() {
+ const results: Record<string, LSPClient.Diagnostic[]> = {};
+ for (const result of await run(async (client) => client.diagnostics)) {
+ for (const [path, diagnostics] of result.entries()) {
+ const arr = results[path] || [];
+ arr.push(...diagnostics);
+ results[path] = arr;
+ }
+ }
+ return results;
+ }
+
+ export async function hover(input: {
+ file: string;
+ line: number;
+ character: number;
+ }) {
+ return run((client) => {
+ return client.connection.sendRequest("textDocument/hover", {
+ textDocument: {
+ uri: `file://${input.file}`,
+ },
+ position: {
+ line: input.line,
+ character: input.character,
+ },
+ });
+ });
+ }
+
+ async function run<T>(
+ input: (client: LSPClient.Info) => Promise<T>,
+ ): Promise<T[]> {
+ const clients = await state().then((x) => [...x.clients.values()]);
+ const tasks = clients.map((x) => input(x));
+ return Promise.all(tasks);
+ }
+
+ const AUTO: {
+ id: string;
+ command: string[];
+ extensions: string[];
+ install?: () => Promise<void>;
+ }[] = [
+ {
+ id: "typescript",
+ command: ["bun", "x", "typescript-language-server", "--stdio"],
+ extensions: [
+ ".ts",
+ ".tsx",
+ ".js",
+ ".jsx",
+ ".mjs",
+ ".cjs",
+ ".mts",
+ ".cts",
+ ".mtsx",
+ ".ctsx",
+ ],
+ },
+ /*
+ {
+ id: "golang",
+ command: ["gopls"],
+ extensions: [".go"],
+ },
+ */
+ ];
+
+ export namespace Diagnostic {
+ export function pretty(diagnostic: LSPClient.Diagnostic) {
+ const severityMap = {
+ 1: "ERROR",
+ 2: "WARN",
+ 3: "INFO",
+ 4: "HINT",
+ };
+
+ const severity = severityMap[diagnostic.severity || 1];
+ const line = diagnostic.range.start.line + 1;
+ const col = diagnostic.range.start.character + 1;
+
+ return `${severity} [${line}:${col}] ${diagnostic.message}`;
+ }
+ }
+}
diff --git a/packages/opencode/src/lsp/language.ts b/packages/opencode/src/lsp/language.ts
new file mode 100644
index 000000000..e28d7a79c
--- /dev/null
+++ b/packages/opencode/src/lsp/language.ts
@@ -0,0 +1,89 @@
+export const LANGUAGE_EXTENSIONS: Record<string, string> = {
+ ".abap": "abap",
+ ".bat": "bat",
+ ".bib": "bibtex",
+ ".bibtex": "bibtex",
+ ".clj": "clojure",
+ ".coffee": "coffeescript",
+ ".c": "c",
+ ".cpp": "cpp",
+ ".cxx": "cpp",
+ ".cc": "cpp",
+ ".c++": "cpp",
+ ".cs": "csharp",
+ ".css": "css",
+ ".d": "d",
+ ".pas": "pascal",
+ ".pascal": "pascal",
+ ".diff": "diff",
+ ".patch": "diff",
+ ".dart": "dart",
+ ".dockerfile": "dockerfile",
+ ".ex": "elixir",
+ ".exs": "elixir",
+ ".erl": "erlang",
+ ".hrl": "erlang",
+ ".fs": "fsharp",
+ ".fsi": "fsharp",
+ ".fsx": "fsharp",
+ ".fsscript": "fsharp",
+ ".gitcommit": "git-commit",
+ ".gitrebase": "git-rebase",
+ ".go": "go",
+ ".groovy": "groovy",
+ ".hbs": "handlebars",
+ ".handlebars": "handlebars",
+ ".hs": "haskell",
+ ".html": "html",
+ ".htm": "html",
+ ".ini": "ini",
+ ".java": "java",
+ ".js": "javascript",
+ ".jsx": "javascriptreact",
+ ".json": "json",
+ ".tex": "latex",
+ ".latex": "latex",
+ ".less": "less",
+ ".lua": "lua",
+ ".makefile": "makefile",
+ makefile: "makefile",
+ ".md": "markdown",
+ ".markdown": "markdown",
+ ".m": "objective-c",
+ ".mm": "objective-cpp",
+ ".pl": "perl",
+ ".pm": "perl6",
+ ".php": "php",
+ ".ps1": "powershell",
+ ".psm1": "powershell",
+ ".pug": "jade",
+ ".jade": "jade",
+ ".py": "python",
+ ".r": "r",
+ ".cshtml": "razor",
+ ".razor": "razor",
+ ".rb": "ruby",
+ ".rs": "rust",
+ ".scss": "scss",
+ ".sass": "sass",
+ ".scala": "scala",
+ ".shader": "shaderlab",
+ ".sh": "shellscript",
+ ".bash": "shellscript",
+ ".zsh": "shellscript",
+ ".ksh": "shellscript",
+ ".sql": "sql",
+ ".swift": "swift",
+ ".ts": "typescript",
+ ".tsx": "typescriptreact",
+ ".mts": "typescript",
+ ".cts": "typescript",
+ ".mtsx": "typescriptreact",
+ ".ctsx": "typescriptreact",
+ ".xml": "xml",
+ ".xsl": "xsl",
+ ".yaml": "yaml",
+ ".yml": "yaml",
+ ".mjs": "javascript",
+ ".cjs": "javascript",
+} as const;
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
new file mode 100644
index 000000000..d4719ffbb
--- /dev/null
+++ b/packages/opencode/src/provider/provider.ts
@@ -0,0 +1,35 @@
+import z from "zod";
+
+export namespace Provider {
+ export const Model = z
+ .object({
+ id: z.string(),
+ name: z.string().optional(),
+ cost: z.object({
+ input: z.number(),
+ inputCached: z.number(),
+ output: z.number(),
+ outputCached: z.number(),
+ }),
+ contextWindow: z.number(),
+ maxOutputTokens: z.number().optional(),
+ attachment: z.boolean(),
+ reasoning: z.boolean().optional(),
+ })
+ .openapi({
+ ref: "Provider.Model",
+ });
+ export type Model = z.output<typeof Model>;
+
+ export const Info = z
+ .object({
+ id: z.string(),
+ name: z.string(),
+ options: z.record(z.string(), z.any()).optional(),
+ models: Model.array(),
+ })
+ .openapi({
+ ref: "Provider.Info",
+ });
+ export type Info = z.output<typeof Info>;
+}
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
new file mode 100644
index 000000000..28591cbd2
--- /dev/null
+++ b/packages/opencode/src/server/server.ts
@@ -0,0 +1,309 @@
+import { Log } from "../util/log";
+import { Bus } from "../bus";
+import { describeRoute, generateSpecs, openAPISpecs } from "hono-openapi";
+import { Hono } from "hono";
+import { streamSSE } from "hono/streaming";
+import { Session } from "../session/session";
+import { resolver, validator as zValidator } from "hono-openapi/zod";
+import { z } from "zod";
+import { LLM } from "../llm/llm";
+import { Message } from "../session/message";
+import { Provider } from "../provider/provider";
+
+export namespace Server {
+ const log = Log.create({ service: "server" });
+ const PORT = 16713;
+
+ export type App = ReturnType<typeof app>;
+
+ function app() {
+ const app = new Hono();
+
+ const result = app
+ .get(
+ "/openapi",
+ openAPISpecs(app, {
+ documentation: {
+ info: {
+ title: "opencode",
+ version: "1.0.0",
+ description: "opencode api",
+ },
+ openapi: "3.0.0",
+ },
+ }),
+ )
+ .get(
+ "/event",
+ describeRoute({
+ description: "Get events",
+ responses: {
+ 200: {
+ description: "Event stream",
+ content: {
+ "application/json": {
+ schema: resolver(
+ Bus.payloads().openapi({
+ ref: "Event",
+ }),
+ ),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ log.info("event connected");
+ return streamSSE(c, async (stream) => {
+ stream.writeSSE({
+ data: JSON.stringify({}),
+ });
+ const unsub = Bus.subscribeAll(async (event) => {
+ await stream.writeSSE({
+ data: JSON.stringify(event),
+ });
+ });
+ await new Promise<void>((resolve) => {
+ stream.onAbort(() => {
+ unsub();
+ resolve();
+ log.info("event disconnected");
+ });
+ });
+ });
+ },
+ )
+ .post(
+ "/session_create",
+ describeRoute({
+ description: "Create a new session",
+ responses: {
+ 200: {
+ description: "Successfully created session",
+ content: {
+ "application/json": {
+ schema: resolver(Session.Info),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const session = await Session.create();
+ return c.json(session);
+ },
+ )
+ .post(
+ "/session_share",
+ describeRoute({
+ description: "Share the session",
+ responses: {
+ 200: {
+ description: "Successfully shared session",
+ content: {
+ "application/json": {
+ schema: resolver(Session.Info),
+ },
+ },
+ },
+ },
+ }),
+ zValidator(
+ "json",
+ z.object({
+ sessionID: z.string(),
+ }),
+ ),
+ async (c) => {
+ const body = c.req.valid("json");
+ await Session.share(body.sessionID);
+ const session = await Session.get(body.sessionID);
+ return c.json(session);
+ },
+ )
+ .post(
+ "/session_messages",
+ describeRoute({
+ description: "Get messages for a session",
+ responses: {
+ 200: {
+ description: "Successfully created session",
+ content: {
+ "application/json": {
+ schema: resolver(Message.Info.array()),
+ },
+ },
+ },
+ },
+ }),
+ zValidator(
+ "json",
+ z.object({
+ sessionID: z.string(),
+ }),
+ ),
+ async (c) => {
+ const messages = await Session.messages(
+ c.req.valid("json").sessionID,
+ );
+ return c.json(messages);
+ },
+ )
+ .post(
+ "/session_list",
+ describeRoute({
+ description: "List all sessions",
+ responses: {
+ 200: {
+ description: "List of sessions",
+ content: {
+ "application/json": {
+ schema: resolver(Session.Info.array()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const sessions = await Array.fromAsync(Session.list());
+ return c.json(sessions);
+ },
+ )
+ .post(
+ "/session_abort",
+ describeRoute({
+ description: "Abort a session",
+ responses: {
+ 200: {
+ description: "Aborted session",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ zValidator(
+ "json",
+ z.object({
+ sessionID: z.string(),
+ }),
+ ),
+ async (c) => {
+ const body = c.req.valid("json");
+ return c.json(Session.abort(body.sessionID));
+ },
+ )
+ .post(
+ "/session_summarize",
+ describeRoute({
+ description: "Summarize the session",
+ responses: {
+ 200: {
+ description: "Summarize the session",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ zValidator(
+ "json",
+ z.object({
+ sessionID: z.string(),
+ providerID: z.string(),
+ modelID: z.string(),
+ }),
+ ),
+ async (c) => {
+ const body = c.req.valid("json");
+ await Session.summarize(body);
+ return c.json(true);
+ },
+ )
+ .post(
+ "/session_chat",
+ describeRoute({
+ description: "Chat with a model",
+ responses: {
+ 200: {
+ description: "Chat with a model",
+ content: {
+ "application/json": {
+ schema: resolver(Message.Info),
+ },
+ },
+ },
+ },
+ }),
+ zValidator(
+ "json",
+ z.object({
+ sessionID: z.string(),
+ providerID: z.string(),
+ modelID: z.string(),
+ parts: Message.Part.array(),
+ }),
+ ),
+ async (c) => {
+ const body = c.req.valid("json");
+ const msg = await Session.chat(body);
+ return c.json(msg);
+ },
+ )
+ .post(
+ "/provider_list",
+ describeRoute({
+ description: "List all providers",
+ responses: {
+ 200: {
+ description: "List of providers",
+ content: {
+ "application/json": {
+ schema: resolver(Provider.Info.array()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const providers = await LLM.providers();
+ const result = [] as (Provider.Info & { key: string })[];
+ for (const [key, provider] of Object.entries(providers)) {
+ result.push({ ...provider.info, key });
+ }
+ return c.json(result);
+ },
+ );
+
+ return result;
+ }
+
+ export async function openapi() {
+ const a = app();
+ const result = await generateSpecs(a, {
+ documentation: {
+ info: {
+ title: "opencode",
+ version: "1.0.0",
+ description: "opencode api",
+ },
+ openapi: "3.0.0",
+ },
+ });
+ return result;
+ }
+
+ export function listen() {
+ const server = Bun.serve({
+ port: PORT,
+ hostname: "0.0.0.0",
+ idleTimeout: 0,
+ fetch: app().fetch,
+ });
+ return server;
+ }
+}
diff --git a/packages/opencode/src/session/message.ts b/packages/opencode/src/session/message.ts
new file mode 100644
index 000000000..11ec5c6b3
--- /dev/null
+++ b/packages/opencode/src/session/message.ts
@@ -0,0 +1,171 @@
+import z from "zod";
+import { Bus } from "../bus";
+
+export namespace Message {
+ export const ToolCall = z
+ .object({
+ state: z.literal("call"),
+ step: z.number().optional(),
+ toolCallId: z.string(),
+ toolName: z.string(),
+ args: z.custom<Required<unknown>>(),
+ })
+ .openapi({
+ ref: "Message.ToolInvocation.ToolCall",
+ });
+ export type ToolCall = z.infer<typeof ToolCall>;
+
+ export const ToolPartialCall = z
+ .object({
+ state: z.literal("partial-call"),
+ step: z.number().optional(),
+ toolCallId: z.string(),
+ toolName: z.string(),
+ args: z.custom<Required<unknown>>(),
+ })
+ .openapi({
+ ref: "Message.ToolInvocation.ToolPartialCall",
+ });
+ export type ToolPartialCall = z.infer<typeof ToolPartialCall>;
+
+ export const ToolResult = z
+ .object({
+ state: z.literal("result"),
+ step: z.number().optional(),
+ toolCallId: z.string(),
+ toolName: z.string(),
+ args: z.custom<Required<unknown>>(),
+ result: z.string(),
+ })
+ .openapi({
+ ref: "Message.ToolInvocation.ToolResult",
+ });
+ export type ToolResult = z.infer<typeof ToolResult>;
+
+ export const ToolInvocation = z
+ .discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult])
+ .openapi({
+ ref: "Message.ToolInvocation",
+ });
+ export type ToolInvocation = z.infer<typeof ToolInvocation>;
+
+ export const TextPart = z
+ .object({
+ type: z.literal("text"),
+ text: z.string(),
+ })
+ .openapi({
+ ref: "Message.Part.Text",
+ });
+ export type TextPart = z.infer<typeof TextPart>;
+
+ export const ReasoningPart = z
+ .object({
+ type: z.literal("reasoning"),
+ text: z.string(),
+ providerMetadata: z.record(z.any()).optional(),
+ })
+ .openapi({
+ ref: "Message.Part.Reasoning",
+ });
+ export type ReasoningPart = z.infer<typeof ReasoningPart>;
+
+ export const ToolInvocationPart = z
+ .object({
+ type: z.literal("tool-invocation"),
+ toolInvocation: ToolInvocation,
+ })
+ .openapi({
+ ref: "Message.Part.ToolInvocation",
+ });
+ export type ToolInvocationPart = z.infer<typeof ToolInvocationPart>;
+
+ export const SourceUrlPart = z
+ .object({
+ type: z.literal("source-url"),
+ sourceId: z.string(),
+ url: z.string(),
+ title: z.string().optional(),
+ providerMetadata: z.record(z.any()).optional(),
+ })
+ .openapi({
+ ref: "Message.Part.SourceUrl",
+ });
+ export type SourceUrlPart = z.infer<typeof SourceUrlPart>;
+
+ export const FilePart = z
+ .object({
+ type: z.literal("file"),
+ mediaType: z.string(),
+ filename: z.string().optional(),
+ url: z.string(),
+ })
+ .openapi({
+ ref: "Message.Part.File",
+ });
+ export type FilePart = z.infer<typeof FilePart>;
+
+ export const StepStartPart = z
+ .object({
+ type: z.literal("step-start"),
+ })
+ .openapi({
+ ref: "Message.Part.StepStart",
+ });
+ export type StepStartPart = z.infer<typeof StepStartPart>;
+
+ export const Part = z
+ .discriminatedUnion("type", [
+ TextPart,
+ ReasoningPart,
+ ToolInvocationPart,
+ SourceUrlPart,
+ FilePart,
+ StepStartPart,
+ ])
+ .openapi({
+ ref: "Message.Part",
+ });
+ export type Part = z.infer<typeof Part>;
+
+ export const Info = z
+ .object({
+ id: z.string(),
+ role: z.enum(["system", "user", "assistant"]),
+ parts: z.array(Part),
+ metadata: z.object({
+ time: z.object({
+ created: z.number(),
+ completed: z.number().optional(),
+ }),
+ sessionID: z.string(),
+ tool: z.record(z.string(), z.any()),
+ assistant: z
+ .object({
+ modelID: z.string(),
+ providerID: z.string(),
+ cost: z.number(),
+ summary: z.boolean().optional(),
+ tokens: z.object({
+ input: z.number(),
+ output: z.number(),
+ reasoning: z.number(),
+ }),
+ })
+ .optional(),
+ }),
+ })
+ .openapi({
+ ref: "Message.Info",
+ });
+ export type Info = z.infer<typeof Info>;
+
+ export const Event = {
+ Updated: Bus.event(
+ "message.updated",
+ z.object({
+ info: Info,
+ }),
+ ),
+ };
+}
diff --git a/packages/opencode/src/session/prompt/anthropic.txt b/packages/opencode/src/session/prompt/anthropic.txt
new file mode 100644
index 000000000..c8ff697e8
--- /dev/null
+++ b/packages/opencode/src/session/prompt/anthropic.txt
@@ -0,0 +1,95 @@
+You are OpenCode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
+
+IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure.
+
+# Memory
+If the current working directory contains a file called OpenCode.md, it will be automatically added to your context. This file serves multiple purposes:
+1. Storing frequently used bash commands (build, test, lint, etc.) so you can use them without searching each time
+2. Recording the user's code style preferences (naming conventions, preferred libraries, etc.)
+3. Maintaining useful information about the codebase structure and organization
+
+When you spend time searching for commands to typecheck, lint, build, or test, you should ask the user if it's okay to add those commands to CONTEXT.md. Similarly, when learning about code style preferences or important codebase information, ask if it's okay to add that to CONTEXT.md so you can remember it for next time.
+
+# Tone and style
+You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
+Remember that your output will be displayed on a command line interface. Your responses can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
+Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.
+If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
+IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
+IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
+IMPORTANT: Keep your responses short, since they will be displayed on a command line interface. You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless user asks for detail. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...". Here are some examples to demonstrate appropriate verbosity:
+<example>
+user: 2 + 2
+assistant: 4
+</example>
+
+<example>
+user: what is 2+2?
+assistant: 4
+</example>
+
+<example>
+user: is 11 a prime number?
+assistant: yes
+</example>
+
+<example>
+user: what command should I run to list files in the current directory?
+assistant: ls
+</example>
+
+<example>
+user: what command should I run to watch files in the current directory?
+assistant: [use the ls tool to list the files in the current directory, then read docs/commands in the relevant file to find out how to watch files]
+npm run dev
+</example>
+
+<example>
+user: How many golf balls fit inside a jetta?
+assistant: 150000
+</example>
+
+<example>
+user: what files are in the directory src/?
+assistant: [runs ls and sees foo.c, bar.c, baz.c]
+user: which file contains the implementation of foo?
+assistant: src/foo.c
+</example>
+
+<example>
+user: write tests for new feature
+assistant: [uses grep and glob search tools to find where similar tests are defined, uses concurrent read file tool use blocks in one tool call to read relevant files at the same time, uses edit/patch file tool to write new tests]
+</example>
+
+# Proactiveness
+You are allowed to be proactive, but only when the user asks you to do something. You should strive to strike a balance between:
+1. Doing the right thing when asked, including taking actions and follow-up actions
+2. Not surprising the user with actions you take without asking
+For example, if the user asks you how to approach something, you should do your best to answer their question first, and not immediately jump into taking actions.
+3. Do not add additional code explanation summary unless requested by the user. After working on a file, just stop, rather than providing an explanation of what you did.
+
+# Following conventions
+When making changes to files, first understand the file's code conventions. Mimic code style, use existing libraries and utilities, and follow existing patterns.
+- NEVER assume that a given library is available, even if it is well known. Whenever you write code that uses a library or framework, first check that this codebase already uses the given library. For example, you might look at neighboring files, or check the package.json (or cargo.toml, and so on depending on the language).
+- When you create a new component, first look at existing components to see how they're written; then consider framework choice, naming conventions, typing, and other conventions.
+- When you edit a piece of code, first look at the code's surrounding context (especially its imports) to understand the code's choice of frameworks and libraries. Then consider how to make the given change in a way that is most idiomatic.
+- Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository.
+
+# Code style
+- Do not add comments to the code you write, unless the user asks you to, or the code is complex and requires additional context.
+
+# Doing tasks
+The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
+1. Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially.
+2. Implement the solution using all tools available to you
+3. Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
+4. VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to opencode.md so that you will know to run it next time.
+
+NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.
+
+# Tool usage policy
+- When doing file search, prefer to use the Agent tool in order to reduce context usage.
+- If you intend to call multiple tools and there are no dependencies between the calls, make all of the independent calls in the same function_calls block.
+- IMPORTANT: The user does not see the full output of the tool responses, so if you need the output of the tool for the response make sure to summarize it for the user.
+
+You MUST answer concisely with fewer than 4 lines of text (not including tool use or code generation), unless user asks for detail.
diff --git a/packages/opencode/src/session/prompt/summarize.txt b/packages/opencode/src/session/prompt/summarize.txt
new file mode 100644
index 000000000..4751c8d71
--- /dev/null
+++ b/packages/opencode/src/session/prompt/summarize.txt
@@ -0,0 +1,10 @@
+You are a helpful AI assistant tasked with summarizing conversations.
+
+When asked to summarize, provide a detailed but concise summary of the conversation.
+Focus on information that would be helpful for continuing the conversation, including:
+- What was done
+- What is currently being worked on
+- Which files are being modified
+- What needs to be done next
+
+Your summary should be comprehensive enough to provide context but concise enough to be quickly understood.
diff --git a/packages/opencode/src/session/prompt/title.txt b/packages/opencode/src/session/prompt/title.txt
new file mode 100644
index 000000000..a57f9082b
--- /dev/null
+++ b/packages/opencode/src/session/prompt/title.txt
@@ -0,0 +1,7 @@
+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
+- do not use quotes or colons
+- the entire text you return will be used as the title
+- never return anything that is more than one sentence (one line) long
diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts
new file mode 100644
index 000000000..6eb5f0373
--- /dev/null
+++ b/packages/opencode/src/session/session.ts
@@ -0,0 +1,498 @@
+import path from "path";
+import { App } from "../app/app";
+import { Identifier } from "../id/id";
+import { LLM } from "../llm/llm";
+import { Storage } from "../storage/storage";
+import { Log } from "../util/log";
+import {
+ convertToModelMessages,
+ generateText,
+ stepCountIs,
+ streamText,
+ type LanguageModelUsage,
+} from "ai";
+import { z } from "zod";
+import * as tools from "../tool";
+import { Decimal } from "decimal.js";
+
+import PROMPT_ANTHROPIC from "./prompt/anthropic.txt";
+import PROMPT_TITLE from "./prompt/title.txt";
+import PROMPT_SUMMARIZE from "./prompt/summarize.txt";
+
+import { Share } from "../share/share";
+import { Message } from "./message";
+import { Bus } from "../bus";
+import type { Provider } from "../provider/provider";
+
+export namespace Session {
+ const log = Log.create({ service: "session" });
+
+ export const Info = z
+ .object({
+ id: Identifier.schema("session"),
+ share: z
+ .object({
+ secret: z.string(),
+ url: z.string(),
+ })
+ .optional(),
+ title: z.string(),
+ time: z.object({
+ created: z.number(),
+ updated: z.number(),
+ }),
+ })
+ .openapi({
+ ref: "session.info",
+ });
+ export type Info = z.output<typeof Info>;
+
+ export const Event = {
+ Updated: Bus.event(
+ "session.updated",
+ z.object({
+ info: Info,
+ }),
+ ),
+ };
+
+ const state = App.state("session", () => {
+ const sessions = new Map<string, Info>();
+ const messages = new Map<string, Message.Info[]>();
+
+ return {
+ sessions,
+ messages,
+ };
+ });
+
+ export async function create() {
+ const result: Info = {
+ id: Identifier.descending("session"),
+ title: "New Session - " + new Date().toISOString(),
+ time: {
+ created: Date.now(),
+ updated: Date.now(),
+ },
+ };
+ log.info("created", result);
+ state().sessions.set(result.id, result);
+ await Storage.writeJSON("session/info/" + result.id, result);
+ share(result.id).then((share) => {
+ update(result.id, (draft) => {
+ draft.share = share;
+ });
+ });
+ Bus.publish(Event.Updated, {
+ info: result,
+ });
+ return result;
+ }
+
+ export async function get(id: string) {
+ const result = state().sessions.get(id);
+ if (result) {
+ return result;
+ }
+ const read = await Storage.readJSON<Info>("session/info/" + id);
+ state().sessions.set(id, read);
+ return read as Info;
+ }
+
+ export async function share(id: string) {
+ const session = await get(id);
+ if (session.share) return session.share;
+ const share = await Share.create(id);
+ await update(id, (draft) => {
+ draft.share = share;
+ });
+ return share;
+ }
+
+ export async function update(id: string, editor: (session: Info) => void) {
+ const { sessions } = state();
+ const session = await get(id);
+ if (!session) return;
+ editor(session);
+ session.time.updated = Date.now();
+ sessions.set(id, session);
+ await Storage.writeJSON("session/info/" + id, session);
+ Bus.publish(Event.Updated, {
+ info: session,
+ });
+ return session;
+ }
+
+ export async function messages(sessionID: string) {
+ const result = [] as Message.Info[];
+ const list = Storage.list("session/message/" + sessionID);
+ for await (const p of list) {
+ const read = await Storage.readJSON<Message.Info>(p).catch(() => {});
+ if (!read) continue;
+ result.push(read);
+ }
+ result.sort((a, b) => (a.id > b.id ? 1 : -1));
+ return result;
+ }
+
+ export async function* list() {
+ for await (const item of Storage.list("session/info")) {
+ const sessionID = path.basename(item, ".json");
+ yield get(sessionID);
+ }
+ }
+
+ export function abort(sessionID: string) {
+ const controller = pending.get(sessionID);
+ if (!controller) return false;
+ controller.abort();
+ pending.delete(sessionID);
+ return true;
+ }
+
+ async function updateMessage(msg: Message.Info) {
+ await Storage.writeJSON(
+ "session/message/" + msg.metadata.sessionID + "/" + msg.id,
+ msg,
+ );
+ Bus.publish(Message.Event.Updated, {
+ info: msg,
+ });
+ }
+
+ export async function chat(input: {
+ sessionID: string;
+ providerID: string;
+ modelID: string;
+ parts: Message.Part[];
+ }) {
+ const l = log.clone().tag("session", input.sessionID);
+ l.info("chatting");
+ const model = await LLM.findModel(input.providerID, input.modelID);
+ let msgs = await messages(input.sessionID);
+ const previous = msgs.at(-1);
+ if (previous?.metadata.assistant) {
+ const tokens =
+ previous.metadata.assistant.tokens.input +
+ previous.metadata.assistant.tokens.output;
+ if (
+ tokens >
+ (model.info.contextWindow - (model.info.maxOutputTokens ?? 0)) * 0.9
+ ) {
+ await summarize({
+ sessionID: input.sessionID,
+ providerID: input.providerID,
+ modelID: input.modelID,
+ });
+ return chat(input);
+ }
+ }
+
+ using abort = lock(input.sessionID);
+
+ const lastSummary = msgs.findLast(
+ (msg) => msg.metadata.assistant?.summary === true,
+ );
+ if (lastSummary)
+ msgs = msgs.filter(
+ (msg) => msg.role === "system" || msg.id >= lastSummary.id,
+ );
+
+ const app = await App.use();
+ if (msgs.length === 0) {
+ const system: Message.Info = {
+ id: Identifier.ascending("message"),
+ role: "system",
+ parts: [
+ {
+ type: "text",
+ text: PROMPT_ANTHROPIC,
+ },
+ ],
+ metadata: {
+ sessionID: input.sessionID,
+ time: {
+ created: Date.now(),
+ },
+ tool: {},
+ },
+ };
+ const contextFile = Bun.file(path.join(app.root, "CONTEXT.md"));
+ if (await contextFile.exists()) {
+ const context = await contextFile.text();
+ system.parts.push({
+ type: "text",
+ text: context,
+ });
+ }
+ msgs.push(system);
+ generateText({
+ messages: convertToModelMessages([
+ {
+ role: "system",
+ parts: [
+ {
+ type: "text",
+ text: PROMPT_TITLE,
+ },
+ ],
+ },
+ {
+ role: "user",
+ parts: input.parts,
+ },
+ ]),
+ model: model.instance,
+ }).then((result) => {
+ return Session.update(input.sessionID, (draft) => {
+ draft.title = result.text;
+ });
+ });
+ await updateMessage(system);
+ }
+ const msg: Message.Info = {
+ role: "user",
+ id: Identifier.ascending("message"),
+ parts: input.parts,
+ metadata: {
+ time: {
+ created: Date.now(),
+ },
+ sessionID: input.sessionID,
+ tool: {},
+ },
+ };
+ msgs.push(msg);
+ await updateMessage(msg);
+
+ const next: Message.Info = {
+ id: Identifier.ascending("message"),
+ role: "assistant",
+ parts: [],
+ metadata: {
+ assistant: {
+ cost: 0,
+ tokens: {
+ input: 0,
+ output: 0,
+ reasoning: 0,
+ },
+ modelID: input.modelID,
+ providerID: input.providerID,
+ },
+ time: {
+ created: Date.now(),
+ },
+ sessionID: input.sessionID,
+ tool: {},
+ },
+ };
+ await updateMessage(next);
+ const result = streamText({
+ onStepFinish: async (step) => {
+ const assistant = next.metadata!.assistant!;
+ const usage = getUsage(step.usage, model.info);
+ assistant.cost = usage.cost;
+ assistant.tokens = usage.tokens;
+ await updateMessage(next);
+ },
+ abortSignal: abort.signal,
+ maxRetries: 6,
+ stopWhen: stepCountIs(1000),
+ messages: convertToModelMessages(msgs),
+ temperature: 0,
+ tools,
+ model: model.instance,
+ });
+ let text: Message.TextPart | undefined;
+ const reader = result.toUIMessageStream().getReader();
+ while (true) {
+ const result = await reader.read().catch((e) => {
+ if (e instanceof DOMException && e.name === "AbortError") {
+ return;
+ }
+ throw e;
+ });
+ if (!result) break;
+ const { done, value } = result;
+ if (done) break;
+ l.info("part", {
+ type: value.type,
+ });
+ switch (value.type) {
+ case "start":
+ break;
+ case "start-step":
+ text = undefined;
+ next.parts.push({
+ type: "step-start",
+ });
+ break;
+ case "text":
+ if (!text) {
+ text = value;
+ next.parts.push(value);
+ break;
+ }
+ text.text += value.text;
+ break;
+
+ case "tool-call":
+ next.parts.push({
+ type: "tool-invocation",
+ toolInvocation: {
+ state: "call",
+ ...value,
+ // hack until zod v4
+ args: value.args as any,
+ },
+ });
+ break;
+
+ case "tool-result":
+ const match = next.parts.find(
+ (p) =>
+ p.type === "tool-invocation" &&
+ p.toolInvocation.toolCallId === value.toolCallId,
+ );
+ if (match && match.type === "tool-invocation") {
+ const { output, metadata } = value.result as any;
+ next.metadata!.tool[value.toolCallId] = metadata;
+ match.toolInvocation = {
+ ...match.toolInvocation,
+ state: "result",
+ result: output,
+ };
+ }
+ break;
+
+ case "finish":
+ break;
+ case "finish-step":
+ break;
+ case "error":
+ log.error("error", value);
+ break;
+
+ default:
+ l.info("unhandled", {
+ type: value.type,
+ });
+ }
+ await updateMessage(next);
+ }
+ next.metadata!.time.completed = Date.now();
+ await updateMessage(next);
+ return next;
+ }
+
+ export async function summarize(input: {
+ sessionID: string;
+ providerID: string;
+ modelID: string;
+ }) {
+ using abort = lock(input.sessionID);
+ const msgs = await messages(input.sessionID);
+ const lastSummary = msgs.findLast(
+ (msg) => msg.metadata.assistant?.summary === true,
+ )?.id;
+ const filtered = msgs.filter(
+ (msg) => msg.role !== "system" && (!lastSummary || msg.id >= lastSummary),
+ );
+ const model = await LLM.findModel(input.providerID, input.modelID);
+ const next: Message.Info = {
+ id: Identifier.ascending("message"),
+ role: "assistant",
+ parts: [],
+ metadata: {
+ tool: {},
+ sessionID: input.sessionID,
+ assistant: {
+ summary: true,
+ cost: 0,
+ modelID: input.modelID,
+ providerID: input.providerID,
+ tokens: {
+ input: 0,
+ output: 0,
+ reasoning: 0,
+ },
+ },
+ time: {
+ created: Date.now(),
+ },
+ },
+ };
+ await updateMessage(next);
+ const result = await generateText({
+ abortSignal: abort.signal,
+ model: model.instance,
+ messages: convertToModelMessages([
+ {
+ role: "system",
+ parts: [
+ {
+ type: "text",
+ text: PROMPT_SUMMARIZE,
+ },
+ ],
+ },
+ ...filtered,
+ {
+ role: "user",
+ parts: [
+ {
+ 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.",
+ },
+ ],
+ },
+ ]),
+ });
+ next.parts.push({
+ type: "text",
+ text: result.text,
+ });
+ const assistant = next.metadata!.assistant!;
+ const usage = getUsage(result.usage, model.info);
+ assistant.cost = usage.cost;
+ assistant.tokens = usage.tokens;
+ 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);
+ const controller = new AbortController();
+ pending.set(sessionID, controller);
+ return {
+ signal: controller.signal,
+ [Symbol.dispose]() {
+ log.info("unlocking", { sessionID });
+ pending.delete(sessionID);
+ },
+ };
+ }
+
+ function getUsage(usage: LanguageModelUsage, model: Provider.Model) {
+ const tokens = {
+ input: usage.inputTokens ?? 0,
+ output: usage.outputTokens ?? 0,
+ reasoning: usage.reasoningTokens ?? 0,
+ };
+ return {
+ cost: new Decimal(0)
+ .add(new Decimal(tokens.input).mul(model.cost.input))
+ .add(new Decimal(tokens.output).mul(model.cost.output))
+ .toNumber(),
+ tokens,
+ };
+ }
+
+ export class BusyError extends Error {
+ constructor(public readonly sessionID: string) {
+ super(`Session ${sessionID} is busy`);
+ }
+ }
+}
diff --git a/packages/opencode/src/share/share.ts b/packages/opencode/src/share/share.ts
new file mode 100644
index 000000000..8d4af8795
--- /dev/null
+++ b/packages/opencode/src/share/share.ts
@@ -0,0 +1,67 @@
+import { App } from "../app/app";
+import { Bus } from "../bus";
+import { Session } from "../session/session";
+import { Storage } from "../storage/storage";
+import { Log } from "../util/log";
+
+export namespace Share {
+ const log = Log.create({ service: "share" });
+
+ let queue: Promise<void> = Promise.resolve();
+ const pending = new Map<string, any>();
+
+ const state = App.state("share", async () => {
+ Bus.subscribe(Storage.Event.Write, async (payload) => {
+ const [root, ...splits] = payload.properties.key.split("/");
+ if (root !== "session") return;
+ const [, sessionID] = splits;
+ const session = await Session.get(sessionID);
+ if (!session.share) return;
+ const { secret } = session.share;
+
+ const key = payload.properties.key;
+ pending.set(key, payload.properties.content);
+
+ queue = queue
+ .then(async () => {
+ const content = pending.get(key);
+ if (content === undefined) return;
+ pending.delete(key);
+
+ return fetch(`${URL}/share_sync`, {
+ method: "POST",
+ body: JSON.stringify({
+ sessionID: sessionID,
+ secret,
+ key: key,
+ content,
+ }),
+ });
+ })
+ .then((x) => {
+ if (x) {
+ log.info("synced", {
+ key: key,
+ status: x.status,
+ });
+ }
+ });
+ });
+ });
+
+ export async function init() {
+ await state();
+ }
+
+ export const URL =
+ process.env["OPENCODE_API"] ?? "https://api.dev.opencode.ai";
+
+ export async function create(sessionID: string) {
+ return fetch(`${URL}/share_create`, {
+ method: "POST",
+ body: JSON.stringify({ sessionID: sessionID }),
+ })
+ .then((x) => x.json())
+ .then((x) => x as { url: string; secret: string });
+ }
+}
diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts
new file mode 100644
index 000000000..983b7e0f9
--- /dev/null
+++ b/packages/opencode/src/storage/storage.ts
@@ -0,0 +1,55 @@
+import { FileStorage } from "@flystorage/file-storage";
+import { LocalStorageAdapter } from "@flystorage/local-fs";
+import fs from "fs/promises";
+import { Log } from "../util/log";
+import { App } from "../app/app";
+import { AppPath } from "../app/path";
+import { Bus } from "../bus";
+import z from "zod";
+
+export namespace Storage {
+ const log = Log.create({ service: "storage" });
+
+ export const Event = {
+ Write: Bus.event(
+ "storage.write",
+ z.object({ key: z.string(), content: z.any() }),
+ ),
+ };
+
+ const state = App.state("storage", async () => {
+ const app = await App.use();
+ const storageDir = AppPath.storage(app.root);
+ await fs.mkdir(storageDir, { recursive: true });
+ const storage = new FileStorage(new LocalStorageAdapter(storageDir));
+ log.info("created", { path: storageDir });
+ return {
+ storage,
+ };
+ });
+
+ export async function readJSON<T>(key: string) {
+ const storage = await state().then((x) => x.storage);
+ const data = await storage.readToString(key + ".json");
+ return JSON.parse(data) as T;
+ }
+
+ export async function writeJSON<T>(key: string, content: T) {
+ const storage = await state().then((x) => x.storage);
+ const json = JSON.stringify(content);
+ await storage.write(key + ".json", json);
+ Bus.publish(Event.Write, { key, content });
+ }
+
+ export async function* list(prefix: string) {
+ try {
+ const storage = await state().then((x) => x.storage);
+ const list = storage.list(prefix);
+ for await (const item of list) {
+ yield item.path.slice(0, -5);
+ }
+ } catch {
+ return;
+ }
+ }
+}
diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts
new file mode 100644
index 000000000..5ad92e3c8
--- /dev/null
+++ b/packages/opencode/src/tool/bash.ts
@@ -0,0 +1,199 @@
+import { z } from "zod";
+import { Tool } from "./tool";
+
+const MAX_OUTPUT_LENGTH = 30000;
+const BANNED_COMMANDS = [
+ "alias",
+ "curl",
+ "curlie",
+ "wget",
+ "axel",
+ "aria2c",
+ "nc",
+ "telnet",
+ "lynx",
+ "w3m",
+ "links",
+ "httpie",
+ "xh",
+ "http-prompt",
+ "chrome",
+ "firefox",
+ "safari",
+];
+const DEFAULT_TIMEOUT = 1 * 60 * 1000;
+const MAX_TIMEOUT = 10 * 60 * 1000;
+
+const DESCRIPTION = `Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
+
+Before executing the command, please follow these steps:
+
+1. Directory Verification:
+ - If the command will create new directories or files, first use the LS tool to verify the parent directory exists and is the correct location
+ - For example, before running "mkdir foo/bar", first use LS to check that "foo" exists and is the intended parent directory
+
+2. Security Check:
+ - For security and to limit the threat of a prompt injection attack, some commands are limited or banned. If you use a disallowed command, you will receive an error message explaining the restriction. Explain the error to the User.
+ - Verify that the command is not one of the banned commands: ${BANNED_COMMANDS.join(", ")}.
+
+3. Command Execution:
+ - After ensuring proper quoting, execute the command.
+ - Capture the output of the command.
+
+4. Output Processing:
+ - If the output exceeds ${MAX_OUTPUT_LENGTH} characters, output will be truncated before being returned to you.
+ - Prepare the output for display to the user.
+
+5. Return Result:
+ - Provide the processed output of the command.
+ - If any errors occurred during execution, include those in the output.
+
+Usage notes:
+- The command argument is required.
+- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 30 minutes.
+- VERY IMPORTANT: You MUST avoid using search commands like 'find' and 'grep'. Instead use Grep, Glob, or Agent tools to search. You MUST avoid read tools like 'cat', 'head', 'tail', and 'ls', and use FileRead and LS tools to read files.
+- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
+- IMPORTANT: All commands share the same shell session. Shell state (environment variables, virtual environments, current directory, etc.) persist between commands. For example, if you set an environment variable as part of a command, the environment variable will persist for subsequent commands.
+- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of 'cd'. You may use 'cd' if the User explicitly requests it.
+<good-example>
+pytest /foo/bar/tests
+</good-example>
+<bad-example>
+cd /foo/bar && pytest tests
+</bad-example>
+
+# Committing changes with git
+
+When the user asks you to create a new git commit, follow these steps carefully:
+
+1. Start with a single message that contains exactly three tool_use blocks that do the following (it is VERY IMPORTANT that you send these tool_use blocks in a single message, otherwise it will feel slow to the user!):
+ - Run a git status command to see all untracked files.
+ - Run a git diff command to see both staged and unstaged changes that will be committed.
+ - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.
+
+2. Use the git context at the start of this conversation to determine which files are relevant to your commit. Add relevant untracked files to the staging area. Do not commit files that were already modified at the start of this conversation, if they are not relevant to your commit.
+
+3. Analyze all staged changes (both previously staged and newly added) and draft a commit message. Wrap your analysis process in <commit_analysis> tags:
+
+<commit_analysis>
+- List the files that have been changed or added
+- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)
+- Brainstorm the purpose or motivation behind these changes
+- Do not use tools to explore code, beyond what is available in the git context
+- Assess the impact of these changes on the overall project
+- Check for any sensitive information that shouldn't be committed
+- Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what"
+- Ensure your language is clear, concise, and to the point
+- Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.)
+- Ensure the message is not generic (avoid words like "Update" or "Fix" without context)
+- Review the draft message to ensure it accurately reflects the changes and their purpose
+</commit_analysis>
+
+4. Create the commit with a message ending with:
+🤖 Generated with opencode
+Co-Authored-By: opencode <[email protected]>
+
+- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:
+<example>
+git commit -m "$(cat <<'EOF'
+ Commit message here.
+
+ 🤖 Generated with opencode
+ Co-Authored-By: opencode <[email protected]>
+ EOF
+ )"
+</example>
+
+5. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
+
+6. Finally, run git status to make sure the commit succeeded.
+
+Important notes:
+- When possible, combine the "git add" and "git commit" commands into a single "git commit -am" command, to speed things up
+- However, be careful not to stage files (e.g. with 'git add .') for commits that aren't part of the change, they may have untracked files they want to keep around, but not commit.
+- NEVER update the git config
+- DO NOT push to the remote repository
+- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.
+- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit
+- Ensure your commit message is meaningful and concise. It should explain the purpose of the changes, not just describe them.
+- Return an empty response - the user will see the git output directly
+
+# Creating pull requests
+Use the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.
+
+IMPORTANT: When the user asks you to create a pull request, follow these steps carefully:
+
+1. Understand the current state of the branch. Remember to send a single message that contains multiple tool_use blocks (it is VERY IMPORTANT that you do this in a single message, otherwise it will feel slow to the user!):
+ - Run a git status command to see all untracked files.
+ - Run a git diff command to see both staged and unstaged changes that will be committed.
+ - Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote
+ - Run a git log command and 'git diff main...HEAD' to understand the full commit history for the current branch (from the time it diverged from the 'main' branch.)
+
+2. Create new branch if needed
+
+3. Commit changes if needed
+
+4. Push to remote with -u flag if needed
+
+5. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (not just the latest commit, but all commits that will be included in the pull request!), and draft a pull request summary. Wrap your analysis process in <pr_analysis> tags:
+
+<pr_analysis>
+- List the commits since diverging from the main branch
+- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)
+- Brainstorm the purpose or motivation behind these changes
+- Assess the impact of these changes on the overall project
+- Do not use tools to explore code, beyond what is available in the git context
+- Check for any sensitive information that shouldn't be committed
+- Draft a concise (1-2 bullet points) pull request summary that focuses on the "why" rather than the "what"
+- Ensure the summary accurately reflects all changes since diverging from the main branch
+- Ensure your language is clear, concise, and to the point
+- Ensure the summary accurately reflects the changes and their purpose (ie. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.)
+- Ensure the summary is not generic (avoid words like "Update" or "Fix" without context)
+- Review the draft summary to ensure it accurately reflects the changes and their purpose
+</pr_analysis>
+
+6. Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.
+<example>
+gh pr create --title "the pr title" --body "$(cat <<'EOF'
+## Summary
+<1-3 bullet points>
+
+## Test plan
+[Checklist of TODOs for testing the pull request...]
+
+🤖 Generated with opencode
+EOF
+)"
+</example>
+
+Important:
+- Return an empty response - the user will see the gh output directly
+- Never update git config`;
+
+export const bash = Tool.define({
+ name: "opencode.bash",
+ description: DESCRIPTION,
+ parameters: z.object({
+ command: z.string(),
+ timeout: z
+ .number()
+ .min(0)
+ .max(MAX_TIMEOUT)
+ .describe("Optional timeout in milliseconds")
+ .optional(),
+ }),
+ async execute(params) {
+ const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
+ if (BANNED_COMMANDS.some((item) => params.command.startsWith(item)))
+ throw new Error(`Command '${params.command}' is not allowed`);
+
+ const process = Bun.spawnSync({
+ cmd: ["bash", "-c", params.command],
+ maxBuffer: MAX_OUTPUT_LENGTH,
+ timeout: timeout,
+ });
+ return {
+ output: process.stdout.toString("utf-8"),
+ };
+ },
+});
diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts
new file mode 100644
index 000000000..9ba99a3be
--- /dev/null
+++ b/packages/opencode/src/tool/edit.ts
@@ -0,0 +1,136 @@
+import { z } from "zod";
+import * as path from "path";
+import { Tool } from "./tool";
+import { FileTimes } from "./util/file-times";
+import { LSP } from "../lsp";
+
+const DESCRIPTION = `Edits files by replacing text, creating new files, or deleting content. For moving or renaming files, use the Bash tool with the 'mv' command instead. For larger file edits, use the FileWrite tool to overwrite files.
+
+Before using this tool:
+
+1. Use the FileRead tool to understand the file's contents and context
+
+2. Verify the directory path is correct (only applicable when creating new files):
+ - Use the LS tool to verify the parent directory exists and is the correct location
+
+To make a file edit, provide the following:
+1. file_path: The relative path to the file to modify (must be relative, not absolute)
+2. old_string: The text to replace (must be unique within the file, and must match the file contents exactly, including all whitespace and indentation)
+3. new_string: The edited text to replace the old_string
+
+Special cases:
+- To create a new file: provide file_path and new_string, leave old_string empty
+- To delete content: provide file_path and old_string, leave new_string empty
+
+The tool will replace ONE occurrence of old_string with new_string in the specified file.
+
+CRITICAL REQUIREMENTS FOR USING THIS TOOL:
+
+1. UNIQUENESS: The old_string MUST uniquely identify the specific instance you want to change. This means:
+ - Include AT LEAST 3-5 lines of context BEFORE the change point
+ - Include AT LEAST 3-5 lines of context AFTER the change point
+ - Include all whitespace, indentation, and surrounding code exactly as it appears in the file
+
+2. SINGLE INSTANCE: This tool can only change ONE instance at a time. If you need to change multiple instances:
+ - Make separate calls to this tool for each instance
+ - Each call must uniquely identify its specific instance using extensive context
+
+3. VERIFICATION: Before using this tool:
+ - Check how many instances of the target text exist in the file
+ - If multiple instances exist, gather enough context to uniquely identify each one
+ - Plan separate tool calls for each instance
+
+WARNING: If you do not follow these requirements:
+ - The tool will fail if old_string matches multiple locations
+ - The tool will fail if old_string doesn't match exactly (including whitespace)
+ - You may change the wrong instance if you don't include enough context
+
+When making edits:
+ - Ensure the edit results in idiomatic, correct code
+ - Do not leave the code in a broken state
+ - Always use relative file paths
+
+Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.`;
+
+export const edit = Tool.define({
+ name: "opencode.edit",
+ description: DESCRIPTION,
+ parameters: z.object({
+ filePath: z.string().describe("The absolute path to the file to modify"),
+ oldString: z.string().describe("The text to replace"),
+ newString: z.string().describe("The text to replace it with"),
+ }),
+ async execute(params) {
+ if (!params.filePath) {
+ throw new Error("filePath is required");
+ }
+
+ let filePath = params.filePath;
+ if (!path.isAbsolute(filePath)) {
+ filePath = path.join(process.cwd(), filePath);
+ }
+
+ await (async () => {
+ if (params.oldString === "") {
+ await Bun.write(filePath, params.newString);
+ return;
+ }
+
+ const read = FileTimes.get(filePath);
+ if (!read)
+ throw new Error(
+ `You must read the file ${filePath} before editing it. Use the View tool first`,
+ );
+ const file = Bun.file(filePath);
+ if (!(await file.exists())) throw new Error(`File ${filePath} not found`);
+ const stats = await file.stat();
+ if (stats.isDirectory())
+ throw new Error(`Path is a directory, not a file: ${filePath}`);
+ if (stats.mtime.getTime() > read.getTime())
+ throw new Error(
+ `File ${filePath} has been modified since it was last read.\nLast modification: ${read.toISOString()}\nLast read: ${stats.mtime.toISOString()}\n\nPlease read the file again before modifying it.`,
+ );
+
+ const content = await file.text();
+ const index = content.indexOf(params.oldString);
+ if (index === -1)
+ throw new Error(
+ `oldString not found in file. Make sure it matches exactly, including whitespace and line breaks`,
+ );
+ const lastIndex = content.lastIndexOf(params.oldString);
+ if (index !== lastIndex)
+ throw new Error(
+ `oldString appears multiple times in the file. Please provide more context to ensure a unique match`,
+ );
+
+ const newContent =
+ content.substring(0, index) +
+ params.newString +
+ content.substring(index + params.oldString.length);
+
+ await file.write(newContent);
+ })();
+
+ FileTimes.write(filePath);
+ FileTimes.read(filePath);
+
+ let output = "";
+ await LSP.file(filePath);
+ const diagnostics = await LSP.diagnostics();
+ for (const [file, issues] of Object.entries(diagnostics)) {
+ if (issues.length === 0) continue;
+ if (file === filePath) {
+ output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`;
+ continue;
+ }
+ output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`;
+ }
+
+ return {
+ metadata: {
+ diagnostics,
+ },
+ output,
+ };
+ },
+});
diff --git a/packages/opencode/src/tool/fetch.ts b/packages/opencode/src/tool/fetch.ts
new file mode 100644
index 000000000..573e0eec7
--- /dev/null
+++ b/packages/opencode/src/tool/fetch.ts
@@ -0,0 +1,137 @@
+import { z } from "zod";
+import { Tool } from "./tool";
+import { JSDOM } from "jsdom";
+import TurndownService from "turndown";
+
+const MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB
+const DEFAULT_TIMEOUT = 30 * 1000; // 30 seconds
+const MAX_TIMEOUT = 120 * 1000; // 2 minutes
+
+const DESCRIPTION = `Fetches content from a URL and returns it in the specified format.
+
+WHEN TO USE THIS TOOL:
+- Use when you need to download content from a URL
+- Helpful for retrieving documentation, API responses, or web content
+- Useful for getting external information to assist with tasks
+
+HOW TO USE:
+- Provide the URL to fetch content from
+- Specify the desired output format (text, markdown, or html)
+- Optionally set a timeout for the request
+
+FEATURES:
+- Supports three output formats: text, markdown, and html
+- Automatically handles HTTP redirects
+- Sets reasonable timeouts to prevent hanging
+- Validates input parameters before making requests
+
+LIMITATIONS:
+- Maximum response size is 5MB
+- Only supports HTTP and HTTPS protocols
+- Cannot handle authentication or cookies
+- Some websites may block automated requests
+
+TIPS:
+- Use text format for plain text content or simple API responses
+- Use markdown format for content that should be rendered with formatting
+- Use html format when you need the raw HTML structure
+- Set appropriate timeouts for potentially slow websites`;
+
+export const Fetch = Tool.define({
+ name: "opencode.fetch",
+ description: DESCRIPTION,
+ parameters: z.object({
+ url: z.string().describe("The URL to fetch content from"),
+ format: z
+ .enum(["text", "markdown", "html"])
+ .describe(
+ "The format to return the content in (text, markdown, or html)",
+ ),
+ timeout: z
+ .number()
+ .min(0)
+ .max(MAX_TIMEOUT / 1000)
+ .describe("Optional timeout in seconds (max 120)")
+ .optional(),
+ }),
+ async execute(params, opts) {
+ // Validate URL
+ if (
+ !params.url.startsWith("http://") &&
+ !params.url.startsWith("https://")
+ ) {
+ throw new Error("URL must start with http:// or https://");
+ }
+
+ const timeout = Math.min(
+ (params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000,
+ MAX_TIMEOUT,
+ );
+
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
+ if (opts?.abortSignal) {
+ opts.abortSignal.addEventListener("abort", () => controller.abort());
+ }
+
+ const response = await fetch(params.url, {
+ signal: controller.signal,
+ headers: {
+ "User-Agent": "opencode/1.0",
+ },
+ });
+
+ clearTimeout(timeoutId);
+
+ if (!response.ok) {
+ throw new Error(`Request failed with status code: ${response.status}`);
+ }
+
+ // Check content length
+ const contentLength = response.headers.get("content-length");
+ if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) {
+ throw new Error("Response too large (exceeds 5MB limit)");
+ }
+
+ const arrayBuffer = await response.arrayBuffer();
+ if (arrayBuffer.byteLength > MAX_RESPONSE_SIZE) {
+ throw new Error("Response too large (exceeds 5MB limit)");
+ }
+
+ const content = new TextDecoder().decode(arrayBuffer);
+ const contentType = response.headers.get("content-type") || "";
+
+ switch (params.format) {
+ case "text":
+ if (contentType.includes("text/html")) {
+ const text = extractTextFromHTML(content);
+ return { output: text };
+ }
+ return { output: content };
+
+ case "markdown":
+ if (contentType.includes("text/html")) {
+ const markdown = convertHTMLToMarkdown(content);
+ return { output: markdown };
+ }
+ return { output: "```\n" + content + "\n```" };
+
+ case "html":
+ return { output: content };
+
+ default:
+ return { output: content };
+ }
+ },
+});
+
+function extractTextFromHTML(html: string): string {
+ const dom = new JSDOM(html);
+ const text = dom.window.document.body?.textContent || "";
+ return text.replace(/\s+/g, " ").trim();
+}
+
+function convertHTMLToMarkdown(html: string): string {
+ const turndownService = new TurndownService();
+ return turndownService.turndown(html);
+}
diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts
new file mode 100644
index 000000000..da47ee4ca
--- /dev/null
+++ b/packages/opencode/src/tool/glob.ts
@@ -0,0 +1,96 @@
+import { z } from "zod";
+import { Tool } from "./tool";
+import { App } from "../app/app";
+
+const DESCRIPTION = `Fast file pattern matching tool that finds files by name and pattern, returning matching paths sorted by modification time (newest first).
+
+WHEN TO USE THIS TOOL:
+- Use when you need to find files by name patterns or extensions
+- Great for finding specific file types across a directory structure
+- Useful for discovering files that match certain naming conventions
+
+HOW TO USE:
+- Provide a glob pattern to match against file paths
+- Optionally specify a starting directory (defaults to current working directory)
+- Results are sorted with most recently modified files first
+
+GLOB PATTERN SYNTAX:
+- '*' matches any sequence of non-separator characters
+- '**' matches any sequence of characters, including separators
+- '?' matches any single non-separator character
+- '[...]' matches any character in the brackets
+- '[!...]' matches any character not in the brackets
+
+COMMON PATTERN EXAMPLES:
+- '*.js' - Find all JavaScript files in the current directory
+- '**/*.js' - Find all JavaScript files in any subdirectory
+- 'src/**/*.{ts,tsx}' - Find all TypeScript files in the src directory
+- '*.{html,css,js}' - Find all HTML, CSS, and JS files
+
+LIMITATIONS:
+- Results are limited to 100 files (newest first)
+- Does not search file contents (use Grep tool for that)
+- Hidden files (starting with '.') are skipped
+
+TIPS:
+- For the most useful results, combine with the Grep tool: first find files with Glob, then search their contents with Grep
+- When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead
+- Always check if results are truncated and refine your search pattern if needed`;
+
+export const glob = Tool.define({
+ name: "opencode.glob",
+ description: DESCRIPTION,
+ parameters: z.object({
+ pattern: z.string().describe("The glob pattern to match files against"),
+ path: z
+ .string()
+ .describe(
+ "The directory to search in. Defaults to the current working directory.",
+ )
+ .optional(),
+ }),
+ async execute(params) {
+ const app = await App.use();
+ const search = params.path || app.root;
+ const limit = 100;
+ const glob = new Bun.Glob(params.pattern);
+ const files = [];
+ let truncated = false;
+ for await (const file of glob.scan({ cwd: search })) {
+ if (files.length >= limit) {
+ truncated = true;
+ break;
+ }
+ const stats = await Bun.file(file)
+ .stat()
+ .then((x) => x.mtime.getTime())
+ .catch(() => 0);
+ files.push({
+ path: file,
+ mtime: stats,
+ });
+ }
+ files.sort((a, b) => b.mtime - a.mtime);
+
+ const output = [];
+ if (files.length === 0) output.push("No files found");
+ if (files.length > 0) {
+ output.push(...files.map((f) => f.path));
+ if (truncated) {
+ output.push("");
+ output.push(
+ "(Results are truncated. Consider using a more specific path or pattern.)",
+ );
+ }
+ }
+
+ return {
+ metadata: {
+ count: files.length,
+ truncated,
+ },
+ output: output.join("\n"),
+ };
+ },
+});
+
diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts
new file mode 100644
index 000000000..8b2375f63
--- /dev/null
+++ b/packages/opencode/src/tool/grep.ts
@@ -0,0 +1,345 @@
+import { z } from "zod";
+import { Tool } from "./tool";
+import { App } from "../app/app";
+import { spawn } from "child_process";
+import { promises as fs } from "fs";
+import path from "path";
+
+const DESCRIPTION = `Fast content search tool that finds files containing specific text or patterns, returning matching file paths sorted by modification time (newest first).
+
+WHEN TO USE THIS TOOL:
+- Use when you need to find files containing specific text or patterns
+- Great for searching code bases for function names, variable declarations, or error messages
+- Useful for finding all files that use a particular API or pattern
+
+HOW TO USE:
+- Provide a regex pattern to search for within file contents
+- Set literal_text=true if you want to search for the exact text with special characters (recommended for non-regex users)
+- Optionally specify a starting directory (defaults to current working directory)
+- Optionally provide an include pattern to filter which files to search
+- Results are sorted with most recently modified files first
+
+REGEX PATTERN SYNTAX (when literal_text=false):
+- Supports standard regular expression syntax
+- 'function' searches for the literal text "function"
+- 'log\\..*Error' finds text starting with "log." and ending with "Error"
+- 'import\\s+.*\\s+from' finds import statements in JavaScript/TypeScript
+
+COMMON INCLUDE PATTERN EXAMPLES:
+- '*.js' - Only search JavaScript files
+- '*.{ts,tsx}' - Only search TypeScript files
+- '*.go' - Only search Go files
+
+LIMITATIONS:
+- Results are limited to 100 files (newest first)
+- Performance depends on the number of files being searched
+- Very large binary files may be skipped
+- Hidden files (starting with '.') are skipped
+
+TIPS:
+- For faster, more targeted searches, first use Glob to find relevant files, then use Grep
+- When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead
+- Always check if results are truncated and refine your search pattern if needed
+- Use literal_text=true when searching for exact text containing special characters like dots, parentheses, etc.`;
+
+interface GrepMatch {
+ path: string;
+ modTime: number;
+ lineNum: number;
+ lineText: string;
+}
+
+function escapeRegexPattern(pattern: string): string {
+ const specialChars = [
+ "\\",
+ ".",
+ "+",
+ "*",
+ "?",
+ "(",
+ ")",
+ "[",
+ "]",
+ "{",
+ "}",
+ "^",
+ "$",
+ "|",
+ ];
+ let escaped = pattern;
+
+ for (const char of specialChars) {
+ escaped = escaped.replaceAll(char, "\\" + char);
+ }
+
+ return escaped;
+}
+
+function globToRegex(glob: string): string {
+ let regexPattern = glob.replaceAll(".", "\\.");
+ regexPattern = regexPattern.replaceAll("*", ".*");
+ regexPattern = regexPattern.replaceAll("?", ".");
+
+ // Handle {a,b,c} patterns
+ regexPattern = regexPattern.replace(/\{([^}]+)\}/g, (_, inner) => {
+ return "(" + inner.replace(/,/g, "|") + ")";
+ });
+
+ return regexPattern;
+}
+
+async function searchWithRipgrep(
+ pattern: string,
+ searchPath: string,
+ include?: string,
+): Promise<GrepMatch[]> {
+ return new Promise((resolve, reject) => {
+ const args = ["-n", pattern];
+ if (include) {
+ args.push("--glob", include);
+ }
+ args.push(searchPath);
+
+ const rg = spawn("rg", args);
+ let output = "";
+ let errorOutput = "";
+
+ rg.stdout.on("data", (data) => {
+ output += data.toString();
+ });
+
+ rg.stderr.on("data", (data) => {
+ errorOutput += data.toString();
+ });
+
+ rg.on("close", async (code) => {
+ if (code === 1) {
+ // No matches found
+ resolve([]);
+ return;
+ }
+
+ if (code !== 0) {
+ reject(new Error(`ripgrep failed: ${errorOutput}`));
+ return;
+ }
+
+ const lines = output.trim().split("\n");
+ const matches: GrepMatch[] = [];
+
+ for (const line of lines) {
+ if (!line) continue;
+
+ // Parse ripgrep output format: file:line:content
+ const parts = line.split(":", 3);
+ if (parts.length < 3) continue;
+
+ const filePath = parts[0];
+ const lineNum = parseInt(parts[1], 10);
+ const lineText = parts[2];
+
+ try {
+ const stats = await fs.stat(filePath);
+ matches.push({
+ path: filePath,
+ modTime: stats.mtime.getTime(),
+ lineNum,
+ lineText,
+ });
+ } catch {
+ // Skip files we can't access
+ continue;
+ }
+ }
+
+ resolve(matches);
+ });
+
+ rg.on("error", (err) => {
+ reject(err);
+ });
+ });
+}
+
+async function searchFilesWithRegex(
+ pattern: string,
+ rootPath: string,
+ include?: string,
+): Promise<GrepMatch[]> {
+ const matches: GrepMatch[] = [];
+ const regex = new RegExp(pattern);
+
+ let includePattern: RegExp | undefined;
+ if (include) {
+ const regexPattern = globToRegex(include);
+ includePattern = new RegExp(regexPattern);
+ }
+
+ async function walkDir(dir: string) {
+ if (matches.length >= 200) return;
+
+ try {
+ const entries = await fs.readdir(dir, { withFileTypes: true });
+
+ for (const entry of entries) {
+ if (matches.length >= 200) break;
+
+ const fullPath = path.join(dir, entry.name);
+
+ if (entry.isDirectory()) {
+ // Skip hidden directories
+ if (entry.name.startsWith(".")) continue;
+ await walkDir(fullPath);
+ } else if (entry.isFile()) {
+ // Skip hidden files
+ if (entry.name.startsWith(".")) continue;
+
+ if (includePattern && !includePattern.test(fullPath)) {
+ continue;
+ }
+
+ try {
+ const content = await fs.readFile(fullPath, "utf-8");
+ const lines = content.split("\n");
+
+ for (let i = 0; i < lines.length; i++) {
+ if (regex.test(lines[i])) {
+ const stats = await fs.stat(fullPath);
+ matches.push({
+ path: fullPath,
+ modTime: stats.mtime.getTime(),
+ lineNum: i + 1,
+ lineText: lines[i],
+ });
+ break; // Only first match per file
+ }
+ }
+ } catch {
+ // Skip files we can't read
+ continue;
+ }
+ }
+ }
+ } catch {
+ // Skip directories we can't read
+ return;
+ }
+ }
+
+ await walkDir(rootPath);
+ return matches;
+}
+
+async function searchFiles(
+ pattern: string,
+ rootPath: string,
+ include?: string,
+ limit: number = 100,
+): Promise<{ matches: GrepMatch[]; truncated: boolean }> {
+ let matches: GrepMatch[];
+
+ try {
+ matches = await searchWithRipgrep(pattern, rootPath, include);
+ } catch {
+ matches = await searchFilesWithRegex(pattern, rootPath, include);
+ }
+
+ // Sort by modification time (newest first)
+ matches.sort((a, b) => b.modTime - a.modTime);
+
+ const truncated = matches.length > limit;
+ if (truncated) {
+ matches = matches.slice(0, limit);
+ }
+
+ return { matches, truncated };
+}
+
+export const grep = Tool.define({
+ name: "opencode.grep",
+ description: DESCRIPTION,
+ parameters: z.object({
+ pattern: z
+ .string()
+ .describe("The regex pattern to search for in file contents"),
+ path: z
+ .string()
+ .describe(
+ "The directory to search in. Defaults to the current working directory.",
+ )
+ .optional(),
+ include: z
+ .string()
+ .describe(
+ 'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
+ )
+ .optional(),
+ literalText: z
+ .boolean()
+ .describe(
+ "If true, the pattern will be treated as literal text with special regex characters escaped. Default is false.",
+ )
+ .optional(),
+ }),
+ async execute(params) {
+ if (!params.pattern) {
+ throw new Error("pattern is required");
+ }
+
+ const app = await App.use();
+ const searchPath = params.path || app.root;
+
+ // If literalText is true, escape the pattern
+ const searchPattern = params.literalText
+ ? escapeRegexPattern(params.pattern)
+ : params.pattern;
+
+ const { matches, truncated } = await searchFiles(
+ searchPattern,
+ searchPath,
+ params.include,
+ 100,
+ );
+
+ if (matches.length === 0) {
+ return {
+ metadata: { matches: 0, truncated },
+ output: "No files found"
+ };
+ }
+
+ const lines = [`Found ${matches.length} matches`];
+
+ let currentFile = "";
+ for (const match of matches) {
+ if (currentFile !== match.path) {
+ if (currentFile !== "") {
+ lines.push("");
+ }
+ currentFile = match.path;
+ lines.push(`${match.path}:`);
+ }
+ if (match.lineNum > 0) {
+ lines.push(` Line ${match.lineNum}: ${match.lineText}`);
+ } else {
+ lines.push(` ${match.path}`);
+ }
+ }
+
+ if (truncated) {
+ lines.push("");
+ lines.push(
+ "(Results are truncated. Consider using a more specific path or pattern.)",
+ );
+ }
+
+ return {
+ metadata: {
+ matches: matches.length,
+ truncated,
+ },
+ output: lines.join("\n"),
+ };
+ },
+});
+
diff --git a/packages/opencode/src/tool/index.ts b/packages/opencode/src/tool/index.ts
new file mode 100644
index 000000000..3930c87c4
--- /dev/null
+++ b/packages/opencode/src/tool/index.ts
@@ -0,0 +1,9 @@
+export * from "./bash";
+export * from "./edit";
+export * from "./fetch";
+export * from "./glob";
+export * from "./grep";
+export * from "./view";
+export * from "./ls";
+export * from "./lsp-diagnostics";
+export * from "./lsp-hover";
diff --git a/packages/opencode/src/tool/ls.ts b/packages/opencode/src/tool/ls.ts
new file mode 100644
index 000000000..46efb307a
--- /dev/null
+++ b/packages/opencode/src/tool/ls.ts
@@ -0,0 +1,96 @@
+import { z } from "zod";
+import { Tool } from "./tool";
+import { App } from "../app/app";
+import * as path from "path";
+
+const IGNORE_PATTERNS = [
+ "node_modules/",
+ "__pycache__/",
+ ".git/",
+ "dist/",
+ "build/",
+ "target/",
+ "vendor/",
+ "bin/",
+ "obj/",
+ ".idea/",
+ ".vscode/",
+];
+
+export const ls = Tool.define({
+ name: "opencode.ls",
+ description: "List directory contents",
+ parameters: z.object({
+ path: z.string().optional(),
+ ignore: z.array(z.string()).optional(),
+ }),
+ async execute(params) {
+ const app = await App.use();
+ const searchPath = path.resolve(app.root, params.path || ".");
+
+ const glob = new Bun.Glob("**/*");
+ const files = [];
+
+ for await (const file of glob.scan({ cwd: searchPath })) {
+ if (file.startsWith(".") || IGNORE_PATTERNS.some((p) => file.includes(p)))
+ continue;
+ if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
+ continue;
+ files.push(file);
+ if (files.length >= 1000) break;
+ }
+
+ // Build directory structure
+ const dirs = new Set<string>();
+ const filesByDir = new Map<string, string[]>();
+
+ for (const file of files) {
+ const dir = path.dirname(file);
+ const parts = dir === "." ? [] : dir.split("/");
+
+ // Add all parent directories
+ for (let i = 0; i <= parts.length; i++) {
+ const dirPath = i === 0 ? "." : parts.slice(0, i).join("/");
+ dirs.add(dirPath);
+ }
+
+ // Add file to its directory
+ if (!filesByDir.has(dir)) filesByDir.set(dir, []);
+ filesByDir.get(dir)!.push(path.basename(file));
+ }
+
+ function renderDir(dirPath: string, depth: number): string {
+ const indent = " ".repeat(depth);
+ let output = "";
+
+ if (depth > 0) {
+ output += `${indent}${path.basename(dirPath)}/\n`;
+ }
+
+ const childIndent = " ".repeat(depth + 1);
+ const children = Array.from(dirs)
+ .filter((d) => path.dirname(d) === dirPath && d !== dirPath)
+ .sort();
+
+ // Render subdirectories first
+ for (const child of children) {
+ output += renderDir(child, depth + 1);
+ }
+
+ // Render files
+ const files = filesByDir.get(dirPath) || [];
+ for (const file of files.sort()) {
+ output += `${childIndent}${file}\n`;
+ }
+
+ return output;
+ }
+
+ const output = `${searchPath}/\n` + renderDir(".", 0);
+
+ return {
+ metadata: { count: files.length, truncated: files.length >= 1000 },
+ output,
+ };
+ },
+});
diff --git a/packages/opencode/src/tool/lsp-diagnostics.ts b/packages/opencode/src/tool/lsp-diagnostics.ts
new file mode 100644
index 000000000..736efc034
--- /dev/null
+++ b/packages/opencode/src/tool/lsp-diagnostics.ts
@@ -0,0 +1,53 @@
+import { z } from "zod";
+import { Tool } from "./tool";
+import path from "path";
+import { LSP } from "../lsp";
+import { App } from "../app/app";
+
+export const LspDiagnosticTool = Tool.define({
+ name: "opencode.lsp_diagnostic",
+ description: `Get diagnostics for a file and/or project.
+
+WHEN TO USE THIS TOOL:
+- Use when you need to check for errors or warnings in your code
+- Helpful for debugging and ensuring code quality
+- Good for getting a quick overview of issues in a file or project
+
+HOW TO USE:
+- Provide a path to a file to get diagnostics for that file
+- Results are displayed in a structured format with severity levels
+
+FEATURES:
+- Displays errors, warnings, and hints
+- Groups diagnostics by severity
+- Provides detailed information about each diagnostic
+
+LIMITATIONS:
+- Results are limited to the diagnostics provided by the LSP clients
+- May not cover all possible issues in the code
+- Does not provide suggestions for fixing issues
+
+TIPS:
+- Use in conjunction with other tools for a comprehensive code review
+- Combine with the LSP client for real-time diagnostics`,
+ parameters: z.object({
+ path: z.string().describe("The path to the file to get diagnostics."),
+ }),
+ execute: async (args) => {
+ const app = await App.use();
+ const normalized = path.isAbsolute(args.path)
+ ? args.path
+ : path.join(app.root, args.path);
+ await LSP.file(normalized);
+ const diagnostics = await LSP.diagnostics();
+ const file = diagnostics[normalized];
+ return {
+ metadata: {
+ diagnostics,
+ },
+ output: file?.length
+ ? file.map(LSP.Diagnostic.pretty).join("\n")
+ : "No errors found",
+ };
+ },
+});
diff --git a/packages/opencode/src/tool/lsp-hover.ts b/packages/opencode/src/tool/lsp-hover.ts
new file mode 100644
index 000000000..c7a132645
--- /dev/null
+++ b/packages/opencode/src/tool/lsp-hover.ts
@@ -0,0 +1,38 @@
+import { z } from "zod";
+import { Tool } from "./tool";
+import path from "path";
+import { LSP } from "../lsp";
+import { App } from "../app/app";
+
+export const LspHoverTool = Tool.define({
+ name: "opencode.lsp_hover",
+ description: `
+ Looks up hover information for a given position in a source file using the Language Server Protocol (LSP).
+ This includes type information, documentation, or symbol details at the specified line and character.
+ Useful for providing code insights, explanations, or context-aware assistance based on the user's current cursor location.
+ `,
+ parameters: z.object({
+ file: z.string().describe("The path to the file to get diagnostics."),
+ line: z.number().describe("The line number to get diagnostics."),
+ character: z.number().describe("The character number to get diagnostics."),
+ }),
+ execute: async (args) => {
+ console.log(args);
+ const app = await App.use();
+ const file = path.isAbsolute(args.file)
+ ? args.file
+ : path.join(app.root, args.file);
+ await LSP.file(file);
+ const result = await LSP.hover({
+ ...args,
+ file,
+ });
+ console.log(result);
+ return {
+ metadata: {
+ result,
+ },
+ output: JSON.stringify(result, null, 2),
+ };
+ },
+});
diff --git a/packages/opencode/src/tool/patch.ts b/packages/opencode/src/tool/patch.ts
new file mode 100644
index 000000000..9f9192fda
--- /dev/null
+++ b/packages/opencode/src/tool/patch.ts
@@ -0,0 +1,420 @@
+import { z } from "zod";
+import * as path from "path";
+import * as fs from "fs/promises";
+import { Tool } from "./tool";
+import { FileTimes } from "./util/file-times";
+
+const DESCRIPTION = `Applies a patch to multiple files in one operation. This tool is useful for making coordinated changes across multiple files.
+
+The patch text must follow this format:
+*** Begin Patch
+*** Update File: /path/to/file
+@@ Context line (unique within the file)
+ Line to keep
+-Line to remove
++Line to add
+ Line to keep
+*** Add File: /path/to/new/file
++Content of the new file
++More content
+*** Delete File: /path/to/file/to/delete
+*** End Patch
+
+Before using this tool:
+1. Use the FileRead tool to understand the files' contents and context
+2. Verify all file paths are correct (use the LS tool)
+
+CRITICAL REQUIREMENTS FOR USING THIS TOOL:
+
+1. UNIQUENESS: Context lines MUST uniquely identify the specific sections you want to change
+2. PRECISION: All whitespace, indentation, and surrounding code must match exactly
+3. VALIDATION: Ensure edits result in idiomatic, correct code
+4. PATHS: Always use absolute file paths (starting with /)
+
+The tool will apply all changes in a single atomic operation.`;
+
+const PatchParams = z.object({
+ patchText: z
+ .string()
+ .describe("The full patch text that describes all changes to be made"),
+});
+
+interface PatchResponseMetadata {
+ changed: string[];
+ additions: number;
+ removals: number;
+}
+
+interface Change {
+ type: "add" | "update" | "delete";
+ old_content?: string;
+ new_content?: string;
+}
+
+interface Commit {
+ changes: Record<string, Change>;
+}
+
+interface PatchOperation {
+ type: "update" | "add" | "delete";
+ filePath: string;
+ hunks?: PatchHunk[];
+ content?: string;
+}
+
+interface PatchHunk {
+ contextLine: string;
+ changes: PatchChange[];
+}
+
+interface PatchChange {
+ type: "keep" | "remove" | "add";
+ content: string;
+}
+
+function identifyFilesNeeded(patchText: string): string[] {
+ const files: string[] = [];
+ const lines = patchText.split("\n");
+ for (const line of lines) {
+ if (
+ line.startsWith("*** Update File:") ||
+ line.startsWith("*** Delete File:")
+ ) {
+ const filePath = line.split(":", 2)[1]?.trim();
+ if (filePath) files.push(filePath);
+ }
+ }
+ return files;
+}
+
+function identifyFilesAdded(patchText: string): string[] {
+ const files: string[] = [];
+ const lines = patchText.split("\n");
+ for (const line of lines) {
+ if (line.startsWith("*** Add File:")) {
+ const filePath = line.split(":", 2)[1]?.trim();
+ if (filePath) files.push(filePath);
+ }
+ }
+ return files;
+}
+
+function textToPatch(
+ patchText: string,
+ _currentFiles: Record<string, string>,
+): [PatchOperation[], number] {
+ const operations: PatchOperation[] = [];
+ const lines = patchText.split("\n");
+ let i = 0;
+ let fuzz = 0;
+
+ while (i < lines.length) {
+ const line = lines[i];
+
+ if (line.startsWith("*** Update File:")) {
+ const filePath = line.split(":", 2)[1]?.trim();
+ if (!filePath) {
+ i++;
+ continue;
+ }
+
+ const hunks: PatchHunk[] = [];
+ i++;
+
+ while (i < lines.length && !lines[i].startsWith("***")) {
+ if (lines[i].startsWith("@@")) {
+ const contextLine = lines[i].substring(2).trim();
+ const changes: PatchChange[] = [];
+ i++;
+
+ while (
+ i < lines.length &&
+ !lines[i].startsWith("@@") &&
+ !lines[i].startsWith("***")
+ ) {
+ const changeLine = lines[i];
+ if (changeLine.startsWith(" ")) {
+ changes.push({ type: "keep", content: changeLine.substring(1) });
+ } else if (changeLine.startsWith("-")) {
+ changes.push({
+ type: "remove",
+ content: changeLine.substring(1),
+ });
+ } else if (changeLine.startsWith("+")) {
+ changes.push({ type: "add", content: changeLine.substring(1) });
+ }
+ i++;
+ }
+
+ hunks.push({ contextLine, changes });
+ } else {
+ i++;
+ }
+ }
+
+ operations.push({ type: "update", filePath, hunks });
+ } else if (line.startsWith("*** Add File:")) {
+ const filePath = line.split(":", 2)[1]?.trim();
+ if (!filePath) {
+ i++;
+ continue;
+ }
+
+ let content = "";
+ i++;
+
+ while (i < lines.length && !lines[i].startsWith("***")) {
+ if (lines[i].startsWith("+")) {
+ content += lines[i].substring(1) + "\n";
+ }
+ i++;
+ }
+
+ operations.push({ type: "add", filePath, content: content.slice(0, -1) });
+ } else if (line.startsWith("*** Delete File:")) {
+ const filePath = line.split(":", 2)[1]?.trim();
+ if (filePath) {
+ operations.push({ type: "delete", filePath });
+ }
+ i++;
+ } else {
+ i++;
+ }
+ }
+
+ return [operations, fuzz];
+}
+
+function patchToCommit(
+ operations: PatchOperation[],
+ currentFiles: Record<string, string>,
+): Commit {
+ const changes: Record<string, Change> = {};
+
+ for (const op of operations) {
+ if (op.type === "delete") {
+ changes[op.filePath] = {
+ type: "delete",
+ old_content: currentFiles[op.filePath] || "",
+ };
+ } else if (op.type === "add") {
+ changes[op.filePath] = {
+ type: "add",
+ new_content: op.content || "",
+ };
+ } else if (op.type === "update" && op.hunks) {
+ const originalContent = currentFiles[op.filePath] || "";
+ const lines = originalContent.split("\n");
+
+ for (const hunk of op.hunks) {
+ const contextIndex = lines.findIndex((line) =>
+ line.includes(hunk.contextLine),
+ );
+ if (contextIndex === -1) {
+ throw new Error(`Context line not found: ${hunk.contextLine}`);
+ }
+
+ let currentIndex = contextIndex;
+ for (const change of hunk.changes) {
+ if (change.type === "keep") {
+ currentIndex++;
+ } else if (change.type === "remove") {
+ lines.splice(currentIndex, 1);
+ } else if (change.type === "add") {
+ lines.splice(currentIndex, 0, change.content);
+ currentIndex++;
+ }
+ }
+ }
+
+ changes[op.filePath] = {
+ type: "update",
+ old_content: originalContent,
+ new_content: lines.join("\n"),
+ };
+ }
+ }
+
+ return { changes };
+}
+
+function generateDiff(
+ oldContent: string,
+ newContent: string,
+ filePath: string,
+): [string, number, number] {
+ // Mock implementation - would need actual diff generation
+ const lines1 = oldContent.split("\n");
+ const lines2 = newContent.split("\n");
+ const additions = Math.max(0, lines2.length - lines1.length);
+ const removals = Math.max(0, lines1.length - lines2.length);
+ return [`--- ${filePath}\n+++ ${filePath}\n`, additions, removals];
+}
+
+async function applyCommit(
+ commit: Commit,
+ writeFile: (path: string, content: string) => Promise<void>,
+ deleteFile: (path: string) => Promise<void>,
+): Promise<void> {
+ for (const [filePath, change] of Object.entries(commit.changes)) {
+ if (change.type === "delete") {
+ await deleteFile(filePath);
+ } else if (change.new_content !== undefined) {
+ await writeFile(filePath, change.new_content);
+ }
+ }
+}
+
+export const patch = Tool.define({
+ name: "opencode.patch",
+ description: DESCRIPTION,
+ parameters: PatchParams,
+ execute: async (params) => {
+ if (!params.patchText) {
+ throw new Error("patchText is required");
+ }
+
+ // Identify all files needed for the patch and verify they've been read
+ const filesToRead = identifyFilesNeeded(params.patchText);
+ for (const filePath of filesToRead) {
+ let absPath = filePath;
+ if (!path.isAbsolute(absPath)) {
+ absPath = path.resolve(process.cwd(), absPath);
+ }
+
+ if (!FileTimes.get(absPath)) {
+ throw new Error(
+ `you must read the file ${filePath} before patching it. Use the FileRead tool first`,
+ );
+ }
+
+ try {
+ const stats = await fs.stat(absPath);
+ if (stats.isDirectory()) {
+ throw new Error(`path is a directory, not a file: ${absPath}`);
+ }
+
+ const lastRead = FileTimes.get(absPath);
+ if (lastRead && stats.mtime > lastRead) {
+ throw new Error(
+ `file ${absPath} has been modified since it was last read (mod time: ${stats.mtime.toISOString()}, last read: ${lastRead.toISOString()})`,
+ );
+ }
+ } catch (error: any) {
+ if (error.code === "ENOENT") {
+ throw new Error(`file not found: ${absPath}`);
+ }
+ throw new Error(`failed to access file: ${error.message}`);
+ }
+ }
+
+ // Check for new files to ensure they don't already exist
+ const filesToAdd = identifyFilesAdded(params.patchText);
+ for (const filePath of filesToAdd) {
+ let absPath = filePath;
+ if (!path.isAbsolute(absPath)) {
+ absPath = path.resolve(process.cwd(), absPath);
+ }
+
+ try {
+ await fs.stat(absPath);
+ throw new Error(`file already exists and cannot be added: ${absPath}`);
+ } catch (error: any) {
+ if (error.code !== "ENOENT") {
+ throw new Error(`failed to check file: ${error.message}`);
+ }
+ }
+ }
+
+ // Load all required files
+ const currentFiles: Record<string, string> = {};
+ for (const filePath of filesToRead) {
+ let absPath = filePath;
+ if (!path.isAbsolute(absPath)) {
+ absPath = path.resolve(process.cwd(), absPath);
+ }
+
+ try {
+ const content = await fs.readFile(absPath, "utf-8");
+ currentFiles[filePath] = content;
+ } catch (error: any) {
+ throw new Error(`failed to read file ${absPath}: ${error.message}`);
+ }
+ }
+
+ // Process the patch
+ const [patch, fuzz] = textToPatch(params.patchText, currentFiles);
+ if (fuzz > 3) {
+ throw new Error(
+ `patch contains fuzzy matches (fuzz level: ${fuzz}). Please make your context lines more precise`,
+ );
+ }
+
+ // Convert patch to commit
+ const commit = patchToCommit(patch, currentFiles);
+
+ // Apply the changes to the filesystem
+ await applyCommit(
+ commit,
+ async (filePath: string, content: string) => {
+ let absPath = filePath;
+ if (!path.isAbsolute(absPath)) {
+ absPath = path.resolve(process.cwd(), absPath);
+ }
+
+ // Create parent directories if needed
+ const dir = path.dirname(absPath);
+ await fs.mkdir(dir, { recursive: true });
+ await fs.writeFile(absPath, content, "utf-8");
+ },
+ async (filePath: string) => {
+ let absPath = filePath;
+ if (!path.isAbsolute(absPath)) {
+ absPath = path.resolve(process.cwd(), absPath);
+ }
+ await fs.unlink(absPath);
+ },
+ );
+
+ // Calculate statistics
+ const changedFiles: string[] = [];
+ let totalAdditions = 0;
+ let totalRemovals = 0;
+
+ for (const [filePath, change] of Object.entries(commit.changes)) {
+ let absPath = filePath;
+ if (!path.isAbsolute(absPath)) {
+ absPath = path.resolve(process.cwd(), absPath);
+ }
+ changedFiles.push(absPath);
+
+ const oldContent = change.old_content || "";
+ const newContent = change.new_content || "";
+
+ // Calculate diff statistics
+ const [, additions, removals] = generateDiff(
+ oldContent,
+ newContent,
+ filePath,
+ );
+ totalAdditions += additions;
+ totalRemovals += removals;
+
+ // Record file operations
+ FileTimes.write(absPath);
+ FileTimes.read(absPath);
+ }
+
+ const result = `Patch applied successfully. ${changedFiles.length} files changed, ${totalAdditions} additions, ${totalRemovals} removals`;
+ const output = result;
+
+ return {
+ metadata: {
+ changed: changedFiles,
+ additions: totalAdditions,
+ removals: totalRemovals,
+ } satisfies PatchResponseMetadata,
+ output,
+ };
+ },
+});
diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts
new file mode 100644
index 000000000..0cd65e007
--- /dev/null
+++ b/packages/opencode/src/tool/tool.ts
@@ -0,0 +1,61 @@
+import { tool, type Tool as AITool } from "ai";
+import { Log } from "../util/log";
+
+const log = Log.create({ service: "tool" });
+
+export namespace Tool {
+ export interface Metadata<
+ Properties extends Record<string, any> = Record<string, any>,
+ > {
+ properties: Properties;
+ time: {
+ start: number;
+ end: number;
+ };
+ }
+ export function define<
+ Params,
+ Output extends { metadata?: any; output: any },
+ Name extends string,
+ >(
+ input: AITool<Params, Output> & {
+ name: Name;
+ },
+ ) {
+ return tool({
+ ...input,
+ execute: async (params, opts) => {
+ log.info("invoking", {
+ id: opts.toolCallId,
+ name: input.name,
+ ...params,
+ });
+ try {
+ const start = Date.now();
+ const result = await input.execute!(params, opts);
+ const metadata: Metadata<Output["metadata"]> = {
+ ...result.metadata,
+ time: {
+ start,
+ end: Date.now(),
+ },
+ };
+ return {
+ metadata,
+ output: result.output,
+ };
+ } catch (e: any) {
+ log.error("error", {
+ msg: e.toString(),
+ });
+ return {
+ metadata: {
+ error: true,
+ },
+ output: "An error occurred: " + e.toString(),
+ };
+ }
+ },
+ });
+ }
+}
diff --git a/packages/opencode/src/tool/util/file-times.ts b/packages/opencode/src/tool/util/file-times.ts
new file mode 100644
index 000000000..8d36d007d
--- /dev/null
+++ b/packages/opencode/src/tool/util/file-times.ts
@@ -0,0 +1,20 @@
+import { App } from "../../app/app";
+
+export namespace FileTimes {
+ export const state = App.state("tool.filetimes", () => ({
+ read: new Map<string, Date>(),
+ write: new Map<string, Date>(),
+ }));
+
+ export function read(filePath: string) {
+ state().read.set(filePath, new Date());
+ }
+
+ export function write(filePath: string) {
+ state().write.set(filePath, new Date());
+ }
+
+ export function get(filePath: string): Date | null {
+ return state().read.get(filePath) || null;
+ }
+}
diff --git a/packages/opencode/src/tool/view.ts b/packages/opencode/src/tool/view.ts
new file mode 100644
index 000000000..ee11881d3
--- /dev/null
+++ b/packages/opencode/src/tool/view.ts
@@ -0,0 +1,152 @@
+import { z } from "zod";
+import * as fs from "fs";
+import * as path from "path";
+import { Tool } from "./tool";
+import { LSP } from "../lsp";
+import { FileTimes } from "./util/file-times";
+
+const MAX_READ_SIZE = 250 * 1024;
+const DEFAULT_READ_LIMIT = 2000;
+const MAX_LINE_LENGTH = 2000;
+
+const DESCRIPTION = `File viewing tool that reads and displays the contents of files with line numbers, allowing you to examine code, logs, or text data.
+
+WHEN TO USE THIS TOOL:
+- Use when you need to read the contents of a specific file
+- Helpful for examining source code, configuration files, or log files
+- Perfect for looking at text-based file formats
+
+HOW TO USE:
+- Provide the path to the file you want to view
+- Optionally specify an offset to start reading from a specific line
+- Optionally specify a limit to control how many lines are read
+
+FEATURES:
+- Displays file contents with line numbers for easy reference
+- Can read from any position in a file using the offset parameter
+- Handles large files by limiting the number of lines read
+- Automatically truncates very long lines for better display
+- Suggests similar file names when the requested file isn't found
+
+LIMITATIONS:
+- Maximum file size is 250KB
+- Default reading limit is 2000 lines
+- Lines longer than 2000 characters are truncated
+- Cannot display binary files or images
+- Images can be identified but not displayed
+
+TIPS:
+- Use with Glob tool to first find files you want to view
+- For code exploration, first use Grep to find relevant files, then View to examine them
+- When viewing large files, use the offset parameter to read specific sections`;
+
+export const view = Tool.define({
+ name: "opencode.view",
+ description: DESCRIPTION,
+ parameters: z.object({
+ filePath: z.string().describe("The path to the file to read"),
+ offset: z
+ .number()
+ .describe("The line number to start reading from (0-based)")
+ .optional(),
+ limit: z
+ .number()
+ .describe("The number of lines to read (defaults to 2000)")
+ .optional(),
+ }),
+ async execute(params) {
+ let filePath = params.filePath;
+ if (!path.isAbsolute(filePath)) {
+ filePath = path.join(process.cwd(), filePath);
+ }
+
+ const file = Bun.file(filePath);
+ if (!(await file.exists())) {
+ const dir = path.dirname(filePath);
+ const base = path.basename(filePath);
+
+ const dirEntries = fs.readdirSync(dir);
+ const suggestions = dirEntries
+ .filter(
+ (entry) =>
+ entry.toLowerCase().includes(base.toLowerCase()) ||
+ base.toLowerCase().includes(entry.toLowerCase()),
+ )
+ .map((entry) => path.join(dir, entry))
+ .slice(0, 3);
+
+ if (suggestions.length > 0) {
+ throw new Error(
+ `File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`,
+ );
+ }
+
+ throw new Error(`File not found: ${filePath}`);
+ }
+ const stats = await file.stat();
+
+ if (stats.size > MAX_READ_SIZE)
+ throw new Error(
+ `File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`,
+ );
+ const limit = params.limit ?? DEFAULT_READ_LIMIT;
+ const offset = params.offset || 0;
+ const isImage = isImageFile(filePath);
+ if (isImage)
+ throw new Error(
+ `This is an image file of type: ${isImage}\nUse a different tool to process images`,
+ );
+ const lines = await file.text().then((text) => text.split("\n"));
+ const raw = lines.slice(offset, offset + limit).map((line) => {
+ return line.length > MAX_LINE_LENGTH
+ ? line.substring(0, MAX_LINE_LENGTH) + "..."
+ : line;
+ });
+ const content = raw.map((line, index) => {
+ return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`;
+ });
+ const preview = raw.slice(0, 20).join("\n");
+
+ let output = "<file>\n";
+ output += content.join("\n");
+
+ if (lines.length > offset + content.length) {
+ output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${
+ offset + content.length
+ })`;
+ }
+ output += "\n</file>";
+
+ // just warms the lsp client
+ LSP.file(filePath);
+ FileTimes.read(filePath);
+
+ return {
+ output,
+ metadata: {
+ preview,
+ },
+ };
+ },
+});
+
+function isImageFile(filePath: string): string | false {
+ const ext = path.extname(filePath).toLowerCase();
+ switch (ext) {
+ case ".jpg":
+ case ".jpeg":
+ return "JPEG";
+ case ".png":
+ return "PNG";
+ case ".gif":
+ return "GIF";
+ case ".bmp":
+ return "BMP";
+ case ".svg":
+ return "SVG";
+ case ".webp":
+ return "WebP";
+ default:
+ return false;
+ }
+}
diff --git a/packages/opencode/src/util/context.ts b/packages/opencode/src/util/context.ts
new file mode 100644
index 000000000..bcaf7ee3c
--- /dev/null
+++ b/packages/opencode/src/util/context.ts
@@ -0,0 +1,25 @@
+import { AsyncLocalStorage } from "async_hooks";
+
+export namespace Context {
+ export class NotFound extends Error {
+ constructor(public readonly name: string) {
+ super(`No context found for ${name}`);
+ }
+ }
+
+ export function create<T>(name: string) {
+ const storage = new AsyncLocalStorage<T>();
+ return {
+ use() {
+ const result = storage.getStore();
+ if (!result) {
+ throw new NotFound(name);
+ }
+ return result;
+ },
+ provide<R>(value: T, fn: () => R) {
+ return storage.run<R>(value, fn);
+ },
+ };
+ }
+}
diff --git a/packages/opencode/src/util/event.ts b/packages/opencode/src/util/event.ts
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/packages/opencode/src/util/event.ts
diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts
new file mode 100644
index 000000000..34707b136
--- /dev/null
+++ b/packages/opencode/src/util/log.ts
@@ -0,0 +1,64 @@
+import path from "path";
+import { AppPath } from "../app/path";
+import fs from "fs/promises";
+export namespace Log {
+ const write = {
+ out: (msg: string) => {
+ process.stdout.write(msg);
+ },
+ err: (msg: string) => {
+ process.stderr.write(msg);
+ },
+ };
+
+ export async function file(directory: string) {
+ const outPath = path.join(AppPath.data(directory), "opencode.out.log");
+ const errPath = path.join(AppPath.data(directory), "opencode.err.log");
+ await fs.truncate(outPath).catch(() => {});
+ await fs.truncate(errPath).catch(() => {});
+ const out = Bun.file(outPath);
+ const err = Bun.file(errPath);
+ const outWriter = out.writer();
+ const errWriter = err.writer();
+ write["out"] = (msg) => {
+ outWriter.write(msg);
+ outWriter.flush();
+ };
+ write["err"] = (msg) => {
+ errWriter.write(msg);
+ errWriter.flush();
+ };
+ }
+
+ export function create(tags?: Record<string, any>) {
+ tags = tags || {};
+
+ function build(message: any, extra?: Record<string, any>) {
+ const prefix = Object.entries({
+ ...tags,
+ ...extra,
+ })
+ .filter(([_, value]) => value !== undefined && value !== null)
+ .map(([key, value]) => `${key}=${value}`)
+ .join(" ");
+ return [new Date().toISOString(), prefix, message].filter(Boolean).join(" ") + "\n";
+ }
+ const result = {
+ info(message?: any, extra?: Record<string, any>) {
+ write.out(build(message, extra));
+ },
+ error(message?: any, extra?: Record<string, any>) {
+ write.err(build(message, extra));
+ },
+ tag(key: string, value: string) {
+ if (tags) tags[key] = value;
+ return result;
+ },
+ clone() {
+ return Log.create({ ...tags });
+ },
+ };
+
+ return result;
+ }
+}
diff --git a/packages/opencode/src/util/scrap.ts b/packages/opencode/src/util/scrap.ts
new file mode 100644
index 000000000..16005acdc
--- /dev/null
+++ b/packages/opencode/src/util/scrap.ts
@@ -0,0 +1,5 @@
+export const foo: string = "42";
+
+export function dummyFunction(): void {
+ console.log("This is a dummy function");
+}
diff --git a/packages/opencode/test/tool/__snapshots__/tool.test.ts.snap b/packages/opencode/test/tool/__snapshots__/tool.test.ts.snap
new file mode 100644
index 000000000..12669e387
--- /dev/null
+++ b/packages/opencode/test/tool/__snapshots__/tool.test.ts.snap
@@ -0,0 +1,17 @@
+// Bun Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`tool.ls basic 1`] = `
+"- /home/thdxr/dev/projects/sst/opencode/js/example/
+ - home/
+ - thdxr/
+ - dev/
+ - projects/
+ - sst/
+ - opencode/
+ - js/
+ - example/
+ - ink.tsx
+ - broken.ts
+ - cli.ts
+"
+`;
diff --git a/packages/opencode/test/tool/tool.test.ts b/packages/opencode/test/tool/tool.test.ts
new file mode 100644
index 000000000..4b6d2efd3
--- /dev/null
+++ b/packages/opencode/test/tool/tool.test.ts
@@ -0,0 +1,55 @@
+import { describe, expect, test } from "bun:test";
+import { App } from "../../src/app/app";
+import { glob } from "../../src/tool/glob";
+import { ls } from "../../src/tool/ls";
+
+describe("tool.glob", () => {
+ test("truncate", async () => {
+ await App.provide({ directory: process.cwd() }, async () => {
+ let result = await glob.execute(
+ {
+ pattern: "./node_modules/**/*",
+ },
+ {
+ toolCallId: "test",
+ messages: [],
+ },
+ );
+ expect(result.metadata.truncated).toBe(true);
+ });
+ });
+ test("basic", async () => {
+ await App.provide({ directory: process.cwd() }, async () => {
+ let result = await glob.execute(
+ {
+ pattern: "*.json",
+ },
+ {
+ toolCallId: "test",
+ messages: [],
+ },
+ );
+ expect(result.metadata).toMatchObject({
+ truncated: false,
+ count: 2,
+ });
+ });
+ });
+});
+
+describe("tool.ls", () => {
+ test("basic", async () => {
+ const result = await App.provide({ directory: process.cwd() }, async () => {
+ return await ls.execute(
+ {
+ path: "./example",
+ },
+ {
+ toolCallId: "test",
+ messages: [],
+ },
+ );
+ });
+ expect(result.output).toMatchSnapshot();
+ });
+});
diff --git a/packages/opencode/tsconfig.json b/packages/opencode/tsconfig.json
new file mode 100644
index 000000000..65fa6c7f3
--- /dev/null
+++ b/packages/opencode/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@tsconfig/bun/tsconfig.json",
+ "compilerOptions": {}
+}
diff --git a/packages/tui/.goreleaser.yml b/packages/tui/.goreleaser.yml
new file mode 100644
index 000000000..1545199d5
--- /dev/null
+++ b/packages/tui/.goreleaser.yml
@@ -0,0 +1,77 @@
+version: 2
+project_name: opencode
+before:
+ hooks:
+builds:
+ - env:
+ - CGO_ENABLED=0
+ goos:
+ - linux
+ - darwin
+ goarch:
+ - amd64
+ - arm64
+ ldflags:
+ - -s -w -X github.com/sst/opencode/internal/version.Version={{.Version}}
+ main: ./main.go
+
+archives:
+ - format: tar.gz
+ name_template: >-
+ opencode-
+ {{- if eq .Os "darwin" }}mac-
+ {{- else if eq .Os "windows" }}windows-
+ {{- else if eq .Os "linux" }}linux-{{end}}
+ {{- if eq .Arch "amd64" }}x86_64
+ {{- else if eq .Arch "#86" }}i386
+ {{- else }}{{ .Arch }}{{ end }}
+ {{- if .Arm }}v{{ .Arm }}{{ end }}
+ format_overrides:
+ - goos: windows
+ format: zip
+checksum:
+ name_template: "checksums.txt"
+snapshot:
+ name_template: "0.0.0-{{ .Timestamp }}"
+aurs:
+ - name: opencode
+ homepage: "https://github.com/sst/opencode"
+ description: "terminal based agent that can build anything"
+ maintainers:
+ - "dax"
+ - "adam"
+ license: "MIT"
+ private_key: "{{ .Env.AUR_KEY }}"
+ git_url: "ssh://[email protected]/opencode-bin.git"
+ provides:
+ - opencode
+ conflicts:
+ - opencode
+ package: |-
+ install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"
+brews:
+ - repository:
+ owner: sst
+ name: homebrew-tap
+nfpms:
+ - maintainer: kujtimiihoxha
+ description: terminal based agent that can build anything
+ formats:
+ - deb
+ - rpm
+ file_name_template: >-
+ {{ .ProjectName }}-
+ {{- if eq .Os "darwin" }}mac
+ {{- else }}{{ .Os }}{{ end }}-{{ .Arch }}
+
+changelog:
+ sort: asc
+ filters:
+ exclude:
+ - "^docs:"
+ - "^doc:"
+ - "^test:"
+ - "^ci:"
+ - "^ignore:"
+ - "^example:"
+ - "^wip:"
diff --git a/packages/tui/app.log b/packages/tui/app.log
new file mode 100644
index 000000000..b985bc5f4
--- /dev/null
+++ b/packages/tui/app.log
@@ -0,0 +1,8 @@
+time=2025-05-30T19:37:27.576-04:00 level=DEBUG msg="Set theme from config" theme=opencode
+time=2025-05-30T19:37:27.580-04:00 level=INFO msg="Reading directory: /home/thdxr"
+time=2025-05-30T19:37:29.815-04:00 level=INFO msg="Cancelling all subscriptions"
+time=2025-05-30T19:37:29.815-04:00 level=INFO msg="subscription cancelled" name=status
+time=2025-05-30T19:37:29.815-04:00 level=INFO msg="All subscription goroutines completed successfully"
+time=2025-05-30T19:37:29.815-04:00 level=INFO msg="TUI message channel closed"
+time=2025-05-30T19:37:29.815-04:00 level=INFO msg="All goroutines cleaned up"
+time=2025-05-30T19:37:29.815-04:00 level=INFO msg="TUI exited" result="{width:272 height:73 currentPage:chat previousPage: pages:map[chat:0xc0002c4280] loadedPages:map[chat:true] status:{app:0xc0002aa690 queue:[] width:272 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002aa690 showPermissions:false permissions:0xc000279408 showHelp:false help:0xc00052da10 showQuit:true quit:0xc0004761f9 showSessionDialog:false sessionDialog:0xc0000adcc0 showCommandDialog:false commandDialog:0xc000429500 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc000261860 showInitDialog:true initDialog:{width:272 height:73 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d6c88 showThemeDialog:false themeDialog:0xc0000adf00 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000adf40}"
diff --git a/packages/tui/cmd/root.go b/packages/tui/cmd/root.go
new file mode 100644
index 000000000..85258d591
--- /dev/null
+++ b/packages/tui/cmd/root.go
@@ -0,0 +1,258 @@
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "sync"
+ "time"
+
+ "log/slog"
+
+ tea "github.com/charmbracelet/bubbletea"
+ zone "github.com/lrstanley/bubblezone"
+ "github.com/spf13/cobra"
+ "github.com/sst/opencode/internal/config"
+ "github.com/sst/opencode/internal/pubsub"
+ "github.com/sst/opencode/internal/tui"
+ "github.com/sst/opencode/internal/tui/app"
+ "github.com/sst/opencode/internal/version"
+)
+
+var rootCmd = &cobra.Command{
+ Use: "OpenCode",
+ Short: "A terminal AI assistant for software development",
+ Long: `OpenCode is a powerful terminal-based AI assistant that helps with software development tasks.
+It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
+to assist developers in writing, debugging, and understanding code directly from the terminal.`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ // If the help flag is set, show the help message
+ if cmd.Flag("help").Changed {
+ cmd.Help()
+ return nil
+ }
+ if cmd.Flag("version").Changed {
+ fmt.Println(version.Version)
+ return nil
+ }
+
+ // Setup logging
+ file, err := os.OpenFile("app.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
+ if err != nil {
+ panic(err)
+ }
+ defer file.Close()
+ logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
+ slog.SetDefault(logger)
+
+ // Load the config
+ debug, _ := cmd.Flags().GetBool("debug")
+ cwd, _ := cmd.Flags().GetString("cwd")
+ if cwd != "" {
+ err := os.Chdir(cwd)
+ if err != nil {
+ return fmt.Errorf("failed to change directory: %v", err)
+ }
+ }
+ if cwd == "" {
+ c, err := os.Getwd()
+ if err != nil {
+ return fmt.Errorf("failed to get current working directory: %v", err)
+ }
+ cwd = c
+ }
+ _, err = config.Load(cwd, debug)
+ if err != nil {
+ return err
+ }
+
+ // Create main context for the application
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ app, err := app.New(ctx)
+ if err != nil {
+ slog.Error("Failed to create app", "error", err)
+ return err
+ }
+
+ // Set up the TUI
+ zone.NewGlobal()
+ program := tea.NewProgram(
+ tui.New(app),
+ tea.WithAltScreen(),
+ )
+
+ evts, err := app.Events.Event(ctx)
+ if err != nil {
+ slog.Error("Failed to subscribe to events", "error", err)
+ return err
+ }
+
+ go func() {
+ for item := range evts {
+ program.Send(item)
+ }
+ }()
+
+ // Setup the subscriptions, this will send services events to the TUI
+ ch, cancelSubs := setupSubscriptions(app, ctx)
+
+ // Create a context for the TUI message handler
+ tuiCtx, tuiCancel := context.WithCancel(ctx)
+ var tuiWg sync.WaitGroup
+ tuiWg.Add(1)
+
+ // Set up message handling for the TUI
+ go func() {
+ defer tuiWg.Done()
+ // defer logging.RecoverPanic("TUI-message-handler", func() {
+ // attemptTUIRecovery(program)
+ // })
+
+ for {
+ select {
+ case <-tuiCtx.Done():
+ slog.Info("TUI message handler shutting down")
+ return
+ case msg, ok := <-ch:
+ if !ok {
+ slog.Info("TUI message channel closed")
+ return
+ }
+ program.Send(msg)
+ }
+ }
+ }()
+
+ // Cleanup function for when the program exits
+ cleanup := func() {
+ // Cancel subscriptions first
+ cancelSubs()
+
+ // Then shutdown the app
+ app.Shutdown()
+
+ // Then cancel TUI message handler
+ tuiCancel()
+
+ // Wait for TUI message handler to finish
+ tuiWg.Wait()
+
+ slog.Info("All goroutines cleaned up")
+ }
+
+ // Run the TUI
+ result, err := program.Run()
+ cleanup()
+
+ if err != nil {
+ slog.Error("TUI error", "error", err)
+ return fmt.Errorf("TUI error: %v", err)
+ }
+
+ slog.Info("TUI exited", "result", result)
+ return nil
+ },
+}
+
+func setupSubscriber[T any](
+ ctx context.Context,
+ wg *sync.WaitGroup,
+ name string,
+ subscriber func(context.Context) <-chan pubsub.Event[T],
+ outputCh chan<- tea.Msg,
+) {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ // defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
+
+ subCh := subscriber(ctx)
+ if subCh == nil {
+ slog.Warn("subscription channel is nil", "name", name)
+ return
+ }
+
+ for {
+ select {
+ case event, ok := <-subCh:
+ if !ok {
+ slog.Info("subscription channel closed", "name", name)
+ return
+ }
+
+ var msg tea.Msg = event
+
+ select {
+ case outputCh <- msg:
+ case <-time.After(2 * time.Second):
+ slog.Warn("message dropped due to slow consumer", "name", name)
+ case <-ctx.Done():
+ slog.Info("subscription cancelled", "name", name)
+ return
+ }
+ case <-ctx.Done():
+ slog.Info("subscription cancelled", "name", name)
+ return
+ }
+ }
+ }()
+}
+
+func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, func()) {
+ ch := make(chan tea.Msg, 100)
+
+ wg := sync.WaitGroup{}
+ ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
+
+ setupSubscriber(ctx, &wg, "status", app.Status.Subscribe, ch)
+
+ cleanupFunc := func() {
+ slog.Info("Cancelling all subscriptions")
+ cancel() // Signal all goroutines to stop
+
+ waitCh := make(chan struct{})
+ go func() {
+ // defer logging.RecoverPanic("subscription-cleanup", nil)
+ wg.Wait()
+ close(waitCh)
+ }()
+
+ select {
+ case <-waitCh:
+ slog.Info("All subscription goroutines completed successfully")
+ close(ch) // Only close after all writers are confirmed done
+ case <-time.After(5 * time.Second):
+ slog.Warn("Timed out waiting for some subscription goroutines to complete")
+ close(ch)
+ }
+ }
+ return ch, cleanupFunc
+}
+
+func Execute() {
+ err := rootCmd.Execute()
+ if err != nil {
+ os.Exit(1)
+ }
+}
+
+func init() {
+ rootCmd.Flags().BoolP("help", "h", false, "Help")
+ rootCmd.Flags().BoolP("version", "v", false, "Version")
+ rootCmd.Flags().BoolP("debug", "d", false, "Debug")
+ rootCmd.Flags().StringP("cwd", "c", "", "Current working directory")
+ rootCmd.Flags().StringP("prompt", "p", "", "Run a single prompt in non-interactive mode")
+ rootCmd.Flags().StringP("output-format", "f", "text", "Output format for non-interactive mode (text, json)")
+ rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
+ rootCmd.Flags().BoolP("verbose", "", false, "Display logs to stderr in non-interactive mode")
+ rootCmd.Flags().StringSlice("allowedTools", nil, "Restrict the agent to only use the specified tools in non-interactive mode (comma-separated list)")
+ rootCmd.Flags().StringSlice("excludedTools", nil, "Prevent the agent from using the specified tools in non-interactive mode (comma-separated list)")
+
+ // Make allowedTools and excludedTools mutually exclusive
+ rootCmd.MarkFlagsMutuallyExclusive("allowedTools", "excludedTools")
+
+ // Make quiet and verbose mutually exclusive
+ rootCmd.MarkFlagsMutuallyExclusive("quiet", "verbose")
+}
diff --git a/packages/tui/go.mod b/packages/tui/go.mod
new file mode 100644
index 000000000..ebd00828b
--- /dev/null
+++ b/packages/tui/go.mod
@@ -0,0 +1,105 @@
+module github.com/sst/opencode
+
+go 1.24.0
+
+require (
+ github.com/alecthomas/chroma/v2 v2.15.0
+ github.com/aymanbagabas/go-udiff v0.2.0
+ github.com/bmatcuk/doublestar/v4 v4.8.1
+ github.com/catppuccin/go v0.3.0
+ github.com/charmbracelet/bubbles v0.21.0
+ github.com/charmbracelet/bubbletea v1.3.4
+ github.com/charmbracelet/glamour v0.9.1
+ github.com/charmbracelet/lipgloss v1.1.0
+ github.com/charmbracelet/x/ansi v0.8.0
+ github.com/lithammer/fuzzysearch v1.1.8
+ github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
+ github.com/muesli/reflow v0.3.0
+ github.com/muesli/termenv v0.16.0
+ github.com/oapi-codegen/runtime v1.1.1
+ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
+ github.com/spf13/cobra v1.9.1
+ github.com/spf13/viper v1.20.0
+ github.com/stretchr/testify v1.10.0
+ rsc.io/qr v0.2.0
+)
+
+require golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
+
+require (
+ dario.cat/mergo v1.0.2 // indirect
+ github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
+ github.com/atombender/go-jsonschema v0.20.0 // indirect
+ github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
+ github.com/fsnotify/fsnotify v1.8.0 // indirect
+ github.com/getkin/kin-openapi v0.127.0 // indirect
+ github.com/go-openapi/jsonpointer v0.21.0 // indirect
+ github.com/go-openapi/swag v0.23.0 // indirect
+ github.com/goccy/go-yaml v1.17.1 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/invopop/yaml v0.3.1 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
+ github.com/mitchellh/go-wordwrap v1.0.1 // indirect
+ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
+ github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 // indirect
+ github.com/perimeterx/marshmallow v1.1.5 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/sanity-io/litter v1.5.8 // indirect
+ github.com/sosodev/duration v1.3.1 // indirect
+ github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect
+ github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
+ golang.org/x/mod v0.24.0 // indirect
+ golang.org/x/tools v0.31.0 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
+)
+
+require (
+ github.com/atotto/clipboard v0.1.4
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/aymerick/douceur v0.2.0 // indirect
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
+ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
+ github.com/charmbracelet/x/term v0.2.1 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/disintegration/imaging v1.6.2
+ github.com/dlclark/regexp2 v1.11.4 // indirect
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+ github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
+ github.com/google/go-cmp v0.7.0 // indirect
+ github.com/gorilla/css v1.0.1 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-localereader v0.0.1 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
+ github.com/microcosm-cc/bluemonday v1.0.27 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.3 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/rogpeppe/go-internal v1.14.1 // indirect
+ github.com/sagikazarmark/locafero v0.7.0 // indirect
+ github.com/sourcegraph/conc v0.3.0 // indirect
+ github.com/spf13/afero v1.12.0 // indirect
+ github.com/spf13/cast v1.7.1 // indirect
+ github.com/spf13/pflag v1.0.6 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ github.com/yuin/goldmark v1.7.8 // indirect
+ github.com/yuin/goldmark-emoji v1.0.5 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ golang.org/x/image v0.26.0
+ golang.org/x/net v0.39.0 // indirect
+ golang.org/x/sync v0.13.0 // indirect
+ golang.org/x/sys v0.32.0 // indirect
+ golang.org/x/term v0.31.0 // indirect
+ golang.org/x/text v0.24.0
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
+
+tool (
+ github.com/atombender/go-jsonschema
+ github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
+)
diff --git a/packages/tui/go.sum b/packages/tui/go.sum
new file mode 100644
index 000000000..c11a945ac
--- /dev/null
+++ b/packages/tui/go.sum
@@ -0,0 +1,338 @@
+dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
+dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
+github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
+github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
+github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
+github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
+github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
+github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
+github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
+github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
+github.com/atombender/go-jsonschema v0.20.0 h1:AHg0LeI0HcjQ686ALwUNqVJjNRcSXpIR6U+wC2J0aFY=
+github.com/atombender/go-jsonschema v0.20.0/go.mod h1:ZmbuR11v2+cMM0PdP6ySxtyZEGFBmhgF4xa4J6Hdls8=
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
+github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
+github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
+github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
+github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
+github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
+github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
+github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
+github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
+github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
+github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
+github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM=
+github.com/charmbracelet/glamour v0.9.1/go.mod h1:+SHvIS8qnwhgTpVMiXwn7OfGomSqff1cHBCI8jLOetk=
+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
+github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
+github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
+github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
+github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
+github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
+github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
+github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
+github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w=
+github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
+github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY=
+github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
+github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
+github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
+github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
+github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
+github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
+github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231 h1:9rjt7AfnrXKNSZhp36A3/4QAZAwGGCGD/p8Bse26zms=
+github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231/go.mod h1:S5etECMx+sZnW0Gm100Ma9J1PgVCTgNyFaqGu2b08b4=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
+github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
+github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
+github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 h1:ykgG34472DWey7TSjd8vIfNykXgjOgYJZoQbKfEeY/Q=
+github.com/oapi-codegen/oapi-codegen/v2 v2.4.1/go.mod h1:N5+lY1tiTDV3V1BeHtOxeWXHoPVeApvsvjJqegfoaz8=
+github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
+github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
+github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
+github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
+github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
+github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
+github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
+github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
+github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
+github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
+github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
+github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
+github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
+github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
+github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/speakeasy-api/openapi-overlay v0.9.0 h1:Wrz6NO02cNlLzx1fB093lBlYxSI54VRhy1aSutx0PQg=
+github.com/speakeasy-api/openapi-overlay v0.9.0/go.mod h1:f5FloQrHA7MsxYg9djzMD5h6dxrHjVVByWKh7an8TRc=
+github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
+github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
+github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
+github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
+github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
+github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
+github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
+github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
+golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
+golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
+golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
+golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
+golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
+rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
diff --git a/packages/tui/internal/completions/files-folders.go b/packages/tui/internal/completions/files-folders.go
new file mode 100644
index 000000000..a405b34cd
--- /dev/null
+++ b/packages/tui/internal/completions/files-folders.go
@@ -0,0 +1,191 @@
+package completions
+
+import (
+ "bytes"
+ "fmt"
+ "os/exec"
+ "path/filepath"
+
+ "github.com/lithammer/fuzzysearch/fuzzy"
+ "github.com/sst/opencode/internal/fileutil"
+ "github.com/sst/opencode/internal/status"
+ "github.com/sst/opencode/internal/tui/components/dialog"
+)
+
+type filesAndFoldersContextGroup struct {
+ prefix string
+}
+
+func (cg *filesAndFoldersContextGroup) GetId() string {
+ return cg.prefix
+}
+
+func (cg *filesAndFoldersContextGroup) GetEntry() dialog.CompletionItemI {
+ return dialog.NewCompletionItem(dialog.CompletionItem{
+ Title: "Files & Folders",
+ Value: "files",
+ })
+}
+
+func processNullTerminatedOutput(outputBytes []byte) []string {
+ if len(outputBytes) > 0 && outputBytes[len(outputBytes)-1] == 0 {
+ outputBytes = outputBytes[:len(outputBytes)-1]
+ }
+
+ if len(outputBytes) == 0 {
+ return []string{}
+ }
+
+ split := bytes.Split(outputBytes, []byte{0})
+ matches := make([]string, 0, len(split))
+
+ for _, p := range split {
+ if len(p) == 0 {
+ continue
+ }
+
+ path := string(p)
+ path = filepath.Join(".", path)
+
+ if !fileutil.SkipHidden(path) {
+ matches = append(matches, path)
+ }
+ }
+
+ return matches
+}
+
+func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) {
+ cmdRg := fileutil.GetRgCmd("") // No glob pattern for this use case
+ cmdFzf := fileutil.GetFzfCmd(query)
+
+ var matches []string
+ // Case 1: Both rg and fzf available
+ if cmdRg != nil && cmdFzf != nil {
+ rgPipe, err := cmdRg.StdoutPipe()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get rg stdout pipe: %w", err)
+ }
+ defer rgPipe.Close()
+
+ cmdFzf.Stdin = rgPipe
+ var fzfOut bytes.Buffer
+ var fzfErr bytes.Buffer
+ cmdFzf.Stdout = &fzfOut
+ cmdFzf.Stderr = &fzfErr
+
+ if err := cmdFzf.Start(); err != nil {
+ return nil, fmt.Errorf("failed to start fzf: %w", err)
+ }
+
+ errRg := cmdRg.Run()
+ errFzf := cmdFzf.Wait()
+
+ if errRg != nil {
+ status.Warn(fmt.Sprintf("rg command failed during pipe: %v", errRg))
+ }
+
+ if errFzf != nil {
+ if exitErr, ok := errFzf.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
+ return []string{}, nil // No matches from fzf
+ }
+ return nil, fmt.Errorf("fzf command failed: %w\nStderr: %s", errFzf, fzfErr.String())
+ }
+
+ matches = processNullTerminatedOutput(fzfOut.Bytes())
+
+ // Case 2: Only rg available
+ } else if cmdRg != nil {
+ status.Debug("Using Ripgrep with fuzzy match fallback for file completions")
+ var rgOut bytes.Buffer
+ var rgErr bytes.Buffer
+ cmdRg.Stdout = &rgOut
+ cmdRg.Stderr = &rgErr
+
+ if err := cmdRg.Run(); err != nil {
+ return nil, fmt.Errorf("rg command failed: %w\nStderr: %s", err, rgErr.String())
+ }
+
+ allFiles := processNullTerminatedOutput(rgOut.Bytes())
+ matches = fuzzy.Find(query, allFiles)
+
+ // Case 3: Only fzf available
+ } else if cmdFzf != nil {
+ status.Debug("Using FZF with doublestar fallback for file completions")
+ files, _, err := fileutil.GlobWithDoublestar("**/*", ".", 0)
+ if err != nil {
+ return nil, fmt.Errorf("failed to list files for fzf: %w", err)
+ }
+
+ allFiles := make([]string, 0, len(files))
+ for _, file := range files {
+ if !fileutil.SkipHidden(file) {
+ allFiles = append(allFiles, file)
+ }
+ }
+
+ var fzfIn bytes.Buffer
+ for _, file := range allFiles {
+ fzfIn.WriteString(file)
+ fzfIn.WriteByte(0)
+ }
+
+ cmdFzf.Stdin = &fzfIn
+ var fzfOut bytes.Buffer
+ var fzfErr bytes.Buffer
+ cmdFzf.Stdout = &fzfOut
+ cmdFzf.Stderr = &fzfErr
+
+ if err := cmdFzf.Run(); err != nil {
+ if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
+ return []string{}, nil
+ }
+ return nil, fmt.Errorf("fzf command failed: %w\nStderr: %s", err, fzfErr.String())
+ }
+
+ matches = processNullTerminatedOutput(fzfOut.Bytes())
+
+ // Case 4: Fallback to doublestar with fuzzy match
+ } else {
+ status.Debug("Using doublestar with fuzzy match for file completions")
+ allFiles, _, err := fileutil.GlobWithDoublestar("**/*", ".", 0)
+ if err != nil {
+ return nil, fmt.Errorf("failed to glob files: %w", err)
+ }
+
+ filteredFiles := make([]string, 0, len(allFiles))
+ for _, file := range allFiles {
+ if !fileutil.SkipHidden(file) {
+ filteredFiles = append(filteredFiles, file)
+ }
+ }
+
+ matches = fuzzy.Find(query, filteredFiles)
+ }
+
+ return matches, nil
+}
+
+func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
+ matches, err := cg.getFiles(query)
+ if err != nil {
+ return nil, err
+ }
+
+ items := make([]dialog.CompletionItemI, 0, len(matches))
+ for _, file := range matches {
+ item := dialog.NewCompletionItem(dialog.CompletionItem{
+ Title: file,
+ Value: file,
+ })
+ items = append(items, item)
+ }
+
+ return items, nil
+}
+
+func NewFileAndFolderContextGroup() dialog.CompletionProvider {
+ return &filesAndFoldersContextGroup{
+ prefix: "file",
+ }
+}
diff --git a/packages/tui/internal/config/config.go b/packages/tui/internal/config/config.go
new file mode 100644
index 000000000..2197f5aa4
--- /dev/null
+++ b/packages/tui/internal/config/config.go
@@ -0,0 +1,266 @@
+// Package config manages application configuration from various sources.
+package config
+
+import (
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "os"
+ "os/user"
+ "path/filepath"
+ "strings"
+
+ "github.com/spf13/viper"
+)
+
+// Data defines storage configuration.
+type Data struct {
+ Directory string `json:"directory,omitempty"`
+}
+
+// TUIConfig defines the configuration for the Terminal User Interface.
+type TUIConfig struct {
+ Theme string `json:"theme,omitempty"`
+ CustomTheme map[string]any `json:"customTheme,omitempty"`
+}
+
+// ShellConfig defines the configuration for the shell used by the bash tool.
+type ShellConfig struct {
+ Path string `json:"path,omitempty"`
+ Args []string `json:"args,omitempty"`
+}
+
+// Config is the main configuration structure for the application.
+type Config struct {
+ Data Data `json:"data"`
+ WorkingDir string `json:"wd,omitempty"`
+ Debug bool `json:"debug,omitempty"`
+ DebugLSP bool `json:"debugLSP,omitempty"`
+ ContextPaths []string `json:"contextPaths,omitempty"`
+ TUI TUIConfig `json:"tui"`
+ Shell ShellConfig `json:"shell,omitempty"`
+}
+
+// Application constants
+const (
+ defaultDataDirectory = ".opencode"
+ defaultLogLevel = "info"
+ appName = "opencode"
+
+ MaxTokensFallbackDefault = 4096
+)
+
+var defaultContextPaths = []string{
+ ".github/copilot-instructions.md",
+ ".cursorrules",
+ ".cursor/rules/",
+ "CLAUDE.md",
+ "CLAUDE.local.md",
+ "CONTEXT.md",
+ "CONTEXT.local.md",
+ "opencode.md",
+ "opencode.local.md",
+ "OpenCode.md",
+ "OpenCode.local.md",
+ "OPENCODE.md",
+ "OPENCODE.local.md",
+}
+
+// Global configuration instance
+var cfg *Config
+
+// Load initializes the configuration from environment variables and config files.
+// If debug is true, debug mode is enabled and log level is set to debug.
+// It returns an error if configuration loading fails.
+func Load(workingDir string, debug bool) (*Config, error) {
+ if cfg != nil {
+ return cfg, nil
+ }
+
+ cfg = &Config{
+ WorkingDir: workingDir,
+ }
+
+ configureViper()
+ setDefaults(debug)
+
+ // Read global config
+ if err := readConfig(viper.ReadInConfig()); err != nil {
+ return cfg, err
+ }
+
+ // Load and merge local config
+ mergeLocalConfig(workingDir)
+
+ // Apply configuration to the struct
+ if err := viper.Unmarshal(cfg); err != nil {
+ return cfg, fmt.Errorf("failed to unmarshal config: %w", err)
+ }
+
+ defaultLevel := slog.LevelInfo
+ if cfg.Debug {
+ defaultLevel = slog.LevelDebug
+ }
+ slog.SetLogLoggerLevel(defaultLevel)
+
+ // Validate configuration
+ if err := Validate(); err != nil {
+ return cfg, fmt.Errorf("config validation failed: %w", err)
+ }
+ return cfg, nil
+}
+
+// configureViper sets up viper's configuration paths and environment variables.
+func configureViper() {
+ viper.SetConfigName(fmt.Sprintf(".%s", appName))
+ viper.SetConfigType("json")
+ viper.AddConfigPath("$HOME")
+ viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
+ viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
+ viper.SetEnvPrefix(strings.ToUpper(appName))
+ viper.AutomaticEnv()
+}
+
+// setDefaults configures default values for configuration options.
+func setDefaults(debug bool) {
+ viper.SetDefault("data.directory", defaultDataDirectory)
+ viper.SetDefault("contextPaths", defaultContextPaths)
+ viper.SetDefault("tui.theme", "opencode")
+
+ if debug {
+ viper.SetDefault("debug", true)
+ viper.Set("log.level", "debug")
+ } else {
+ viper.SetDefault("debug", false)
+ viper.SetDefault("log.level", defaultLogLevel)
+ }
+}
+
+// readConfig handles the result of reading a configuration file.
+func readConfig(err error) error {
+ if err == nil {
+ return nil
+ }
+
+ // It's okay if the config file doesn't exist
+ if _, ok := err.(viper.ConfigFileNotFoundError); ok {
+ return nil
+ }
+
+ return fmt.Errorf("failed to read config: %w", err)
+}
+
+// mergeLocalConfig loads and merges configuration from the local directory.
+func mergeLocalConfig(workingDir string) {
+ local := viper.New()
+ local.SetConfigName(fmt.Sprintf(".%s", appName))
+ local.SetConfigType("json")
+ local.AddConfigPath(workingDir)
+
+ // Merge local config if it exists
+ if err := local.ReadInConfig(); err == nil {
+ viper.MergeConfigMap(local.AllSettings())
+ }
+}
+
+// Validate checks if the configuration is valid and applies defaults where needed.
+func Validate() error {
+ if cfg == nil {
+ return fmt.Errorf("config not loaded")
+ }
+
+ return nil
+}
+
+// Get returns the current configuration.
+// It's safe to call this function multiple times.
+func Get() *Config {
+ return cfg
+}
+
+// WorkingDirectory returns the current working directory from the configuration.
+func WorkingDirectory() string {
+ if cfg == nil {
+ panic("config not loaded")
+ }
+ return cfg.WorkingDir
+}
+
+// GetHostname returns the system hostname or "User" if it can't be determined
+func GetHostname() (string, error) {
+ hostname, err := os.Hostname()
+ if err != nil {
+ return "User", err
+ }
+ return hostname, nil
+}
+
+// GetUsername returns the current user's username
+func GetUsername() (string, error) {
+ currentUser, err := user.Current()
+ if err != nil {
+ return "User", err
+ }
+ return currentUser.Username, nil
+}
+
+func updateCfgFile(updateCfg func(config *Config)) error {
+ if cfg == nil {
+ return fmt.Errorf("config not loaded")
+ }
+
+ // Get the config file path
+ configFile := viper.ConfigFileUsed()
+ var configData []byte
+ if configFile == "" {
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ return fmt.Errorf("failed to get home directory: %w", err)
+ }
+ configFile = filepath.Join(homeDir, fmt.Sprintf(".%s.json", appName))
+ slog.Info("config file not found, creating new one", "path", configFile)
+ configData = []byte(`{}`)
+ } else {
+ // Read the existing config file
+ data, err := os.ReadFile(configFile)
+ if err != nil {
+ return fmt.Errorf("failed to read config file: %w", err)
+ }
+ configData = data
+ }
+
+ // Parse the JSON
+ var userCfg *Config
+ if err := json.Unmarshal(configData, &userCfg); err != nil {
+ return fmt.Errorf("failed to parse config file: %w", err)
+ }
+
+ updateCfg(userCfg)
+
+ // Write the updated config back to file
+ updatedData, err := json.MarshalIndent(userCfg, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to marshal config: %w", err)
+ }
+
+ if err := os.WriteFile(configFile, updatedData, 0o644); err != nil {
+ return fmt.Errorf("failed to write config file: %w", err)
+ }
+
+ return nil
+}
+
+// UpdateTheme updates the theme in the configuration and writes it to the config file.
+func UpdateTheme(themeName string) error {
+ if cfg == nil {
+ return fmt.Errorf("config not loaded")
+ }
+
+ // Update the in-memory config
+ cfg.TUI.Theme = themeName
+
+ // Update the file config
+ return updateCfgFile(func(config *Config) {
+ config.TUI.Theme = themeName
+ })
+}
diff --git a/packages/tui/internal/config/init.go b/packages/tui/internal/config/init.go
new file mode 100644
index 000000000..5f8860f52
--- /dev/null
+++ b/packages/tui/internal/config/init.go
@@ -0,0 +1,60 @@
+package config
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+)
+
+const (
+ // InitFlagFilename is the name of the file that indicates whether the project has been initialized
+ InitFlagFilename = "init"
+)
+
+// ProjectInitFlag represents the initialization status for a project directory
+type ProjectInitFlag struct {
+ Initialized bool `json:"initialized"`
+}
+
+// ShouldShowInitDialog checks if the initialization dialog should be shown for the current directory
+func ShouldShowInitDialog() (bool, error) {
+ if cfg == nil {
+ return false, fmt.Errorf("config not loaded")
+ }
+
+ // Create the flag file path
+ flagFilePath := filepath.Join(cfg.Data.Directory, InitFlagFilename)
+
+ // Check if the flag file exists
+ _, err := os.Stat(flagFilePath)
+ if err == nil {
+ // File exists, don't show the dialog
+ return false, nil
+ }
+
+ // If the error is not "file not found", return the error
+ if !os.IsNotExist(err) {
+ return false, fmt.Errorf("failed to check init flag file: %w", err)
+ }
+
+ // File doesn't exist, show the dialog
+ return true, nil
+}
+
+// MarkProjectInitialized marks the current project as initialized
+func MarkProjectInitialized() error {
+ if cfg == nil {
+ return fmt.Errorf("config not loaded")
+ }
+ // Create the flag file path
+ flagFilePath := filepath.Join(cfg.Data.Directory, InitFlagFilename)
+
+ // Create an empty file to mark the project as initialized
+ file, err := os.Create(flagFilePath)
+ if err != nil {
+ return fmt.Errorf("failed to create init flag file: %w", err)
+ }
+ defer file.Close()
+
+ return nil
+}
diff --git a/packages/tui/internal/diff/diff.go b/packages/tui/internal/diff/diff.go
new file mode 100644
index 000000000..350db664a
--- /dev/null
+++ b/packages/tui/internal/diff/diff.go
@@ -0,0 +1,869 @@
+package diff
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/alecthomas/chroma/v2"
+ "github.com/alecthomas/chroma/v2/formatters"
+ "github.com/alecthomas/chroma/v2/lexers"
+ "github.com/alecthomas/chroma/v2/styles"
+ "github.com/aymanbagabas/go-udiff"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/x/ansi"
+ "github.com/sergi/go-diff/diffmatchpatch"
+ "github.com/sst/opencode/internal/config"
+ "github.com/sst/opencode/internal/tui/theme"
+)
+
+// -------------------------------------------------------------------------
+// Core Types
+// -------------------------------------------------------------------------
+
+// LineType represents the kind of line in a diff.
+type LineType int
+
+const (
+ LineContext LineType = iota // Line exists in both files
+ LineAdded // Line added in the new file
+ LineRemoved // Line removed from the old file
+)
+
+// Segment represents a portion of a line for intra-line highlighting
+type Segment struct {
+ Start int
+ End int
+ Type LineType
+ Text string
+}
+
+// DiffLine represents a single line in a diff
+type DiffLine struct {
+ OldLineNo int // Line number in old file (0 for added lines)
+ NewLineNo int // Line number in new file (0 for removed lines)
+ Kind LineType // Type of line (added, removed, context)
+ Content string // Content of the line
+ Segments []Segment // Segments for intraline highlighting
+}
+
+// Hunk represents a section of changes in a diff
+type Hunk struct {
+ Header string
+ Lines []DiffLine
+}
+
+// DiffResult contains the parsed result of a diff
+type DiffResult struct {
+ OldFile string
+ NewFile string
+ Hunks []Hunk
+}
+
+// linePair represents a pair of lines for side-by-side display
+type linePair struct {
+ left *DiffLine
+ right *DiffLine
+}
+
+// -------------------------------------------------------------------------
+// Parse Configuration
+// -------------------------------------------------------------------------
+
+// ParseConfig configures the behavior of diff parsing
+type ParseConfig struct {
+ ContextSize int // Number of context lines to include
+}
+
+// ParseOption modifies a ParseConfig
+type ParseOption func(*ParseConfig)
+
+// WithContextSize sets the number of context lines to include
+func WithContextSize(size int) ParseOption {
+ return func(p *ParseConfig) {
+ if size >= 0 {
+ p.ContextSize = size
+ }
+ }
+}
+
+// -------------------------------------------------------------------------
+// Side-by-Side Configuration
+// -------------------------------------------------------------------------
+
+// SideBySideConfig configures the rendering of side-by-side diffs
+type SideBySideConfig struct {
+ TotalWidth int
+}
+
+// SideBySideOption modifies a SideBySideConfig
+type SideBySideOption func(*SideBySideConfig)
+
+// NewSideBySideConfig creates a SideBySideConfig with default values
+func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig {
+ config := SideBySideConfig{
+ TotalWidth: 160, // Default width for side-by-side view
+ }
+
+ for _, opt := range opts {
+ opt(&config)
+ }
+
+ return config
+}
+
+// WithTotalWidth sets the total width for side-by-side view
+func WithTotalWidth(width int) SideBySideOption {
+ return func(s *SideBySideConfig) {
+ if width > 0 {
+ s.TotalWidth = width
+ }
+ }
+}
+
+// -------------------------------------------------------------------------
+// Diff Parsing
+// -------------------------------------------------------------------------
+
+// ParseUnifiedDiff parses a unified diff format string into structured data
+func ParseUnifiedDiff(diff string) (DiffResult, error) {
+ var result DiffResult
+ var currentHunk *Hunk
+
+ hunkHeaderRe := regexp.MustCompile(`^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@`)
+ lines := strings.Split(diff, "\n")
+
+ var oldLine, newLine int
+ inFileHeader := true
+
+ for _, line := range lines {
+ // Parse file headers
+ if inFileHeader {
+ if strings.HasPrefix(line, "--- a/") {
+ result.OldFile = strings.TrimPrefix(line, "--- a/")
+ continue
+ }
+ if strings.HasPrefix(line, "+++ b/") {
+ result.NewFile = strings.TrimPrefix(line, "+++ b/")
+ inFileHeader = false
+ continue
+ }
+ }
+
+ // Parse hunk headers
+ if matches := hunkHeaderRe.FindStringSubmatch(line); matches != nil {
+ if currentHunk != nil {
+ result.Hunks = append(result.Hunks, *currentHunk)
+ }
+ currentHunk = &Hunk{
+ Header: line,
+ Lines: []DiffLine{},
+ }
+
+ oldStart, _ := strconv.Atoi(matches[1])
+ newStart, _ := strconv.Atoi(matches[3])
+ oldLine = oldStart
+ newLine = newStart
+ continue
+ }
+
+ // Ignore "No newline at end of file" markers
+ if strings.HasPrefix(line, "\\ No newline at end of file") {
+ continue
+ }
+
+ if currentHunk == nil {
+ continue
+ }
+
+ // Process the line based on its prefix
+ if len(line) > 0 {
+ switch line[0] {
+ case '+':
+ currentHunk.Lines = append(currentHunk.Lines, DiffLine{
+ OldLineNo: 0,
+ NewLineNo: newLine,
+ Kind: LineAdded,
+ Content: line[1:],
+ })
+ newLine++
+ case '-':
+ currentHunk.Lines = append(currentHunk.Lines, DiffLine{
+ OldLineNo: oldLine,
+ NewLineNo: 0,
+ Kind: LineRemoved,
+ Content: line[1:],
+ })
+ oldLine++
+ default:
+ currentHunk.Lines = append(currentHunk.Lines, DiffLine{
+ OldLineNo: oldLine,
+ NewLineNo: newLine,
+ Kind: LineContext,
+ Content: line,
+ })
+ oldLine++
+ newLine++
+ }
+ } else {
+ // Handle empty lines
+ currentHunk.Lines = append(currentHunk.Lines, DiffLine{
+ OldLineNo: oldLine,
+ NewLineNo: newLine,
+ Kind: LineContext,
+ Content: "",
+ })
+ oldLine++
+ newLine++
+ }
+ }
+
+ // Add the last hunk if there is one
+ if currentHunk != nil {
+ result.Hunks = append(result.Hunks, *currentHunk)
+ }
+
+ return result, nil
+}
+
+// HighlightIntralineChanges updates lines in a hunk to show character-level differences
+func HighlightIntralineChanges(h *Hunk) {
+ var updated []DiffLine
+ dmp := diffmatchpatch.New()
+
+ for i := 0; i < len(h.Lines); i++ {
+ // Look for removed line followed by added line
+ if i+1 < len(h.Lines) &&
+ h.Lines[i].Kind == LineRemoved &&
+ h.Lines[i+1].Kind == LineAdded {
+
+ oldLine := h.Lines[i]
+ newLine := h.Lines[i+1]
+
+ // Find character-level differences
+ patches := dmp.DiffMain(oldLine.Content, newLine.Content, false)
+ patches = dmp.DiffCleanupSemantic(patches)
+ patches = dmp.DiffCleanupMerge(patches)
+ patches = dmp.DiffCleanupEfficiency(patches)
+
+ segments := make([]Segment, 0)
+
+ removeStart := 0
+ addStart := 0
+ for _, patch := range patches {
+ switch patch.Type {
+ case diffmatchpatch.DiffDelete:
+ segments = append(segments, Segment{
+ Start: removeStart,
+ End: removeStart + len(patch.Text),
+ Type: LineRemoved,
+ Text: patch.Text,
+ })
+ removeStart += len(patch.Text)
+ case diffmatchpatch.DiffInsert:
+ segments = append(segments, Segment{
+ Start: addStart,
+ End: addStart + len(patch.Text),
+ Type: LineAdded,
+ Text: patch.Text,
+ })
+ addStart += len(patch.Text)
+ default:
+ // Context text, no highlighting needed
+ removeStart += len(patch.Text)
+ addStart += len(patch.Text)
+ }
+ }
+ oldLine.Segments = segments
+ newLine.Segments = segments
+
+ updated = append(updated, oldLine, newLine)
+ i++ // Skip the next line as we've already processed it
+ } else {
+ updated = append(updated, h.Lines[i])
+ }
+ }
+
+ h.Lines = updated
+}
+
+// pairLines converts a flat list of diff lines to pairs for side-by-side display
+func pairLines(lines []DiffLine) []linePair {
+ var pairs []linePair
+ i := 0
+
+ for i < len(lines) {
+ switch lines[i].Kind {
+ case LineRemoved:
+ // Check if the next line is an addition, if so pair them
+ if i+1 < len(lines) && lines[i+1].Kind == LineAdded {
+ pairs = append(pairs, linePair{left: &lines[i], right: &lines[i+1]})
+ i += 2
+ } else {
+ pairs = append(pairs, linePair{left: &lines[i], right: nil})
+ i++
+ }
+ case LineAdded:
+ pairs = append(pairs, linePair{left: nil, right: &lines[i]})
+ i++
+ case LineContext:
+ pairs = append(pairs, linePair{left: &lines[i], right: &lines[i]})
+ i++
+ }
+ }
+
+ return pairs
+}
+
+// -------------------------------------------------------------------------
+// Syntax Highlighting
+// -------------------------------------------------------------------------
+
+// SyntaxHighlight applies syntax highlighting to text based on file extension
+func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipgloss.TerminalColor) error {
+ t := theme.CurrentTheme()
+
+ // Determine the language lexer to use
+ l := lexers.Match(fileName)
+ if l == nil {
+ l = lexers.Analyse(source)
+ }
+ if l == nil {
+ l = lexers.Fallback
+ }
+ l = chroma.Coalesce(l)
+
+ // Get the formatter
+ f := formatters.Get(formatter)
+ if f == nil {
+ f = formatters.Fallback
+ }
+
+ // Dynamic theme based on current theme values
+ syntaxThemeXml := fmt.Sprintf(`
+ <style name="opencode-theme">
+ <!-- Base colors -->
+ <entry type="Background" style="bg:%s"/>
+ <entry type="Text" style="%s"/>
+ <entry type="Other" style="%s"/>
+ <entry type="Error" style="%s"/>
+ <!-- Keywords -->
+ <entry type="Keyword" style="%s"/>
+ <entry type="KeywordConstant" style="%s"/>
+ <entry type="KeywordDeclaration" style="%s"/>
+ <entry type="KeywordNamespace" style="%s"/>
+ <entry type="KeywordPseudo" style="%s"/>
+ <entry type="KeywordReserved" style="%s"/>
+ <entry type="KeywordType" style="%s"/>
+ <!-- Names -->
+ <entry type="Name" style="%s"/>
+ <entry type="NameAttribute" style="%s"/>
+ <entry type="NameBuiltin" style="%s"/>
+ <entry type="NameBuiltinPseudo" style="%s"/>
+ <entry type="NameClass" style="%s"/>
+ <entry type="NameConstant" style="%s"/>
+ <entry type="NameDecorator" style="%s"/>
+ <entry type="NameEntity" style="%s"/>
+ <entry type="NameException" style="%s"/>
+ <entry type="NameFunction" style="%s"/>
+ <entry type="NameLabel" style="%s"/>
+ <entry type="NameNamespace" style="%s"/>
+ <entry type="NameOther" style="%s"/>
+ <entry type="NameTag" style="%s"/>
+ <entry type="NameVariable" style="%s"/>
+ <entry type="NameVariableClass" style="%s"/>
+ <entry type="NameVariableGlobal" style="%s"/>
+ <entry type="NameVariableInstance" style="%s"/>
+ <!-- Literals -->
+ <entry type="Literal" style="%s"/>
+ <entry type="LiteralDate" style="%s"/>
+ <entry type="LiteralString" style="%s"/>
+ <entry type="LiteralStringBacktick" style="%s"/>
+ <entry type="LiteralStringChar" style="%s"/>
+ <entry type="LiteralStringDoc" style="%s"/>
+ <entry type="LiteralStringDouble" style="%s"/>
+ <entry type="LiteralStringEscape" style="%s"/>
+ <entry type="LiteralStringHeredoc" style="%s"/>
+ <entry type="LiteralStringInterpol" style="%s"/>
+ <entry type="LiteralStringOther" style="%s"/>
+ <entry type="LiteralStringRegex" style="%s"/>
+ <entry type="LiteralStringSingle" style="%s"/>
+ <entry type="LiteralStringSymbol" style="%s"/>
+ <!-- Numbers -->
+ <entry type="LiteralNumber" style="%s"/>
+ <entry type="LiteralNumberBin" style="%s"/>
+ <entry type="LiteralNumberFloat" style="%s"/>
+ <entry type="LiteralNumberHex" style="%s"/>
+ <entry type="LiteralNumberInteger" style="%s"/>
+ <entry type="LiteralNumberIntegerLong" style="%s"/>
+ <entry type="LiteralNumberOct" style="%s"/>
+ <!-- Operators -->
+ <entry type="Operator" style="%s"/>
+ <entry type="OperatorWord" style="%s"/>
+ <entry type="Punctuation" style="%s"/>
+ <!-- Comments -->
+ <entry type="Comment" style="%s"/>
+ <entry type="CommentHashbang" style="%s"/>
+ <entry type="CommentMultiline" style="%s"/>
+ <entry type="CommentSingle" style="%s"/>
+ <entry type="CommentSpecial" style="%s"/>
+ <entry type="CommentPreproc" style="%s"/>
+ <!-- Generic styles -->
+ <entry type="Generic" style="%s"/>
+ <entry type="GenericDeleted" style="%s"/>
+ <entry type="GenericEmph" style="italic %s"/>
+ <entry type="GenericError" style="%s"/>
+ <entry type="GenericHeading" style="bold %s"/>
+ <entry type="GenericInserted" style="%s"/>
+ <entry type="GenericOutput" style="%s"/>
+ <entry type="GenericPrompt" style="%s"/>
+ <entry type="GenericStrong" style="bold %s"/>
+ <entry type="GenericSubheading" style="bold %s"/>
+ <entry type="GenericTraceback" style="%s"/>
+ <entry type="GenericUnderline" style="underline"/>
+ <entry type="TextWhitespace" style="%s"/>
+</style>
+`,
+ getColor(t.Background()), // Background
+ getColor(t.Text()), // Text
+ getColor(t.Text()), // Other
+ getColor(t.Error()), // Error
+
+ getColor(t.SyntaxKeyword()), // Keyword
+ getColor(t.SyntaxKeyword()), // KeywordConstant
+ getColor(t.SyntaxKeyword()), // KeywordDeclaration
+ getColor(t.SyntaxKeyword()), // KeywordNamespace
+ getColor(t.SyntaxKeyword()), // KeywordPseudo
+ getColor(t.SyntaxKeyword()), // KeywordReserved
+ getColor(t.SyntaxType()), // KeywordType
+
+ getColor(t.Text()), // Name
+ getColor(t.SyntaxVariable()), // NameAttribute
+ getColor(t.SyntaxType()), // NameBuiltin
+ getColor(t.SyntaxVariable()), // NameBuiltinPseudo
+ getColor(t.SyntaxType()), // NameClass
+ getColor(t.SyntaxVariable()), // NameConstant
+ getColor(t.SyntaxFunction()), // NameDecorator
+ getColor(t.SyntaxVariable()), // NameEntity
+ getColor(t.SyntaxType()), // NameException
+ getColor(t.SyntaxFunction()), // NameFunction
+ getColor(t.Text()), // NameLabel
+ getColor(t.SyntaxType()), // NameNamespace
+ getColor(t.SyntaxVariable()), // NameOther
+ getColor(t.SyntaxKeyword()), // NameTag
+ getColor(t.SyntaxVariable()), // NameVariable
+ getColor(t.SyntaxVariable()), // NameVariableClass
+ getColor(t.SyntaxVariable()), // NameVariableGlobal
+ getColor(t.SyntaxVariable()), // NameVariableInstance
+
+ getColor(t.SyntaxString()), // Literal
+ getColor(t.SyntaxString()), // LiteralDate
+ getColor(t.SyntaxString()), // LiteralString
+ getColor(t.SyntaxString()), // LiteralStringBacktick
+ getColor(t.SyntaxString()), // LiteralStringChar
+ getColor(t.SyntaxString()), // LiteralStringDoc
+ getColor(t.SyntaxString()), // LiteralStringDouble
+ getColor(t.SyntaxString()), // LiteralStringEscape
+ getColor(t.SyntaxString()), // LiteralStringHeredoc
+ getColor(t.SyntaxString()), // LiteralStringInterpol
+ getColor(t.SyntaxString()), // LiteralStringOther
+ getColor(t.SyntaxString()), // LiteralStringRegex
+ getColor(t.SyntaxString()), // LiteralStringSingle
+ getColor(t.SyntaxString()), // LiteralStringSymbol
+
+ getColor(t.SyntaxNumber()), // LiteralNumber
+ getColor(t.SyntaxNumber()), // LiteralNumberBin
+ getColor(t.SyntaxNumber()), // LiteralNumberFloat
+ getColor(t.SyntaxNumber()), // LiteralNumberHex
+ getColor(t.SyntaxNumber()), // LiteralNumberInteger
+ getColor(t.SyntaxNumber()), // LiteralNumberIntegerLong
+ getColor(t.SyntaxNumber()), // LiteralNumberOct
+
+ getColor(t.SyntaxOperator()), // Operator
+ getColor(t.SyntaxKeyword()), // OperatorWord
+ getColor(t.SyntaxPunctuation()), // Punctuation
+
+ getColor(t.SyntaxComment()), // Comment
+ getColor(t.SyntaxComment()), // CommentHashbang
+ getColor(t.SyntaxComment()), // CommentMultiline
+ getColor(t.SyntaxComment()), // CommentSingle
+ getColor(t.SyntaxComment()), // CommentSpecial
+ getColor(t.SyntaxKeyword()), // CommentPreproc
+
+ getColor(t.Text()), // Generic
+ getColor(t.Error()), // GenericDeleted
+ getColor(t.Text()), // GenericEmph
+ getColor(t.Error()), // GenericError
+ getColor(t.Text()), // GenericHeading
+ getColor(t.Success()), // GenericInserted
+ getColor(t.TextMuted()), // GenericOutput
+ getColor(t.Text()), // GenericPrompt
+ getColor(t.Text()), // GenericStrong
+ getColor(t.Text()), // GenericSubheading
+ getColor(t.Error()), // GenericTraceback
+ getColor(t.Text()), // TextWhitespace
+ )
+
+ r := strings.NewReader(syntaxThemeXml)
+ style := chroma.MustNewXMLStyle(r)
+
+ // Modify the style to use the provided background
+ s, err := style.Builder().Transform(
+ func(t chroma.StyleEntry) chroma.StyleEntry {
+ r, g, b, _ := bg.RGBA()
+ t.Background = chroma.NewColour(uint8(r>>8), uint8(g>>8), uint8(b>>8))
+ return t
+ },
+ ).Build()
+ if err != nil {
+ s = styles.Fallback
+ }
+
+ // Tokenize and format
+ it, err := l.Tokenise(nil, source)
+ if err != nil {
+ return err
+ }
+
+ return f.Format(w, s, it)
+}
+
+// getColor returns the appropriate hex color string based on terminal background
+func getColor(adaptiveColor lipgloss.AdaptiveColor) string {
+ if lipgloss.HasDarkBackground() {
+ return adaptiveColor.Dark
+ }
+ return adaptiveColor.Light
+}
+
+// highlightLine applies syntax highlighting to a single line
+func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) string {
+ var buf bytes.Buffer
+ err := SyntaxHighlight(&buf, line, fileName, "terminal16m", bg)
+ if err != nil {
+ return line
+ }
+ return buf.String()
+}
+
+// createStyles generates the lipgloss styles needed for rendering diffs
+func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
+ removedLineStyle = lipgloss.NewStyle().Background(t.DiffRemovedBg())
+ addedLineStyle = lipgloss.NewStyle().Background(t.DiffAddedBg())
+ contextLineStyle = lipgloss.NewStyle().Background(t.DiffContextBg())
+ lineNumberStyle = lipgloss.NewStyle().Foreground(t.DiffLineNumber())
+
+ return
+}
+
+// -------------------------------------------------------------------------
+// Rendering Functions
+// -------------------------------------------------------------------------
+
+// applyHighlighting applies intra-line highlighting to a piece of text
+func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.AdaptiveColor) string {
+ // Find all ANSI sequences in the content
+ ansiRegex := regexp.MustCompile(`\x1b(?:[@-Z\\-_]|\[[0-9?]*(?:;[0-9?]*)*[@-~])`)
+ ansiMatches := ansiRegex.FindAllStringIndex(content, -1)
+
+ // Build a mapping of visible character positions to their actual indices
+ visibleIdx := 0
+ ansiSequences := make(map[int]string)
+ lastAnsiSeq := "\x1b[0m" // Default reset sequence
+
+ for i := 0; i < len(content); {
+ isAnsi := false
+ for _, match := range ansiMatches {
+ if match[0] == i {
+ ansiSequences[visibleIdx] = content[match[0]:match[1]]
+ lastAnsiSeq = content[match[0]:match[1]]
+ i = match[1]
+ isAnsi = true
+ break
+ }
+ }
+ if isAnsi {
+ continue
+ }
+
+ // For non-ANSI positions, store the last ANSI sequence
+ if _, exists := ansiSequences[visibleIdx]; !exists {
+ ansiSequences[visibleIdx] = lastAnsiSeq
+ }
+ visibleIdx++
+ i++
+ }
+
+ // Apply highlighting
+ var sb strings.Builder
+ inSelection := false
+ currentPos := 0
+
+ // Get the appropriate color based on terminal background
+ bgColor := lipgloss.Color(getColor(highlightBg))
+ fgColor := lipgloss.Color(getColor(theme.CurrentTheme().Background()))
+
+ for i := 0; i < len(content); {
+ // Check if we're at an ANSI sequence
+ isAnsi := false
+ for _, match := range ansiMatches {
+ if match[0] == i {
+ sb.WriteString(content[match[0]:match[1]]) // Preserve ANSI sequence
+ i = match[1]
+ isAnsi = true
+ break
+ }
+ }
+ if isAnsi {
+ continue
+ }
+
+ // Check for segment boundaries
+ for _, seg := range segments {
+ if seg.Type == segmentType {
+ if currentPos == seg.Start {
+ inSelection = true
+ }
+ if currentPos == seg.End {
+ inSelection = false
+ }
+ }
+ }
+
+ // Get current character
+ char := string(content[i])
+
+ if inSelection {
+ // Get the current styling
+ currentStyle := ansiSequences[currentPos]
+
+ // Apply foreground and background highlight
+ sb.WriteString("\x1b[38;2;")
+ r, g, b, _ := fgColor.RGBA()
+ sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
+ sb.WriteString("\x1b[48;2;")
+ r, g, b, _ = bgColor.RGBA()
+ sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
+ sb.WriteString(char)
+
+ // Full reset of all attributes to ensure clean state
+ sb.WriteString("\x1b[0m")
+
+ // Reapply the original ANSI sequence
+ sb.WriteString(currentStyle)
+ } else {
+ // Not in selection, just copy the character
+ sb.WriteString(char)
+ }
+
+ currentPos++
+ i++
+ }
+
+ return sb.String()
+}
+
+// renderDiffColumnLine is a helper function that handles the common logic for rendering diff columns
+func renderDiffColumnLine(
+ fileName string,
+ dl *DiffLine,
+ colWidth int,
+ isLeftColumn bool,
+ t theme.Theme,
+) string {
+ if dl == nil {
+ contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg())
+ return contextLineStyle.Width(colWidth).Render("")
+ }
+
+ removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(t)
+
+ // Determine line style based on line type and column
+ var marker string
+ var bgStyle lipgloss.Style
+ var lineNum string
+ var highlightType LineType
+ var highlightColor lipgloss.AdaptiveColor
+
+ if isLeftColumn {
+ // Left column logic
+ switch dl.Kind {
+ case LineRemoved:
+ marker = "-"
+ bgStyle = removedLineStyle
+ lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
+ highlightType = LineRemoved
+ highlightColor = t.DiffHighlightRemoved()
+ case LineAdded:
+ marker = "?"
+ bgStyle = contextLineStyle
+ case LineContext:
+ marker = " "
+ bgStyle = contextLineStyle
+ }
+
+ // Format line number for left column
+ if dl.OldLineNo > 0 {
+ lineNum = fmt.Sprintf("%6d", dl.OldLineNo)
+ }
+ } else {
+ // Right column logic
+ switch dl.Kind {
+ case LineAdded:
+ marker = "+"
+ bgStyle = addedLineStyle
+ lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
+ highlightType = LineAdded
+ highlightColor = t.DiffHighlightAdded()
+ case LineRemoved:
+ marker = "?"
+ bgStyle = contextLineStyle
+ case LineContext:
+ marker = " "
+ bgStyle = contextLineStyle
+ }
+
+ // Format line number for right column
+ if dl.NewLineNo > 0 {
+ lineNum = fmt.Sprintf("%6d", dl.NewLineNo)
+ }
+ }
+
+ // Style the marker based on line type
+ var styledMarker string
+ switch dl.Kind {
+ case LineRemoved:
+ styledMarker = removedLineStyle.Foreground(t.DiffRemoved()).Render(marker)
+ case LineAdded:
+ styledMarker = addedLineStyle.Foreground(t.DiffAdded()).Render(marker)
+ case LineContext:
+ styledMarker = contextLineStyle.Foreground(t.TextMuted()).Render(marker)
+ default:
+ styledMarker = marker
+ }
+
+ // Create the line prefix
+ prefix := lineNumberStyle.Render(lineNum + " " + styledMarker)
+
+ // Apply syntax highlighting
+ content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
+
+ // Apply intra-line highlighting if needed
+ if (dl.Kind == LineRemoved && isLeftColumn || dl.Kind == LineAdded && !isLeftColumn) && len(dl.Segments) > 0 {
+ content = applyHighlighting(content, dl.Segments, highlightType, highlightColor)
+ }
+
+ // Add a padding space for added/removed lines
+ if (dl.Kind == LineRemoved && isLeftColumn) || (dl.Kind == LineAdded && !isLeftColumn) {
+ content = bgStyle.Render(" ") + content
+ }
+
+ // Create the final line and truncate if needed
+ lineText := prefix + content
+ return bgStyle.MaxHeight(1).Width(colWidth).Render(
+ ansi.Truncate(
+ lineText,
+ colWidth,
+ lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
+ ),
+ )
+}
+
+// renderLeftColumn formats the left side of a side-by-side diff
+func renderLeftColumn(fileName string, dl *DiffLine, colWidth int) string {
+ return renderDiffColumnLine(fileName, dl, colWidth, true, theme.CurrentTheme())
+}
+
+// renderRightColumn formats the right side of a side-by-side diff
+func renderRightColumn(fileName string, dl *DiffLine, colWidth int) string {
+ return renderDiffColumnLine(fileName, dl, colWidth, false, theme.CurrentTheme())
+}
+
+// -------------------------------------------------------------------------
+// Public API
+// -------------------------------------------------------------------------
+
+// RenderSideBySideHunk formats a hunk for side-by-side display
+func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) string {
+ // Apply options to create the configuration
+ config := NewSideBySideConfig(opts...)
+
+ // Make a copy of the hunk so we don't modify the original
+ hunkCopy := Hunk{Lines: make([]DiffLine, len(h.Lines))}
+ copy(hunkCopy.Lines, h.Lines)
+
+ // Highlight changes within lines
+ HighlightIntralineChanges(&hunkCopy)
+
+ // Pair lines for side-by-side display
+ pairs := pairLines(hunkCopy.Lines)
+
+ // Calculate column width
+ colWidth := config.TotalWidth / 2
+
+ leftWidth := colWidth
+ rightWidth := config.TotalWidth - colWidth
+ var sb strings.Builder
+ for _, p := range pairs {
+ leftStr := renderLeftColumn(fileName, p.left, leftWidth)
+ rightStr := renderRightColumn(fileName, p.right, rightWidth)
+ sb.WriteString(leftStr + rightStr + "\n")
+ }
+
+ return sb.String()
+}
+
+// FormatDiff creates a side-by-side formatted view of a diff
+func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
+ t := theme.CurrentTheme()
+ diffResult, err := ParseUnifiedDiff(diffText)
+ if err != nil {
+ return "", err
+ }
+
+ var sb strings.Builder
+ config := NewSideBySideConfig(opts...)
+ for _, h := range diffResult.Hunks {
+ sb.WriteString(
+ lipgloss.NewStyle().
+ Background(t.DiffHunkHeader()).
+ Foreground(t.Background()).
+ Width(config.TotalWidth).
+ Render(h.Header) + "\n",
+ )
+ sb.WriteString(RenderSideBySideHunk(diffResult.OldFile, h, opts...))
+ }
+
+ return sb.String(), nil
+}
+
+// GenerateDiff creates a unified diff from two file contents
+func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, int) {
+ // remove the cwd prefix and ensure consistent path format
+ // this prevents issues with absolute paths in different environments
+ cwd := config.WorkingDirectory()
+ fileName = strings.TrimPrefix(fileName, cwd)
+ fileName = strings.TrimPrefix(fileName, "/")
+
+ edits := udiff.Strings(beforeContent, afterContent)
+ unified, _ := udiff.ToUnified("a/"+fileName, "b/"+fileName, beforeContent, edits, 8)
+
+ var (
+ additions = 0
+ removals = 0
+ )
+
+ lines := strings.SplitSeq(unified, "\n")
+ for line := range lines {
+ if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") {
+ additions++
+ } else if strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---") {
+ removals++
+ }
+ }
+
+ return unified, additions, removals
+}
diff --git a/packages/tui/internal/diff/diff_test.go b/packages/tui/internal/diff/diff_test.go
new file mode 100644
index 000000000..4c014e45c
--- /dev/null
+++ b/packages/tui/internal/diff/diff_test.go
@@ -0,0 +1,103 @@
+package diff
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/charmbracelet/lipgloss"
+ "github.com/stretchr/testify/assert"
+)
+
+// TestApplyHighlighting tests the applyHighlighting function with various ANSI sequences
+func TestApplyHighlighting(t *testing.T) {
+ t.Parallel()
+
+ // Mock theme colors for testing
+ mockHighlightBg := lipgloss.AdaptiveColor{
+ Dark: "#FF0000", // Red background for highlighting
+ Light: "#FF0000",
+ }
+
+ // Test cases
+ tests := []struct {
+ name string
+ content string
+ segments []Segment
+ segmentType LineType
+ expectContains string
+ }{
+ {
+ name: "Simple text with no ANSI",
+ content: "This is a test",
+ segments: []Segment{{Start: 0, End: 4, Type: LineAdded}},
+ segmentType: LineAdded,
+ // Should contain full reset sequence after highlighting
+ expectContains: "\x1b[0m",
+ },
+ {
+ name: "Text with existing ANSI foreground",
+ content: "This \x1b[32mis\x1b[0m a test", // "is" in green
+ segments: []Segment{{Start: 5, End: 7, Type: LineAdded}},
+ segmentType: LineAdded,
+ // Should contain full reset sequence after highlighting
+ expectContains: "\x1b[0m",
+ },
+ {
+ name: "Text with existing ANSI background",
+ content: "This \x1b[42mis\x1b[0m a test", // "is" with green background
+ segments: []Segment{{Start: 5, End: 7, Type: LineAdded}},
+ segmentType: LineAdded,
+ // Should contain full reset sequence after highlighting
+ expectContains: "\x1b[0m",
+ },
+ {
+ name: "Text with complex ANSI styling",
+ content: "This \x1b[1;32;45mis\x1b[0m a test", // "is" bold green on magenta
+ segments: []Segment{{Start: 5, End: 7, Type: LineAdded}},
+ segmentType: LineAdded,
+ // Should contain full reset sequence after highlighting
+ expectContains: "\x1b[0m",
+ },
+ }
+
+ for _, tc := range tests {
+ tc := tc // Capture range variable for parallel testing
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ result := applyHighlighting(tc.content, tc.segments, tc.segmentType, mockHighlightBg)
+
+ // Verify the result contains the expected sequence
+ assert.Contains(t, result, tc.expectContains,
+ "Result should contain full reset sequence")
+
+ // Print the result for manual inspection if needed
+ if t.Failed() {
+ fmt.Printf("Original: %q\nResult: %q\n", tc.content, result)
+ }
+ })
+ }
+}
+
+// TestApplyHighlightingWithMultipleSegments tests highlighting multiple segments
+func TestApplyHighlightingWithMultipleSegments(t *testing.T) {
+ t.Parallel()
+
+ // Mock theme colors for testing
+ mockHighlightBg := lipgloss.AdaptiveColor{
+ Dark: "#FF0000", // Red background for highlighting
+ Light: "#FF0000",
+ }
+
+ content := "This is a test with multiple segments to highlight"
+ segments := []Segment{
+ {Start: 0, End: 4, Type: LineAdded}, // "This"
+ {Start: 8, End: 9, Type: LineAdded}, // "a"
+ {Start: 15, End: 23, Type: LineAdded}, // "multiple"
+ }
+
+ result := applyHighlighting(content, segments, LineAdded, mockHighlightBg)
+
+ // Verify the result contains the full reset sequence
+ assert.Contains(t, result, "\x1b[0m",
+ "Result should contain full reset sequence")
+} \ No newline at end of file
diff --git a/packages/tui/internal/diff/patch.go b/packages/tui/internal/diff/patch.go
new file mode 100644
index 000000000..49242f7ef
--- /dev/null
+++ b/packages/tui/internal/diff/patch.go
@@ -0,0 +1,740 @@
+package diff
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+type ActionType string
+
+const (
+ ActionAdd ActionType = "add"
+ ActionDelete ActionType = "delete"
+ ActionUpdate ActionType = "update"
+)
+
+type FileChange struct {
+ Type ActionType
+ OldContent *string
+ NewContent *string
+ MovePath *string
+}
+
+type Commit struct {
+ Changes map[string]FileChange
+}
+
+type Chunk struct {
+ OrigIndex int // line index of the first line in the original file
+ DelLines []string // lines to delete
+ InsLines []string // lines to insert
+}
+
+type PatchAction struct {
+ Type ActionType
+ NewFile *string
+ Chunks []Chunk
+ MovePath *string
+}
+
+type Patch struct {
+ Actions map[string]PatchAction
+}
+
+type DiffError struct {
+ message string
+}
+
+func (e DiffError) Error() string {
+ return e.message
+}
+
+// Helper functions for error handling
+func NewDiffError(message string) DiffError {
+ return DiffError{message: message}
+}
+
+func fileError(action, reason, path string) DiffError {
+ return NewDiffError(fmt.Sprintf("%s File Error: %s: %s", action, reason, path))
+}
+
+func contextError(index int, context string, isEOF bool) DiffError {
+ prefix := "Invalid Context"
+ if isEOF {
+ prefix = "Invalid EOF Context"
+ }
+ return NewDiffError(fmt.Sprintf("%s %d:\n%s", prefix, index, context))
+}
+
+type Parser struct {
+ currentFiles map[string]string
+ lines []string
+ index int
+ patch Patch
+ fuzz int
+}
+
+func NewParser(currentFiles map[string]string, lines []string) *Parser {
+ return &Parser{
+ currentFiles: currentFiles,
+ lines: lines,
+ index: 0,
+ patch: Patch{Actions: make(map[string]PatchAction, len(currentFiles))},
+ fuzz: 0,
+ }
+}
+
+func (p *Parser) isDone(prefixes []string) bool {
+ if p.index >= len(p.lines) {
+ return true
+ }
+ for _, prefix := range prefixes {
+ if strings.HasPrefix(p.lines[p.index], prefix) {
+ return true
+ }
+ }
+ return false
+}
+
+func (p *Parser) startsWith(prefix any) bool {
+ var prefixes []string
+ switch v := prefix.(type) {
+ case string:
+ prefixes = []string{v}
+ case []string:
+ prefixes = v
+ }
+
+ for _, pfx := range prefixes {
+ if strings.HasPrefix(p.lines[p.index], pfx) {
+ return true
+ }
+ }
+ return false
+}
+
+func (p *Parser) readStr(prefix string, returnEverything bool) string {
+ if p.index >= len(p.lines) {
+ return "" // Changed from panic to return empty string for safer operation
+ }
+ if strings.HasPrefix(p.lines[p.index], prefix) {
+ var text string
+ if returnEverything {
+ text = p.lines[p.index]
+ } else {
+ text = p.lines[p.index][len(prefix):]
+ }
+ p.index++
+ return text
+ }
+ return ""
+}
+
+func (p *Parser) Parse() error {
+ endPatchPrefixes := []string{"*** End Patch"}
+
+ for !p.isDone(endPatchPrefixes) {
+ path := p.readStr("*** Update File: ", false)
+ if path != "" {
+ if _, exists := p.patch.Actions[path]; exists {
+ return fileError("Update", "Duplicate Path", path)
+ }
+ moveTo := p.readStr("*** Move to: ", false)
+ if _, exists := p.currentFiles[path]; !exists {
+ return fileError("Update", "Missing File", path)
+ }
+ text := p.currentFiles[path]
+ action, err := p.parseUpdateFile(text)
+ if err != nil {
+ return err
+ }
+ if moveTo != "" {
+ action.MovePath = &moveTo
+ }
+ p.patch.Actions[path] = action
+ continue
+ }
+
+ path = p.readStr("*** Delete File: ", false)
+ if path != "" {
+ if _, exists := p.patch.Actions[path]; exists {
+ return fileError("Delete", "Duplicate Path", path)
+ }
+ if _, exists := p.currentFiles[path]; !exists {
+ return fileError("Delete", "Missing File", path)
+ }
+ p.patch.Actions[path] = PatchAction{Type: ActionDelete, Chunks: []Chunk{}}
+ continue
+ }
+
+ path = p.readStr("*** Add File: ", false)
+ if path != "" {
+ if _, exists := p.patch.Actions[path]; exists {
+ return fileError("Add", "Duplicate Path", path)
+ }
+ if _, exists := p.currentFiles[path]; exists {
+ return fileError("Add", "File already exists", path)
+ }
+ action, err := p.parseAddFile()
+ if err != nil {
+ return err
+ }
+ p.patch.Actions[path] = action
+ continue
+ }
+
+ return NewDiffError(fmt.Sprintf("Unknown Line: %s", p.lines[p.index]))
+ }
+
+ if !p.startsWith("*** End Patch") {
+ return NewDiffError("Missing End Patch")
+ }
+ p.index++
+
+ return nil
+}
+
+func (p *Parser) parseUpdateFile(text string) (PatchAction, error) {
+ action := PatchAction{Type: ActionUpdate, Chunks: []Chunk{}}
+ fileLines := strings.Split(text, "\n")
+ index := 0
+
+ endPrefixes := []string{
+ "*** End Patch",
+ "*** Update File:",
+ "*** Delete File:",
+ "*** Add File:",
+ "*** End of File",
+ }
+
+ for !p.isDone(endPrefixes) {
+ defStr := p.readStr("@@ ", false)
+ sectionStr := ""
+ if defStr == "" && p.index < len(p.lines) && p.lines[p.index] == "@@" {
+ sectionStr = p.lines[p.index]
+ p.index++
+ }
+ if defStr == "" && sectionStr == "" && index != 0 {
+ return action, NewDiffError(fmt.Sprintf("Invalid Line:\n%s", p.lines[p.index]))
+ }
+ if strings.TrimSpace(defStr) != "" {
+ found := false
+ for i := range fileLines[:index] {
+ if fileLines[i] == defStr {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ for i := index; i < len(fileLines); i++ {
+ if fileLines[i] == defStr {
+ index = i + 1
+ found = true
+ break
+ }
+ }
+ }
+
+ if !found {
+ for i := range fileLines[:index] {
+ if strings.TrimSpace(fileLines[i]) == strings.TrimSpace(defStr) {
+ found = true
+ break
+ }
+ }
+ }
+
+ if !found {
+ for i := index; i < len(fileLines); i++ {
+ if strings.TrimSpace(fileLines[i]) == strings.TrimSpace(defStr) {
+ index = i + 1
+ p.fuzz++
+ found = true
+ break
+ }
+ }
+ }
+ }
+
+ nextChunkContext, chunks, endPatchIndex, eof := peekNextSection(p.lines, p.index)
+ newIndex, fuzz := findContext(fileLines, nextChunkContext, index, eof)
+ if newIndex == -1 {
+ ctxText := strings.Join(nextChunkContext, "\n")
+ return action, contextError(index, ctxText, eof)
+ }
+ p.fuzz += fuzz
+
+ for _, ch := range chunks {
+ ch.OrigIndex += newIndex
+ action.Chunks = append(action.Chunks, ch)
+ }
+ index = newIndex + len(nextChunkContext)
+ p.index = endPatchIndex
+ }
+ return action, nil
+}
+
+func (p *Parser) parseAddFile() (PatchAction, error) {
+ lines := make([]string, 0, 16) // Preallocate space for better performance
+ endPrefixes := []string{
+ "*** End Patch",
+ "*** Update File:",
+ "*** Delete File:",
+ "*** Add File:",
+ }
+
+ for !p.isDone(endPrefixes) {
+ s := p.readStr("", true)
+ if !strings.HasPrefix(s, "+") {
+ return PatchAction{}, NewDiffError(fmt.Sprintf("Invalid Add File Line: %s", s))
+ }
+ lines = append(lines, s[1:])
+ }
+
+ newFile := strings.Join(lines, "\n")
+ return PatchAction{
+ Type: ActionAdd,
+ NewFile: &newFile,
+ Chunks: []Chunk{},
+ }, nil
+}
+
+// Refactored to use a matcher function for each comparison type
+func findContextCore(lines []string, context []string, start int) (int, int) {
+ if len(context) == 0 {
+ return start, 0
+ }
+
+ // Try exact match
+ if idx, fuzz := tryFindMatch(lines, context, start, func(a, b string) bool {
+ return a == b
+ }); idx >= 0 {
+ return idx, fuzz
+ }
+
+ // Try trimming right whitespace
+ if idx, fuzz := tryFindMatch(lines, context, start, func(a, b string) bool {
+ return strings.TrimRight(a, " \t") == strings.TrimRight(b, " \t")
+ }); idx >= 0 {
+ return idx, fuzz
+ }
+
+ // Try trimming all whitespace
+ if idx, fuzz := tryFindMatch(lines, context, start, func(a, b string) bool {
+ return strings.TrimSpace(a) == strings.TrimSpace(b)
+ }); idx >= 0 {
+ return idx, fuzz
+ }
+
+ return -1, 0
+}
+
+// Helper function to DRY up the match logic
+func tryFindMatch(lines []string, context []string, start int,
+ compareFunc func(string, string) bool,
+) (int, int) {
+ for i := start; i < len(lines); i++ {
+ if i+len(context) <= len(lines) {
+ match := true
+ for j := range context {
+ if !compareFunc(lines[i+j], context[j]) {
+ match = false
+ break
+ }
+ }
+ if match {
+ // Return fuzz level: 0 for exact, 1 for trimRight, 100 for trimSpace
+ var fuzz int
+ if compareFunc("a ", "a") && !compareFunc("a", "b") {
+ fuzz = 1
+ } else if compareFunc("a ", "a") {
+ fuzz = 100
+ }
+ return i, fuzz
+ }
+ }
+ }
+ return -1, 0
+}
+
+func findContext(lines []string, context []string, start int, eof bool) (int, int) {
+ if eof {
+ newIndex, fuzz := findContextCore(lines, context, len(lines)-len(context))
+ if newIndex != -1 {
+ return newIndex, fuzz
+ }
+ newIndex, fuzz = findContextCore(lines, context, start)
+ return newIndex, fuzz + 10000
+ }
+ return findContextCore(lines, context, start)
+}
+
+func peekNextSection(lines []string, initialIndex int) ([]string, []Chunk, int, bool) {
+ index := initialIndex
+ old := make([]string, 0, 32) // Preallocate for better performance
+ delLines := make([]string, 0, 8)
+ insLines := make([]string, 0, 8)
+ chunks := make([]Chunk, 0, 4)
+ mode := "keep"
+
+ // End conditions for the section
+ endSectionConditions := func(s string) bool {
+ return strings.HasPrefix(s, "@@") ||
+ strings.HasPrefix(s, "*** End Patch") ||
+ strings.HasPrefix(s, "*** Update File:") ||
+ strings.HasPrefix(s, "*** Delete File:") ||
+ strings.HasPrefix(s, "*** Add File:") ||
+ strings.HasPrefix(s, "*** End of File") ||
+ s == "***" ||
+ strings.HasPrefix(s, "***")
+ }
+
+ for index < len(lines) {
+ s := lines[index]
+ if endSectionConditions(s) {
+ break
+ }
+ index++
+ lastMode := mode
+ line := s
+
+ if len(line) > 0 {
+ switch line[0] {
+ case '+':
+ mode = "add"
+ case '-':
+ mode = "delete"
+ case ' ':
+ mode = "keep"
+ default:
+ mode = "keep"
+ line = " " + line
+ }
+ } else {
+ mode = "keep"
+ line = " "
+ }
+
+ line = line[1:]
+ if mode == "keep" && lastMode != mode {
+ if len(insLines) > 0 || len(delLines) > 0 {
+ chunks = append(chunks, Chunk{
+ OrigIndex: len(old) - len(delLines),
+ DelLines: delLines,
+ InsLines: insLines,
+ })
+ }
+ delLines = make([]string, 0, 8)
+ insLines = make([]string, 0, 8)
+ }
+ switch mode {
+ case "delete":
+ delLines = append(delLines, line)
+ old = append(old, line)
+ case "add":
+ insLines = append(insLines, line)
+ default:
+ old = append(old, line)
+ }
+ }
+
+ if len(insLines) > 0 || len(delLines) > 0 {
+ chunks = append(chunks, Chunk{
+ OrigIndex: len(old) - len(delLines),
+ DelLines: delLines,
+ InsLines: insLines,
+ })
+ }
+
+ if index < len(lines) && lines[index] == "*** End of File" {
+ index++
+ return old, chunks, index, true
+ }
+ return old, chunks, index, false
+}
+
+func TextToPatch(text string, orig map[string]string) (Patch, int, error) {
+ text = strings.TrimSpace(text)
+ lines := strings.Split(text, "\n")
+ if len(lines) < 2 || !strings.HasPrefix(lines[0], "*** Begin Patch") || lines[len(lines)-1] != "*** End Patch" {
+ return Patch{}, 0, NewDiffError("Invalid patch text")
+ }
+ parser := NewParser(orig, lines)
+ parser.index = 1
+ if err := parser.Parse(); err != nil {
+ return Patch{}, 0, err
+ }
+ return parser.patch, parser.fuzz, nil
+}
+
+func IdentifyFilesNeeded(text string) []string {
+ text = strings.TrimSpace(text)
+ lines := strings.Split(text, "\n")
+ result := make(map[string]bool)
+
+ for _, line := range lines {
+ if strings.HasPrefix(line, "*** Update File: ") {
+ result[line[len("*** Update File: "):]] = true
+ }
+ if strings.HasPrefix(line, "*** Delete File: ") {
+ result[line[len("*** Delete File: "):]] = true
+ }
+ }
+
+ files := make([]string, 0, len(result))
+ for file := range result {
+ files = append(files, file)
+ }
+ return files
+}
+
+func IdentifyFilesAdded(text string) []string {
+ text = strings.TrimSpace(text)
+ lines := strings.Split(text, "\n")
+ result := make(map[string]bool)
+
+ for _, line := range lines {
+ if strings.HasPrefix(line, "*** Add File: ") {
+ result[line[len("*** Add File: "):]] = true
+ }
+ }
+
+ files := make([]string, 0, len(result))
+ for file := range result {
+ files = append(files, file)
+ }
+ return files
+}
+
+func getUpdatedFile(text string, action PatchAction, path string) (string, error) {
+ if action.Type != ActionUpdate {
+ return "", errors.New("expected UPDATE action")
+ }
+ origLines := strings.Split(text, "\n")
+ destLines := make([]string, 0, len(origLines)) // Preallocate with capacity
+ origIndex := 0
+
+ for _, chunk := range action.Chunks {
+ if chunk.OrigIndex > len(origLines) {
+ return "", NewDiffError(fmt.Sprintf("%s: chunk.orig_index %d > len(lines) %d", path, chunk.OrigIndex, len(origLines)))
+ }
+ if origIndex > chunk.OrigIndex {
+ return "", NewDiffError(fmt.Sprintf("%s: orig_index %d > chunk.orig_index %d", path, origIndex, chunk.OrigIndex))
+ }
+ destLines = append(destLines, origLines[origIndex:chunk.OrigIndex]...)
+ delta := chunk.OrigIndex - origIndex
+ origIndex += delta
+
+ if len(chunk.InsLines) > 0 {
+ destLines = append(destLines, chunk.InsLines...)
+ }
+ origIndex += len(chunk.DelLines)
+ }
+
+ destLines = append(destLines, origLines[origIndex:]...)
+ return strings.Join(destLines, "\n"), nil
+}
+
+func PatchToCommit(patch Patch, orig map[string]string) (Commit, error) {
+ commit := Commit{Changes: make(map[string]FileChange, len(patch.Actions))}
+ for pathKey, action := range patch.Actions {
+ switch action.Type {
+ case ActionDelete:
+ oldContent := orig[pathKey]
+ commit.Changes[pathKey] = FileChange{
+ Type: ActionDelete,
+ OldContent: &oldContent,
+ }
+ case ActionAdd:
+ commit.Changes[pathKey] = FileChange{
+ Type: ActionAdd,
+ NewContent: action.NewFile,
+ }
+ case ActionUpdate:
+ newContent, err := getUpdatedFile(orig[pathKey], action, pathKey)
+ if err != nil {
+ return Commit{}, err
+ }
+ oldContent := orig[pathKey]
+ fileChange := FileChange{
+ Type: ActionUpdate,
+ OldContent: &oldContent,
+ NewContent: &newContent,
+ }
+ if action.MovePath != nil {
+ fileChange.MovePath = action.MovePath
+ }
+ commit.Changes[pathKey] = fileChange
+ }
+ }
+ return commit, nil
+}
+
+func AssembleChanges(orig map[string]string, updatedFiles map[string]string) Commit {
+ commit := Commit{Changes: make(map[string]FileChange, len(updatedFiles))}
+ for p, newContent := range updatedFiles {
+ oldContent, exists := orig[p]
+ if exists && oldContent == newContent {
+ continue
+ }
+
+ if exists && newContent != "" {
+ commit.Changes[p] = FileChange{
+ Type: ActionUpdate,
+ OldContent: &oldContent,
+ NewContent: &newContent,
+ }
+ } else if newContent != "" {
+ commit.Changes[p] = FileChange{
+ Type: ActionAdd,
+ NewContent: &newContent,
+ }
+ } else if exists {
+ commit.Changes[p] = FileChange{
+ Type: ActionDelete,
+ OldContent: &oldContent,
+ }
+ } else {
+ return commit // Changed from panic to simply return current commit
+ }
+ }
+ return commit
+}
+
+func LoadFiles(paths []string, openFn func(string) (string, error)) (map[string]string, error) {
+ orig := make(map[string]string, len(paths))
+ for _, p := range paths {
+ content, err := openFn(p)
+ if err != nil {
+ return nil, fileError("Open", "File not found", p)
+ }
+ orig[p] = content
+ }
+ return orig, nil
+}
+
+func ApplyCommit(commit Commit, writeFn func(string, string) error, removeFn func(string) error) error {
+ for p, change := range commit.Changes {
+ switch change.Type {
+ case ActionDelete:
+ if err := removeFn(p); err != nil {
+ return err
+ }
+ case ActionAdd:
+ if change.NewContent == nil {
+ return NewDiffError(fmt.Sprintf("Add action for %s has nil new_content", p))
+ }
+ if err := writeFn(p, *change.NewContent); err != nil {
+ return err
+ }
+ case ActionUpdate:
+ if change.NewContent == nil {
+ return NewDiffError(fmt.Sprintf("Update action for %s has nil new_content", p))
+ }
+ if change.MovePath != nil {
+ if err := writeFn(*change.MovePath, *change.NewContent); err != nil {
+ return err
+ }
+ if err := removeFn(p); err != nil {
+ return err
+ }
+ } else {
+ if err := writeFn(p, *change.NewContent); err != nil {
+ return err
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func ProcessPatch(text string, openFn func(string) (string, error), writeFn func(string, string) error, removeFn func(string) error) (string, error) {
+ if !strings.HasPrefix(text, "*** Begin Patch") {
+ return "", NewDiffError("Patch must start with *** Begin Patch")
+ }
+ paths := IdentifyFilesNeeded(text)
+ orig, err := LoadFiles(paths, openFn)
+ if err != nil {
+ return "", err
+ }
+
+ patch, fuzz, err := TextToPatch(text, orig)
+ if err != nil {
+ return "", err
+ }
+
+ if fuzz > 0 {
+ return "", NewDiffError(fmt.Sprintf("Patch contains fuzzy matches (fuzz level: %d)", fuzz))
+ }
+
+ commit, err := PatchToCommit(patch, orig)
+ if err != nil {
+ return "", err
+ }
+
+ if err := ApplyCommit(commit, writeFn, removeFn); err != nil {
+ return "", err
+ }
+
+ return "Patch applied successfully", nil
+}
+
+func OpenFile(p string) (string, error) {
+ data, err := os.ReadFile(p)
+ if err != nil {
+ return "", err
+ }
+ return string(data), nil
+}
+
+func WriteFile(p string, content string) error {
+ if filepath.IsAbs(p) {
+ return NewDiffError("We do not support absolute paths.")
+ }
+
+ dir := filepath.Dir(p)
+ if dir != "." {
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ return err
+ }
+ }
+
+ return os.WriteFile(p, []byte(content), 0o644)
+}
+
+func RemoveFile(p string) error {
+ return os.Remove(p)
+}
+
+func ValidatePatch(patchText string, files map[string]string) (bool, string, error) {
+ if !strings.HasPrefix(patchText, "*** Begin Patch") {
+ return false, "Patch must start with *** Begin Patch", nil
+ }
+
+ neededFiles := IdentifyFilesNeeded(patchText)
+ for _, filePath := range neededFiles {
+ if _, exists := files[filePath]; !exists {
+ return false, fmt.Sprintf("File not found: %s", filePath), nil
+ }
+ }
+
+ patch, fuzz, err := TextToPatch(patchText, files)
+ if err != nil {
+ return false, err.Error(), nil
+ }
+
+ if fuzz > 0 {
+ return false, fmt.Sprintf("Patch contains fuzzy matches (fuzz level: %d)", fuzz), nil
+ }
+
+ _, err = PatchToCommit(patch, files)
+ if err != nil {
+ return false, err.Error(), nil
+ }
+
+ return true, "Patch is valid", nil
+}
diff --git a/packages/tui/internal/fileutil/fileutil.go b/packages/tui/internal/fileutil/fileutil.go
new file mode 100644
index 000000000..b48152f7a
--- /dev/null
+++ b/packages/tui/internal/fileutil/fileutil.go
@@ -0,0 +1,163 @@
+package fileutil
+
+import (
+ "fmt"
+ "io/fs"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/bmatcuk/doublestar/v4"
+ "github.com/sst/opencode/internal/status"
+)
+
+var (
+ rgPath string
+ fzfPath string
+)
+
+func Init() {
+ var err error
+ rgPath, err = exec.LookPath("rg")
+ if err != nil {
+ status.Warn("Ripgrep (rg) not found in $PATH. Some features might be limited or slower.")
+ rgPath = ""
+ }
+ fzfPath, err = exec.LookPath("fzf")
+ if err != nil {
+ status.Warn("FZF not found in $PATH. Some features might be limited or slower.")
+ fzfPath = ""
+ }
+}
+
+func GetRgCmd(globPattern string) *exec.Cmd {
+ if rgPath == "" {
+ return nil
+ }
+ rgArgs := []string{
+ "--files",
+ "-L",
+ "--null",
+ }
+ if globPattern != "" {
+ if !filepath.IsAbs(globPattern) && !strings.HasPrefix(globPattern, "/") {
+ globPattern = "/" + globPattern
+ }
+ rgArgs = append(rgArgs, "--glob", globPattern)
+ }
+ cmd := exec.Command(rgPath, rgArgs...)
+ cmd.Dir = "."
+ return cmd
+}
+
+func GetFzfCmd(query string) *exec.Cmd {
+ if fzfPath == "" {
+ return nil
+ }
+ fzfArgs := []string{
+ "--filter",
+ query,
+ "--read0",
+ "--print0",
+ }
+ cmd := exec.Command(fzfPath, fzfArgs...)
+ cmd.Dir = "."
+ return cmd
+}
+
+type FileInfo struct {
+ Path string
+ ModTime time.Time
+}
+
+func SkipHidden(path string) bool {
+ // Check for hidden files (starting with a dot)
+ base := filepath.Base(path)
+ if base != "." && strings.HasPrefix(base, ".") {
+ return true
+ }
+
+ commonIgnoredDirs := map[string]bool{
+ ".opencode": true,
+ "node_modules": true,
+ "vendor": true,
+ "dist": true,
+ "build": true,
+ "target": true,
+ ".git": true,
+ ".idea": true,
+ ".vscode": true,
+ "__pycache__": true,
+ "bin": true,
+ "obj": true,
+ "out": true,
+ "coverage": true,
+ "tmp": true,
+ "temp": true,
+ "logs": true,
+ "generated": true,
+ "bower_components": true,
+ "jspm_packages": true,
+ }
+
+ parts := strings.Split(path, string(os.PathSeparator))
+ for _, part := range parts {
+ if commonIgnoredDirs[part] {
+ return true
+ }
+ }
+ return false
+}
+
+func GlobWithDoublestar(pattern, searchPath string, limit int) ([]string, bool, error) {
+ fsys := os.DirFS(searchPath)
+ relPattern := strings.TrimPrefix(pattern, "/")
+ var matches []FileInfo
+
+ err := doublestar.GlobWalk(fsys, relPattern, func(path string, d fs.DirEntry) error {
+ if d.IsDir() {
+ return nil
+ }
+ if SkipHidden(path) {
+ return nil
+ }
+ info, err := d.Info()
+ if err != nil {
+ return nil
+ }
+ absPath := path
+ if !strings.HasPrefix(absPath, searchPath) && searchPath != "." {
+ absPath = filepath.Join(searchPath, absPath)
+ } else if !strings.HasPrefix(absPath, "/") && searchPath == "." {
+ absPath = filepath.Join(searchPath, absPath) // Ensure relative paths are joined correctly
+ }
+
+ matches = append(matches, FileInfo{Path: absPath, ModTime: info.ModTime()})
+ if limit > 0 && len(matches) >= limit*2 {
+ return fs.SkipAll
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, false, fmt.Errorf("glob walk error: %w", err)
+ }
+
+ sort.Slice(matches, func(i, j int) bool {
+ return matches[i].ModTime.After(matches[j].ModTime)
+ })
+
+ truncated := false
+ if limit > 0 && len(matches) > limit {
+ matches = matches[:limit]
+ truncated = true
+ }
+
+ results := make([]string, len(matches))
+ for i, m := range matches {
+ results[i] = m.Path
+ }
+ return results, truncated, nil
+}
diff --git a/packages/tui/internal/format/format.go b/packages/tui/internal/format/format.go
new file mode 100644
index 000000000..321f5c102
--- /dev/null
+++ b/packages/tui/internal/format/format.go
@@ -0,0 +1,46 @@
+package format
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// OutputFormat represents the format for non-interactive mode output
+type OutputFormat string
+
+const (
+ // TextFormat is plain text output (default)
+ TextFormat OutputFormat = "text"
+
+ // JSONFormat is output wrapped in a JSON object
+ JSONFormat OutputFormat = "json"
+)
+
+// IsValid checks if the output format is valid
+func (f OutputFormat) IsValid() bool {
+ return f == TextFormat || f == JSONFormat
+}
+
+// String returns the string representation of the output format
+func (f OutputFormat) String() string {
+ return string(f)
+}
+
+// FormatOutput formats the given content according to the specified format
+func FormatOutput(content string, format OutputFormat) (string, error) {
+ switch format {
+ case TextFormat:
+ return content, nil
+ case JSONFormat:
+ jsonData := map[string]string{
+ "response": content,
+ }
+ jsonBytes, err := json.MarshalIndent(jsonData, "", " ")
+ if err != nil {
+ return "", fmt.Errorf("failed to marshal JSON: %w", err)
+ }
+ return string(jsonBytes), nil
+ default:
+ return "", fmt.Errorf("unsupported output format: %s", format)
+ }
+}
diff --git a/packages/tui/internal/format/format_test.go b/packages/tui/internal/format/format_test.go
new file mode 100644
index 000000000..04054a7c4
--- /dev/null
+++ b/packages/tui/internal/format/format_test.go
@@ -0,0 +1,90 @@
+package format
+
+import (
+ "testing"
+)
+
+func TestOutputFormat_IsValid(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ format OutputFormat
+ want bool
+ }{
+ {
+ name: "text format",
+ format: TextFormat,
+ want: true,
+ },
+ {
+ name: "json format",
+ format: JSONFormat,
+ want: true,
+ },
+ {
+ name: "invalid format",
+ format: "invalid",
+ want: false,
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ if got := tt.format.IsValid(); got != tt.want {
+ t.Errorf("OutputFormat.IsValid() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestFormatOutput(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ content string
+ format OutputFormat
+ want string
+ wantErr bool
+ }{
+ {
+ name: "text format",
+ content: "test content",
+ format: TextFormat,
+ want: "test content",
+ wantErr: false,
+ },
+ {
+ name: "json format",
+ content: "test content",
+ format: JSONFormat,
+ want: "{\n \"response\": \"test content\"\n}",
+ wantErr: false,
+ },
+ {
+ name: "invalid format",
+ content: "test content",
+ format: "invalid",
+ want: "",
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ got, err := FormatOutput(tt.content, tt.format)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("FormatOutput() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("FormatOutput() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/packages/tui/internal/pubsub/broker.go b/packages/tui/internal/pubsub/broker.go
new file mode 100644
index 000000000..05a4476c8
--- /dev/null
+++ b/packages/tui/internal/pubsub/broker.go
@@ -0,0 +1,113 @@
+package pubsub
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "sync"
+ "time"
+)
+
+const defaultChannelBufferSize = 100
+
+type Broker[T any] struct {
+ subs map[chan Event[T]]context.CancelFunc
+ mu sync.RWMutex
+ isClosed bool
+}
+
+func NewBroker[T any]() *Broker[T] {
+ return &Broker[T]{
+ subs: make(map[chan Event[T]]context.CancelFunc),
+ }
+}
+
+func (b *Broker[T]) Shutdown() {
+ b.mu.Lock()
+ if b.isClosed {
+ b.mu.Unlock()
+ return
+ }
+ b.isClosed = true
+
+ for ch, cancel := range b.subs {
+ cancel()
+ close(ch)
+ delete(b.subs, ch)
+ }
+ b.mu.Unlock()
+ slog.Debug("PubSub broker shut down", "type", fmt.Sprintf("%T", *new(T)))
+}
+
+func (b *Broker[T]) Subscribe(ctx context.Context) <-chan Event[T] {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ if b.isClosed {
+ closedCh := make(chan Event[T])
+ close(closedCh)
+ return closedCh
+ }
+
+ subCtx, subCancel := context.WithCancel(ctx)
+ subscriberChannel := make(chan Event[T], defaultChannelBufferSize)
+ b.subs[subscriberChannel] = subCancel
+
+ go func() {
+ <-subCtx.Done()
+ b.mu.Lock()
+ defer b.mu.Unlock()
+ if _, ok := b.subs[subscriberChannel]; ok {
+ close(subscriberChannel)
+ delete(b.subs, subscriberChannel)
+ }
+ }()
+
+ return subscriberChannel
+}
+
+func (b *Broker[T]) Publish(eventType EventType, payload T) {
+ b.mu.RLock()
+ defer b.mu.RUnlock()
+
+ if b.isClosed {
+ slog.Warn("Attempted to publish on a closed pubsub broker", "type", eventType, "payload_type", fmt.Sprintf("%T", payload))
+ return
+ }
+
+ event := Event[T]{Type: eventType, Payload: payload}
+
+ for ch := range b.subs {
+ // Non-blocking send with a fallback to a goroutine to prevent slow subscribers
+ // from blocking the publisher.
+ select {
+ case ch <- event:
+ // Successfully sent
+ default:
+ // Subscriber channel is full or receiver is slow.
+ // Send in a new goroutine to avoid blocking the publisher.
+ // This might lead to out-of-order delivery for this specific slow subscriber.
+ go func(sChan chan Event[T], ev Event[T]) {
+ // Re-check if broker is closed before attempting send in goroutine
+ b.mu.RLock()
+ isBrokerClosed := b.isClosed
+ b.mu.RUnlock()
+ if isBrokerClosed {
+ return
+ }
+
+ select {
+ case sChan <- ev:
+ case <-time.After(2 * time.Second): // Timeout for slow subscriber
+ slog.Warn("PubSub: Dropped event for slow subscriber after timeout", "type", ev.Type)
+ }
+ }(ch, event)
+ }
+ }
+}
+
+func (b *Broker[T]) GetSubscriberCount() int {
+ b.mu.RLock()
+ defer b.mu.RUnlock()
+ return len(b.subs)
+}
diff --git a/packages/tui/internal/pubsub/broker_test.go b/packages/tui/internal/pubsub/broker_test.go
new file mode 100644
index 000000000..b4caa98f3
--- /dev/null
+++ b/packages/tui/internal/pubsub/broker_test.go
@@ -0,0 +1,144 @@
+package pubsub
+
+import (
+ "context"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestBrokerSubscribe(t *testing.T) {
+ t.Parallel()
+
+ t.Run("with cancellable context", func(t *testing.T) {
+ t.Parallel()
+ broker := NewBroker[string]()
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ ch := broker.Subscribe(ctx)
+ assert.NotNil(t, ch)
+ assert.Equal(t, 1, broker.GetSubscriberCount())
+
+ // Cancel the context should remove the subscription
+ cancel()
+ time.Sleep(10 * time.Millisecond) // Give time for goroutine to process
+ assert.Equal(t, 0, broker.GetSubscriberCount())
+ })
+
+ t.Run("with background context", func(t *testing.T) {
+ t.Parallel()
+ broker := NewBroker[string]()
+
+ // Using context.Background() should not leak goroutines
+ ch := broker.Subscribe(context.Background())
+ assert.NotNil(t, ch)
+ assert.Equal(t, 1, broker.GetSubscriberCount())
+
+ // Shutdown should clean up all subscriptions
+ broker.Shutdown()
+ assert.Equal(t, 0, broker.GetSubscriberCount())
+ })
+}
+
+func TestBrokerPublish(t *testing.T) {
+ t.Parallel()
+ broker := NewBroker[string]()
+ ctx := t.Context()
+
+ ch := broker.Subscribe(ctx)
+
+ // Publish a message
+ broker.Publish(EventTypeCreated, "test message")
+
+ // Verify message is received
+ select {
+ case event := <-ch:
+ assert.Equal(t, EventTypeCreated, event.Type)
+ assert.Equal(t, "test message", event.Payload)
+ case <-time.After(100 * time.Millisecond):
+ t.Fatal("timeout waiting for message")
+ }
+}
+
+func TestBrokerShutdown(t *testing.T) {
+ t.Parallel()
+ broker := NewBroker[string]()
+
+ // Create multiple subscribers
+ ch1 := broker.Subscribe(context.Background())
+ ch2 := broker.Subscribe(context.Background())
+
+ assert.Equal(t, 2, broker.GetSubscriberCount())
+
+ // Shutdown should close all channels and clean up
+ broker.Shutdown()
+
+ // Verify channels are closed
+ _, ok1 := <-ch1
+ _, ok2 := <-ch2
+ assert.False(t, ok1, "channel 1 should be closed")
+ assert.False(t, ok2, "channel 2 should be closed")
+
+ // Verify subscriber count is reset
+ assert.Equal(t, 0, broker.GetSubscriberCount())
+}
+
+func TestBrokerConcurrency(t *testing.T) {
+ t.Parallel()
+ broker := NewBroker[int]()
+
+ // Create a large number of subscribers
+ const numSubscribers = 100
+ var wg sync.WaitGroup
+ wg.Add(numSubscribers)
+
+ // Create a channel to collect received events
+ receivedEvents := make(chan int, numSubscribers)
+
+ for i := range numSubscribers {
+ go func(id int) {
+ defer wg.Done()
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ ch := broker.Subscribe(ctx)
+
+ // Receive one message then cancel
+ select {
+ case event := <-ch:
+ receivedEvents <- event.Payload
+ case <-time.After(1 * time.Second):
+ t.Errorf("timeout waiting for message %d", id)
+ }
+ cancel()
+ }(i)
+ }
+
+ // Give subscribers time to set up
+ time.Sleep(10 * time.Millisecond)
+
+ // Publish messages to all subscribers
+ for i := range numSubscribers {
+ broker.Publish(EventTypeCreated, i)
+ }
+
+ // Wait for all subscribers to finish
+ wg.Wait()
+ close(receivedEvents)
+
+ // Give time for cleanup goroutines to run
+ time.Sleep(10 * time.Millisecond)
+
+ // Verify all subscribers are cleaned up
+ assert.Equal(t, 0, broker.GetSubscriberCount())
+
+ // Verify we received the expected number of events
+ count := 0
+ for range receivedEvents {
+ count++
+ }
+ assert.Equal(t, numSubscribers, count)
+}
diff --git a/packages/tui/internal/pubsub/events.go b/packages/tui/internal/pubsub/events.go
new file mode 100644
index 000000000..e3910f9f5
--- /dev/null
+++ b/packages/tui/internal/pubsub/events.go
@@ -0,0 +1,24 @@
+package pubsub
+
+import "context"
+
+type EventType string
+
+const (
+ EventTypeCreated EventType = "created"
+ EventTypeUpdated EventType = "updated"
+ EventTypeDeleted EventType = "deleted"
+)
+
+type Event[T any] struct {
+ Type EventType
+ Payload T
+}
+
+type Subscriber[T any] interface {
+ Subscribe(ctx context.Context) <-chan Event[T]
+}
+
+type Publisher[T any] interface {
+ Publish(eventType EventType, payload T)
+}
diff --git a/packages/tui/internal/status/status.go b/packages/tui/internal/status/status.go
new file mode 100644
index 000000000..3648a64ae
--- /dev/null
+++ b/packages/tui/internal/status/status.go
@@ -0,0 +1,142 @@
+package status
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "sync"
+ "time"
+
+ "github.com/sst/opencode/internal/pubsub"
+)
+
+type Level string
+
+const (
+ LevelInfo Level = "info"
+ LevelWarn Level = "warn"
+ LevelError Level = "error"
+ LevelDebug Level = "debug"
+)
+
+type StatusMessage struct {
+ Level Level `json:"level"`
+ Message string `json:"message"`
+ Timestamp time.Time `json:"timestamp"`
+ Critical bool `json:"critical"`
+ Duration time.Duration `json:"duration"`
+}
+
+// StatusOption is a function that configures a status message
+type StatusOption func(*StatusMessage)
+
+// WithCritical marks a status message as critical, causing it to be displayed immediately
+func WithCritical(critical bool) StatusOption {
+ return func(msg *StatusMessage) {
+ msg.Critical = critical
+ }
+}
+
+// WithDuration sets a custom display duration for a status message
+func WithDuration(duration time.Duration) StatusOption {
+ return func(msg *StatusMessage) {
+ msg.Duration = duration
+ }
+}
+
+const (
+ EventStatusPublished pubsub.EventType = "status_published"
+)
+
+type Service interface {
+ pubsub.Subscriber[StatusMessage]
+
+ Info(message string, opts ...StatusOption)
+ Warn(message string, opts ...StatusOption)
+ Error(message string, opts ...StatusOption)
+ Debug(message string, opts ...StatusOption)
+}
+
+type service struct {
+ broker *pubsub.Broker[StatusMessage]
+ mu sync.RWMutex
+}
+
+var globalStatusService *service
+
+func InitService() error {
+ if globalStatusService != nil {
+ return fmt.Errorf("status service already initialized")
+ }
+ broker := pubsub.NewBroker[StatusMessage]()
+ globalStatusService = &service{
+ broker: broker,
+ }
+ return nil
+}
+
+func GetService() Service {
+ if globalStatusService == nil {
+ panic("status service not initialized. Call status.InitService() at application startup.")
+ }
+ return globalStatusService
+}
+
+func (s *service) Info(message string, opts ...StatusOption) {
+ s.publish(LevelInfo, message, opts...)
+ slog.Info(message)
+}
+
+func (s *service) Warn(message string, opts ...StatusOption) {
+ s.publish(LevelWarn, message, opts...)
+ slog.Warn(message)
+}
+
+func (s *service) Error(message string, opts ...StatusOption) {
+ s.publish(LevelError, message, opts...)
+ slog.Error(message)
+}
+
+func (s *service) Debug(message string, opts ...StatusOption) {
+ s.publish(LevelDebug, message, opts...)
+ slog.Debug(message)
+}
+
+func (s *service) publish(level Level, messageText string, opts ...StatusOption) {
+ statusMsg := StatusMessage{
+ Level: level,
+ Message: messageText,
+ Timestamp: time.Now(),
+ }
+
+ // Apply all options
+ for _, opt := range opts {
+ opt(&statusMsg)
+ }
+
+ s.broker.Publish(EventStatusPublished, statusMsg)
+}
+
+func (s *service) Subscribe(ctx context.Context) <-chan pubsub.Event[StatusMessage] {
+ return s.broker.Subscribe(ctx)
+}
+
+func Info(message string, opts ...StatusOption) {
+ GetService().Info(message, opts...)
+}
+
+func Warn(message string, opts ...StatusOption) {
+ GetService().Warn(message, opts...)
+}
+
+func Error(message string, opts ...StatusOption) {
+ GetService().Error(message, opts...)
+}
+
+func Debug(message string, opts ...StatusOption) {
+ GetService().Debug(message, opts...)
+}
+
+func Subscribe(ctx context.Context) <-chan pubsub.Event[StatusMessage] {
+ return GetService().Subscribe(ctx)
+}
diff --git a/packages/tui/internal/tui/app/app.go b/packages/tui/internal/tui/app/app.go
new file mode 100644
index 000000000..8320d815b
--- /dev/null
+++ b/packages/tui/internal/tui/app/app.go
@@ -0,0 +1,215 @@
+package app
+
+import (
+ "context"
+ "fmt"
+
+ "log/slog"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/sst/opencode/internal/config"
+ "github.com/sst/opencode/internal/fileutil"
+ "github.com/sst/opencode/internal/status"
+ "github.com/sst/opencode/internal/tui/state"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/internal/tui/util"
+ "github.com/sst/opencode/pkg/client"
+)
+
+type App struct {
+ Client *client.ClientWithResponses
+ Events *client.Client
+ Provider *client.ProviderInfo
+ Model *client.ProviderModel
+ Session *client.SessionInfo
+ Messages []client.MessageInfo
+ Status status.Service
+
+ PrimaryAgentOLD AgentService
+
+ // UI state
+ filepickerOpen bool
+ completionDialogOpen bool
+}
+
+func New(ctx context.Context) (*App, error) {
+ // Initialize status service (still needed for UI notifications)
+ err := status.InitService()
+ if err != nil {
+ slog.Error("Failed to initialize status service", "error", err)
+ return nil, err
+ }
+
+ // Initialize file utilities
+ fileutil.Init()
+
+ // Create HTTP client
+ url := "http://localhost:16713"
+ httpClient, err := client.NewClientWithResponses(url)
+ if err != nil {
+ slog.Error("Failed to create client", "error", err)
+ return nil, err
+ }
+ eventClient, err := client.NewClient(url)
+ if err != nil {
+ slog.Error("Failed to create event client", "error", err)
+ return nil, err
+ }
+
+ // Create service bridges
+ agentBridge := NewAgentServiceBridge(httpClient)
+
+ app := &App{
+ Client: httpClient,
+ Events: eventClient,
+ Session: &client.SessionInfo{},
+ Messages: []client.MessageInfo{},
+ PrimaryAgentOLD: agentBridge,
+ Status: status.GetService(),
+ }
+
+ // Initialize theme based on configuration
+ app.initTheme()
+
+ return app, nil
+}
+
+type Attachment struct {
+ FilePath string
+ FileName string
+ MimeType string
+ Content []byte
+}
+
+// Create creates a new session
+func (a *App) SendChatMessage(ctx context.Context, text string, attachments []Attachment) tea.Cmd {
+ var cmds []tea.Cmd
+ if a.Session.Id == "" {
+ resp, err := a.Client.PostSessionCreateWithResponse(ctx)
+ if err != nil {
+ status.Error(err.Error())
+ return nil
+ }
+ if resp.StatusCode() != 200 {
+ status.Error(fmt.Sprintf("failed to create session: %d", resp.StatusCode()))
+ return nil
+ }
+
+ info := resp.JSON200
+ a.Session = info
+
+ cmds = append(cmds, util.CmdHandler(state.SessionSelectedMsg(info)))
+ }
+
+ // TODO: Handle attachments when API supports them
+ if len(attachments) > 0 {
+ // For now, ignore attachments
+ // return "", fmt.Errorf("attachments not supported yet")
+ }
+
+ part := client.MessagePart{}
+ part.FromMessagePartText(client.MessagePartText{
+ Type: "text",
+ Text: text,
+ })
+ parts := []client.MessagePart{part}
+
+ go a.Client.PostSessionChatWithResponse(ctx, client.PostSessionChatJSONRequestBody{
+ SessionID: a.Session.Id,
+ Parts: parts,
+ ProviderID: a.Provider.Id,
+ ModelID: a.Model.Id,
+ })
+
+ // The actual response will come through SSE
+ // For now, just return success
+ return tea.Batch(cmds...)
+}
+
+func (a *App) ListSessions(ctx context.Context) ([]client.SessionInfo, error) {
+ resp, err := a.Client.PostSessionListWithResponse(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if resp.StatusCode() != 200 {
+ return nil, fmt.Errorf("failed to list sessions: %d", resp.StatusCode())
+ }
+ if resp.JSON200 == nil {
+ return []client.SessionInfo{}, nil
+ }
+
+ sessions := *resp.JSON200
+ return sessions, nil
+}
+
+func (a *App) ListMessages(ctx context.Context, sessionId string) ([]client.MessageInfo, error) {
+ resp, err := a.Client.PostSessionMessagesWithResponse(ctx, client.PostSessionMessagesJSONRequestBody{SessionID: sessionId})
+ if err != nil {
+ return nil, err
+ }
+ if resp.StatusCode() != 200 {
+ return nil, fmt.Errorf("failed to list messages: %d", resp.StatusCode())
+ }
+ if resp.JSON200 == nil {
+ return []client.MessageInfo{}, nil
+ }
+ messages := *resp.JSON200
+ return messages, nil
+}
+
+func (a *App) ListProviders(ctx context.Context) ([]client.ProviderInfo, error) {
+ resp, err := a.Client.PostProviderListWithResponse(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if resp.StatusCode() != 200 {
+ return nil, fmt.Errorf("failed to list sessions: %d", resp.StatusCode())
+ }
+ if resp.JSON200 == nil {
+ return []client.ProviderInfo{}, nil
+ }
+
+ providers := *resp.JSON200
+ return providers, nil
+}
+
+// initTheme sets the application theme based on the configuration
+func (app *App) initTheme() {
+ cfg := config.Get()
+ if cfg == nil || cfg.TUI.Theme == "" {
+ return // Use default theme
+ }
+
+ // Try to set the theme from config
+ err := theme.SetTheme(cfg.TUI.Theme)
+ if err != nil {
+ slog.Warn("Failed to set theme from config, using default theme", "theme", cfg.TUI.Theme, "error", err)
+ } else {
+ slog.Debug("Set theme from config", "theme", cfg.TUI.Theme)
+ }
+}
+
+// IsFilepickerOpen returns whether the filepicker is currently open
+func (app *App) IsFilepickerOpen() bool {
+ return app.filepickerOpen
+}
+
+// SetFilepickerOpen sets the state of the filepicker
+func (app *App) SetFilepickerOpen(open bool) {
+ app.filepickerOpen = open
+}
+
+// IsCompletionDialogOpen returns whether the completion dialog is currently open
+func (app *App) IsCompletionDialogOpen() bool {
+ return app.completionDialogOpen
+}
+
+// SetCompletionDialogOpen sets the state of the completion dialog
+func (app *App) SetCompletionDialogOpen(open bool) {
+ app.completionDialogOpen = open
+}
+
+// Shutdown performs a clean shutdown of the application
+func (app *App) Shutdown() {
+ // TODO: cleanup?
+}
diff --git a/packages/tui/internal/tui/app/bridge.go b/packages/tui/internal/tui/app/bridge.go
new file mode 100644
index 000000000..cd149f6b3
--- /dev/null
+++ b/packages/tui/internal/tui/app/bridge.go
@@ -0,0 +1,42 @@
+package app
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/sst/opencode/pkg/client"
+)
+
+// AgentServiceBridge provides a minimal agent service that sends messages to the API
+type AgentServiceBridge struct {
+ client *client.ClientWithResponses
+}
+
+// NewAgentServiceBridge creates a new agent service bridge
+func NewAgentServiceBridge(client *client.ClientWithResponses) *AgentServiceBridge {
+ return &AgentServiceBridge{client: client}
+}
+
+// Cancel cancels the current generation - NOT IMPLEMENTED IN API YET
+func (a *AgentServiceBridge) Cancel(sessionID string) error {
+ // TODO: Not implemented in TypeScript API yet
+ return nil
+}
+
+// IsBusy checks if the agent is busy - NOT IMPLEMENTED IN API YET
+func (a *AgentServiceBridge) IsBusy() bool {
+ // TODO: Not implemented in TypeScript API yet
+ return false
+}
+
+// IsSessionBusy checks if the agent is busy for a specific session - NOT IMPLEMENTED IN API YET
+func (a *AgentServiceBridge) IsSessionBusy(sessionID string) bool {
+ // TODO: Not implemented in TypeScript API yet
+ return false
+}
+
+// CompactSession compacts a session - NOT IMPLEMENTED IN API YET
+func (a *AgentServiceBridge) CompactSession(ctx context.Context, sessionID string, force bool) error {
+ // TODO: Not implemented in TypeScript API yet
+ return fmt.Errorf("session compaction not implemented in API")
+}
diff --git a/packages/tui/internal/tui/app/interfaces.go b/packages/tui/internal/tui/app/interfaces.go
new file mode 100644
index 000000000..a396ef586
--- /dev/null
+++ b/packages/tui/internal/tui/app/interfaces.go
@@ -0,0 +1,13 @@
+package app
+
+import (
+ "context"
+)
+
+// AgentService defines the interface for agent operations
+type AgentService interface {
+ Cancel(sessionID string) error
+ IsBusy() bool
+ IsSessionBusy(sessionID string) bool
+ CompactSession(ctx context.Context, sessionID string, force bool) error
+}
diff --git a/packages/tui/internal/tui/components/chat/chat.go b/packages/tui/internal/tui/components/chat/chat.go
new file mode 100644
index 000000000..2fabea43d
--- /dev/null
+++ b/packages/tui/internal/tui/components/chat/chat.go
@@ -0,0 +1,133 @@
+package chat
+
+import (
+ "fmt"
+ "sort"
+
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/x/ansi"
+ "github.com/sst/opencode/internal/config"
+ "github.com/sst/opencode/internal/tui/app"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/internal/version"
+)
+
+type SendMsg struct {
+ Text string
+ Attachments []app.Attachment
+}
+
+func header(width int) string {
+ return lipgloss.JoinVertical(
+ lipgloss.Top,
+ logo(width),
+ repo(width),
+ "",
+ cwd(width),
+ )
+}
+
+func lspsConfigured(width int) string {
+ // cfg := config.Get()
+ title := "LSP Servers"
+ title = ansi.Truncate(title, width, "…")
+
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ lsps := baseStyle.
+ Width(width).
+ Foreground(t.Primary()).
+ Bold(true).
+ Render(title)
+
+ // Get LSP names and sort them for consistent ordering
+ var lspNames []string
+ // for name := range cfg.LSP {
+ // lspNames = append(lspNames, name)
+ // }
+ sort.Strings(lspNames)
+
+ var lspViews []string
+ // for _, name := range lspNames {
+ // lsp := cfg.LSP[name]
+ // lspName := baseStyle.
+ // Foreground(t.Text()).
+ // Render(fmt.Sprintf("• %s", name))
+
+ // cmd := lsp.Command
+ // cmd = ansi.Truncate(cmd, width-lipgloss.Width(lspName)-3, "…")
+
+ // lspPath := baseStyle.
+ // Foreground(t.TextMuted()).
+ // Render(fmt.Sprintf(" (%s)", cmd))
+
+ // lspViews = append(lspViews,
+ // baseStyle.
+ // Width(width).
+ // Render(
+ // lipgloss.JoinHorizontal(
+ // lipgloss.Left,
+ // lspName,
+ // lspPath,
+ // ),
+ // ),
+ // )
+ // }
+
+ return baseStyle.
+ Width(width).
+ Render(
+ lipgloss.JoinVertical(
+ lipgloss.Left,
+ lsps,
+ lipgloss.JoinVertical(
+ lipgloss.Left,
+ lspViews...,
+ ),
+ ),
+ )
+}
+
+func logo(width int) string {
+ logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode")
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ versionText := baseStyle.
+ Foreground(t.TextMuted()).
+ Render(version.Version)
+
+ return baseStyle.
+ Bold(true).
+ Width(width).
+ Render(
+ lipgloss.JoinHorizontal(
+ lipgloss.Left,
+ logo,
+ " ",
+ versionText,
+ ),
+ )
+}
+
+func repo(width int) string {
+ repo := "github.com/sst/opencode"
+ t := theme.CurrentTheme()
+
+ return styles.BaseStyle().
+ Foreground(t.TextMuted()).
+ Width(width).
+ Render(repo)
+}
+
+func cwd(width int) string {
+ cwd := fmt.Sprintf("cwd: %s", config.WorkingDirectory())
+ t := theme.CurrentTheme()
+
+ return styles.BaseStyle().
+ Foreground(t.TextMuted()).
+ Width(width).
+ Render(cwd)
+}
diff --git a/packages/tui/internal/tui/components/chat/editor.go b/packages/tui/internal/tui/components/chat/editor.go
new file mode 100644
index 000000000..6586f2020
--- /dev/null
+++ b/packages/tui/internal/tui/components/chat/editor.go
@@ -0,0 +1,406 @@
+package chat
+
+import (
+ "fmt"
+ "log/slog"
+ "os"
+ "os/exec"
+ "slices"
+ "strings"
+ "unicode"
+
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/textarea"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/status"
+ "github.com/sst/opencode/internal/tui/app"
+ "github.com/sst/opencode/internal/tui/components/dialog"
+ "github.com/sst/opencode/internal/tui/image"
+ "github.com/sst/opencode/internal/tui/layout"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/internal/tui/util"
+)
+
+type editorCmp struct {
+ width int
+ height int
+ app *app.App
+ textarea textarea.Model
+ attachments []app.Attachment
+ deleteMode bool
+ history []string
+ historyIndex int
+ currentMessage string
+}
+
+type EditorKeyMaps struct {
+ Send key.Binding
+ OpenEditor key.Binding
+ Paste key.Binding
+ HistoryUp key.Binding
+ HistoryDown key.Binding
+}
+
+type bluredEditorKeyMaps struct {
+ Send key.Binding
+ Focus key.Binding
+ OpenEditor key.Binding
+}
+type DeleteAttachmentKeyMaps struct {
+ AttachmentDeleteMode key.Binding
+ Escape key.Binding
+ DeleteAllAttachments key.Binding
+}
+
+var editorMaps = EditorKeyMaps{
+ Send: key.NewBinding(
+ key.WithKeys("enter", "ctrl+s"),
+ key.WithHelp("enter", "send message"),
+ ),
+ OpenEditor: key.NewBinding(
+ key.WithKeys("ctrl+e"),
+ key.WithHelp("ctrl+e", "open editor"),
+ ),
+ Paste: key.NewBinding(
+ key.WithKeys("ctrl+v"),
+ key.WithHelp("ctrl+v", "paste content"),
+ ),
+ HistoryUp: key.NewBinding(
+ key.WithKeys("up"),
+ key.WithHelp("up", "previous message"),
+ ),
+ HistoryDown: key.NewBinding(
+ key.WithKeys("down"),
+ key.WithHelp("down", "next message"),
+ ),
+}
+
+var DeleteKeyMaps = DeleteAttachmentKeyMaps{
+ AttachmentDeleteMode: key.NewBinding(
+ key.WithKeys("ctrl+r"),
+ key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
+ ),
+ Escape: key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "cancel delete mode"),
+ ),
+ DeleteAllAttachments: key.NewBinding(
+ key.WithKeys("r"),
+ key.WithHelp("ctrl+r+r", "delete all attachments"),
+ ),
+}
+
+const (
+ maxAttachments = 5
+)
+
+func (m *editorCmp) openEditor(value string) tea.Cmd {
+ editor := os.Getenv("EDITOR")
+ if editor == "" {
+ editor = "nvim"
+ }
+
+ tmpfile, err := os.CreateTemp("", "msg_*.md")
+ tmpfile.WriteString(value)
+ if err != nil {
+ status.Error(err.Error())
+ return nil
+ }
+ tmpfile.Close()
+ c := exec.Command(editor, tmpfile.Name()) //nolint:gosec
+ c.Stdin = os.Stdin
+ c.Stdout = os.Stdout
+ c.Stderr = os.Stderr
+ return tea.ExecProcess(c, func(err error) tea.Msg {
+ if err != nil {
+ status.Error(err.Error())
+ return nil
+ }
+ content, err := os.ReadFile(tmpfile.Name())
+ if err != nil {
+ status.Error(err.Error())
+ return nil
+ }
+ if len(content) == 0 {
+ status.Warn("Message is empty")
+ return nil
+ }
+ os.Remove(tmpfile.Name())
+ attachments := m.attachments
+ m.attachments = nil
+ return SendMsg{
+ Text: string(content),
+ Attachments: attachments,
+ }
+ })
+}
+
+func (m *editorCmp) Init() tea.Cmd {
+ return textarea.Blink
+}
+
+func (m *editorCmp) send() tea.Cmd {
+ value := m.textarea.Value()
+ m.textarea.Reset()
+ attachments := m.attachments
+
+ // Save to history if not empty and not a duplicate of the last entry
+ if value != "" {
+ if len(m.history) == 0 || m.history[len(m.history)-1] != value {
+ m.history = append(m.history, value)
+ }
+ m.historyIndex = len(m.history)
+ m.currentMessage = ""
+ }
+
+ m.attachments = nil
+ if value == "" {
+ return nil
+ }
+ return tea.Batch(
+ util.CmdHandler(SendMsg{
+ Text: value,
+ Attachments: attachments,
+ }),
+ )
+}
+
+func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmd tea.Cmd
+ switch msg := msg.(type) {
+ case dialog.ThemeChangedMsg:
+ m.textarea = CreateTextArea(&m.textarea)
+ case dialog.CompletionSelectedMsg:
+ existingValue := m.textarea.Value()
+ modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
+
+ m.textarea.SetValue(modifiedValue)
+ return m, nil
+ case dialog.AttachmentAddedMsg:
+ if len(m.attachments) >= maxAttachments {
+ status.Error(fmt.Sprintf("cannot add more than %d images", maxAttachments))
+ return m, cmd
+ }
+ m.attachments = append(m.attachments, msg.Attachment)
+ case tea.KeyMsg:
+ if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) {
+ m.deleteMode = true
+ return m, nil
+ }
+ if key.Matches(msg, DeleteKeyMaps.DeleteAllAttachments) && m.deleteMode {
+ m.deleteMode = false
+ m.attachments = nil
+ return m, nil
+ }
+ if m.deleteMode && len(msg.Runes) > 0 && unicode.IsDigit(msg.Runes[0]) {
+ num := int(msg.Runes[0] - '0')
+ m.deleteMode = false
+ if num < 10 && len(m.attachments) > num {
+ if num == 0 {
+ m.attachments = m.attachments[num+1:]
+ } else {
+ m.attachments = slices.Delete(m.attachments, num, num+1)
+ }
+ return m, nil
+ }
+ }
+ if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
+ key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
+ return m, nil
+ }
+ if key.Matches(msg, editorMaps.OpenEditor) {
+ // if m.app.PrimaryAgentOLD.IsSessionBusy(m.app.CurrentSessionOLD.ID) {
+ // status.Warn("Agent is working, please wait...")
+ // return m, nil
+ // }
+ value := m.textarea.Value()
+ m.textarea.Reset()
+ return m, m.openEditor(value)
+ }
+ if key.Matches(msg, DeleteKeyMaps.Escape) {
+ m.deleteMode = false
+ return m, nil
+ }
+
+ if key.Matches(msg, editorMaps.Paste) {
+ imageBytes, text, err := image.GetImageFromClipboard()
+ if err != nil {
+ slog.Error(err.Error())
+ return m, cmd
+ }
+ if len(imageBytes) != 0 {
+ attachmentName := fmt.Sprintf("clipboard-image-%d", len(m.attachments))
+ attachment := app.Attachment{FilePath: attachmentName, FileName: attachmentName, Content: imageBytes, MimeType: "image/png"}
+ m.attachments = append(m.attachments, attachment)
+ } else {
+ m.textarea.SetValue(m.textarea.Value() + text)
+ }
+ return m, cmd
+ }
+
+ // Handle history navigation with up/down arrow keys
+ // Only handle history navigation if the filepicker is not open and completion dialog is not open
+ if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryUp) && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() {
+ // Get the current line number
+ currentLine := m.textarea.Line()
+
+ // Only navigate history if we're at the first line
+ if currentLine == 0 && len(m.history) > 0 {
+ // Save current message if we're just starting to navigate
+ if m.historyIndex == len(m.history) {
+ m.currentMessage = m.textarea.Value()
+ }
+
+ // Go to previous message in history
+ if m.historyIndex > 0 {
+ m.historyIndex--
+ m.textarea.SetValue(m.history[m.historyIndex])
+ }
+ return m, nil
+ }
+ }
+
+ if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryDown) && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() {
+ // Get the current line number and total lines
+ currentLine := m.textarea.Line()
+ value := m.textarea.Value()
+ lines := strings.Split(value, "\n")
+ totalLines := len(lines)
+
+ // Only navigate history if we're at the last line
+ if currentLine == totalLines-1 {
+ if m.historyIndex < len(m.history)-1 {
+ // Go to next message in history
+ m.historyIndex++
+ m.textarea.SetValue(m.history[m.historyIndex])
+ } else if m.historyIndex == len(m.history)-1 {
+ // Return to the current message being composed
+ m.historyIndex = len(m.history)
+ m.textarea.SetValue(m.currentMessage)
+ }
+ return m, nil
+ }
+ }
+
+ // Handle Enter key
+ if m.textarea.Focused() && key.Matches(msg, editorMaps.Send) {
+ value := m.textarea.Value()
+ if len(value) > 0 && value[len(value)-1] == '\\' {
+ // If the last character is a backslash, remove it and add a newline
+ m.textarea.SetValue(value[:len(value)-1] + "\n")
+ return m, nil
+ } else {
+ // Otherwise, send the message
+ return m, m.send()
+ }
+ }
+
+ }
+ m.textarea, cmd = m.textarea.Update(msg)
+ return m, cmd
+}
+
+func (m *editorCmp) View() string {
+ t := theme.CurrentTheme()
+
+ // Style the prompt with theme colors
+ style := lipgloss.NewStyle().
+ Padding(0, 0, 0, 1).
+ Bold(true).
+ Foreground(t.Primary())
+
+ if len(m.attachments) == 0 {
+ return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View())
+ }
+ m.textarea.SetHeight(m.height - 1)
+ return lipgloss.JoinVertical(lipgloss.Top,
+ m.attachmentsContent(),
+ lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"),
+ m.textarea.View()),
+ )
+}
+
+func (m *editorCmp) SetSize(width, height int) tea.Cmd {
+ m.width = width
+ m.height = height
+ m.textarea.SetWidth(width - 3) // account for the prompt and padding right
+ m.textarea.SetHeight(height)
+ return nil
+}
+
+func (m *editorCmp) GetSize() (int, int) {
+ return m.textarea.Width(), m.textarea.Height()
+}
+
+func (m *editorCmp) attachmentsContent() string {
+ var styledAttachments []string
+ t := theme.CurrentTheme()
+ attachmentStyles := styles.BaseStyle().
+ MarginLeft(1).
+ Background(t.TextMuted()).
+ Foreground(t.Text())
+ for i, attachment := range m.attachments {
+ var filename string
+ if len(attachment.FileName) > 10 {
+ filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, attachment.FileName[0:7])
+ } else {
+ filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, attachment.FileName)
+ }
+ if m.deleteMode {
+ filename = fmt.Sprintf("%d%s", i, filename)
+ }
+ styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
+ }
+ content := lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...)
+ return content
+}
+
+func (m *editorCmp) BindingKeys() []key.Binding {
+ bindings := []key.Binding{}
+ bindings = append(bindings, layout.KeyMapToSlice(editorMaps)...)
+ bindings = append(bindings, layout.KeyMapToSlice(DeleteKeyMaps)...)
+ return bindings
+}
+
+func CreateTextArea(existing *textarea.Model) textarea.Model {
+ t := theme.CurrentTheme()
+ bgColor := t.Background()
+ textColor := t.Text()
+ textMutedColor := t.TextMuted()
+
+ ta := textarea.New()
+ ta.BlurredStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
+ ta.BlurredStyle.CursorLine = styles.BaseStyle().Background(bgColor)
+ ta.BlurredStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
+ ta.BlurredStyle.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
+ ta.FocusedStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
+ ta.FocusedStyle.CursorLine = styles.BaseStyle().Background(bgColor)
+ ta.FocusedStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
+ ta.FocusedStyle.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
+
+ ta.Prompt = " "
+ ta.ShowLineNumbers = false
+ ta.CharLimit = -1
+
+ if existing != nil {
+ ta.SetValue(existing.Value())
+ ta.SetWidth(existing.Width())
+ ta.SetHeight(existing.Height())
+ }
+
+ ta.Focus()
+ return ta
+}
+
+func NewEditorCmp(app *app.App) tea.Model {
+ ta := CreateTextArea(nil)
+ return &editorCmp{
+ app: app,
+ textarea: ta,
+ history: []string{},
+ historyIndex: 0,
+ currentMessage: "",
+ }
+}
diff --git a/packages/tui/internal/tui/components/chat/message.go b/packages/tui/internal/tui/components/chat/message.go
new file mode 100644
index 000000000..feed7ec59
--- /dev/null
+++ b/packages/tui/internal/tui/components/chat/message.go
@@ -0,0 +1,716 @@
+package chat
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/x/ansi"
+ "github.com/sst/opencode/internal/config"
+ "github.com/sst/opencode/internal/diff"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/pkg/client"
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
+)
+
+const (
+ maxResultHeight = 10
+)
+
+func toMarkdown(content string, width int) string {
+ r := styles.GetMarkdownRenderer(width)
+ rendered, _ := r.Render(content)
+ return strings.TrimSuffix(rendered, "\n")
+}
+
+func renderUserMessage(msg client.MessageInfo, width int) string {
+ t := theme.CurrentTheme()
+ style := styles.BaseStyle().
+ BorderLeft(true).
+ Foreground(t.TextMuted()).
+ BorderForeground(t.Secondary()).
+ BorderStyle(lipgloss.ThickBorder())
+
+ baseStyle := styles.BaseStyle()
+ // var styledAttachments []string
+ // attachmentStyles := baseStyle.
+ // MarginLeft(1).
+ // Background(t.TextMuted()).
+ // Foreground(t.Text())
+ // for _, attachment := range msg.BinaryContent() {
+ // file := filepath.Base(attachment.Path)
+ // var filename string
+ // if len(file) > 10 {
+ // filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, file[0:7])
+ // } else {
+ // filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, file)
+ // }
+ // styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
+ // }
+
+ // Add timestamp info
+ timestamp := time.UnixMilli(int64(msg.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
+ username, _ := config.GetUsername()
+ info := baseStyle.
+ Foreground(t.TextMuted()).
+ Render(fmt.Sprintf(" %s (%s)", username, timestamp))
+
+ content := ""
+ // if len(styledAttachments) > 0 {
+ // attachmentContent := baseStyle.Width(width).Render(lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...))
+ // content = renderMessage(msg.Content().String(), true, isFocused, width, append(info, attachmentContent)...)
+ // } else {
+ for _, p := range msg.Parts {
+ part, err := p.ValueByDiscriminator()
+ if err != nil {
+ continue //TODO: handle error?
+ }
+
+ switch part.(type) {
+ case client.MessagePartText:
+ textPart := part.(client.MessagePartText)
+ text := toMarkdown(textPart.Text, width)
+ content = style.Render(lipgloss.JoinVertical(lipgloss.Left, text, info))
+ }
+ }
+
+ return styles.ForceReplaceBackgroundWithLipgloss(content, t.Background())
+}
+
+func convertToMap(input *any) (map[string]any, bool) {
+ if input == nil {
+ return nil, false // Handle nil pointer
+ }
+ value := *input // Dereference the pointer to get the interface value
+ m, ok := value.(map[string]any) // Type assertion
+ return m, ok
+}
+
+func renderAssistantMessage(
+ msg client.MessageInfo,
+ width int,
+ showToolMessages bool,
+) string {
+ t := theme.CurrentTheme()
+ style := styles.BaseStyle().
+ BorderLeft(true).
+ Foreground(t.TextMuted()).
+ BorderForeground(t.Primary()).
+ BorderStyle(lipgloss.ThickBorder())
+ toolStyle := styles.BaseStyle().
+ BorderLeft(true).
+ Foreground(t.TextMuted()).
+ BorderForeground(t.TextMuted()).
+ BorderStyle(lipgloss.ThickBorder())
+
+ baseStyle := styles.BaseStyle()
+ messages := []string{}
+
+ // content := strings.TrimSpace(msg.Content().String())
+ // thinking := msg.IsThinking()
+ // thinkingContent := msg.ReasoningContent().Thinking
+ // finished := msg.IsFinished()
+ // finishData := msg.FinishPart()
+
+ // Add timestamp info
+ timestamp := time.UnixMilli(int64(msg.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
+ modelName := msg.Metadata.Assistant.ModelID
+ info := baseStyle.
+ Foreground(t.TextMuted()).
+ Render(fmt.Sprintf(" %s (%s)", modelName, timestamp))
+
+ for _, p := range msg.Parts {
+ part, err := p.ValueByDiscriminator()
+ if err != nil {
+ continue //TODO: handle error?
+ }
+
+ switch part.(type) {
+ case client.MessagePartText:
+ textPart := part.(client.MessagePartText)
+ text := toMarkdown(textPart.Text, width)
+ content := style.Render(lipgloss.JoinVertical(lipgloss.Left, text, info))
+ message := styles.ForceReplaceBackgroundWithLipgloss(content, t.Background())
+ messages = append(messages, message)
+
+ case client.MessagePartToolInvocation:
+ if !showToolMessages {
+ continue
+ }
+
+ toolInvocationPart := part.(client.MessagePartToolInvocation)
+ toolInvocation, _ := toolInvocationPart.ToolInvocation.ValueByDiscriminator()
+ switch toolInvocation.(type) {
+ case client.MessageToolInvocationToolCall:
+ toolCall := toolInvocation.(client.MessageToolInvocationToolCall)
+ toolName := renderToolName(toolCall.ToolName)
+
+ var toolArgs []string
+ toolMap, _ := convertToMap(toolCall.Args)
+ for _, arg := range toolMap {
+ toolArgs = append(toolArgs, fmt.Sprintf("%v", arg))
+ }
+ params := renderParams(width-lipgloss.Width(toolName)-1, toolArgs...)
+ title := styles.Padded().Render(fmt.Sprintf("%s: %s", toolName, params))
+
+ content := toolStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
+ title,
+ " In progress...",
+ ))
+ message := styles.ForceReplaceBackgroundWithLipgloss(content, t.Background())
+ messages = append(messages, message)
+
+ case client.MessageToolInvocationToolResult:
+ toolInvocationResult := toolInvocation.(client.MessageToolInvocationToolResult)
+ toolName := renderToolName(toolInvocationResult.ToolName)
+ var toolArgs []string
+ toolMap, _ := convertToMap(toolInvocationResult.Args)
+ for _, arg := range toolMap {
+ toolArgs = append(toolArgs, fmt.Sprintf("%v", arg))
+ }
+ params := renderParams(width-lipgloss.Width(toolName)-1, toolArgs...)
+ title := styles.Padded().Render(fmt.Sprintf("%s: %s", toolName, params))
+ metadata := msg.Metadata.Tool[toolInvocationResult.ToolCallId].(map[string]any)
+
+ var markdown string
+ if toolInvocationResult.ToolName == "edit" {
+ filename := toolMap["filePath"].(string)
+ title = styles.Padded().Render(fmt.Sprintf("%s: %s", toolName, filename))
+ oldString := toolMap["oldString"].(string)
+ newString := toolMap["newString"].(string)
+ patch, _, _ := diff.GenerateDiff(oldString, newString, filename)
+ formattedDiff, _ := diff.FormatDiff(patch, diff.WithTotalWidth(width))
+ markdown = strings.TrimSpace(formattedDiff)
+ message := toolStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
+ title,
+ markdown,
+ ))
+ messages = append(messages, message)
+ } else if toolInvocationResult.ToolName == "view" {
+ result := toolInvocationResult.Result
+ if metadata["preview"] != nil {
+ result = metadata["preview"].(string)
+ }
+ filename := toolMap["filePath"].(string)
+ ext := filepath.Ext(filename)
+ if ext == "" {
+ ext = ""
+ } else {
+ ext = strings.ToLower(ext[1:])
+ }
+ result = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(result, 10))
+ markdown = toMarkdown(result, width)
+ content := toolStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
+ title,
+ markdown,
+ ))
+ message := styles.ForceReplaceBackgroundWithLipgloss(content, t.Background())
+ messages = append(messages, message)
+ } else {
+ result := truncateHeight(strings.TrimSpace(toolInvocationResult.Result), 10)
+ markdown = toMarkdown(result, width)
+ content := toolStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
+ title,
+ markdown,
+ ))
+ message := styles.ForceReplaceBackgroundWithLipgloss(content, t.Background())
+ messages = append(messages, message)
+ }
+ }
+ }
+ }
+
+ // if finished {
+ // // Add finish info if available
+ // switch finishData.Reason {
+ // case message.FinishReasonCanceled:
+ // info = append(info, baseStyle.
+ // Width(width-1).
+ // Foreground(t.Warning()).
+ // Render("(canceled)"),
+ // )
+ // case message.FinishReasonError:
+ // info = append(info, baseStyle.
+ // Width(width-1).
+ // Foreground(t.Error()).
+ // Render("(error)"),
+ // )
+ // case message.FinishReasonPermissionDenied:
+ // info = append(info, baseStyle.
+ // Width(width-1).
+ // Foreground(t.Info()).
+ // Render("(permission denied)"),
+ // )
+ // }
+ // }
+
+ // if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) {
+ // if content == "" {
+ // content = "*Finished without output*"
+ // }
+ //
+ // content = renderMessage(content, false, width, info...)
+ // messages = append(messages, content)
+ // // position += messages[0].height
+ // position++ // for the space
+ // } else if thinking && thinkingContent != "" {
+ // // Render the thinking content with timestamp
+ // content = renderMessage(thinkingContent, false, width, info...)
+ // messages = append(messages, content)
+ // position += lipgloss.Height(content)
+ // position++ // for the space
+ // }
+
+ // Only render tool messages if they should be shown
+ if showToolMessages {
+ // for i, toolCall := range msg.ToolCalls() {
+ // toolCallContent := renderToolMessage(
+ // toolCall,
+ // allMessages,
+ // messagesService,
+ // focusedUIMessageId,
+ // false,
+ // width,
+ // i+1,
+ // )
+ // messages = append(messages, toolCallContent)
+ // }
+ }
+
+ return strings.Join(messages, "\n\n")
+}
+
+func renderToolName(name string) string {
+ switch name {
+ // case agent.AgentToolName:
+ // return "Task"
+ case "ls":
+ return "List"
+ default:
+ return cases.Title(language.Und).String(name)
+ }
+}
+
+func renderToolAction(name string) string {
+ switch name {
+ // case agent.AgentToolName:
+ // return "Preparing prompt..."
+ case "bash":
+ return "Building command..."
+ case "edit":
+ return "Preparing edit..."
+ case "fetch":
+ return "Writing fetch..."
+ case "glob":
+ return "Finding files..."
+ case "grep":
+ return "Searching content..."
+ case "ls":
+ return "Listing directory..."
+ case "view":
+ return "Reading file..."
+ case "write":
+ return "Preparing write..."
+ case "patch":
+ return "Preparing patch..."
+ case "batch":
+ return "Running batch operations..."
+ }
+ return "Working..."
+}
+
+// renders params, params[0] (params[1]=params[2] ....)
+func renderParams(paramsWidth int, params ...string) string {
+ if len(params) == 0 {
+ return ""
+ }
+ mainParam := params[0]
+ if len(mainParam) > paramsWidth {
+ mainParam = mainParam[:paramsWidth-3] + "..."
+ }
+
+ if len(params) == 1 {
+ return mainParam
+ }
+ otherParams := params[1:]
+ // create pairs of key/value
+ // if odd number of params, the last one is a key without value
+ if len(otherParams)%2 != 0 {
+ otherParams = append(otherParams, "")
+ }
+ parts := make([]string, 0, len(otherParams)/2)
+ for i := 0; i < len(otherParams); i += 2 {
+ key := otherParams[i]
+ value := otherParams[i+1]
+ if value == "" {
+ continue
+ }
+ parts = append(parts, fmt.Sprintf("%s=%s", key, value))
+ }
+
+ partsRendered := strings.Join(parts, ", ")
+ remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 5 // for the space
+ if remainingWidth < 30 {
+ // No space for the params, just show the main
+ return mainParam
+ }
+
+ if len(parts) > 0 {
+ mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
+ }
+
+ return ansi.Truncate(mainParam, paramsWidth, "...")
+}
+
+func removeWorkingDirPrefix(path string) string {
+ wd := config.WorkingDirectory()
+ if strings.HasPrefix(path, wd) {
+ path = strings.TrimPrefix(path, wd)
+ }
+ if strings.HasPrefix(path, "/") {
+ path = strings.TrimPrefix(path, "/")
+ }
+ if strings.HasPrefix(path, "./") {
+ path = strings.TrimPrefix(path, "./")
+ }
+ if strings.HasPrefix(path, "../") {
+ path = strings.TrimPrefix(path, "../")
+ }
+ return path
+}
+
+func renderToolParams(paramWidth int, toolCall any) string {
+ params := ""
+ switch toolCall {
+ // // case agent.AgentToolName:
+ // // var params agent.AgentParams
+ // // json.Unmarshal([]byte(toolCall.Input), &params)
+ // // prompt := strings.ReplaceAll(params.Prompt, "\n", " ")
+ // // return renderParams(paramWidth, prompt)
+ // case "bash":
+ // var params tools.BashParams
+ // json.Unmarshal([]byte(toolCall.Input), &params)
+ // command := strings.ReplaceAll(params.Command, "\n", " ")
+ // return renderParams(paramWidth, command)
+ // case "edit":
+ // var params tools.EditParams
+ // json.Unmarshal([]byte(toolCall.Input), &params)
+ // filePath := removeWorkingDirPrefix(params.FilePath)
+ // return renderParams(paramWidth, filePath)
+ // case "fetch":
+ // var params tools.FetchParams
+ // json.Unmarshal([]byte(toolCall.Input), &params)
+ // url := params.URL
+ // toolParams := []string{
+ // url,
+ // }
+ // if params.Format != "" {
+ // toolParams = append(toolParams, "format", params.Format)
+ // }
+ // if params.Timeout != 0 {
+ // toolParams = append(toolParams, "timeout", (time.Duration(params.Timeout) * time.Second).String())
+ // }
+ // return renderParams(paramWidth, toolParams...)
+ // case tools.GlobToolName:
+ // var params tools.GlobParams
+ // json.Unmarshal([]byte(toolCall.Input), &params)
+ // pattern := params.Pattern
+ // toolParams := []string{
+ // pattern,
+ // }
+ // if params.Path != "" {
+ // toolParams = append(toolParams, "path", params.Path)
+ // }
+ // return renderParams(paramWidth, toolParams...)
+ // case tools.GrepToolName:
+ // var params tools.GrepParams
+ // json.Unmarshal([]byte(toolCall.Input), &params)
+ // pattern := params.Pattern
+ // toolParams := []string{
+ // pattern,
+ // }
+ // if params.Path != "" {
+ // toolParams = append(toolParams, "path", params.Path)
+ // }
+ // if params.Include != "" {
+ // toolParams = append(toolParams, "include", params.Include)
+ // }
+ // if params.LiteralText {
+ // toolParams = append(toolParams, "literal", "true")
+ // }
+ // return renderParams(paramWidth, toolParams...)
+ // case tools.LSToolName:
+ // var params tools.LSParams
+ // json.Unmarshal([]byte(toolCall.Input), &params)
+ // path := params.Path
+ // if path == "" {
+ // path = "."
+ // }
+ // return renderParams(paramWidth, path)
+ // case tools.ViewToolName:
+ // var params tools.ViewParams
+ // json.Unmarshal([]byte(toolCall.Input), &params)
+ // filePath := removeWorkingDirPrefix(params.FilePath)
+ // toolParams := []string{
+ // filePath,
+ // }
+ // if params.Limit != 0 {
+ // toolParams = append(toolParams, "limit", fmt.Sprintf("%d", params.Limit))
+ // }
+ // if params.Offset != 0 {
+ // toolParams = append(toolParams, "offset", fmt.Sprintf("%d", params.Offset))
+ // }
+ // return renderParams(paramWidth, toolParams...)
+ // case tools.WriteToolName:
+ // var params tools.WriteParams
+ // json.Unmarshal([]byte(toolCall.Input), &params)
+ // filePath := removeWorkingDirPrefix(params.FilePath)
+ // return renderParams(paramWidth, filePath)
+ // case tools.BatchToolName:
+ // var params tools.BatchParams
+ // json.Unmarshal([]byte(toolCall.Input), &params)
+ // return renderParams(paramWidth, fmt.Sprintf("%d parallel calls", len(params.Calls)))
+ // default:
+ // input := strings.ReplaceAll(toolCall, "\n", " ")
+ // params = renderParams(paramWidth, input)
+ }
+ return params
+}
+
+func truncateHeight(content string, height int) string {
+ lines := strings.Split(content, "\n")
+ if len(lines) > height {
+ return strings.Join(lines[:height], "\n")
+ }
+ return content
+}
+
+func renderToolResponse(toolCall any, response any, width int) string {
+ return ""
+ // t := theme.CurrentTheme()
+ // baseStyle := styles.BaseStyle()
+ //
+ // if response.IsError {
+ // errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " "))
+ // errContent = ansi.Truncate(errContent, width-1, "...")
+ // return baseStyle.
+ // Width(width).
+ // Foreground(t.Error()).
+ // Render(errContent)
+ // }
+ //
+ // resultContent := truncateHeight(response.Content, maxResultHeight)
+ // switch toolCall.Name {
+ // case agent.AgentToolName:
+ // return styles.ForceReplaceBackgroundWithLipgloss(
+ // toMarkdown(resultContent, false, width),
+ // t.Background(),
+ // )
+ // case tools.BashToolName:
+ // resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent)
+ // return styles.ForceReplaceBackgroundWithLipgloss(
+ // toMarkdown(resultContent, width),
+ // t.Background(),
+ // )
+ // case tools.EditToolName:
+ // metadata := tools.EditResponseMetadata{}
+ // json.Unmarshal([]byte(response.Metadata), &metadata)
+ // formattedDiff, _ := diff.FormatDiff(metadata.Diff, diff.WithTotalWidth(width))
+ // return formattedDiff
+ // case tools.FetchToolName:
+ // var params tools.FetchParams
+ // json.Unmarshal([]byte(toolCall.Input), &params)
+ // mdFormat := "markdown"
+ // switch params.Format {
+ // case "text":
+ // mdFormat = "text"
+ // case "html":
+ // mdFormat = "html"
+ // }
+ // resultContent = fmt.Sprintf("```%s\n%s\n```", mdFormat, resultContent)
+ // return styles.ForceReplaceBackgroundWithLipgloss(
+ // toMarkdown(resultContent, width),
+ // t.Background(),
+ // )
+ // case tools.GlobToolName:
+ // return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
+ // case tools.GrepToolName:
+ // return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
+ // case tools.LSToolName:
+ // return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
+ // case tools.ViewToolName:
+ // metadata := tools.ViewResponseMetadata{}
+ // json.Unmarshal([]byte(response.Metadata), &metadata)
+ // ext := filepath.Ext(metadata.FilePath)
+ // if ext == "" {
+ // ext = ""
+ // } else {
+ // ext = strings.ToLower(ext[1:])
+ // }
+ // resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(metadata.Content, maxResultHeight))
+ // return styles.ForceReplaceBackgroundWithLipgloss(
+ // toMarkdown(resultContent, width),
+ // t.Background(),
+ // )
+ // case tools.WriteToolName:
+ // params := tools.WriteParams{}
+ // json.Unmarshal([]byte(toolCall.Input), &params)
+ // metadata := tools.WriteResponseMetadata{}
+ // json.Unmarshal([]byte(response.Metadata), &metadata)
+ // ext := filepath.Ext(params.FilePath)
+ // if ext == "" {
+ // ext = ""
+ // } else {
+ // ext = strings.ToLower(ext[1:])
+ // }
+ // resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(params.Content, maxResultHeight))
+ // return styles.ForceReplaceBackgroundWithLipgloss(
+ // toMarkdown(resultContent, width),
+ // t.Background(),
+ // )
+ // case tools.BatchToolName:
+ // var batchResult tools.BatchResult
+ // if err := json.Unmarshal([]byte(resultContent), &batchResult); err != nil {
+ // return baseStyle.Width(width).Foreground(t.Error()).Render(fmt.Sprintf("Error parsing batch result: %s", err))
+ // }
+ //
+ // var toolCalls []string
+ // for i, result := range batchResult.Results {
+ // toolName := renderToolName(result.ToolName)
+ //
+ // // Format the tool input as a string
+ // inputStr := string(result.ToolInput)
+ //
+ // // Format the result
+ // var resultStr string
+ // if result.Error != "" {
+ // resultStr = fmt.Sprintf("Error: %s", result.Error)
+ // } else {
+ // var toolResponse tools.ToolResponse
+ // if err := json.Unmarshal(result.Result, &toolResponse); err != nil {
+ // resultStr = "Error parsing tool response"
+ // } else {
+ // resultStr = truncateHeight(toolResponse.Content, 3)
+ // }
+ // }
+ //
+ // // Format the tool call
+ // toolCall := fmt.Sprintf("%d. %s: %s\n %s", i+1, toolName, inputStr, resultStr)
+ // toolCalls = append(toolCalls, toolCall)
+ // }
+ //
+ // return baseStyle.Width(width).Foreground(t.TextMuted()).Render(strings.Join(toolCalls, "\n\n"))
+ // default:
+ // resultContent = fmt.Sprintf("```text\n%s\n```", resultContent)
+ // return styles.ForceReplaceBackgroundWithLipgloss(
+ // toMarkdown(resultContent, width),
+ // t.Background(),
+ // )
+ // }
+}
+
+// func renderToolMessage(
+// toolCall message.ToolCall,
+// allMessages []message.Message,
+// messagesService message.Service,
+// focusedUIMessageId string,
+// nested bool,
+// width int,
+// position int,
+// ) string {
+// if nested {
+// width = width - 3
+// }
+//
+// t := theme.CurrentTheme()
+// baseStyle := styles.BaseStyle()
+//
+// style := baseStyle.
+// Width(width - 1).
+// BorderLeft(true).
+// BorderStyle(lipgloss.ThickBorder()).
+// PaddingLeft(1).
+// BorderForeground(t.TextMuted())
+//
+// response := findToolResponse(toolCall.ID, allMessages)
+// toolNameText := baseStyle.Foreground(t.TextMuted()).
+// Render(fmt.Sprintf("%s: ", renderToolName(toolCall.Name)))
+//
+// if !toolCall.Finished {
+// // Get a brief description of what the tool is doing
+// toolAction := renderToolAction(toolCall.Name)
+//
+// progressText := baseStyle.
+// Width(width - 2 - lipgloss.Width(toolNameText)).
+// Foreground(t.TextMuted()).
+// Render(fmt.Sprintf("%s", toolAction))
+//
+// content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText))
+// return content
+// }
+//
+// params := renderToolParams(width-1-lipgloss.Width(toolNameText), toolCall)
+// responseContent := ""
+// if response != nil {
+// responseContent = renderToolResponse(toolCall, *response, width-2)
+// responseContent = strings.TrimSuffix(responseContent, "\n")
+// } else {
+// responseContent = baseStyle.
+// Italic(true).
+// Width(width - 2).
+// Foreground(t.TextMuted()).
+// Render("Waiting for response...")
+// }
+//
+// parts := []string{}
+// if !nested {
+// formattedParams := baseStyle.
+// Width(width - 2 - lipgloss.Width(toolNameText)).
+// Foreground(t.TextMuted()).
+// Render(params)
+//
+// parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, formattedParams))
+// } else {
+// prefix := baseStyle.
+// Foreground(t.TextMuted()).
+// Render(" └ ")
+// formattedParams := baseStyle.
+// Width(width - 2 - lipgloss.Width(toolNameText)).
+// Foreground(t.TextMuted()).
+// Render(params)
+// parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolNameText, formattedParams))
+// }
+//
+// // if toolCall.Name == agent.AgentToolName {
+// // taskMessages, _ := messagesService.List(context.Background(), toolCall.ID)
+// // toolCalls := []message.ToolCall{}
+// // for _, v := range taskMessages {
+// // toolCalls = append(toolCalls, v.ToolCalls()...)
+// // }
+// // for _, call := range toolCalls {
+// // rendered := renderToolMessage(call, []message.Message{}, messagesService, focusedUIMessageId, true, width, 0)
+// // parts = append(parts, rendered.content)
+// // }
+// // }
+// if responseContent != "" && !nested {
+// parts = append(parts, responseContent)
+// }
+//
+// content := style.Render(
+// lipgloss.JoinVertical(
+// lipgloss.Left,
+// parts...,
+// ),
+// )
+// if nested {
+// content = lipgloss.JoinVertical(
+// lipgloss.Left,
+// parts...,
+// )
+// }
+// return content
+// }
diff --git a/packages/tui/internal/tui/components/chat/messages.go b/packages/tui/internal/tui/components/chat/messages.go
new file mode 100644
index 000000000..ae6f2a687
--- /dev/null
+++ b/packages/tui/internal/tui/components/chat/messages.go
@@ -0,0 +1,344 @@
+package chat
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/spinner"
+ "github.com/charmbracelet/bubbles/viewport"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/tui/app"
+ "github.com/sst/opencode/internal/tui/components/dialog"
+ "github.com/sst/opencode/internal/tui/state"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/pkg/client"
+)
+
+type messagesCmp struct {
+ app *app.App
+ width, height int
+ viewport viewport.Model
+ spinner spinner.Model
+ rendering bool
+ attachments viewport.Model
+ showToolMessages bool
+}
+type renderFinishedMsg struct{}
+type ToggleToolMessagesMsg struct{}
+
+type MessageKeys struct {
+ PageDown key.Binding
+ PageUp key.Binding
+ HalfPageUp key.Binding
+ HalfPageDown key.Binding
+}
+
+var messageKeys = MessageKeys{
+ PageDown: key.NewBinding(
+ key.WithKeys("pgdown"),
+ key.WithHelp("f/pgdn", "page down"),
+ ),
+ PageUp: key.NewBinding(
+ key.WithKeys("pgup"),
+ key.WithHelp("b/pgup", "page up"),
+ ),
+ HalfPageUp: key.NewBinding(
+ key.WithKeys("ctrl+u"),
+ key.WithHelp("ctrl+u", "½ page up"),
+ ),
+ HalfPageDown: key.NewBinding(
+ key.WithKeys("ctrl+d", "ctrl+d"),
+ key.WithHelp("ctrl+d", "½ page down"),
+ ),
+}
+
+func (m *messagesCmp) Init() tea.Cmd {
+ return tea.Batch(m.viewport.Init(), m.spinner.Tick)
+}
+
+func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+ switch msg := msg.(type) {
+ case dialog.ThemeChangedMsg:
+ m.renderView()
+ return m, nil
+ case ToggleToolMessagesMsg:
+ m.showToolMessages = !m.showToolMessages
+ m.renderView()
+ return m, nil
+ case state.SessionSelectedMsg:
+ cmd := m.Reload()
+ return m, cmd
+ case state.SessionClearedMsg:
+ cmd := m.Reload()
+ return m, cmd
+ case tea.KeyMsg:
+ if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
+ key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
+ u, cmd := m.viewport.Update(msg)
+ m.viewport = u
+ cmds = append(cmds, cmd)
+ }
+ case renderFinishedMsg:
+ m.rendering = false
+ m.viewport.GotoBottom()
+ case state.StateUpdatedMsg:
+ m.renderView()
+ m.viewport.GotoBottom()
+ }
+
+ spinner, cmd := m.spinner.Update(msg)
+ m.spinner = spinner
+ cmds = append(cmds, cmd)
+ return m, tea.Batch(cmds...)
+}
+
+func (m *messagesCmp) renderView() {
+ if m.width == 0 {
+ return
+ }
+
+ messages := make([]string, 0)
+ for _, msg := range m.app.Messages {
+ switch msg.Role {
+ case client.User:
+ content := renderUserMessage(msg, m.width)
+ messages = append(messages, content+"\n")
+ case client.Assistant:
+ content := renderAssistantMessage(msg, m.width, m.showToolMessages)
+ messages = append(messages, content+"\n")
+ }
+ }
+
+ m.viewport.SetContent(
+ styles.BaseStyle().
+ Render(
+ lipgloss.JoinVertical(
+ lipgloss.Top,
+ messages...,
+ ),
+ ),
+ )
+}
+
+func (m *messagesCmp) View() string {
+ baseStyle := styles.BaseStyle()
+
+ if m.rendering {
+ return baseStyle.
+ Width(m.width).
+ Render(
+ lipgloss.JoinVertical(
+ lipgloss.Top,
+ "Loading...",
+ m.working(),
+ m.help(),
+ ),
+ )
+ }
+
+ if len(m.app.Messages) == 0 {
+ content := baseStyle.
+ Width(m.width).
+ Height(m.height - 1).
+ Render(
+ m.initialScreen(),
+ )
+
+ return baseStyle.
+ Width(m.width).
+ Render(
+ lipgloss.JoinVertical(
+ lipgloss.Top,
+ content,
+ "",
+ m.help(),
+ ),
+ )
+ }
+
+ return baseStyle.
+ Width(m.width).
+ Render(
+ lipgloss.JoinVertical(
+ lipgloss.Top,
+ m.viewport.View(),
+ m.working(),
+ m.help(),
+ ),
+ )
+}
+
+// func hasToolsWithoutResponse(messages []message.Message) bool {
+// toolCalls := make([]message.ToolCall, 0)
+// toolResults := make([]message.ToolResult, 0)
+// for _, m := range messages {
+// toolCalls = append(toolCalls, m.ToolCalls()...)
+// toolResults = append(toolResults, m.ToolResults()...)
+// }
+//
+// for _, v := range toolCalls {
+// found := false
+// for _, r := range toolResults {
+// if v.ID == r.ToolCallID {
+// found = true
+// break
+// }
+// }
+// if !found && v.Finished {
+// return true
+// }
+// }
+// return false
+// }
+
+// func hasUnfinishedToolCalls(messages []message.Message) bool {
+// toolCalls := make([]message.ToolCall, 0)
+// for _, m := range messages {
+// toolCalls = append(toolCalls, m.ToolCalls()...)
+// }
+// for _, v := range toolCalls {
+// if !v.Finished {
+// return true
+// }
+// }
+// return false
+// }
+
+func (m *messagesCmp) working() string {
+ text := ""
+ if len(m.app.Messages) > 0 {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ task := ""
+
+ lastMessage := m.app.Messages[len(m.app.Messages)-1]
+ if lastMessage.Metadata.Time.Completed == nil {
+ task = "Working..."
+ }
+ // lastMessage := m.app.Messages[len(m.app.Messages)-1]
+ // if hasToolsWithoutResponse(m.app.Messages) {
+ // task = "Waiting for tool response..."
+ // } else if hasUnfinishedToolCalls(m.app.Messages) {
+ // task = "Building tool call..."
+ // } else if !lastMessage.IsFinished() {
+ // task = "Generating..."
+ // }
+ if task != "" {
+ text += baseStyle.
+ Width(m.width).
+ Foreground(t.Primary()).
+ Bold(true).
+ Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
+ }
+ }
+ return text
+}
+
+func (m *messagesCmp) help() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ text := ""
+
+ if m.app.PrimaryAgentOLD.IsBusy() {
+ text += lipgloss.JoinHorizontal(
+ lipgloss.Left,
+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
+ baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to interrupt"),
+ )
+ } else {
+ text += lipgloss.JoinHorizontal(
+ lipgloss.Left,
+ baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send,"),
+ baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render("+"),
+ baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for newline,"),
+ baseStyle.Foreground(t.Text()).Bold(true).Render(" ↑↓"),
+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for history,"),
+ baseStyle.Foreground(t.Text()).Bold(true).Render(" ctrl+h"),
+ baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to toggle tool messages"),
+ )
+ }
+ return baseStyle.
+ Width(m.width).
+ Render(text)
+}
+
+func (m *messagesCmp) initialScreen() string {
+ baseStyle := styles.BaseStyle()
+
+ return baseStyle.Width(m.width).Render(
+ lipgloss.JoinVertical(
+ lipgloss.Top,
+ header(m.width),
+ "",
+ lspsConfigured(m.width),
+ ),
+ )
+}
+
+func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
+ if m.width == width && m.height == height {
+ return nil
+ }
+ m.width = width
+ m.height = height
+ m.viewport.Width = width
+ m.viewport.Height = height - 2
+ m.attachments.Width = width + 40
+ m.attachments.Height = 3
+ m.renderView()
+ return nil
+}
+
+func (m *messagesCmp) GetSize() (int, int) {
+ return m.width, m.height
+}
+
+func (m *messagesCmp) Reload() tea.Cmd {
+ m.rendering = true
+ return func() tea.Msg {
+ m.renderView()
+ return renderFinishedMsg{}
+ }
+}
+
+func (m *messagesCmp) BindingKeys() []key.Binding {
+ return []key.Binding{
+ m.viewport.KeyMap.PageDown,
+ m.viewport.KeyMap.PageUp,
+ m.viewport.KeyMap.HalfPageUp,
+ m.viewport.KeyMap.HalfPageDown,
+ }
+}
+
+func NewMessagesCmp(app *app.App) tea.Model {
+ customSpinner := spinner.Spinner{
+ Frames: []string{" ", "┃", "┃"},
+ FPS: time.Second / 3,
+ }
+ s := spinner.New(spinner.WithSpinner(customSpinner))
+
+ vp := viewport.New(0, 0)
+ attachments := viewport.New(0, 0)
+ vp.KeyMap.PageUp = messageKeys.PageUp
+ vp.KeyMap.PageDown = messageKeys.PageDown
+ vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
+ vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
+
+ return &messagesCmp{
+ app: app,
+ viewport: vp,
+ spinner: s,
+ attachments: attachments,
+ showToolMessages: true,
+ }
+}
diff --git a/packages/tui/internal/tui/components/chat/sidebar.go b/packages/tui/internal/tui/components/chat/sidebar.go
new file mode 100644
index 000000000..d6895a164
--- /dev/null
+++ b/packages/tui/internal/tui/components/chat/sidebar.go
@@ -0,0 +1,220 @@
+package chat
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/config"
+ "github.com/sst/opencode/internal/tui/app"
+ "github.com/sst/opencode/internal/tui/state"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+)
+
+type sidebarCmp struct {
+ app *app.App
+ width, height int
+ modFiles map[string]struct {
+ additions int
+ removals int
+ }
+}
+
+func (m *sidebarCmp) Init() tea.Cmd {
+ // TODO: History service not implemented in API yet
+ // Initialize the modified files map
+ m.modFiles = make(map[string]struct {
+ additions int
+ removals int
+ })
+ return nil
+}
+
+func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg.(type) {
+ case state.SessionSelectedMsg:
+ // TODO: History service not implemented in API yet
+ // ctx := context.Background()
+ // m.loadModifiedFiles(ctx)
+ // case pubsub.Event[history.File]:
+ // TODO: History service not implemented in API yet
+ // if msg.Payload.SessionID == m.app.CurrentSession.ID {
+ // // Process the individual file change instead of reloading all files
+ // ctx := context.Background()
+ // m.processFileChanges(ctx, msg.Payload)
+ // }
+ }
+ return m, nil
+}
+
+func (m *sidebarCmp) View() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+ shareUrl := ""
+ if m.app.Session.Share != nil {
+ shareUrl = baseStyle.Foreground(t.TextMuted()).Render(m.app.Session.Share.Url)
+ }
+
+ // qrcode := ""
+ // if m.app.Session.ShareID != nil {
+ // url := "https://dev.opencode.ai/share?id="
+ // qrcode, _, _ = qr.Generate(url + m.app.Session.Id)
+ // }
+
+ return baseStyle.
+ Width(m.width).
+ PaddingLeft(4).
+ PaddingRight(1).
+ Render(
+ lipgloss.JoinVertical(
+ lipgloss.Top,
+ header(m.width),
+ " ",
+ m.sessionSection(),
+ shareUrl,
+ ),
+ )
+}
+
+func (m *sidebarCmp) sessionSection() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ sessionKey := baseStyle.
+ Foreground(t.Primary()).
+ Bold(true).
+ Render("Session")
+
+ sessionValue := baseStyle.
+ Foreground(t.Text()).
+ Render(fmt.Sprintf(": %s", m.app.Session.Title))
+
+ return sessionKey + sessionValue
+}
+
+func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ stats := ""
+ if additions > 0 && removals > 0 {
+ additionsStr := baseStyle.
+ Foreground(t.Success()).
+ PaddingLeft(1).
+ Render(fmt.Sprintf("+%d", additions))
+
+ removalsStr := baseStyle.
+ Foreground(t.Error()).
+ PaddingLeft(1).
+ Render(fmt.Sprintf("-%d", removals))
+
+ content := lipgloss.JoinHorizontal(lipgloss.Left, additionsStr, removalsStr)
+ stats = baseStyle.Width(lipgloss.Width(content)).Render(content)
+ } else if additions > 0 {
+ additionsStr := fmt.Sprintf(" %s", baseStyle.
+ PaddingLeft(1).
+ Foreground(t.Success()).
+ Render(fmt.Sprintf("+%d", additions)))
+ stats = baseStyle.Width(lipgloss.Width(additionsStr)).Render(additionsStr)
+ } else if removals > 0 {
+ removalsStr := fmt.Sprintf(" %s", baseStyle.
+ PaddingLeft(1).
+ Foreground(t.Error()).
+ Render(fmt.Sprintf("-%d", removals)))
+ stats = baseStyle.Width(lipgloss.Width(removalsStr)).Render(removalsStr)
+ }
+
+ filePathStr := baseStyle.Render(filePath)
+
+ return baseStyle.
+ Width(m.width).
+ Render(
+ lipgloss.JoinHorizontal(
+ lipgloss.Left,
+ filePathStr,
+ stats,
+ ),
+ )
+}
+
+func (m *sidebarCmp) modifiedFiles() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ modifiedFiles := baseStyle.
+ Width(m.width).
+ Foreground(t.Primary()).
+ Bold(true).
+ Render("Modified Files:")
+
+ // If no modified files, show a placeholder message
+ if m.modFiles == nil || len(m.modFiles) == 0 {
+ message := "No modified files"
+ remainingWidth := m.width - lipgloss.Width(message)
+ if remainingWidth > 0 {
+ message += strings.Repeat(" ", remainingWidth)
+ }
+ return baseStyle.
+ Width(m.width).
+ Render(
+ lipgloss.JoinVertical(
+ lipgloss.Top,
+ modifiedFiles,
+ baseStyle.Foreground(t.TextMuted()).Render(message),
+ ),
+ )
+ }
+
+ // Sort file paths alphabetically for consistent ordering
+ var paths []string
+ for path := range m.modFiles {
+ paths = append(paths, path)
+ }
+ sort.Strings(paths)
+
+ // Create views for each file in sorted order
+ var fileViews []string
+ for _, path := range paths {
+ stats := m.modFiles[path]
+ fileViews = append(fileViews, m.modifiedFile(path, stats.additions, stats.removals))
+ }
+
+ return baseStyle.
+ Width(m.width).
+ Render(
+ lipgloss.JoinVertical(
+ lipgloss.Top,
+ modifiedFiles,
+ lipgloss.JoinVertical(
+ lipgloss.Left,
+ fileViews...,
+ ),
+ ),
+ )
+}
+
+func (m *sidebarCmp) SetSize(width, height int) tea.Cmd {
+ m.width = width
+ m.height = height
+ return nil
+}
+
+func (m *sidebarCmp) GetSize() (int, int) {
+ return m.width, m.height
+}
+
+func NewSidebarCmp(app *app.App) tea.Model {
+ return &sidebarCmp{
+ app: app,
+ }
+}
+
+// Helper function to get the display path for a file
+func getDisplayPath(path string) string {
+ workingDir := config.WorkingDirectory()
+ displayPath := strings.TrimPrefix(path, workingDir)
+ return strings.TrimPrefix(displayPath, "/")
+}
diff --git a/packages/tui/internal/tui/components/core/status.go b/packages/tui/internal/tui/components/core/status.go
new file mode 100644
index 000000000..18a0ad6b1
--- /dev/null
+++ b/packages/tui/internal/tui/components/core/status.go
@@ -0,0 +1,366 @@
+package core
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/pubsub"
+ "github.com/sst/opencode/internal/status"
+ "github.com/sst/opencode/internal/tui/app"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+)
+
+type StatusCmp interface {
+ tea.Model
+ SetHelpWidgetMsg(string)
+}
+
+type statusCmp struct {
+ app *app.App
+ queue []status.StatusMessage
+ width int
+ messageTTL time.Duration
+ activeUntil time.Time
+}
+
+// clearMessageCmd is a command that clears status messages after a timeout
+func (m statusCmp) clearMessageCmd() tea.Cmd {
+ return tea.Tick(time.Second, func(t time.Time) tea.Msg {
+ return statusCleanupMsg{time: t}
+ })
+}
+
+// statusCleanupMsg is a message that triggers cleanup of expired status messages
+type statusCleanupMsg struct {
+ time time.Time
+}
+
+func (m statusCmp) Init() tea.Cmd {
+ return m.clearMessageCmd()
+}
+
+func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ m.width = msg.Width
+ return m, nil
+ case pubsub.Event[status.StatusMessage]:
+ if msg.Type == status.EventStatusPublished {
+ // If this is a critical message, move it to the front of the queue
+ if msg.Payload.Critical {
+ // Insert at the front of the queue
+ m.queue = append([]status.StatusMessage{msg.Payload}, m.queue...)
+
+ // Reset active time to show critical message immediately
+ m.activeUntil = time.Time{}
+ } else {
+ // Otherwise, just add it to the queue
+ m.queue = append(m.queue, msg.Payload)
+
+ // If this is the first message and nothing is active, activate it immediately
+ if len(m.queue) == 1 && m.activeUntil.IsZero() {
+ now := time.Now()
+ duration := m.messageTTL
+ if msg.Payload.Duration > 0 {
+ duration = msg.Payload.Duration
+ }
+ m.activeUntil = now.Add(duration)
+ }
+ }
+ }
+ case statusCleanupMsg:
+ now := msg.time
+
+ // If the active message has expired, remove it and activate the next one
+ if !m.activeUntil.IsZero() && m.activeUntil.Before(now) {
+ // Current message expired, remove it if we have one
+ if len(m.queue) > 0 {
+ m.queue = m.queue[1:]
+ }
+ m.activeUntil = time.Time{}
+ }
+
+ // If we have messages in queue but none are active, activate the first one
+ if len(m.queue) > 0 && m.activeUntil.IsZero() {
+ // Use custom duration if specified, otherwise use default
+ duration := m.messageTTL
+ if m.queue[0].Duration > 0 {
+ duration = m.queue[0].Duration
+ }
+ m.activeUntil = now.Add(duration)
+ }
+
+ return m, m.clearMessageCmd()
+ }
+ return m, nil
+}
+
+var helpWidget = ""
+
+// getHelpWidget returns the help widget with current theme colors
+func getHelpWidget(helpText string) string {
+ t := theme.CurrentTheme()
+ if helpText == "" {
+ helpText = "ctrl+? help"
+ }
+
+ return styles.Padded().
+ Background(t.TextMuted()).
+ Foreground(t.BackgroundDarker()).
+ Bold(true).
+ Render(helpText)
+}
+
+func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) string {
+ // Format tokens in human-readable format (e.g., 110K, 1.2M)
+ var formattedTokens string
+ switch {
+ case tokens >= 1_000_000:
+ formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
+ case tokens >= 1_000:
+ formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
+ default:
+ formattedTokens = fmt.Sprintf("%d", int(tokens))
+ }
+
+ // Remove .0 suffix if present
+ if strings.HasSuffix(formattedTokens, ".0K") {
+ formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
+ }
+ if strings.HasSuffix(formattedTokens, ".0M") {
+ formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
+ }
+
+ // Format cost with $ symbol and 2 decimal places
+ formattedCost := fmt.Sprintf("$%.2f", cost)
+
+ percentage := (float64(tokens) / float64(contextWindow)) * 100
+
+ return fmt.Sprintf("Tokens: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
+}
+
+func (m statusCmp) View() string {
+ t := theme.CurrentTheme()
+ // modelID := config.Get().Agents[config.AgentPrimary].Model
+ // model := models.SupportedModels[modelID]
+
+ // Initialize the help widget
+ status := getHelpWidget("")
+
+ if m.app.Session.Id != "" {
+ tokens := float32(0)
+ cost := float32(0)
+ contextWindow := float32(200_000) // TODO: Get context window from model
+
+ for _, message := range m.app.Messages {
+ if message.Metadata.Assistant != nil {
+ cost += message.Metadata.Assistant.Cost
+ usage := message.Metadata.Assistant.Tokens
+ tokens += (usage.Input + usage.Output + usage.Reasoning)
+ }
+ }
+
+ tokensInfo := styles.Padded().
+ Background(t.Text()).
+ Foreground(t.BackgroundSecondary()).
+ Render(formatTokensAndCost(tokens, contextWindow, cost))
+ status += tokensInfo
+ }
+
+ diagnostics := styles.Padded().Background(t.BackgroundDarker()).Render(m.projectDiagnostics())
+
+ modelName := m.model()
+
+ statusWidth := max(
+ 0,
+ m.width-
+ lipgloss.Width(status)-
+ lipgloss.Width(modelName)-
+ lipgloss.Width(diagnostics),
+ )
+
+ const minInlineWidth = 30
+
+ // Display the first status message if available
+ var statusMessage string
+ if len(m.queue) > 0 {
+ sm := m.queue[0]
+ infoStyle := styles.Padded().
+ Foreground(t.Background())
+
+ switch sm.Level {
+ case "info":
+ infoStyle = infoStyle.Background(t.Info())
+ case "warn":
+ infoStyle = infoStyle.Background(t.Warning())
+ case "error":
+ infoStyle = infoStyle.Background(t.Error())
+ case "debug":
+ infoStyle = infoStyle.Background(t.TextMuted())
+ }
+
+ // Truncate message if it's longer than available width
+ msg := sm.Message
+ availWidth := statusWidth - 10
+
+ // If we have enough space, show inline
+ if availWidth >= minInlineWidth {
+ if len(msg) > availWidth && availWidth > 0 {
+ msg = msg[:availWidth] + "..."
+ }
+ status += infoStyle.Width(statusWidth).Render(msg)
+ } else {
+ // Otherwise, prepare a full-width message to show above
+ if len(msg) > m.width-10 && m.width > 10 {
+ msg = msg[:m.width-10] + "..."
+ }
+ statusMessage = infoStyle.Width(m.width).Render(msg)
+
+ // Add empty space in the status bar
+ status += styles.Padded().
+ Foreground(t.Text()).
+ Background(t.BackgroundSecondary()).
+ Width(statusWidth).
+ Render("")
+ }
+ } else {
+ status += styles.Padded().
+ Foreground(t.Text()).
+ Background(t.BackgroundSecondary()).
+ Width(statusWidth).
+ Render("")
+ }
+
+ status += diagnostics
+ status += modelName
+
+ // If we have a separate status message, prepend it
+ if statusMessage != "" {
+ return statusMessage + "\n" + status
+ } else {
+ blank := styles.BaseStyle().Background(t.Background()).Width(m.width).Render("")
+ return blank + "\n" + status
+ }
+}
+
+func (m *statusCmp) projectDiagnostics() string {
+ t := theme.CurrentTheme()
+
+ // Check if any LSP server is still initializing
+ initializing := false
+ // for _, client := range m.app.LSPClients {
+ // if client.GetServerState() == lsp.StateStarting {
+ // initializing = true
+ // break
+ // }
+ // }
+
+ // If any server is initializing, show that status
+ if initializing {
+ return lipgloss.NewStyle().
+ Foreground(t.Warning()).
+ Render(fmt.Sprintf("%s Initializing LSP...", styles.SpinnerIcon))
+ }
+
+ // errorDiagnostics := []protocol.Diagnostic{}
+ // warnDiagnostics := []protocol.Diagnostic{}
+ // hintDiagnostics := []protocol.Diagnostic{}
+ // infoDiagnostics := []protocol.Diagnostic{}
+ // for _, client := range m.app.LSPClients {
+ // for _, d := range client.GetDiagnostics() {
+ // for _, diag := range d {
+ // switch diag.Severity {
+ // case protocol.SeverityError:
+ // errorDiagnostics = append(errorDiagnostics, diag)
+ // case protocol.SeverityWarning:
+ // warnDiagnostics = append(warnDiagnostics, diag)
+ // case protocol.SeverityHint:
+ // hintDiagnostics = append(hintDiagnostics, diag)
+ // case protocol.SeverityInformation:
+ // infoDiagnostics = append(infoDiagnostics, diag)
+ // }
+ // }
+ // }
+ // }
+ return styles.ForceReplaceBackgroundWithLipgloss(
+ styles.Padded().Render("No diagnostics"),
+ t.BackgroundDarker(),
+ )
+
+ // if len(errorDiagnostics) == 0 &&
+ // len(warnDiagnostics) == 0 &&
+ // len(infoDiagnostics) == 0 &&
+ // len(hintDiagnostics) == 0 {
+ // return styles.ForceReplaceBackgroundWithLipgloss(
+ // styles.Padded().Render("No diagnostics"),
+ // t.BackgroundDarker(),
+ // )
+ // }
+
+ // diagnostics := []string{}
+ //
+ // errStr := lipgloss.NewStyle().
+ // Background(t.BackgroundDarker()).
+ // Foreground(t.Error()).
+ // Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics)))
+ // diagnostics = append(diagnostics, errStr)
+ //
+ // warnStr := lipgloss.NewStyle().
+ // Background(t.BackgroundDarker()).
+ // Foreground(t.Warning()).
+ // Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics)))
+ // diagnostics = append(diagnostics, warnStr)
+ //
+ // infoStr := lipgloss.NewStyle().
+ // Background(t.BackgroundDarker()).
+ // Foreground(t.Info()).
+ // Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics)))
+ // diagnostics = append(diagnostics, infoStr)
+ //
+ // hintStr := lipgloss.NewStyle().
+ // Background(t.BackgroundDarker()).
+ // Foreground(t.Text()).
+ // Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics)))
+ // diagnostics = append(diagnostics, hintStr)
+ //
+ // return styles.ForceReplaceBackgroundWithLipgloss(
+ // styles.Padded().Render(strings.Join(diagnostics, " ")),
+ // t.BackgroundDarker(),
+ // )
+}
+
+func (m statusCmp) model() string {
+ t := theme.CurrentTheme()
+ model := "None"
+ if m.app.Model != nil {
+ model = *m.app.Model.Name
+ }
+
+ return styles.Padded().
+ Background(t.Secondary()).
+ Foreground(t.Background()).
+ Render(model)
+}
+
+func (m statusCmp) SetHelpWidgetMsg(s string) {
+ // Update the help widget text using the getHelpWidget function
+ helpWidget = getHelpWidget(s)
+}
+
+func NewStatusCmp(app *app.App) StatusCmp {
+ // Initialize the help widget with default text
+ helpWidget = getHelpWidget("")
+
+ statusComponent := &statusCmp{
+ app: app,
+ queue: []status.StatusMessage{},
+ messageTTL: 4 * time.Second,
+ activeUntil: time.Time{},
+ }
+
+ return statusComponent
+}
diff --git a/packages/tui/internal/tui/components/dialog/arguments.go b/packages/tui/internal/tui/components/dialog/arguments.go
new file mode 100644
index 000000000..fed79bce3
--- /dev/null
+++ b/packages/tui/internal/tui/components/dialog/arguments.go
@@ -0,0 +1,257 @@
+package dialog
+
+import (
+ "fmt"
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/textinput"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/internal/tui/util"
+)
+
+type argumentsDialogKeyMap struct {
+ Enter key.Binding
+ Escape key.Binding
+}
+
+// ShortHelp implements key.Map.
+func (k argumentsDialogKeyMap) ShortHelp() []key.Binding {
+ return []key.Binding{
+ key.NewBinding(
+ key.WithKeys("enter"),
+ key.WithHelp("enter", "confirm"),
+ ),
+ key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "cancel"),
+ ),
+ }
+}
+
+// FullHelp implements key.Map.
+func (k argumentsDialogKeyMap) FullHelp() [][]key.Binding {
+ return [][]key.Binding{k.ShortHelp()}
+}
+
+// ShowMultiArgumentsDialogMsg is a message that is sent to show the multi-arguments dialog.
+type ShowMultiArgumentsDialogMsg struct {
+ CommandID string
+ Content string
+ ArgNames []string
+}
+
+// CloseMultiArgumentsDialogMsg is a message that is sent when the multi-arguments dialog is closed.
+type CloseMultiArgumentsDialogMsg struct {
+ Submit bool
+ CommandID string
+ Content string
+ Args map[string]string
+}
+
+// MultiArgumentsDialogCmp is a component that asks the user for multiple command arguments.
+type MultiArgumentsDialogCmp struct {
+ width, height int
+ inputs []textinput.Model
+ focusIndex int
+ keys argumentsDialogKeyMap
+ commandID string
+ content string
+ argNames []string
+}
+
+// NewMultiArgumentsDialogCmp creates a new MultiArgumentsDialogCmp.
+func NewMultiArgumentsDialogCmp(commandID, content string, argNames []string) MultiArgumentsDialogCmp {
+ t := theme.CurrentTheme()
+ inputs := make([]textinput.Model, len(argNames))
+
+ for i, name := range argNames {
+ ti := textinput.New()
+ ti.Placeholder = fmt.Sprintf("Enter value for %s...", name)
+ ti.Width = 40
+ ti.Prompt = ""
+ ti.PlaceholderStyle = ti.PlaceholderStyle.Background(t.Background())
+ ti.PromptStyle = ti.PromptStyle.Background(t.Background())
+ ti.TextStyle = ti.TextStyle.Background(t.Background())
+
+ // Only focus the first input initially
+ if i == 0 {
+ ti.Focus()
+ ti.PromptStyle = ti.PromptStyle.Foreground(t.Primary())
+ ti.TextStyle = ti.TextStyle.Foreground(t.Primary())
+ } else {
+ ti.Blur()
+ }
+
+ inputs[i] = ti
+ }
+
+ return MultiArgumentsDialogCmp{
+ inputs: inputs,
+ keys: argumentsDialogKeyMap{},
+ commandID: commandID,
+ content: content,
+ argNames: argNames,
+ focusIndex: 0,
+ }
+}
+
+// Init implements tea.Model.
+func (m MultiArgumentsDialogCmp) Init() tea.Cmd {
+ // Make sure only the first input is focused
+ for i := range m.inputs {
+ if i == 0 {
+ m.inputs[i].Focus()
+ } else {
+ m.inputs[i].Blur()
+ }
+ }
+
+ return textinput.Blink
+}
+
+// Update implements tea.Model.
+func (m MultiArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+ t := theme.CurrentTheme()
+
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
+ return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
+ Submit: false,
+ CommandID: m.commandID,
+ Content: m.content,
+ Args: nil,
+ })
+ case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
+ // If we're on the last input, submit the form
+ if m.focusIndex == len(m.inputs)-1 {
+ args := make(map[string]string)
+ for i, name := range m.argNames {
+ args[name] = m.inputs[i].Value()
+ }
+ return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
+ Submit: true,
+ CommandID: m.commandID,
+ Content: m.content,
+ Args: args,
+ })
+ }
+ // Otherwise, move to the next input
+ m.inputs[m.focusIndex].Blur()
+ m.focusIndex++
+ m.inputs[m.focusIndex].Focus()
+ m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
+ m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
+ case key.Matches(msg, key.NewBinding(key.WithKeys("tab"))):
+ // Move to the next input
+ m.inputs[m.focusIndex].Blur()
+ m.focusIndex = (m.focusIndex + 1) % len(m.inputs)
+ m.inputs[m.focusIndex].Focus()
+ m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
+ m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
+ case key.Matches(msg, key.NewBinding(key.WithKeys("shift+tab"))):
+ // Move to the previous input
+ m.inputs[m.focusIndex].Blur()
+ m.focusIndex = (m.focusIndex - 1 + len(m.inputs)) % len(m.inputs)
+ m.inputs[m.focusIndex].Focus()
+ m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
+ m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
+ }
+ case tea.WindowSizeMsg:
+ m.width = msg.Width
+ m.height = msg.Height
+ }
+
+ // Update the focused input
+ var cmd tea.Cmd
+ m.inputs[m.focusIndex], cmd = m.inputs[m.focusIndex].Update(msg)
+ cmds = append(cmds, cmd)
+
+ return m, tea.Batch(cmds...)
+}
+
+// View implements tea.Model.
+func (m MultiArgumentsDialogCmp) View() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ // Calculate width needed for content
+ maxWidth := 60 // Width for explanation text
+
+ title := lipgloss.NewStyle().
+ Foreground(t.Primary()).
+ Bold(true).
+ Width(maxWidth).
+ Padding(0, 1).
+ Background(t.Background()).
+ Render("Command Arguments")
+
+ explanation := lipgloss.NewStyle().
+ Foreground(t.Text()).
+ Width(maxWidth).
+ Padding(0, 1).
+ Background(t.Background()).
+ Render("This command requires multiple arguments. Please enter values for each:")
+
+ // Create input fields for each argument
+ inputFields := make([]string, len(m.inputs))
+ for i, input := range m.inputs {
+ // Highlight the label of the focused input
+ labelStyle := lipgloss.NewStyle().
+ Width(maxWidth).
+ Padding(1, 1, 0, 1).
+ Background(t.Background())
+
+ if i == m.focusIndex {
+ labelStyle = labelStyle.Foreground(t.Primary()).Bold(true)
+ } else {
+ labelStyle = labelStyle.Foreground(t.TextMuted())
+ }
+
+ label := labelStyle.Render(m.argNames[i] + ":")
+
+ field := lipgloss.NewStyle().
+ Foreground(t.Text()).
+ Width(maxWidth).
+ Padding(0, 1).
+ Background(t.Background()).
+ Render(input.View())
+
+ inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
+ }
+
+ maxWidth = min(maxWidth, m.width-10)
+
+ // Join all elements vertically
+ elements := []string{title, explanation}
+ elements = append(elements, inputFields...)
+
+ content := lipgloss.JoinVertical(
+ lipgloss.Left,
+ elements...,
+ )
+
+ return baseStyle.Padding(1, 2).
+ Border(lipgloss.RoundedBorder()).
+ BorderBackground(t.Background()).
+ BorderForeground(t.TextMuted()).
+ Background(t.Background()).
+ Width(lipgloss.Width(content) + 4).
+ Render(content)
+}
+
+// SetSize sets the size of the component.
+func (m *MultiArgumentsDialogCmp) SetSize(width, height int) {
+ m.width = width
+ m.height = height
+}
+
+// Bindings implements layout.Bindings.
+func (m MultiArgumentsDialogCmp) Bindings() []key.Binding {
+ return m.keys.ShortHelp()
+}
diff --git a/packages/tui/internal/tui/components/dialog/commands.go b/packages/tui/internal/tui/components/dialog/commands.go
new file mode 100644
index 000000000..b989154c6
--- /dev/null
+++ b/packages/tui/internal/tui/components/dialog/commands.go
@@ -0,0 +1,180 @@
+package dialog
+
+import (
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ utilComponents "github.com/sst/opencode/internal/tui/components/util"
+ "github.com/sst/opencode/internal/tui/layout"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/internal/tui/util"
+)
+
+// Command represents a command that can be executed
+type Command struct {
+ ID string
+ Title string
+ Description string
+ Handler func(cmd Command) tea.Cmd
+}
+
+func (ci Command) Render(selected bool, width int) string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ descStyle := baseStyle.Width(width).Foreground(t.TextMuted())
+ itemStyle := baseStyle.Width(width).
+ Foreground(t.Text()).
+ Background(t.Background())
+
+ if selected {
+ itemStyle = itemStyle.
+ Background(t.Primary()).
+ Foreground(t.Background()).
+ Bold(true)
+ descStyle = descStyle.
+ Background(t.Primary()).
+ Foreground(t.Background())
+ }
+
+ title := itemStyle.Padding(0, 1).Render(ci.Title)
+ if ci.Description != "" {
+ description := descStyle.Padding(0, 1).Render(ci.Description)
+ return lipgloss.JoinVertical(lipgloss.Left, title, description)
+ }
+ return title
+}
+
+// CommandSelectedMsg is sent when a command is selected
+type CommandSelectedMsg struct {
+ Command Command
+}
+
+// CloseCommandDialogMsg is sent when the command dialog is closed
+type CloseCommandDialogMsg struct{}
+
+// CommandDialog interface for the command selection dialog
+type CommandDialog interface {
+ tea.Model
+ layout.Bindings
+ SetCommands(commands []Command)
+}
+
+type commandDialogCmp struct {
+ listView utilComponents.SimpleList[Command]
+ width int
+ height int
+}
+
+type commandKeyMap struct {
+ Enter key.Binding
+ Escape key.Binding
+}
+
+var commandKeys = commandKeyMap{
+ Enter: key.NewBinding(
+ key.WithKeys("enter"),
+ key.WithHelp("enter", "select command"),
+ ),
+ Escape: key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "close"),
+ ),
+}
+
+func (c *commandDialogCmp) Init() tea.Cmd {
+ return c.listView.Init()
+}
+
+func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, commandKeys.Enter):
+ selectedItem, idx := c.listView.GetSelectedItem()
+ if idx != -1 {
+ return c, util.CmdHandler(CommandSelectedMsg{
+ Command: selectedItem,
+ })
+ }
+ case key.Matches(msg, commandKeys.Escape):
+ return c, util.CmdHandler(CloseCommandDialogMsg{})
+ }
+ case tea.WindowSizeMsg:
+ c.width = msg.Width
+ c.height = msg.Height
+ }
+
+ u, cmd := c.listView.Update(msg)
+ c.listView = u.(utilComponents.SimpleList[Command])
+ cmds = append(cmds, cmd)
+
+ return c, tea.Batch(cmds...)
+}
+
+func (c *commandDialogCmp) View() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ maxWidth := 40
+
+ commands := c.listView.GetItems()
+
+ for _, cmd := range commands {
+ if len(cmd.Title) > maxWidth-4 {
+ maxWidth = len(cmd.Title) + 4
+ }
+ if cmd.Description != "" {
+ if len(cmd.Description) > maxWidth-4 {
+ maxWidth = len(cmd.Description) + 4
+ }
+ }
+ }
+
+ c.listView.SetMaxWidth(maxWidth)
+
+ title := baseStyle.
+ Foreground(t.Primary()).
+ Bold(true).
+ Width(maxWidth).
+ Padding(0, 1).
+ Render("Commands")
+
+ content := lipgloss.JoinVertical(
+ lipgloss.Left,
+ title,
+ baseStyle.Width(maxWidth).Render(""),
+ baseStyle.Width(maxWidth).Render(c.listView.View()),
+ baseStyle.Width(maxWidth).Render(""),
+ )
+
+ return baseStyle.Padding(1, 2).
+ Border(lipgloss.RoundedBorder()).
+ BorderBackground(t.Background()).
+ BorderForeground(t.TextMuted()).
+ Width(lipgloss.Width(content) + 4).
+ Render(content)
+}
+
+func (c *commandDialogCmp) BindingKeys() []key.Binding {
+ return layout.KeyMapToSlice(commandKeys)
+}
+
+func (c *commandDialogCmp) SetCommands(commands []Command) {
+ c.listView.SetItems(commands)
+}
+
+// NewCommandDialogCmp creates a new command selection dialog
+func NewCommandDialogCmp() CommandDialog {
+ listView := utilComponents.NewSimpleList[Command](
+ []Command{},
+ 10,
+ "No commands available",
+ true,
+ )
+ return &commandDialogCmp{
+ listView: listView,
+ }
+}
diff --git a/packages/tui/internal/tui/components/dialog/complete.go b/packages/tui/internal/tui/components/dialog/complete.go
new file mode 100644
index 000000000..57193d00c
--- /dev/null
+++ b/packages/tui/internal/tui/components/dialog/complete.go
@@ -0,0 +1,263 @@
+package dialog
+
+import (
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/textarea"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/status"
+ utilComponents "github.com/sst/opencode/internal/tui/components/util"
+ "github.com/sst/opencode/internal/tui/layout"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/internal/tui/util"
+)
+
+type CompletionItem struct {
+ title string
+ Title string
+ Value string
+}
+
+type CompletionItemI interface {
+ utilComponents.SimpleListItem
+ GetValue() string
+ DisplayValue() string
+}
+
+func (ci *CompletionItem) Render(selected bool, width int) string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ itemStyle := baseStyle.
+ Width(width).
+ Padding(0, 1)
+
+ if selected {
+ itemStyle = itemStyle.
+ Background(t.Background()).
+ Foreground(t.Primary()).
+ Bold(true)
+ }
+
+ title := itemStyle.Render(
+ ci.GetValue(),
+ )
+
+ return title
+}
+
+func (ci *CompletionItem) DisplayValue() string {
+ return ci.Title
+}
+
+func (ci *CompletionItem) GetValue() string {
+ return ci.Value
+}
+
+func NewCompletionItem(completionItem CompletionItem) CompletionItemI {
+ return &completionItem
+}
+
+type CompletionProvider interface {
+ GetId() string
+ GetEntry() CompletionItemI
+ GetChildEntries(query string) ([]CompletionItemI, error)
+}
+
+type CompletionSelectedMsg struct {
+ SearchString string
+ CompletionValue string
+}
+
+type CompletionDialogCompleteItemMsg struct {
+ Value string
+}
+
+type CompletionDialogCloseMsg struct{}
+
+type CompletionDialog interface {
+ tea.Model
+ layout.Bindings
+ SetWidth(width int)
+}
+
+type completionDialogCmp struct {
+ query string
+ completionProvider CompletionProvider
+ width int
+ height int
+ pseudoSearchTextArea textarea.Model
+ listView utilComponents.SimpleList[CompletionItemI]
+}
+
+type completionDialogKeyMap struct {
+ Complete key.Binding
+ Cancel key.Binding
+}
+
+var completionDialogKeys = completionDialogKeyMap{
+ Complete: key.NewBinding(
+ key.WithKeys("tab", "enter"),
+ ),
+ Cancel: key.NewBinding(
+ key.WithKeys(" ", "esc", "backspace"),
+ ),
+}
+
+func (c *completionDialogCmp) Init() tea.Cmd {
+ return nil
+}
+
+func (c *completionDialogCmp) complete(item CompletionItemI) tea.Cmd {
+ value := c.pseudoSearchTextArea.Value()
+
+ if value == "" {
+ return nil
+ }
+
+ return tea.Batch(
+ util.CmdHandler(CompletionSelectedMsg{
+ SearchString: value,
+ CompletionValue: item.GetValue(),
+ }),
+ c.close(),
+ )
+}
+
+func (c *completionDialogCmp) close() tea.Cmd {
+ c.listView.SetItems([]CompletionItemI{})
+ c.pseudoSearchTextArea.Reset()
+ c.pseudoSearchTextArea.Blur()
+
+ return util.CmdHandler(CompletionDialogCloseMsg{})
+}
+
+func (c *completionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ if c.pseudoSearchTextArea.Focused() {
+
+ if !key.Matches(msg, completionDialogKeys.Complete) {
+
+ var cmd tea.Cmd
+ c.pseudoSearchTextArea, cmd = c.pseudoSearchTextArea.Update(msg)
+ cmds = append(cmds, cmd)
+
+ var query string
+ query = c.pseudoSearchTextArea.Value()
+ if query != "" {
+ query = query[1:]
+ }
+
+ if query != c.query {
+ items, err := c.completionProvider.GetChildEntries(query)
+ if err != nil {
+ status.Error(err.Error())
+ }
+
+ c.listView.SetItems(items)
+ c.query = query
+ }
+
+ u, cmd := c.listView.Update(msg)
+ c.listView = u.(utilComponents.SimpleList[CompletionItemI])
+
+ cmds = append(cmds, cmd)
+ }
+
+ switch {
+ case key.Matches(msg, completionDialogKeys.Complete):
+ item, i := c.listView.GetSelectedItem()
+ if i == -1 {
+ return c, nil
+ }
+
+ cmd := c.complete(item)
+
+ return c, cmd
+ case key.Matches(msg, completionDialogKeys.Cancel):
+ // Only close on backspace when there are no characters left
+ if msg.String() != "backspace" || len(c.pseudoSearchTextArea.Value()) <= 0 {
+ return c, c.close()
+ }
+ }
+
+ return c, tea.Batch(cmds...)
+ } else {
+ items, err := c.completionProvider.GetChildEntries("")
+ if err != nil {
+ status.Error(err.Error())
+ }
+
+ c.listView.SetItems(items)
+ c.pseudoSearchTextArea.SetValue(msg.String())
+ return c, c.pseudoSearchTextArea.Focus()
+ }
+ case tea.WindowSizeMsg:
+ c.width = msg.Width
+ c.height = msg.Height
+ }
+
+ return c, tea.Batch(cmds...)
+}
+
+func (c *completionDialogCmp) View() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ maxWidth := 40
+
+ completions := c.listView.GetItems()
+
+ for _, cmd := range completions {
+ title := cmd.DisplayValue()
+ if len(title) > maxWidth-4 {
+ maxWidth = len(title) + 4
+ }
+ }
+
+ c.listView.SetMaxWidth(maxWidth)
+
+ return baseStyle.Padding(0, 0).
+ Border(lipgloss.NormalBorder()).
+ BorderBottom(false).
+ BorderRight(false).
+ BorderLeft(false).
+ BorderBackground(t.Background()).
+ BorderForeground(t.TextMuted()).
+ Width(c.width).
+ Render(c.listView.View())
+}
+
+func (c *completionDialogCmp) SetWidth(width int) {
+ c.width = width
+}
+
+func (c *completionDialogCmp) BindingKeys() []key.Binding {
+ return layout.KeyMapToSlice(completionDialogKeys)
+}
+
+func NewCompletionDialogCmp(completionProvider CompletionProvider) CompletionDialog {
+ ti := textarea.New()
+
+ items, err := completionProvider.GetChildEntries("")
+ if err != nil {
+ status.Error(err.Error())
+ }
+
+ li := utilComponents.NewSimpleList(
+ items,
+ 7,
+ "No file matches found",
+ false,
+ )
+
+ return &completionDialogCmp{
+ query: "",
+ completionProvider: completionProvider,
+ pseudoSearchTextArea: ti,
+ listView: li,
+ }
+}
diff --git a/packages/tui/internal/tui/components/dialog/custom_commands.go b/packages/tui/internal/tui/components/dialog/custom_commands.go
new file mode 100644
index 000000000..be6746feb
--- /dev/null
+++ b/packages/tui/internal/tui/components/dialog/custom_commands.go
@@ -0,0 +1,186 @@
+package dialog
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/sst/opencode/internal/config"
+ "github.com/sst/opencode/internal/tui/util"
+)
+
+// Command prefix constants
+const (
+ UserCommandPrefix = "user:"
+ ProjectCommandPrefix = "project:"
+)
+
+// namedArgPattern is a regex pattern to find named arguments in the format $NAME
+var namedArgPattern = regexp.MustCompile(`\$([A-Z][A-Z0-9_]*)`)
+
+// LoadCustomCommands loads custom commands from both XDG_CONFIG_HOME and project data directory
+func LoadCustomCommands() ([]Command, error) {
+ cfg := config.Get()
+ if cfg == nil {
+ return nil, fmt.Errorf("config not loaded")
+ }
+
+ var commands []Command
+
+ // Load user commands from XDG_CONFIG_HOME/opencode/commands
+ xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
+ if xdgConfigHome == "" {
+ // Default to ~/.config if XDG_CONFIG_HOME is not set
+ home, err := os.UserHomeDir()
+ if err == nil {
+ xdgConfigHome = filepath.Join(home, ".config")
+ }
+ }
+
+ if xdgConfigHome != "" {
+ userCommandsDir := filepath.Join(xdgConfigHome, "opencode", "commands")
+ userCommands, err := loadCommandsFromDir(userCommandsDir, UserCommandPrefix)
+ if err != nil {
+ // Log error but continue - we'll still try to load other commands
+ fmt.Printf("Warning: failed to load user commands from XDG_CONFIG_HOME: %v\n", err)
+ } else {
+ commands = append(commands, userCommands...)
+ }
+ }
+
+ // Load commands from $HOME/.opencode/commands
+ home, err := os.UserHomeDir()
+ if err == nil {
+ homeCommandsDir := filepath.Join(home, ".opencode", "commands")
+ homeCommands, err := loadCommandsFromDir(homeCommandsDir, UserCommandPrefix)
+ if err != nil {
+ // Log error but continue - we'll still try to load other commands
+ fmt.Printf("Warning: failed to load home commands: %v\n", err)
+ } else {
+ commands = append(commands, homeCommands...)
+ }
+ }
+
+ // Load project commands from data directory
+ projectCommandsDir := filepath.Join(cfg.Data.Directory, "commands")
+ projectCommands, err := loadCommandsFromDir(projectCommandsDir, ProjectCommandPrefix)
+ if err != nil {
+ // Log error but return what we have so far
+ fmt.Printf("Warning: failed to load project commands: %v\n", err)
+ } else {
+ commands = append(commands, projectCommands...)
+ }
+
+ return commands, nil
+}
+
+// loadCommandsFromDir loads commands from a specific directory with the given prefix
+func loadCommandsFromDir(commandsDir string, prefix string) ([]Command, error) {
+ // Check if the commands directory exists
+ if _, err := os.Stat(commandsDir); os.IsNotExist(err) {
+ // Create the commands directory if it doesn't exist
+ if err := os.MkdirAll(commandsDir, 0755); err != nil {
+ return nil, fmt.Errorf("failed to create commands directory %s: %w", commandsDir, err)
+ }
+ // Return empty list since we just created the directory
+ return []Command{}, nil
+ }
+
+ var commands []Command
+
+ // Walk through the commands directory and load all .md files
+ err := filepath.Walk(commandsDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ // Skip directories
+ if info.IsDir() {
+ return nil
+ }
+
+ // Only process markdown files
+ if !strings.HasSuffix(strings.ToLower(info.Name()), ".md") {
+ return nil
+ }
+
+ // Read the file content
+ content, err := os.ReadFile(path)
+ if err != nil {
+ return fmt.Errorf("failed to read command file %s: %w", path, err)
+ }
+
+ // Get the command ID from the file name without the .md extension
+ commandID := strings.TrimSuffix(info.Name(), filepath.Ext(info.Name()))
+
+ // Get relative path from commands directory
+ relPath, err := filepath.Rel(commandsDir, path)
+ if err != nil {
+ return fmt.Errorf("failed to get relative path for %s: %w", path, err)
+ }
+
+ // Create the command ID from the relative path
+ // Replace directory separators with colons
+ commandIDPath := strings.ReplaceAll(filepath.Dir(relPath), string(filepath.Separator), ":")
+ if commandIDPath != "." {
+ commandID = commandIDPath + ":" + commandID
+ }
+
+ // Create a command
+ command := Command{
+ ID: prefix + commandID,
+ Title: prefix + commandID,
+ Description: fmt.Sprintf("Custom command from %s", relPath),
+ Handler: func(cmd Command) tea.Cmd {
+ commandContent := string(content)
+
+ // Check for named arguments
+ matches := namedArgPattern.FindAllStringSubmatch(commandContent, -1)
+ if len(matches) > 0 {
+ // Extract unique argument names
+ argNames := make([]string, 0)
+ argMap := make(map[string]bool)
+
+ for _, match := range matches {
+ argName := match[1] // Group 1 is the name without $
+ if !argMap[argName] {
+ argMap[argName] = true
+ argNames = append(argNames, argName)
+ }
+ }
+
+ // Show multi-arguments dialog for all named arguments
+ return util.CmdHandler(ShowMultiArgumentsDialogMsg{
+ CommandID: cmd.ID,
+ Content: commandContent,
+ ArgNames: argNames,
+ })
+ }
+
+ // No arguments needed, run command directly
+ return util.CmdHandler(CommandRunCustomMsg{
+ Content: commandContent,
+ Args: nil, // No arguments
+ })
+ },
+ }
+
+ commands = append(commands, command)
+ return nil
+ })
+
+ if err != nil {
+ return nil, fmt.Errorf("failed to load custom commands from %s: %w", commandsDir, err)
+ }
+
+ return commands, nil
+}
+
+// CommandRunCustomMsg is sent when a custom command is executed
+type CommandRunCustomMsg struct {
+ Content string
+ Args map[string]string // Map of argument names to values
+}
diff --git a/packages/tui/internal/tui/components/dialog/custom_commands_test.go b/packages/tui/internal/tui/components/dialog/custom_commands_test.go
new file mode 100644
index 000000000..3468ac3b0
--- /dev/null
+++ b/packages/tui/internal/tui/components/dialog/custom_commands_test.go
@@ -0,0 +1,106 @@
+package dialog
+
+import (
+ "testing"
+ "regexp"
+)
+
+func TestNamedArgPattern(t *testing.T) {
+ testCases := []struct {
+ input string
+ expected []string
+ }{
+ {
+ input: "This is a test with $ARGUMENTS placeholder",
+ expected: []string{"ARGUMENTS"},
+ },
+ {
+ input: "This is a test with $FOO and $BAR placeholders",
+ expected: []string{"FOO", "BAR"},
+ },
+ {
+ input: "This is a test with $FOO_BAR and $BAZ123 placeholders",
+ expected: []string{"FOO_BAR", "BAZ123"},
+ },
+ {
+ input: "This is a test with no placeholders",
+ expected: []string{},
+ },
+ {
+ input: "This is a test with $FOO appearing twice: $FOO",
+ expected: []string{"FOO"},
+ },
+ {
+ input: "This is a test with $1INVALID placeholder",
+ expected: []string{},
+ },
+ }
+
+ for _, tc := range testCases {
+ matches := namedArgPattern.FindAllStringSubmatch(tc.input, -1)
+
+ // Extract unique argument names
+ argNames := make([]string, 0)
+ argMap := make(map[string]bool)
+
+ for _, match := range matches {
+ argName := match[1] // Group 1 is the name without $
+ if !argMap[argName] {
+ argMap[argName] = true
+ argNames = append(argNames, argName)
+ }
+ }
+
+ // Check if we got the expected number of arguments
+ if len(argNames) != len(tc.expected) {
+ t.Errorf("Expected %d arguments, got %d for input: %s", len(tc.expected), len(argNames), tc.input)
+ continue
+ }
+
+ // Check if we got the expected argument names
+ for _, expectedArg := range tc.expected {
+ found := false
+ for _, actualArg := range argNames {
+ if actualArg == expectedArg {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf("Expected argument %s not found in %v for input: %s", expectedArg, argNames, tc.input)
+ }
+ }
+ }
+}
+
+func TestRegexPattern(t *testing.T) {
+ pattern := regexp.MustCompile(`\$([A-Z][A-Z0-9_]*)`)
+
+ validMatches := []string{
+ "$FOO",
+ "$BAR",
+ "$FOO_BAR",
+ "$BAZ123",
+ "$ARGUMENTS",
+ }
+
+ invalidMatches := []string{
+ "$foo",
+ "$1BAR",
+ "$_FOO",
+ "FOO",
+ "$",
+ }
+
+ for _, valid := range validMatches {
+ if !pattern.MatchString(valid) {
+ t.Errorf("Expected %s to match, but it didn't", valid)
+ }
+ }
+
+ for _, invalid := range invalidMatches {
+ if pattern.MatchString(invalid) {
+ t.Errorf("Expected %s not to match, but it did", invalid)
+ }
+ }
+} \ No newline at end of file
diff --git a/packages/tui/internal/tui/components/dialog/filepicker.go b/packages/tui/internal/tui/components/dialog/filepicker.go
new file mode 100644
index 000000000..088e205f2
--- /dev/null
+++ b/packages/tui/internal/tui/components/dialog/filepicker.go
@@ -0,0 +1,485 @@
+package dialog
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "log/slog"
+
+ "github.com/atotto/clipboard"
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/textinput"
+ "github.com/charmbracelet/bubbles/viewport"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/status"
+ "github.com/sst/opencode/internal/tui/app"
+ "github.com/sst/opencode/internal/tui/image"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/internal/tui/util"
+)
+
+const (
+ maxAttachmentSize = int64(5 * 1024 * 1024) // 5MB
+ downArrow = "down"
+ upArrow = "up"
+)
+
+type FilePrickerKeyMap struct {
+ Enter key.Binding
+ Down key.Binding
+ Up key.Binding
+ Forward key.Binding
+ Backward key.Binding
+ OpenFilePicker key.Binding
+ Esc key.Binding
+ InsertCWD key.Binding
+ Paste key.Binding
+}
+
+var filePickerKeyMap = FilePrickerKeyMap{
+ Enter: key.NewBinding(
+ key.WithKeys("enter"),
+ key.WithHelp("enter", "select file/enter directory"),
+ ),
+ Down: key.NewBinding(
+ key.WithKeys("j", downArrow),
+ key.WithHelp("↓/j", "down"),
+ ),
+ Up: key.NewBinding(
+ key.WithKeys("k", upArrow),
+ key.WithHelp("↑/k", "up"),
+ ),
+ Forward: key.NewBinding(
+ key.WithKeys("l"),
+ key.WithHelp("l", "enter directory"),
+ ),
+ Backward: key.NewBinding(
+ key.WithKeys("h", "backspace"),
+ key.WithHelp("h/backspace", "go back"),
+ ),
+ OpenFilePicker: key.NewBinding(
+ key.WithKeys("ctrl+f"),
+ key.WithHelp("ctrl+f", "open file picker"),
+ ),
+ Esc: key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "close/exit"),
+ ),
+ InsertCWD: key.NewBinding(
+ key.WithKeys("i"),
+ key.WithHelp("i", "manual path input"),
+ ),
+ Paste: key.NewBinding(
+ key.WithKeys("ctrl+v"),
+ key.WithHelp("ctrl+v", "paste file/directory path"),
+ ),
+}
+
+type filepickerCmp struct {
+ basePath string
+ width int
+ height int
+ cursor int
+ err error
+ cursorChain stack
+ viewport viewport.Model
+ dirs []os.DirEntry
+ cwdDetails *DirNode
+ selectedFile string
+ cwd textinput.Model
+ ShowFilePicker bool
+ app *app.App
+}
+
+type DirNode struct {
+ parent *DirNode
+ child *DirNode
+ directory string
+}
+type stack []int
+
+func (s stack) Push(v int) stack {
+ return append(s, v)
+}
+
+func (s stack) Pop() (stack, int) {
+ l := len(s)
+ return s[:l-1], s[l-1]
+}
+
+type AttachmentAddedMsg struct {
+ Attachment app.Attachment
+}
+
+func (f *filepickerCmp) Init() tea.Cmd {
+ return nil
+}
+
+func (f *filepickerCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmd tea.Cmd
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ f.width = 60
+ f.height = 20
+ f.viewport.Width = 80
+ f.viewport.Height = 22
+ f.cursor = 0
+ f.getCurrentFileBelowCursor()
+ case tea.KeyMsg:
+ if f.cwd.Focused() {
+ f.cwd, cmd = f.cwd.Update(msg)
+ }
+ switch {
+ case key.Matches(msg, filePickerKeyMap.InsertCWD):
+ f.cwd.Focus()
+ return f, cmd
+ case key.Matches(msg, filePickerKeyMap.Esc):
+ if f.cwd.Focused() {
+ f.cwd.Blur()
+ }
+ case key.Matches(msg, filePickerKeyMap.Down):
+ if !f.cwd.Focused() || msg.String() == downArrow {
+ if f.cursor < len(f.dirs)-1 {
+ f.cursor++
+ f.getCurrentFileBelowCursor()
+ }
+ }
+ case key.Matches(msg, filePickerKeyMap.Up):
+ if !f.cwd.Focused() || msg.String() == upArrow {
+ if f.cursor > 0 {
+ f.cursor--
+ f.getCurrentFileBelowCursor()
+ }
+ }
+ case key.Matches(msg, filePickerKeyMap.Enter):
+ var path string
+ var isPathDir bool
+ if f.cwd.Focused() {
+ path = f.cwd.Value()
+ fileInfo, err := os.Stat(path)
+ if err != nil {
+ status.Error("Invalid path")
+ return f, cmd
+ }
+ isPathDir = fileInfo.IsDir()
+ } else {
+ path = filepath.Join(f.cwdDetails.directory, "/", f.dirs[f.cursor].Name())
+ isPathDir = f.dirs[f.cursor].IsDir()
+ }
+ if isPathDir {
+ newWorkingDir := DirNode{parent: f.cwdDetails, directory: path}
+ f.cwdDetails.child = &newWorkingDir
+ f.cwdDetails = f.cwdDetails.child
+ f.cursorChain = f.cursorChain.Push(f.cursor)
+ f.dirs = readDir(f.cwdDetails.directory, false)
+ f.cursor = 0
+ f.cwd.SetValue(f.cwdDetails.directory)
+ f.getCurrentFileBelowCursor()
+ } else {
+ f.selectedFile = path
+ return f.addAttachmentToMessage()
+ }
+ case key.Matches(msg, filePickerKeyMap.Esc):
+ if !f.cwd.Focused() {
+ f.cursorChain = make(stack, 0)
+ f.cursor = 0
+ } else {
+ f.cwd.Blur()
+ }
+ case key.Matches(msg, filePickerKeyMap.Forward):
+ if !f.cwd.Focused() {
+ if f.dirs[f.cursor].IsDir() {
+ path := filepath.Join(f.cwdDetails.directory, "/", f.dirs[f.cursor].Name())
+ newWorkingDir := DirNode{parent: f.cwdDetails, directory: path}
+ f.cwdDetails.child = &newWorkingDir
+ f.cwdDetails = f.cwdDetails.child
+ f.cursorChain = f.cursorChain.Push(f.cursor)
+ f.dirs = readDir(f.cwdDetails.directory, false)
+ f.cursor = 0
+ f.cwd.SetValue(f.cwdDetails.directory)
+ f.getCurrentFileBelowCursor()
+ }
+ }
+ case key.Matches(msg, filePickerKeyMap.Backward):
+ if !f.cwd.Focused() {
+ if len(f.cursorChain) != 0 && f.cwdDetails.parent != nil {
+ f.cursorChain, f.cursor = f.cursorChain.Pop()
+ f.cwdDetails = f.cwdDetails.parent
+ f.cwdDetails.child = nil
+ f.dirs = readDir(f.cwdDetails.directory, false)
+ f.cwd.SetValue(f.cwdDetails.directory)
+ f.getCurrentFileBelowCursor()
+ }
+ }
+ case key.Matches(msg, filePickerKeyMap.Paste):
+ if f.cwd.Focused() {
+ val, err := clipboard.ReadAll()
+ if err != nil {
+ slog.Error("failed to read clipboard")
+ return f, cmd
+ }
+ f.cwd.SetValue(f.cwd.Value() + val)
+ }
+ case key.Matches(msg, filePickerKeyMap.OpenFilePicker):
+ f.dirs = readDir(f.cwdDetails.directory, false)
+ f.cursor = 0
+ f.getCurrentFileBelowCursor()
+ }
+ }
+ return f, cmd
+}
+
+func (f *filepickerCmp) addAttachmentToMessage() (tea.Model, tea.Cmd) {
+ // modeInfo := GetSelectedModel(config.Get())
+ // if !modeInfo.SupportsAttachments {
+ // status.Error(fmt.Sprintf("Model %s doesn't support attachments", modeInfo.Name))
+ // return f, nil
+ // }
+
+ selectedFilePath := f.selectedFile
+ if !isExtSupported(selectedFilePath) {
+ status.Error("Unsupported file")
+ return f, nil
+ }
+
+ isFileLarge, err := image.ValidateFileSize(selectedFilePath, maxAttachmentSize)
+ if err != nil {
+ status.Error("unable to read the image")
+ return f, nil
+ }
+ if isFileLarge {
+ status.Error("file too large, max 5MB")
+ return f, nil
+ }
+
+ content, err := os.ReadFile(selectedFilePath)
+ if err != nil {
+ status.Error("Unable read selected file")
+ return f, nil
+ }
+
+ mimeBufferSize := min(512, len(content))
+ mimeType := http.DetectContentType(content[:mimeBufferSize])
+ fileName := filepath.Base(selectedFilePath)
+ attachment := app.Attachment{FilePath: selectedFilePath, FileName: fileName, MimeType: mimeType, Content: content}
+ f.selectedFile = ""
+ return f, util.CmdHandler(AttachmentAddedMsg{attachment})
+}
+
+func (f *filepickerCmp) View() string {
+ t := theme.CurrentTheme()
+ const maxVisibleDirs = 20
+ const maxWidth = 80
+
+ adjustedWidth := maxWidth
+ for _, file := range f.dirs {
+ if len(file.Name()) > adjustedWidth-4 { // Account for padding
+ adjustedWidth = len(file.Name()) + 4
+ }
+ }
+ adjustedWidth = max(30, min(adjustedWidth, f.width-15)) + 1
+
+ files := make([]string, 0, maxVisibleDirs)
+ startIdx := 0
+
+ if len(f.dirs) > maxVisibleDirs {
+ halfVisible := maxVisibleDirs / 2
+ if f.cursor >= halfVisible && f.cursor < len(f.dirs)-halfVisible {
+ startIdx = f.cursor - halfVisible
+ } else if f.cursor >= len(f.dirs)-halfVisible {
+ startIdx = len(f.dirs) - maxVisibleDirs
+ }
+ }
+
+ endIdx := min(startIdx+maxVisibleDirs, len(f.dirs))
+
+ for i := startIdx; i < endIdx; i++ {
+ file := f.dirs[i]
+ itemStyle := styles.BaseStyle().Width(adjustedWidth)
+
+ if i == f.cursor {
+ itemStyle = itemStyle.
+ Background(t.Primary()).
+ Foreground(t.Background()).
+ Bold(true)
+ }
+ filename := file.Name()
+
+ if len(filename) > adjustedWidth-4 {
+ filename = filename[:adjustedWidth-7] + "..."
+ }
+ if file.IsDir() {
+ filename = filename + "/"
+ }
+
+ files = append(files, itemStyle.Padding(0, 1).Render(filename))
+ }
+
+ // Pad to always show exactly 21 lines
+ for len(files) < maxVisibleDirs {
+ files = append(files, styles.BaseStyle().Width(adjustedWidth).Render(""))
+ }
+
+ currentPath := styles.BaseStyle().
+ Height(1).
+ Width(adjustedWidth).
+ Render(f.cwd.View())
+
+ viewportstyle := lipgloss.NewStyle().
+ Width(f.viewport.Width).
+ Background(t.Background()).
+ Border(lipgloss.RoundedBorder()).
+ BorderForeground(t.TextMuted()).
+ BorderBackground(t.Background()).
+ Padding(2).
+ Render(f.viewport.View())
+ var insertExitText string
+ if f.IsCWDFocused() {
+ insertExitText = "Press esc to exit typing path"
+ } else {
+ insertExitText = "Press i to start typing path"
+ }
+
+ content := lipgloss.JoinVertical(
+ lipgloss.Left,
+ currentPath,
+ styles.BaseStyle().Width(adjustedWidth).Render(""),
+ styles.BaseStyle().Width(adjustedWidth).Render(lipgloss.JoinVertical(lipgloss.Left, files...)),
+ styles.BaseStyle().Width(adjustedWidth).Render(""),
+ styles.BaseStyle().Foreground(t.TextMuted()).Width(adjustedWidth).Render(insertExitText),
+ )
+
+ f.cwd.SetValue(f.cwd.Value())
+ contentStyle := styles.BaseStyle().Padding(1, 2).
+ Border(lipgloss.RoundedBorder()).
+ BorderBackground(t.Background()).
+ BorderForeground(t.TextMuted()).
+ Width(lipgloss.Width(content) + 4)
+
+ return lipgloss.JoinHorizontal(lipgloss.Center, contentStyle.Render(content), viewportstyle)
+}
+
+type FilepickerCmp interface {
+ tea.Model
+ ToggleFilepicker(showFilepicker bool)
+ IsCWDFocused() bool
+}
+
+func (f *filepickerCmp) ToggleFilepicker(showFilepicker bool) {
+ f.ShowFilePicker = showFilepicker
+}
+
+func (f *filepickerCmp) IsCWDFocused() bool {
+ return f.cwd.Focused()
+}
+
+func NewFilepickerCmp(app *app.App) FilepickerCmp {
+ homepath, err := os.UserHomeDir()
+ if err != nil {
+ slog.Error("error loading user files")
+ return nil
+ }
+ baseDir := DirNode{parent: nil, directory: homepath}
+ dirs := readDir(homepath, false)
+ viewport := viewport.New(0, 0)
+ currentDirectory := textinput.New()
+ currentDirectory.CharLimit = 200
+ currentDirectory.Width = 44
+ currentDirectory.Cursor.Blink = true
+ currentDirectory.SetValue(baseDir.directory)
+ return &filepickerCmp{cwdDetails: &baseDir, dirs: dirs, cursorChain: make(stack, 0), viewport: viewport, cwd: currentDirectory, app: app}
+}
+
+func (f *filepickerCmp) getCurrentFileBelowCursor() {
+ if len(f.dirs) == 0 || f.cursor < 0 || f.cursor >= len(f.dirs) {
+ slog.Error(fmt.Sprintf("Invalid cursor position. Dirs length: %d, Cursor: %d", len(f.dirs), f.cursor))
+ f.viewport.SetContent("Preview unavailable")
+ return
+ }
+
+ dir := f.dirs[f.cursor]
+ filename := dir.Name()
+ if !dir.IsDir() && isExtSupported(filename) {
+ fullPath := f.cwdDetails.directory + "/" + dir.Name()
+
+ go func() {
+ imageString, err := image.ImagePreview(f.viewport.Width-4, fullPath)
+ if err != nil {
+ slog.Error(err.Error())
+ f.viewport.SetContent("Preview unavailable")
+ return
+ }
+
+ f.viewport.SetContent(imageString)
+ }()
+ } else {
+ f.viewport.SetContent("Preview unavailable")
+ }
+}
+
+func readDir(path string, showHidden bool) []os.DirEntry {
+ slog.Info(fmt.Sprintf("Reading directory: %s", path))
+
+ entriesChan := make(chan []os.DirEntry, 1)
+ errChan := make(chan error, 1)
+
+ go func() {
+ dirEntries, err := os.ReadDir(path)
+ if err != nil {
+ status.Error(err.Error())
+ errChan <- err
+ return
+ }
+ entriesChan <- dirEntries
+ }()
+
+ select {
+ case dirEntries := <-entriesChan:
+ sort.Slice(dirEntries, func(i, j int) bool {
+ if dirEntries[i].IsDir() == dirEntries[j].IsDir() {
+ return dirEntries[i].Name() < dirEntries[j].Name()
+ }
+ return dirEntries[i].IsDir()
+ })
+
+ if showHidden {
+ return dirEntries
+ }
+
+ var sanitizedDirEntries []os.DirEntry
+ for _, dirEntry := range dirEntries {
+ isHidden, _ := IsHidden(dirEntry.Name())
+ if !isHidden {
+ if dirEntry.IsDir() || isExtSupported(dirEntry.Name()) {
+ sanitizedDirEntries = append(sanitizedDirEntries, dirEntry)
+ }
+ }
+ }
+
+ return sanitizedDirEntries
+
+ case <-errChan:
+ status.Error(fmt.Sprintf("Error reading directory %s", path))
+ return []os.DirEntry{}
+
+ case <-time.After(5 * time.Second):
+ status.Error(fmt.Sprintf("Timeout reading directory %s", path))
+ return []os.DirEntry{}
+ }
+}
+
+func IsHidden(file string) (bool, error) {
+ return strings.HasPrefix(file, "."), nil
+}
+
+func isExtSupported(path string) bool {
+ ext := strings.ToLower(filepath.Ext(path))
+ return (ext == ".jpg" || ext == ".jpeg" || ext == ".webp" || ext == ".png")
+}
diff --git a/packages/tui/internal/tui/components/dialog/help.go b/packages/tui/internal/tui/components/dialog/help.go
new file mode 100644
index 000000000..1f7f53e11
--- /dev/null
+++ b/packages/tui/internal/tui/components/dialog/help.go
@@ -0,0 +1,200 @@
+package dialog
+
+import (
+ "strings"
+
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+)
+
+type helpCmp struct {
+ width int
+ height int
+ keys []key.Binding
+}
+
+func (h *helpCmp) Init() tea.Cmd {
+ return nil
+}
+
+func (h *helpCmp) SetBindings(k []key.Binding) {
+ h.keys = k
+}
+
+func (h *helpCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ h.width = 90
+ h.height = msg.Height
+ }
+ return h, nil
+}
+
+func removeDuplicateBindings(bindings []key.Binding) []key.Binding {
+ seen := make(map[string]struct{})
+ result := make([]key.Binding, 0, len(bindings))
+
+ // Process bindings in reverse order
+ for i := len(bindings) - 1; i >= 0; i-- {
+ b := bindings[i]
+ k := strings.Join(b.Keys(), " ")
+ if _, ok := seen[k]; ok {
+ // duplicate, skip
+ continue
+ }
+ seen[k] = struct{}{}
+ // Add to the beginning of result to maintain original order
+ result = append([]key.Binding{b}, result...)
+ }
+
+ return result
+}
+
+func (h *helpCmp) render() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ helpKeyStyle := styles.Bold().
+ Background(t.Background()).
+ Foreground(t.Text()).
+ Padding(0, 1, 0, 0)
+
+ helpDescStyle := styles.Regular().
+ Background(t.Background()).
+ Foreground(t.TextMuted())
+
+ // Compile list of bindings to render
+ bindings := removeDuplicateBindings(h.keys)
+
+ // Enumerate through each group of bindings, populating a series of
+ // pairs of columns, one for keys, one for descriptions
+ var (
+ pairs []string
+ width int
+ rows = 12 - 2
+ )
+
+ for i := 0; i < len(bindings); i += rows {
+ var (
+ keys []string
+ descs []string
+ )
+ for j := i; j < min(i+rows, len(bindings)); j++ {
+ keys = append(keys, helpKeyStyle.Render(bindings[j].Help().Key))
+ descs = append(descs, helpDescStyle.Render(bindings[j].Help().Desc))
+ }
+
+ // Render pair of columns; beyond the first pair, render a three space
+ // left margin, in order to visually separate the pairs.
+ var cols []string
+ if len(pairs) > 0 {
+ cols = []string{baseStyle.Render(" ")}
+ }
+
+ maxDescWidth := 0
+ for _, desc := range descs {
+ if maxDescWidth < lipgloss.Width(desc) {
+ maxDescWidth = lipgloss.Width(desc)
+ }
+ }
+ for i := range descs {
+ remainingWidth := maxDescWidth - lipgloss.Width(descs[i])
+ if remainingWidth > 0 {
+ descs[i] = descs[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
+ }
+ }
+ maxKeyWidth := 0
+ for _, key := range keys {
+ if maxKeyWidth < lipgloss.Width(key) {
+ maxKeyWidth = lipgloss.Width(key)
+ }
+ }
+ for i := range keys {
+ remainingWidth := maxKeyWidth - lipgloss.Width(keys[i])
+ if remainingWidth > 0 {
+ keys[i] = keys[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
+ }
+ }
+
+ cols = append(cols,
+ strings.Join(keys, "\n"),
+ strings.Join(descs, "\n"),
+ )
+
+ pair := baseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...))
+ // check whether it exceeds the maximum width avail (the width of the
+ // terminal, subtracting 2 for the borders).
+ width += lipgloss.Width(pair)
+ if width > h.width-2 {
+ break
+ }
+ pairs = append(pairs, pair)
+ }
+
+ // https://github.com/charmbracelet/lipgloss/issues/209
+ if len(pairs) > 1 {
+ prefix := pairs[:len(pairs)-1]
+ lastPair := pairs[len(pairs)-1]
+ prefix = append(prefix, lipgloss.Place(
+ lipgloss.Width(lastPair), // width
+ lipgloss.Height(prefix[0]), // height
+ lipgloss.Left, // x
+ lipgloss.Top, // y
+ lastPair, // content
+ lipgloss.WithWhitespaceBackground(t.Background()),
+ ))
+ content := baseStyle.Width(h.width).Render(
+ lipgloss.JoinHorizontal(
+ lipgloss.Top,
+ prefix...,
+ ),
+ )
+ return content
+ }
+
+ // Join pairs of columns and enclose in a border
+ content := baseStyle.Width(h.width).Render(
+ lipgloss.JoinHorizontal(
+ lipgloss.Top,
+ pairs...,
+ ),
+ )
+ return content
+}
+
+func (h *helpCmp) View() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ content := h.render()
+ header := baseStyle.
+ Bold(true).
+ Width(lipgloss.Width(content)).
+ Foreground(t.Primary()).
+ Render("Keyboard Shortcuts")
+
+ return baseStyle.Padding(1).
+ Border(lipgloss.RoundedBorder()).
+ BorderForeground(t.TextMuted()).
+ Width(h.width).
+ BorderBackground(t.Background()).
+ Render(
+ lipgloss.JoinVertical(lipgloss.Center,
+ header,
+ baseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
+ content,
+ ),
+ )
+}
+
+type HelpCmp interface {
+ tea.Model
+ SetBindings([]key.Binding)
+}
+
+func NewHelpCmp() HelpCmp {
+ return &helpCmp{}
+}
diff --git a/packages/tui/internal/tui/components/dialog/init.go b/packages/tui/internal/tui/components/dialog/init.go
new file mode 100644
index 000000000..2ef8546f6
--- /dev/null
+++ b/packages/tui/internal/tui/components/dialog/init.go
@@ -0,0 +1,189 @@
+package dialog
+
+import (
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/internal/tui/util"
+)
+
+// InitDialogCmp is a component that asks the user if they want to initialize the project.
+type InitDialogCmp struct {
+ width, height int
+ selected int
+ keys initDialogKeyMap
+}
+
+// NewInitDialogCmp creates a new InitDialogCmp.
+func NewInitDialogCmp() InitDialogCmp {
+ return InitDialogCmp{
+ selected: 0,
+ keys: initDialogKeyMap{},
+ }
+}
+
+type initDialogKeyMap struct {
+ Tab key.Binding
+ Left key.Binding
+ Right key.Binding
+ Enter key.Binding
+ Escape key.Binding
+ Y key.Binding
+ N key.Binding
+}
+
+// ShortHelp implements key.Map.
+func (k initDialogKeyMap) ShortHelp() []key.Binding {
+ return []key.Binding{
+ key.NewBinding(
+ key.WithKeys("tab", "left", "right"),
+ key.WithHelp("tab/←/→", "toggle selection"),
+ ),
+ key.NewBinding(
+ key.WithKeys("enter"),
+ key.WithHelp("enter", "confirm"),
+ ),
+ key.NewBinding(
+ key.WithKeys("esc", "q"),
+ key.WithHelp("esc/q", "cancel"),
+ ),
+ key.NewBinding(
+ key.WithKeys("y", "n"),
+ key.WithHelp("y/n", "yes/no"),
+ ),
+ }
+}
+
+// FullHelp implements key.Map.
+func (k initDialogKeyMap) FullHelp() [][]key.Binding {
+ return [][]key.Binding{k.ShortHelp()}
+}
+
+// Init implements tea.Model.
+func (m InitDialogCmp) Init() tea.Cmd {
+ return nil
+}
+
+// Update implements tea.Model.
+func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
+ return m, util.CmdHandler(CloseInitDialogMsg{Initialize: false})
+ case key.Matches(msg, key.NewBinding(key.WithKeys("tab", "left", "right", "h", "l"))):
+ m.selected = (m.selected + 1) % 2
+ return m, nil
+ case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
+ return m, util.CmdHandler(CloseInitDialogMsg{Initialize: m.selected == 0})
+ case key.Matches(msg, key.NewBinding(key.WithKeys("y"))):
+ return m, util.CmdHandler(CloseInitDialogMsg{Initialize: true})
+ case key.Matches(msg, key.NewBinding(key.WithKeys("n"))):
+ return m, util.CmdHandler(CloseInitDialogMsg{Initialize: false})
+ }
+ case tea.WindowSizeMsg:
+ m.width = msg.Width
+ m.height = msg.Height
+ }
+ return m, nil
+}
+
+// View implements tea.Model.
+func (m InitDialogCmp) View() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ // Calculate width needed for content
+ maxWidth := 60 // Width for explanation text
+
+ title := baseStyle.
+ Foreground(t.Primary()).
+ Bold(true).
+ Width(maxWidth).
+ Padding(0, 1).
+ Render("Initialize Project")
+
+ explanation := baseStyle.
+ Foreground(t.Text()).
+ Width(maxWidth).
+ Padding(0, 1).
+ Render("Initialization generates a new CONTEXT.md file that contains information about your codebase, this file serves as memory for each project, you can freely add to it to help the agents be better at their job.")
+
+ question := baseStyle.
+ Foreground(t.Text()).
+ Width(maxWidth).
+ Padding(1, 1).
+ Render("Would you like to initialize this project?")
+
+ maxWidth = min(maxWidth, m.width-10)
+ yesStyle := baseStyle
+ noStyle := baseStyle
+
+ if m.selected == 0 {
+ yesStyle = yesStyle.
+ Background(t.Primary()).
+ Foreground(t.Background()).
+ Bold(true)
+ noStyle = noStyle.
+ Background(t.Background()).
+ Foreground(t.Primary())
+ } else {
+ noStyle = noStyle.
+ Background(t.Primary()).
+ Foreground(t.Background()).
+ Bold(true)
+ yesStyle = yesStyle.
+ Background(t.Background()).
+ Foreground(t.Primary())
+ }
+
+ yes := yesStyle.Padding(0, 3).Render("Yes")
+ no := noStyle.Padding(0, 3).Render("No")
+
+ buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, baseStyle.Render(" "), no)
+ buttons = baseStyle.
+ Width(maxWidth).
+ Padding(1, 0).
+ Render(buttons)
+
+ content := lipgloss.JoinVertical(
+ lipgloss.Left,
+ title,
+ baseStyle.Width(maxWidth).Render(""),
+ explanation,
+ question,
+ buttons,
+ baseStyle.Width(maxWidth).Render(""),
+ )
+
+ return baseStyle.Padding(1, 2).
+ Border(lipgloss.RoundedBorder()).
+ BorderBackground(t.Background()).
+ BorderForeground(t.TextMuted()).
+ Width(lipgloss.Width(content) + 4).
+ Render(content)
+}
+
+// SetSize sets the size of the component.
+func (m *InitDialogCmp) SetSize(width, height int) {
+ m.width = width
+ m.height = height
+}
+
+// Bindings implements layout.Bindings.
+func (m InitDialogCmp) Bindings() []key.Binding {
+ return m.keys.ShortHelp()
+}
+
+// CloseInitDialogMsg is a message that is sent when the init dialog is closed.
+type CloseInitDialogMsg struct {
+ Initialize bool
+}
+
+// ShowInitDialogMsg is a message that is sent to show the init dialog.
+type ShowInitDialogMsg struct {
+ Show bool
+}
diff --git a/packages/tui/internal/tui/components/dialog/models.go b/packages/tui/internal/tui/components/dialog/models.go
new file mode 100644
index 000000000..2dd1e2fea
--- /dev/null
+++ b/packages/tui/internal/tui/components/dialog/models.go
@@ -0,0 +1,327 @@
+package dialog
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/tui/app"
+ "github.com/sst/opencode/internal/tui/layout"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/internal/tui/util"
+ "github.com/sst/opencode/pkg/client"
+)
+
+const (
+ numVisibleModels = 10
+ maxDialogWidth = 40
+)
+
+// CloseModelDialogMsg is sent when a model is selected
+type CloseModelDialogMsg struct {
+ Provider *client.ProviderInfo
+ Model *client.ProviderModel
+}
+
+// ModelDialog interface for the model selection dialog
+type ModelDialog interface {
+ tea.Model
+ layout.Bindings
+
+ SetProviders(providers []client.ProviderInfo)
+}
+
+type modelDialogCmp struct {
+ app *app.App
+ availableProviders []client.ProviderInfo
+ provider client.ProviderInfo
+ model *client.ProviderModel
+
+ selectedIdx int
+ width int
+ height int
+ scrollOffset int
+ hScrollOffset int
+ hScrollPossible bool
+}
+
+type modelKeyMap struct {
+ Up key.Binding
+ Down key.Binding
+ Left key.Binding
+ Right key.Binding
+ Enter key.Binding
+ Escape key.Binding
+ J key.Binding
+ K key.Binding
+ H key.Binding
+ L key.Binding
+}
+
+var modelKeys = modelKeyMap{
+ Up: key.NewBinding(
+ key.WithKeys("up"),
+ key.WithHelp("↑", "previous model"),
+ ),
+ Down: key.NewBinding(
+ key.WithKeys("down"),
+ key.WithHelp("↓", "next model"),
+ ),
+ Left: key.NewBinding(
+ key.WithKeys("left"),
+ key.WithHelp("←", "scroll left"),
+ ),
+ Right: key.NewBinding(
+ key.WithKeys("right"),
+ key.WithHelp("→", "scroll right"),
+ ),
+ Enter: key.NewBinding(
+ key.WithKeys("enter"),
+ key.WithHelp("enter", "select model"),
+ ),
+ Escape: key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "close"),
+ ),
+ J: key.NewBinding(
+ key.WithKeys("j"),
+ key.WithHelp("j", "next model"),
+ ),
+ K: key.NewBinding(
+ key.WithKeys("k"),
+ key.WithHelp("k", "previous model"),
+ ),
+ H: key.NewBinding(
+ key.WithKeys("h"),
+ key.WithHelp("h", "scroll left"),
+ ),
+ L: key.NewBinding(
+ key.WithKeys("l"),
+ key.WithHelp("l", "scroll right"),
+ ),
+}
+
+func (m *modelDialogCmp) Init() tea.Cmd {
+ // cfg := config.Get()
+ // modelInfo := GetSelectedModel(cfg)
+ // m.availableProviders = getEnabledProviders(cfg)
+ // m.hScrollPossible = len(m.availableProviders) > 1
+
+ // m.provider = modelInfo.Provider
+ // m.hScrollOffset = findProviderIndex(m.availableProviders, m.provider)
+
+ // m.setupModelsForProvider(m.provider)
+
+ m.availableProviders, _ = m.app.ListProviders(context.Background())
+ m.hScrollOffset = 0
+ m.hScrollPossible = len(m.availableProviders) > 1
+ m.provider = m.availableProviders[m.hScrollOffset]
+
+ return nil
+}
+
+func (m *modelDialogCmp) SetProviders(providers []client.ProviderInfo) {
+ m.availableProviders = providers
+}
+
+func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, modelKeys.Up) || key.Matches(msg, modelKeys.K):
+ m.moveSelectionUp()
+ case key.Matches(msg, modelKeys.Down) || key.Matches(msg, modelKeys.J):
+ m.moveSelectionDown()
+ case key.Matches(msg, modelKeys.Left) || key.Matches(msg, modelKeys.H):
+ if m.hScrollPossible {
+ m.switchProvider(-1)
+ }
+ case key.Matches(msg, modelKeys.Right) || key.Matches(msg, modelKeys.L):
+ if m.hScrollPossible {
+ m.switchProvider(1)
+ }
+ case key.Matches(msg, modelKeys.Enter):
+ return m, util.CmdHandler(CloseModelDialogMsg{Provider: &m.provider, Model: &m.provider.Models[m.selectedIdx]})
+ case key.Matches(msg, modelKeys.Escape):
+ return m, util.CmdHandler(CloseModelDialogMsg{})
+ }
+ case tea.WindowSizeMsg:
+ m.width = msg.Width
+ m.height = msg.Height
+ }
+
+ return m, nil
+}
+
+// moveSelectionUp moves the selection up or wraps to bottom
+func (m *modelDialogCmp) moveSelectionUp() {
+ if m.selectedIdx > 0 {
+ m.selectedIdx--
+ } else {
+ m.selectedIdx = len(m.provider.Models) - 1
+ m.scrollOffset = max(0, len(m.provider.Models)-numVisibleModels)
+ }
+
+ // Keep selection visible
+ if m.selectedIdx < m.scrollOffset {
+ m.scrollOffset = m.selectedIdx
+ }
+}
+
+// moveSelectionDown moves the selection down or wraps to top
+func (m *modelDialogCmp) moveSelectionDown() {
+ if m.selectedIdx < len(m.provider.Models)-1 {
+ m.selectedIdx++
+ } else {
+ m.selectedIdx = 0
+ m.scrollOffset = 0
+ }
+
+ // Keep selection visible
+ if m.selectedIdx >= m.scrollOffset+numVisibleModels {
+ m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
+ }
+}
+
+func (m *modelDialogCmp) switchProvider(offset int) {
+ newOffset := m.hScrollOffset + offset
+
+ // Ensure we stay within bounds
+ if newOffset < 0 {
+ newOffset = len(m.availableProviders) - 1
+ }
+ if newOffset >= len(m.availableProviders) {
+ newOffset = 0
+ }
+
+ m.hScrollOffset = newOffset
+ m.provider = m.availableProviders[m.hScrollOffset]
+ m.setupModelsForProvider(m.provider.Id)
+}
+
+func (m *modelDialogCmp) View() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ // Capitalize first letter of provider name
+ title := baseStyle.
+ Foreground(t.Primary()).
+ Bold(true).
+ Width(maxDialogWidth).
+ Padding(0, 0, 1).
+ Render(fmt.Sprintf("Select %s Model", m.provider.Name))
+
+ // Render visible models
+ endIdx := min(m.scrollOffset+numVisibleModels, len(m.provider.Models))
+ modelItems := make([]string, 0, endIdx-m.scrollOffset)
+
+ for i := m.scrollOffset; i < endIdx; i++ {
+ itemStyle := baseStyle.Width(maxDialogWidth)
+ if i == m.selectedIdx {
+ itemStyle = itemStyle.Background(t.Primary()).
+ Foreground(t.Background()).Bold(true)
+ }
+ modelItems = append(modelItems, itemStyle.Render(*m.provider.Models[i].Name))
+ }
+
+ scrollIndicator := m.getScrollIndicators(maxDialogWidth)
+
+ content := lipgloss.JoinVertical(
+ lipgloss.Left,
+ title,
+ baseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
+ scrollIndicator,
+ )
+
+ return baseStyle.Padding(1, 2).
+ Border(lipgloss.RoundedBorder()).
+ BorderBackground(t.Background()).
+ BorderForeground(t.TextMuted()).
+ Width(lipgloss.Width(content) + 4).
+ Render(content)
+}
+
+func (m *modelDialogCmp) getScrollIndicators(maxWidth int) string {
+ var indicator string
+
+ if len(m.provider.Models) > numVisibleModels {
+ if m.scrollOffset > 0 {
+ indicator += "↑ "
+ }
+ if m.scrollOffset+numVisibleModels < len(m.provider.Models) {
+ indicator += "↓ "
+ }
+ }
+
+ if m.hScrollPossible {
+ if m.hScrollOffset > 0 {
+ indicator = "← " + indicator
+ }
+ if m.hScrollOffset < len(m.availableProviders)-1 {
+ indicator += "→"
+ }
+ }
+
+ if indicator == "" {
+ return ""
+ }
+
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ return baseStyle.
+ Foreground(t.Primary()).
+ Width(maxWidth).
+ Align(lipgloss.Right).
+ Bold(true).
+ Render(indicator)
+}
+
+func (m *modelDialogCmp) BindingKeys() []key.Binding {
+ return layout.KeyMapToSlice(modelKeys)
+}
+
+// findProviderIndex returns the index of the provider in the list, or -1 if not found
+// func findProviderIndex(providers []string, provider string) int {
+// for i, p := range providers {
+// if p == provider {
+// return i
+// }
+// }
+// return -1
+// }
+
+func (m *modelDialogCmp) setupModelsForProvider(_ string) {
+ m.selectedIdx = 0
+ m.scrollOffset = 0
+
+ // cfg := config.Get()
+ // agentCfg := cfg.Agents[config.AgentPrimary]
+ // selectedModelId := agentCfg.Model
+
+ // m.provider = provider
+ // m.models = getModelsForProvider(provider)
+
+ // Try to select the current model if it belongs to this provider
+ // if provider == models.SupportedModels[selectedModelId].Provider {
+ // for i, model := range m.models {
+ // if model.ID == selectedModelId {
+ // m.selectedIdx = i
+ // // Adjust scroll position to keep selected model visible
+ // if m.selectedIdx >= numVisibleModels {
+ // m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
+ // }
+ // break
+ // }
+ // }
+ // }
+}
+
+func NewModelDialogCmp(app *app.App) ModelDialog {
+ return &modelDialogCmp{
+ app: app,
+ }
+}
diff --git a/packages/tui/internal/tui/components/dialog/permission.go b/packages/tui/internal/tui/components/dialog/permission.go
new file mode 100644
index 000000000..0e5afdeab
--- /dev/null
+++ b/packages/tui/internal/tui/components/dialog/permission.go
@@ -0,0 +1,502 @@
+package dialog
+
+import (
+ "fmt"
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/viewport"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/tui/layout"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/internal/tui/util"
+ "strings"
+)
+
+type PermissionAction string
+
+// Permission responses
+const (
+ PermissionAllow PermissionAction = "allow"
+ PermissionAllowForSession PermissionAction = "allow_session"
+ PermissionDeny PermissionAction = "deny"
+)
+
+// PermissionResponseMsg represents the user's response to a permission request
+type PermissionResponseMsg struct {
+ // Permission permission.PermissionRequest
+ Action PermissionAction
+}
+
+// PermissionDialogCmp interface for permission dialog component
+type PermissionDialogCmp interface {
+ tea.Model
+ layout.Bindings
+ // SetPermissions(permission permission.PermissionRequest) tea.Cmd
+}
+
+type permissionsMapping struct {
+ Left key.Binding
+ Right key.Binding
+ EnterSpace key.Binding
+ Allow key.Binding
+ AllowSession key.Binding
+ Deny key.Binding
+ Tab key.Binding
+}
+
+var permissionsKeys = permissionsMapping{
+ Left: key.NewBinding(
+ key.WithKeys("left"),
+ key.WithHelp("←", "switch options"),
+ ),
+ Right: key.NewBinding(
+ key.WithKeys("right"),
+ key.WithHelp("→", "switch options"),
+ ),
+ EnterSpace: key.NewBinding(
+ key.WithKeys("enter", " "),
+ key.WithHelp("enter/space", "confirm"),
+ ),
+ Allow: key.NewBinding(
+ key.WithKeys("a"),
+ key.WithHelp("a", "allow"),
+ ),
+ AllowSession: key.NewBinding(
+ key.WithKeys("s"),
+ key.WithHelp("s", "allow for session"),
+ ),
+ Deny: key.NewBinding(
+ key.WithKeys("d"),
+ key.WithHelp("d", "deny"),
+ ),
+ Tab: key.NewBinding(
+ key.WithKeys("tab"),
+ key.WithHelp("tab", "switch options"),
+ ),
+}
+
+// permissionDialogCmp is the implementation of PermissionDialog
+type permissionDialogCmp struct {
+ width int
+ height int
+ // permission permission.PermissionRequest
+ windowSize tea.WindowSizeMsg
+ contentViewPort viewport.Model
+ selectedOption int // 0: Allow, 1: Allow for session, 2: Deny
+
+ diffCache map[string]string
+ markdownCache map[string]string
+}
+
+func (p *permissionDialogCmp) Init() tea.Cmd {
+ return p.contentViewPort.Init()
+}
+
+func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ p.windowSize = msg
+ cmd := p.SetSize()
+ cmds = append(cmds, cmd)
+ p.markdownCache = make(map[string]string)
+ p.diffCache = make(map[string]string)
+ // case tea.KeyMsg:
+ // switch {
+ // case key.Matches(msg, permissionsKeys.Right) || key.Matches(msg, permissionsKeys.Tab):
+ // p.selectedOption = (p.selectedOption + 1) % 3
+ // return p, nil
+ // case key.Matches(msg, permissionsKeys.Left):
+ // p.selectedOption = (p.selectedOption + 2) % 3
+ // case key.Matches(msg, permissionsKeys.EnterSpace):
+ // return p, p.selectCurrentOption()
+ // case key.Matches(msg, permissionsKeys.Allow):
+ // return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllow, Permission: p.permission})
+ // case key.Matches(msg, permissionsKeys.AllowSession):
+ // return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllowForSession, Permission: p.permission})
+ // case key.Matches(msg, permissionsKeys.Deny):
+ // return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionDeny, Permission: p.permission})
+ // default:
+ // // Pass other keys to viewport
+ // viewPort, cmd := p.contentViewPort.Update(msg)
+ // p.contentViewPort = viewPort
+ // cmds = append(cmds, cmd)
+ // }
+ }
+
+ return p, tea.Batch(cmds...)
+}
+
+func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
+ var action PermissionAction
+
+ switch p.selectedOption {
+ case 0:
+ action = PermissionAllow
+ case 1:
+ action = PermissionAllowForSession
+ case 2:
+ action = PermissionDeny
+ }
+
+ return util.CmdHandler(PermissionResponseMsg{Action: action}) // , Permission: p.permission})
+}
+
+func (p *permissionDialogCmp) renderButtons() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ allowStyle := baseStyle
+ allowSessionStyle := baseStyle
+ denyStyle := baseStyle
+ spacerStyle := baseStyle.Background(t.Background())
+
+ // Style the selected button
+ switch p.selectedOption {
+ case 0:
+ allowStyle = allowStyle.Background(t.Primary()).Foreground(t.Background())
+ allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
+ denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
+ case 1:
+ allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
+ allowSessionStyle = allowSessionStyle.Background(t.Primary()).Foreground(t.Background())
+ denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
+ case 2:
+ allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
+ allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
+ denyStyle = denyStyle.Background(t.Primary()).Foreground(t.Background())
+ }
+
+ allowButton := allowStyle.Padding(0, 1).Render("Allow (a)")
+ allowSessionButton := allowSessionStyle.Padding(0, 1).Render("Allow for session (s)")
+ denyButton := denyStyle.Padding(0, 1).Render("Deny (d)")
+
+ content := lipgloss.JoinHorizontal(
+ lipgloss.Left,
+ allowButton,
+ spacerStyle.Render(" "),
+ allowSessionButton,
+ spacerStyle.Render(" "),
+ denyButton,
+ spacerStyle.Render(" "),
+ )
+
+ remainingWidth := p.width - lipgloss.Width(content)
+ if remainingWidth > 0 {
+ content = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + content
+ }
+ return content
+}
+
+func (p *permissionDialogCmp) renderHeader() string {
+ return "NOT IMPLEMENTED"
+ // t := theme.CurrentTheme()
+ // baseStyle := styles.BaseStyle()
+ //
+ // toolKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Tool")
+ // toolValue := baseStyle.
+ // Foreground(t.Text()).
+ // Width(p.width - lipgloss.Width(toolKey)).
+ // Render(fmt.Sprintf(": %s", p.permission.ToolName))
+ //
+ // pathKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Path")
+ //
+ // // Get the current working directory to display relative path
+ // relativePath := p.permission.Path
+ // if filepath.IsAbs(relativePath) {
+ // if cwd, err := filepath.Rel(config.WorkingDirectory(), relativePath); err == nil {
+ // relativePath = cwd
+ // }
+ // }
+ //
+ // pathValue := baseStyle.
+ // Foreground(t.Text()).
+ // Width(p.width - lipgloss.Width(pathKey)).
+ // Render(fmt.Sprintf(": %s", relativePath))
+ //
+ // headerParts := []string{
+ // lipgloss.JoinHorizontal(
+ // lipgloss.Left,
+ // toolKey,
+ // toolValue,
+ // ),
+ // baseStyle.Render(strings.Repeat(" ", p.width)),
+ // lipgloss.JoinHorizontal(
+ // lipgloss.Left,
+ // pathKey,
+ // pathValue,
+ // ),
+ // baseStyle.Render(strings.Repeat(" ", p.width)),
+ // }
+ //
+ // // Add tool-specific header information
+ // switch p.permission.ToolName {
+ // case "bash":
+ // headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Command"))
+ // case "edit":
+ // headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
+ // case "write":
+ // headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
+ // case "fetch":
+ // headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("URL"))
+ // }
+ //
+ // return lipgloss.NewStyle().Background(t.Background()).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
+}
+
+func (p *permissionDialogCmp) renderBashContent() string {
+ // t := theme.CurrentTheme()
+ // baseStyle := styles.BaseStyle()
+ //
+ // if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
+ // content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
+ //
+ // // Use the cache for markdown rendering
+ // renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
+ // r := styles.GetMarkdownRenderer(p.width - 10)
+ // s, err := r.Render(content)
+ // return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
+ // })
+ //
+ // finalContent := baseStyle.
+ // Width(p.contentViewPort.Width).
+ // Render(renderedContent)
+ // p.contentViewPort.SetContent(finalContent)
+ // return p.styleViewport()
+ // }
+ return ""
+}
+
+func (p *permissionDialogCmp) renderEditContent() string {
+ // if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
+ // diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
+ // return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
+ // })
+ //
+ // p.contentViewPort.SetContent(diff)
+ // return p.styleViewport()
+ // }
+ return ""
+}
+
+func (p *permissionDialogCmp) renderPatchContent() string {
+ // if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
+ // diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
+ // return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
+ // })
+ //
+ // p.contentViewPort.SetContent(diff)
+ // return p.styleViewport()
+ // }
+ return ""
+}
+
+func (p *permissionDialogCmp) renderWriteContent() string {
+ // if pr, ok := p.permission.Params.(tools.WritePermissionsParams); ok {
+ // // Use the cache for diff rendering
+ // diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
+ // return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
+ // })
+ //
+ // p.contentViewPort.SetContent(diff)
+ // return p.styleViewport()
+ // }
+ return ""
+}
+
+func (p *permissionDialogCmp) renderFetchContent() string {
+ // t := theme.CurrentTheme()
+ // baseStyle := styles.BaseStyle()
+ //
+ // if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
+ // content := fmt.Sprintf("```bash\n%s\n```", pr.URL)
+ //
+ // // Use the cache for markdown rendering
+ // renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
+ // r := styles.GetMarkdownRenderer(p.width - 10)
+ // s, err := r.Render(content)
+ // return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
+ // })
+ //
+ // finalContent := baseStyle.
+ // Width(p.contentViewPort.Width).
+ // Render(renderedContent)
+ // p.contentViewPort.SetContent(finalContent)
+ // return p.styleViewport()
+ // }
+ return ""
+}
+
+func (p *permissionDialogCmp) renderDefaultContent() string {
+ // t := theme.CurrentTheme()
+ // baseStyle := styles.BaseStyle()
+ //
+ // content := p.permission.Description
+ //
+ // // Use the cache for markdown rendering
+ // renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
+ // r := styles.GetMarkdownRenderer(p.width - 10)
+ // s, err := r.Render(content)
+ // return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
+ // })
+ //
+ // finalContent := baseStyle.
+ // Width(p.contentViewPort.Width).
+ // Render(renderedContent)
+ // p.contentViewPort.SetContent(finalContent)
+ //
+ // if renderedContent == "" {
+ // return ""
+ // }
+ //
+ return p.styleViewport()
+}
+
+func (p *permissionDialogCmp) styleViewport() string {
+ t := theme.CurrentTheme()
+ contentStyle := lipgloss.NewStyle().
+ Background(t.Background())
+
+ return contentStyle.Render(p.contentViewPort.View())
+}
+
+func (p *permissionDialogCmp) render() string {
+ return "NOT IMPLEMENTED"
+ // t := theme.CurrentTheme()
+ // baseStyle := styles.BaseStyle()
+ //
+ // title := baseStyle.
+ // Bold(true).
+ // Width(p.width - 4).
+ // Foreground(t.Primary()).
+ // Render("Permission Required")
+ // // Render header
+ // headerContent := p.renderHeader()
+ // // Render buttons
+ // buttons := p.renderButtons()
+ //
+ // // Calculate content height dynamically based on window size
+ // p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(buttons) - 2 - lipgloss.Height(title)
+ // p.contentViewPort.Width = p.width - 4
+ //
+ // // Render content based on tool type
+ // var contentFinal string
+ // switch p.permission.ToolName {
+ // case "bash":
+ // contentFinal = p.renderBashContent()
+ // case "edit":
+ // contentFinal = p.renderEditContent()
+ // case "patch":
+ // contentFinal = p.renderPatchContent()
+ // case "write":
+ // contentFinal = p.renderWriteContent()
+ // case "fetch":
+ // contentFinal = p.renderFetchContent()
+ // default:
+ // contentFinal = p.renderDefaultContent()
+ // }
+ //
+ // content := lipgloss.JoinVertical(
+ // lipgloss.Top,
+ // title,
+ // baseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
+ // headerContent,
+ // contentFinal,
+ // buttons,
+ // baseStyle.Render(strings.Repeat(" ", p.width-4)),
+ // )
+ //
+ // return baseStyle.
+ // Padding(1, 0, 0, 1).
+ // Border(lipgloss.RoundedBorder()).
+ // BorderBackground(t.Background()).
+ // BorderForeground(t.TextMuted()).
+ // Width(p.width).
+ // Height(p.height).
+ // Render(
+ // content,
+ // )
+}
+
+func (p *permissionDialogCmp) View() string {
+ return p.render()
+}
+
+func (p *permissionDialogCmp) BindingKeys() []key.Binding {
+ return layout.KeyMapToSlice(permissionsKeys)
+}
+
+func (p *permissionDialogCmp) SetSize() tea.Cmd {
+ // if p.permission.ID == "" {
+ // return nil
+ // }
+ // switch p.permission.ToolName {
+ // case "bash":
+ // p.width = int(float64(p.windowSize.Width) * 0.4)
+ // p.height = int(float64(p.windowSize.Height) * 0.3)
+ // case "edit":
+ // p.width = int(float64(p.windowSize.Width) * 0.8)
+ // p.height = int(float64(p.windowSize.Height) * 0.8)
+ // case "write":
+ // p.width = int(float64(p.windowSize.Width) * 0.8)
+ // p.height = int(float64(p.windowSize.Height) * 0.8)
+ // case "fetch":
+ // p.width = int(float64(p.windowSize.Width) * 0.4)
+ // p.height = int(float64(p.windowSize.Height) * 0.3)
+ // default:
+ // p.width = int(float64(p.windowSize.Width) * 0.7)
+ // p.height = int(float64(p.windowSize.Height) * 0.5)
+ // }
+ return nil
+}
+
+// func (p *permissionDialogCmp) SetPermissions(permission permission.PermissionRequest) tea.Cmd {
+// p.permission = permission
+// return p.SetSize()
+// }
+
+// Helper to get or set cached diff content
+func (c *permissionDialogCmp) GetOrSetDiff(key string, generator func() (string, error)) string {
+ if cached, ok := c.diffCache[key]; ok {
+ return cached
+ }
+
+ content, err := generator()
+ if err != nil {
+ return fmt.Sprintf("Error formatting diff: %v", err)
+ }
+
+ c.diffCache[key] = content
+
+ return content
+}
+
+// Helper to get or set cached markdown content
+func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (string, error)) string {
+ if cached, ok := c.markdownCache[key]; ok {
+ return cached
+ }
+
+ content, err := generator()
+ if err != nil {
+ return fmt.Sprintf("Error rendering markdown: %v", err)
+ }
+
+ c.markdownCache[key] = content
+
+ return content
+}
+
+func NewPermissionDialogCmp() PermissionDialogCmp {
+ // Create viewport for content
+ contentViewport := viewport.New(0, 0)
+
+ return &permissionDialogCmp{
+ contentViewPort: contentViewport,
+ selectedOption: 0, // Default to "Allow"
+ diffCache: make(map[string]string),
+ markdownCache: make(map[string]string),
+ }
+}
diff --git a/packages/tui/internal/tui/components/dialog/quit.go b/packages/tui/internal/tui/components/dialog/quit.go
new file mode 100644
index 000000000..3fd2ea920
--- /dev/null
+++ b/packages/tui/internal/tui/components/dialog/quit.go
@@ -0,0 +1,136 @@
+package dialog
+
+import (
+ "strings"
+
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/tui/layout"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/internal/tui/util"
+)
+
+const question = "Are you sure you want to quit?"
+
+type CloseQuitMsg struct{}
+
+type QuitDialog interface {
+ tea.Model
+ layout.Bindings
+}
+
+type quitDialogCmp struct {
+ selectedNo bool
+}
+
+type helpMapping struct {
+ LeftRight key.Binding
+ EnterSpace key.Binding
+ Yes key.Binding
+ No key.Binding
+ Tab key.Binding
+}
+
+var helpKeys = helpMapping{
+ LeftRight: key.NewBinding(
+ key.WithKeys("left", "right"),
+ key.WithHelp("←/→", "switch options"),
+ ),
+ EnterSpace: key.NewBinding(
+ key.WithKeys("enter", " "),
+ key.WithHelp("enter/space", "confirm"),
+ ),
+ Yes: key.NewBinding(
+ key.WithKeys("y", "Y"),
+ key.WithHelp("y/Y", "yes"),
+ ),
+ No: key.NewBinding(
+ key.WithKeys("n", "N"),
+ key.WithHelp("n/N", "no"),
+ ),
+ Tab: key.NewBinding(
+ key.WithKeys("tab"),
+ key.WithHelp("tab", "switch options"),
+ ),
+}
+
+func (q *quitDialogCmp) Init() tea.Cmd {
+ return nil
+}
+
+func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, helpKeys.LeftRight) || key.Matches(msg, helpKeys.Tab):
+ q.selectedNo = !q.selectedNo
+ return q, nil
+ case key.Matches(msg, helpKeys.EnterSpace):
+ if !q.selectedNo {
+ return q, tea.Quit
+ }
+ return q, util.CmdHandler(CloseQuitMsg{})
+ case key.Matches(msg, helpKeys.Yes):
+ return q, tea.Quit
+ case key.Matches(msg, helpKeys.No):
+ return q, util.CmdHandler(CloseQuitMsg{})
+ }
+ }
+ return q, nil
+}
+
+func (q *quitDialogCmp) View() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ yesStyle := baseStyle
+ noStyle := baseStyle
+ spacerStyle := baseStyle.Background(t.Background())
+
+ if q.selectedNo {
+ noStyle = noStyle.Background(t.Primary()).Foreground(t.Background())
+ yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary())
+ } else {
+ yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background())
+ noStyle = noStyle.Background(t.Background()).Foreground(t.Primary())
+ }
+
+ yesButton := yesStyle.Padding(0, 1).Render("Yes")
+ noButton := noStyle.Padding(0, 1).Render("No")
+
+ buttons := lipgloss.JoinHorizontal(lipgloss.Left, yesButton, spacerStyle.Render(" "), noButton)
+
+ width := lipgloss.Width(question)
+ remainingWidth := width - lipgloss.Width(buttons)
+ if remainingWidth > 0 {
+ buttons = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + buttons
+ }
+
+ content := baseStyle.Render(
+ lipgloss.JoinVertical(
+ lipgloss.Center,
+ question,
+ "",
+ buttons,
+ ),
+ )
+
+ return baseStyle.Padding(1, 2).
+ Border(lipgloss.RoundedBorder()).
+ BorderBackground(t.Background()).
+ BorderForeground(t.TextMuted()).
+ Width(lipgloss.Width(content) + 4).
+ Render(content)
+}
+
+func (q *quitDialogCmp) BindingKeys() []key.Binding {
+ return layout.KeyMapToSlice(helpKeys)
+}
+
+func NewQuitCmp() QuitDialog {
+ return &quitDialogCmp{
+ selectedNo: true,
+ }
+}
diff --git a/packages/tui/internal/tui/components/dialog/session.go b/packages/tui/internal/tui/components/dialog/session.go
new file mode 100644
index 000000000..99aa41515
--- /dev/null
+++ b/packages/tui/internal/tui/components/dialog/session.go
@@ -0,0 +1,230 @@
+package dialog
+
+import (
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/tui/layout"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/internal/tui/util"
+ "github.com/sst/opencode/pkg/client"
+)
+
+// CloseSessionDialogMsg is sent when the session dialog is closed
+type CloseSessionDialogMsg struct {
+ Session *client.SessionInfo
+}
+
+// SessionDialog interface for the session switching dialog
+type SessionDialog interface {
+ tea.Model
+ layout.Bindings
+ SetSessions(sessions []client.SessionInfo)
+ SetSelectedSession(sessionID string)
+}
+
+type sessionDialogCmp struct {
+ sessions []client.SessionInfo
+ selectedIdx int
+ width int
+ height int
+ selectedSessionID string
+}
+
+type sessionKeyMap struct {
+ Up key.Binding
+ Down key.Binding
+ Enter key.Binding
+ Escape key.Binding
+ J key.Binding
+ K key.Binding
+}
+
+var sessionKeys = sessionKeyMap{
+ Up: key.NewBinding(
+ key.WithKeys("up"),
+ key.WithHelp("↑", "previous session"),
+ ),
+ Down: key.NewBinding(
+ key.WithKeys("down"),
+ key.WithHelp("↓", "next session"),
+ ),
+ Enter: key.NewBinding(
+ key.WithKeys("enter"),
+ key.WithHelp("enter", "select session"),
+ ),
+ Escape: key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "close"),
+ ),
+ J: key.NewBinding(
+ key.WithKeys("j"),
+ key.WithHelp("j", "next session"),
+ ),
+ K: key.NewBinding(
+ key.WithKeys("k"),
+ key.WithHelp("k", "previous session"),
+ ),
+}
+
+func (s *sessionDialogCmp) Init() tea.Cmd {
+ return nil
+}
+
+func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ s.width = msg.Width
+ s.height = msg.Height
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, sessionKeys.Up) || key.Matches(msg, sessionKeys.K):
+ if s.selectedIdx > 0 {
+ s.selectedIdx--
+ }
+ return s, nil
+ case key.Matches(msg, sessionKeys.Down) || key.Matches(msg, sessionKeys.J):
+ if s.selectedIdx < len(s.sessions)-1 {
+ s.selectedIdx++
+ }
+ return s, nil
+ case key.Matches(msg, sessionKeys.Enter):
+ if len(s.sessions) > 0 {
+ selectedSession := s.sessions[s.selectedIdx]
+ s.selectedSessionID = selectedSession.Id
+
+ return s, util.CmdHandler(CloseSessionDialogMsg{
+ Session: &selectedSession,
+ })
+ }
+ case key.Matches(msg, sessionKeys.Escape):
+ return s, util.CmdHandler(CloseSessionDialogMsg{})
+ }
+ }
+ return s, nil
+}
+
+func (s *sessionDialogCmp) View() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ if len(s.sessions) == 0 {
+ return baseStyle.Padding(1, 2).
+ Border(lipgloss.RoundedBorder()).
+ BorderBackground(t.Background()).
+ BorderForeground(t.TextMuted()).
+ Width(40).
+ Render("No sessions available")
+ }
+
+ // Calculate max width needed for session titles
+ maxWidth := 40 // Minimum width
+ for _, sess := range s.sessions {
+ if len(sess.Title) > maxWidth-4 { // Account for padding
+ maxWidth = len(sess.Title) + 4
+ }
+ }
+
+ maxWidth = max(30, min(maxWidth, s.width-15)) // Limit width to avoid overflow
+
+ // Limit height to avoid taking up too much screen space
+ maxVisibleSessions := min(10, len(s.sessions))
+
+ // Build the session list
+ sessionItems := make([]string, 0, maxVisibleSessions)
+ startIdx := 0
+
+ // If we have more sessions than can be displayed, adjust the start index
+ if len(s.sessions) > maxVisibleSessions {
+ // Center the selected item when possible
+ halfVisible := maxVisibleSessions / 2
+ if s.selectedIdx >= halfVisible && s.selectedIdx < len(s.sessions)-halfVisible {
+ startIdx = s.selectedIdx - halfVisible
+ } else if s.selectedIdx >= len(s.sessions)-halfVisible {
+ startIdx = len(s.sessions) - maxVisibleSessions
+ }
+ }
+
+ endIdx := min(startIdx+maxVisibleSessions, len(s.sessions))
+
+ for i := startIdx; i < endIdx; i++ {
+ sess := s.sessions[i]
+ itemStyle := baseStyle.Width(maxWidth)
+
+ if i == s.selectedIdx {
+ itemStyle = itemStyle.
+ Background(t.Primary()).
+ Foreground(t.Background()).
+ Bold(true)
+ }
+
+ sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title))
+ }
+
+ title := baseStyle.
+ Foreground(t.Primary()).
+ Bold(true).
+ Width(maxWidth).
+ Padding(0, 1).
+ Render("Switch Session")
+
+ content := lipgloss.JoinVertical(
+ lipgloss.Left,
+ title,
+ baseStyle.Width(maxWidth).Render(""),
+ baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
+ baseStyle.Width(maxWidth).Render(""),
+ )
+
+ return baseStyle.Padding(1, 2).
+ Border(lipgloss.RoundedBorder()).
+ BorderBackground(t.Background()).
+ BorderForeground(t.TextMuted()).
+ Width(lipgloss.Width(content) + 4).
+ Render(content)
+}
+
+func (s *sessionDialogCmp) BindingKeys() []key.Binding {
+ return layout.KeyMapToSlice(sessionKeys)
+}
+
+func (s *sessionDialogCmp) SetSessions(sessions []client.SessionInfo) {
+ s.sessions = sessions
+
+ // If we have a selected session ID, find its index
+ if s.selectedSessionID != "" {
+ for i, sess := range sessions {
+ if sess.Id == s.selectedSessionID {
+ s.selectedIdx = i
+ return
+ }
+ }
+ }
+
+ // Default to first session if selected not found
+ s.selectedIdx = 0
+}
+
+func (s *sessionDialogCmp) SetSelectedSession(sessionID string) {
+ s.selectedSessionID = sessionID
+
+ // Update the selected index if sessions are already loaded
+ if len(s.sessions) > 0 {
+ for i, sess := range s.sessions {
+ if sess.Id == sessionID {
+ s.selectedIdx = i
+ return
+ }
+ }
+ }
+}
+
+// NewSessionDialogCmp creates a new session switching dialog
+func NewSessionDialogCmp() SessionDialog {
+ return &sessionDialogCmp{
+ sessions: []client.SessionInfo{},
+ selectedIdx: 0,
+ selectedSessionID: "",
+ }
+}
diff --git a/packages/tui/internal/tui/components/dialog/theme.go b/packages/tui/internal/tui/components/dialog/theme.go
new file mode 100644
index 000000000..54856e8a9
--- /dev/null
+++ b/packages/tui/internal/tui/components/dialog/theme.go
@@ -0,0 +1,199 @@
+package dialog
+
+import (
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/status"
+ "github.com/sst/opencode/internal/tui/layout"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/internal/tui/util"
+)
+
+// ThemeChangedMsg is sent when the theme is changed
+type ThemeChangedMsg struct {
+ ThemeName string
+}
+
+// CloseThemeDialogMsg is sent when the theme dialog is closed
+type CloseThemeDialogMsg struct{}
+
+// ThemeDialog interface for the theme switching dialog
+type ThemeDialog interface {
+ tea.Model
+ layout.Bindings
+}
+
+type themeDialogCmp struct {
+ themes []string
+ selectedIdx int
+ width int
+ height int
+ currentTheme string
+}
+
+type themeKeyMap struct {
+ Up key.Binding
+ Down key.Binding
+ Enter key.Binding
+ Escape key.Binding
+ J key.Binding
+ K key.Binding
+}
+
+var themeKeys = themeKeyMap{
+ Up: key.NewBinding(
+ key.WithKeys("up"),
+ key.WithHelp("↑", "previous theme"),
+ ),
+ Down: key.NewBinding(
+ key.WithKeys("down"),
+ key.WithHelp("↓", "next theme"),
+ ),
+ Enter: key.NewBinding(
+ key.WithKeys("enter"),
+ key.WithHelp("enter", "select theme"),
+ ),
+ Escape: key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "close"),
+ ),
+ J: key.NewBinding(
+ key.WithKeys("j"),
+ key.WithHelp("j", "next theme"),
+ ),
+ K: key.NewBinding(
+ key.WithKeys("k"),
+ key.WithHelp("k", "previous theme"),
+ ),
+}
+
+func (t *themeDialogCmp) Init() tea.Cmd {
+ // Load available themes and update selectedIdx based on current theme
+ t.themes = theme.AvailableThemes()
+ t.currentTheme = theme.CurrentThemeName()
+
+ // Find the current theme in the list
+ for i, name := range t.themes {
+ if name == t.currentTheme {
+ t.selectedIdx = i
+ break
+ }
+ }
+
+ return nil
+}
+
+func (t *themeDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, themeKeys.Up) || key.Matches(msg, themeKeys.K):
+ if t.selectedIdx > 0 {
+ t.selectedIdx--
+ }
+ return t, nil
+ case key.Matches(msg, themeKeys.Down) || key.Matches(msg, themeKeys.J):
+ if t.selectedIdx < len(t.themes)-1 {
+ t.selectedIdx++
+ }
+ return t, nil
+ case key.Matches(msg, themeKeys.Enter):
+ if len(t.themes) > 0 {
+ previousTheme := theme.CurrentThemeName()
+ selectedTheme := t.themes[t.selectedIdx]
+ if previousTheme == selectedTheme {
+ return t, util.CmdHandler(CloseThemeDialogMsg{})
+ }
+ if err := theme.SetTheme(selectedTheme); err != nil {
+ status.Error(err.Error())
+ return t, nil
+ }
+ return t, util.CmdHandler(ThemeChangedMsg{
+ ThemeName: selectedTheme,
+ })
+ }
+ case key.Matches(msg, themeKeys.Escape):
+ return t, util.CmdHandler(CloseThemeDialogMsg{})
+ }
+ case tea.WindowSizeMsg:
+ t.width = msg.Width
+ t.height = msg.Height
+ }
+ return t, nil
+}
+
+func (t *themeDialogCmp) View() string {
+ currentTheme := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ if len(t.themes) == 0 {
+ return baseStyle.Padding(1, 2).
+ Border(lipgloss.RoundedBorder()).
+ BorderBackground(currentTheme.Background()).
+ BorderForeground(currentTheme.TextMuted()).
+ Width(40).
+ Render("No themes available")
+ }
+
+ // Calculate max width needed for theme names
+ maxWidth := 40 // Minimum width
+ for _, themeName := range t.themes {
+ if len(themeName) > maxWidth-4 { // Account for padding
+ maxWidth = len(themeName) + 4
+ }
+ }
+
+ maxWidth = max(30, min(maxWidth, t.width-15)) // Limit width to avoid overflow
+
+ // Build the theme list
+ themeItems := make([]string, 0, len(t.themes))
+ for i, themeName := range t.themes {
+ itemStyle := baseStyle.Width(maxWidth)
+
+ if i == t.selectedIdx {
+ itemStyle = itemStyle.
+ Background(currentTheme.Primary()).
+ Foreground(currentTheme.Background()).
+ Bold(true)
+ }
+
+ themeItems = append(themeItems, itemStyle.Padding(0, 1).Render(themeName))
+ }
+
+ title := baseStyle.
+ Foreground(currentTheme.Primary()).
+ Bold(true).
+ Width(maxWidth).
+ Padding(0, 1).
+ Render("Select Theme")
+
+ content := lipgloss.JoinVertical(
+ lipgloss.Left,
+ title,
+ baseStyle.Width(maxWidth).Render(""),
+ baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, themeItems...)),
+ baseStyle.Width(maxWidth).Render(""),
+ )
+
+ return baseStyle.Padding(1, 2).
+ Border(lipgloss.RoundedBorder()).
+ BorderBackground(currentTheme.Background()).
+ BorderForeground(currentTheme.TextMuted()).
+ Width(lipgloss.Width(content) + 4).
+ Render(content)
+}
+
+func (t *themeDialogCmp) BindingKeys() []key.Binding {
+ return layout.KeyMapToSlice(themeKeys)
+}
+
+// NewThemeDialogCmp creates a new theme switching dialog
+func NewThemeDialogCmp() ThemeDialog {
+ return &themeDialogCmp{
+ themes: []string{},
+ selectedIdx: 0,
+ currentTheme: "",
+ }
+}
diff --git a/packages/tui/internal/tui/components/dialog/tools.go b/packages/tui/internal/tui/components/dialog/tools.go
new file mode 100644
index 000000000..76e6ff227
--- /dev/null
+++ b/packages/tui/internal/tui/components/dialog/tools.go
@@ -0,0 +1,178 @@
+package dialog
+
+import (
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ utilComponents "github.com/sst/opencode/internal/tui/components/util"
+ "github.com/sst/opencode/internal/tui/layout"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+)
+
+const (
+ maxToolsDialogWidth = 60
+ maxVisibleTools = 15
+)
+
+// ToolsDialog interface for the tools list dialog
+type ToolsDialog interface {
+ tea.Model
+ layout.Bindings
+ SetTools(tools []string)
+}
+
+// ShowToolsDialogMsg is sent to show the tools dialog
+type ShowToolsDialogMsg struct {
+ Show bool
+}
+
+// CloseToolsDialogMsg is sent when the tools dialog is closed
+type CloseToolsDialogMsg struct{}
+
+type toolItem struct {
+ name string
+}
+
+func (t toolItem) Render(selected bool, width int) string {
+ th := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle().
+ Width(width).
+ Background(th.Background())
+
+ if selected {
+ baseStyle = baseStyle.
+ Background(th.Primary()).
+ Foreground(th.Background()).
+ Bold(true)
+ } else {
+ baseStyle = baseStyle.
+ Foreground(th.Text())
+ }
+
+ return baseStyle.Render(t.name)
+}
+
+type toolsDialogCmp struct {
+ tools []toolItem
+ width int
+ height int
+ list utilComponents.SimpleList[toolItem]
+}
+
+type toolsKeyMap struct {
+ Up key.Binding
+ Down key.Binding
+ Escape key.Binding
+ J key.Binding
+ K key.Binding
+}
+
+var toolsKeys = toolsKeyMap{
+ Up: key.NewBinding(
+ key.WithKeys("up"),
+ key.WithHelp("↑", "previous tool"),
+ ),
+ Down: key.NewBinding(
+ key.WithKeys("down"),
+ key.WithHelp("↓", "next tool"),
+ ),
+ Escape: key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "close"),
+ ),
+ J: key.NewBinding(
+ key.WithKeys("j"),
+ key.WithHelp("j", "next tool"),
+ ),
+ K: key.NewBinding(
+ key.WithKeys("k"),
+ key.WithHelp("k", "previous tool"),
+ ),
+}
+
+func (m *toolsDialogCmp) Init() tea.Cmd {
+ return nil
+}
+
+func (m *toolsDialogCmp) SetTools(tools []string) {
+ var toolItems []toolItem
+ for _, name := range tools {
+ toolItems = append(toolItems, toolItem{name: name})
+ }
+
+ m.tools = toolItems
+ m.list.SetItems(toolItems)
+}
+
+func (m *toolsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, toolsKeys.Escape):
+ return m, func() tea.Msg { return CloseToolsDialogMsg{} }
+ // Pass other key messages to the list component
+ default:
+ var cmd tea.Cmd
+ listModel, cmd := m.list.Update(msg)
+ m.list = listModel.(utilComponents.SimpleList[toolItem])
+ return m, cmd
+ }
+ case tea.WindowSizeMsg:
+ m.width = msg.Width
+ m.height = msg.Height
+ }
+
+ // For non-key messages
+ var cmd tea.Cmd
+ listModel, cmd := m.list.Update(msg)
+ m.list = listModel.(utilComponents.SimpleList[toolItem])
+ return m, cmd
+}
+
+func (m *toolsDialogCmp) View() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle().Background(t.Background())
+
+ title := baseStyle.
+ Foreground(t.Primary()).
+ Bold(true).
+ Width(maxToolsDialogWidth).
+ Padding(0, 0, 1).
+ Render("Available Tools")
+
+ // Calculate dialog width based on content
+ dialogWidth := min(maxToolsDialogWidth, m.width/2)
+ m.list.SetMaxWidth(dialogWidth)
+
+ content := lipgloss.JoinVertical(
+ lipgloss.Left,
+ title,
+ m.list.View(),
+ )
+
+ return baseStyle.Padding(1, 2).
+ Border(lipgloss.RoundedBorder()).
+ BorderBackground(t.Background()).
+ BorderForeground(t.TextMuted()).
+ Background(t.Background()).
+ Width(lipgloss.Width(content) + 4).
+ Render(content)
+}
+
+func (m *toolsDialogCmp) BindingKeys() []key.Binding {
+ return layout.KeyMapToSlice(toolsKeys)
+}
+
+func NewToolsDialogCmp() ToolsDialog {
+ list := utilComponents.NewSimpleList[toolItem](
+ []toolItem{},
+ maxVisibleTools,
+ "No tools available",
+ true,
+ )
+
+ return &toolsDialogCmp{
+ list: list,
+ }
+} \ No newline at end of file
diff --git a/packages/tui/internal/tui/components/qr/qr.go b/packages/tui/internal/tui/components/qr/qr.go
new file mode 100644
index 000000000..42a60bb5e
--- /dev/null
+++ b/packages/tui/internal/tui/components/qr/qr.go
@@ -0,0 +1,58 @@
+package qr
+
+import (
+ "strings"
+
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/tui/theme"
+ "rsc.io/qr"
+)
+
+var tops_bottoms = []rune{' ', '▀', '▄', '█'}
+
+// Generate a text string to a QR code, which you can write to a terminal or file.
+func Generate(text string) (string, int, error) {
+ code, err := qr.Encode(text, qr.Level(0))
+ if err != nil {
+ return "", 0, err
+ }
+
+ t := theme.CurrentTheme()
+ if t == nil {
+ return "", 0, err
+ }
+
+ // Create lipgloss style for QR code with theme colors
+ qrStyle := lipgloss.NewStyle().
+ Foreground(t.Text()).
+ Background(t.Background())
+
+ var result strings.Builder
+
+ // content
+ for y := 0; y < code.Size-1; y += 2 {
+ var line strings.Builder
+ for x := 0; x < code.Size; x += 1 {
+ var num int8
+ if code.Black(x, y) {
+ num += 1
+ }
+ if code.Black(x, y+1) {
+ num += 2
+ }
+ line.WriteRune(tops_bottoms[num])
+ }
+ result.WriteString(qrStyle.Render(line.String()) + "\n")
+ }
+
+ // add lower border when required (only required when QR size is odd)
+ if code.Size%2 == 1 {
+ var borderLine strings.Builder
+ for range code.Size {
+ borderLine.WriteRune('▀')
+ }
+ result.WriteString(qrStyle.Render(borderLine.String()) + "\n")
+ }
+
+ return result.String(), code.Size, nil
+}
diff --git a/packages/tui/internal/tui/components/spinner/spinner.go b/packages/tui/internal/tui/components/spinner/spinner.go
new file mode 100644
index 000000000..5e1af8771
--- /dev/null
+++ b/packages/tui/internal/tui/components/spinner/spinner.go
@@ -0,0 +1,127 @@
+package spinner
+
+import (
+ "context"
+ "fmt"
+ "os"
+
+ "github.com/charmbracelet/bubbles/spinner"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+)
+
+// Spinner wraps the bubbles spinner for both interactive and non-interactive mode
+type Spinner struct {
+ model spinner.Model
+ done chan struct{}
+ prog *tea.Program
+ ctx context.Context
+ cancel context.CancelFunc
+}
+
+// spinnerModel is the tea.Model for the spinner
+type spinnerModel struct {
+ spinner spinner.Model
+ message string
+ quitting bool
+}
+
+func (m spinnerModel) Init() tea.Cmd {
+ return m.spinner.Tick
+}
+
+func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ m.quitting = true
+ return m, tea.Quit
+ case spinner.TickMsg:
+ var cmd tea.Cmd
+ m.spinner, cmd = m.spinner.Update(msg)
+ return m, cmd
+ case quitMsg:
+ m.quitting = true
+ return m, tea.Quit
+ default:
+ return m, nil
+ }
+}
+
+func (m spinnerModel) View() string {
+ if m.quitting {
+ return ""
+ }
+ return fmt.Sprintf("%s %s", m.spinner.View(), m.message)
+}
+
+// quitMsg is sent when we want to quit the spinner
+type quitMsg struct{}
+
+// NewSpinner creates a new spinner with the given message
+func NewSpinner(message string) *Spinner {
+ s := spinner.New()
+ s.Spinner = spinner.Dot
+ s.Style = s.Style.Foreground(s.Style.GetForeground())
+
+ ctx, cancel := context.WithCancel(context.Background())
+
+ model := spinnerModel{
+ spinner: s,
+ message: message,
+ }
+
+ prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
+
+ return &Spinner{
+ model: s,
+ done: make(chan struct{}),
+ prog: prog,
+ ctx: ctx,
+ cancel: cancel,
+ }
+}
+
+// NewThemedSpinner creates a new spinner with the given message and color
+func NewThemedSpinner(message string, color lipgloss.AdaptiveColor) *Spinner {
+ s := spinner.New()
+ s.Spinner = spinner.Dot
+ s.Style = s.Style.Foreground(color)
+
+ ctx, cancel := context.WithCancel(context.Background())
+
+ model := spinnerModel{
+ spinner: s,
+ message: message,
+ }
+
+ prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
+
+ return &Spinner{
+ model: s,
+ done: make(chan struct{}),
+ prog: prog,
+ ctx: ctx,
+ cancel: cancel,
+ }
+}
+
+// Start begins the spinner animation
+func (s *Spinner) Start() {
+ go func() {
+ defer close(s.done)
+ go func() {
+ <-s.ctx.Done()
+ s.prog.Send(quitMsg{})
+ }()
+ _, err := s.prog.Run()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
+ }
+ }()
+}
+
+// Stop ends the spinner animation
+func (s *Spinner) Stop() {
+ s.cancel()
+ <-s.done
+} \ No newline at end of file
diff --git a/packages/tui/internal/tui/components/spinner/spinner_test.go b/packages/tui/internal/tui/components/spinner/spinner_test.go
new file mode 100644
index 000000000..065726e91
--- /dev/null
+++ b/packages/tui/internal/tui/components/spinner/spinner_test.go
@@ -0,0 +1,24 @@
+package spinner
+
+import (
+ "testing"
+ "time"
+)
+
+func TestSpinner(t *testing.T) {
+ t.Parallel()
+
+ // Create a spinner
+ s := NewSpinner("Test spinner")
+
+ // Start the spinner
+ s.Start()
+
+ // Wait a bit to let it run
+ time.Sleep(100 * time.Millisecond)
+
+ // Stop the spinner
+ s.Stop()
+
+ // If we got here without panicking, the test passes
+} \ No newline at end of file
diff --git a/packages/tui/internal/tui/components/util/simple-list.go b/packages/tui/internal/tui/components/util/simple-list.go
new file mode 100644
index 000000000..7b8c4b1cb
--- /dev/null
+++ b/packages/tui/internal/tui/components/util/simple-list.go
@@ -0,0 +1,159 @@
+package utilComponents
+
+import (
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/tui/layout"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+)
+
+type SimpleListItem interface {
+ Render(selected bool, width int) string
+}
+
+type SimpleList[T SimpleListItem] interface {
+ tea.Model
+ layout.Bindings
+ SetMaxWidth(maxWidth int)
+ GetSelectedItem() (item T, idx int)
+ SetItems(items []T)
+ GetItems() []T
+}
+
+type simpleListCmp[T SimpleListItem] struct {
+ fallbackMsg string
+ items []T
+ selectedIdx int
+ maxWidth int
+ maxVisibleItems int
+ useAlphaNumericKeys bool
+ width int
+ height int
+}
+
+type simpleListKeyMap struct {
+ Up key.Binding
+ Down key.Binding
+ UpAlpha key.Binding
+ DownAlpha key.Binding
+}
+
+var simpleListKeys = simpleListKeyMap{
+ Up: key.NewBinding(
+ key.WithKeys("up"),
+ key.WithHelp("↑", "previous list item"),
+ ),
+ Down: key.NewBinding(
+ key.WithKeys("down"),
+ key.WithHelp("↓", "next list item"),
+ ),
+ UpAlpha: key.NewBinding(
+ key.WithKeys("k"),
+ key.WithHelp("k", "previous list item"),
+ ),
+ DownAlpha: key.NewBinding(
+ key.WithKeys("j"),
+ key.WithHelp("j", "next list item"),
+ ),
+}
+
+func (c *simpleListCmp[T]) Init() tea.Cmd {
+ return nil
+}
+
+func (c *simpleListCmp[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, simpleListKeys.Up) || (c.useAlphaNumericKeys && key.Matches(msg, simpleListKeys.UpAlpha)):
+ if c.selectedIdx > 0 {
+ c.selectedIdx--
+ }
+ return c, nil
+ case key.Matches(msg, simpleListKeys.Down) || (c.useAlphaNumericKeys && key.Matches(msg, simpleListKeys.DownAlpha)):
+ if c.selectedIdx < len(c.items)-1 {
+ c.selectedIdx++
+ }
+ return c, nil
+ }
+ }
+
+ return c, nil
+}
+
+func (c *simpleListCmp[T]) BindingKeys() []key.Binding {
+ return layout.KeyMapToSlice(simpleListKeys)
+}
+
+func (c *simpleListCmp[T]) GetSelectedItem() (T, int) {
+ if len(c.items) > 0 {
+ return c.items[c.selectedIdx], c.selectedIdx
+ }
+
+ var zero T
+ return zero, -1
+}
+
+func (c *simpleListCmp[T]) SetItems(items []T) {
+ c.selectedIdx = 0
+ c.items = items
+}
+
+func (c *simpleListCmp[T]) GetItems() []T {
+ return c.items
+}
+
+func (c *simpleListCmp[T]) SetMaxWidth(width int) {
+ c.maxWidth = width
+}
+
+func (c *simpleListCmp[T]) View() string {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ items := c.items
+ maxWidth := c.maxWidth
+ maxVisibleItems := min(c.maxVisibleItems, len(items))
+ startIdx := 0
+
+ if len(items) <= 0 {
+ return baseStyle.
+ Background(t.Background()).
+ Padding(0, 1).
+ Width(maxWidth).
+ Render(c.fallbackMsg)
+ }
+
+ if len(items) > maxVisibleItems {
+ halfVisible := maxVisibleItems / 2
+ if c.selectedIdx >= halfVisible && c.selectedIdx < len(items)-halfVisible {
+ startIdx = c.selectedIdx - halfVisible
+ } else if c.selectedIdx >= len(items)-halfVisible {
+ startIdx = len(items) - maxVisibleItems
+ }
+ }
+
+ endIdx := min(startIdx+maxVisibleItems, len(items))
+
+ listItems := make([]string, 0, maxVisibleItems)
+
+ for i := startIdx; i < endIdx; i++ {
+ item := items[i]
+ title := item.Render(i == c.selectedIdx, maxWidth)
+ listItems = append(listItems, title)
+ }
+
+ return lipgloss.JoinVertical(lipgloss.Left, listItems...)
+}
+
+func NewSimpleList[T SimpleListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) SimpleList[T] {
+ return &simpleListCmp[T]{
+ fallbackMsg: fallbackMsg,
+ items: items,
+ maxVisibleItems: maxVisibleItems,
+ useAlphaNumericKeys: useAlphaNumericKeys,
+ selectedIdx: 0,
+ }
+}
diff --git a/packages/tui/internal/tui/image/clipboard_unix.go b/packages/tui/internal/tui/image/clipboard_unix.go
new file mode 100644
index 000000000..3cb590207
--- /dev/null
+++ b/packages/tui/internal/tui/image/clipboard_unix.go
@@ -0,0 +1,49 @@
+//go:build !windows
+
+package image
+
+import (
+ "bytes"
+ "fmt"
+ "image"
+ "github.com/atotto/clipboard"
+)
+
+func GetImageFromClipboard() ([]byte, string, error) {
+ text, err := clipboard.ReadAll()
+ if err != nil {
+ return nil, "", fmt.Errorf("Error reading clipboard")
+ }
+
+ if text == "" {
+ return nil, "", nil
+ }
+
+ binaryData := []byte(text)
+ imageBytes, err := binaryToImage(binaryData)
+ if err != nil {
+ return nil, text, nil
+ }
+ return imageBytes, "", nil
+
+}
+
+
+
+func binaryToImage(data []byte) ([]byte, error) {
+ reader := bytes.NewReader(data)
+ img, _, err := image.Decode(reader)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to covert bytes to image")
+ }
+
+ return ImageToBytes(img)
+}
+
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
diff --git a/packages/tui/internal/tui/image/clipboard_windows.go b/packages/tui/internal/tui/image/clipboard_windows.go
new file mode 100644
index 000000000..6431ce3d4
--- /dev/null
+++ b/packages/tui/internal/tui/image/clipboard_windows.go
@@ -0,0 +1,192 @@
+//go:build windows
+
+package image
+
+import (
+ "bytes"
+ "fmt"
+ "image"
+ "image/color"
+ "log/slog"
+ "syscall"
+ "unsafe"
+)
+
+var (
+ user32 = syscall.NewLazyDLL("user32.dll")
+ kernel32 = syscall.NewLazyDLL("kernel32.dll")
+ openClipboard = user32.NewProc("OpenClipboard")
+ closeClipboard = user32.NewProc("CloseClipboard")
+ getClipboardData = user32.NewProc("GetClipboardData")
+ isClipboardFormatAvailable = user32.NewProc("IsClipboardFormatAvailable")
+ globalLock = kernel32.NewProc("GlobalLock")
+ globalUnlock = kernel32.NewProc("GlobalUnlock")
+ globalSize = kernel32.NewProc("GlobalSize")
+)
+
+const (
+ CF_TEXT = 1
+ CF_UNICODETEXT = 13
+ CF_DIB = 8
+)
+
+type BITMAPINFOHEADER struct {
+ BiSize uint32
+ BiWidth int32
+ BiHeight int32
+ BiPlanes uint16
+ BiBitCount uint16
+ BiCompression uint32
+ BiSizeImage uint32
+ BiXPelsPerMeter int32
+ BiYPelsPerMeter int32
+ BiClrUsed uint32
+ BiClrImportant uint32
+}
+
+func GetImageFromClipboard() ([]byte, string, error) {
+ ret, _, _ := openClipboard.Call(0)
+ if ret == 0 {
+ return nil, "", fmt.Errorf("failed to open clipboard")
+ }
+ defer func(closeClipboard *syscall.LazyProc, a ...uintptr) {
+ _, _, err := closeClipboard.Call(a...)
+ if err != nil {
+ slog.Error("close clipboard failed")
+ return
+ }
+ }(closeClipboard)
+ isTextAvailable, _, _ := isClipboardFormatAvailable.Call(uintptr(CF_TEXT))
+ isUnicodeTextAvailable, _, _ := isClipboardFormatAvailable.Call(uintptr(CF_UNICODETEXT))
+
+ if isTextAvailable != 0 || isUnicodeTextAvailable != 0 {
+ // Get text from clipboard
+ var formatToUse uintptr = CF_TEXT
+ if isUnicodeTextAvailable != 0 {
+ formatToUse = CF_UNICODETEXT
+ }
+
+ hClipboardText, _, _ := getClipboardData.Call(formatToUse)
+ if hClipboardText != 0 {
+ textPtr, _, _ := globalLock.Call(hClipboardText)
+ if textPtr != 0 {
+ defer func(globalUnlock *syscall.LazyProc, a ...uintptr) {
+ _, _, err := globalUnlock.Call(a...)
+ if err != nil {
+ slog.Error("Global unlock failed")
+ return
+ }
+ }(globalUnlock, hClipboardText)
+
+ // Get clipboard text
+ var clipboardText string
+ if formatToUse == CF_UNICODETEXT {
+ // Convert wide string to Go string
+ clipboardText = syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(textPtr))[:])
+ } else {
+ // Get size of ANSI text
+ size, _, _ := globalSize.Call(hClipboardText)
+ if size > 0 {
+ // Convert ANSI string to Go string
+ textBytes := make([]byte, size)
+ copy(textBytes, (*[1 << 20]byte)(unsafe.Pointer(textPtr))[:size:size])
+ clipboardText = bytesToString(textBytes)
+ }
+ }
+
+ // Check if the text is not empty
+ if clipboardText != "" {
+ return nil, clipboardText, nil
+ }
+ }
+ }
+ }
+ hClipboardData, _, _ := getClipboardData.Call(uintptr(CF_DIB))
+ if hClipboardData == 0 {
+ return nil, "", fmt.Errorf("failed to get clipboard data")
+ }
+
+ dataPtr, _, _ := globalLock.Call(hClipboardData)
+ if dataPtr == 0 {
+ return nil, "", fmt.Errorf("failed to lock clipboard data")
+ }
+ defer func(globalUnlock *syscall.LazyProc, a ...uintptr) {
+ _, _, err := globalUnlock.Call(a...)
+ if err != nil {
+ slog.Error("Global unlock failed")
+ return
+ }
+ }(globalUnlock, hClipboardData)
+
+ bmiHeader := (*BITMAPINFOHEADER)(unsafe.Pointer(dataPtr))
+
+ width := int(bmiHeader.BiWidth)
+ height := int(bmiHeader.BiHeight)
+ if height < 0 {
+ height = -height
+ }
+ bitsPerPixel := int(bmiHeader.BiBitCount)
+
+ img := image.NewRGBA(image.Rect(0, 0, width, height))
+
+ var bitsOffset uintptr
+ if bitsPerPixel <= 8 {
+ numColors := uint32(1) << bitsPerPixel
+ if bmiHeader.BiClrUsed > 0 {
+ numColors = bmiHeader.BiClrUsed
+ }
+ bitsOffset = unsafe.Sizeof(*bmiHeader) + uintptr(numColors*4)
+ } else {
+ bitsOffset = unsafe.Sizeof(*bmiHeader)
+ }
+
+ for y := range height {
+ for x := range width {
+
+ srcY := height - y - 1
+ if bmiHeader.BiHeight < 0 {
+ srcY = y
+ }
+
+ var pixelPointer unsafe.Pointer
+ var r, g, b, a uint8
+
+ switch bitsPerPixel {
+ case 24:
+ stride := (width*3 + 3) &^ 3
+ pixelPointer = unsafe.Pointer(dataPtr + bitsOffset + uintptr(srcY*stride+x*3))
+ b = *(*byte)(pixelPointer)
+ g = *(*byte)(unsafe.Add(pixelPointer, 1))
+ r = *(*byte)(unsafe.Add(pixelPointer, 2))
+ a = 255
+ case 32:
+ pixelPointer = unsafe.Pointer(dataPtr + bitsOffset + uintptr(srcY*width*4+x*4))
+ b = *(*byte)(pixelPointer)
+ g = *(*byte)(unsafe.Add(pixelPointer, 1))
+ r = *(*byte)(unsafe.Add(pixelPointer, 2))
+ a = *(*byte)(unsafe.Add(pixelPointer, 3))
+ if a == 0 {
+ a = 255
+ }
+ default:
+ return nil, "", fmt.Errorf("unsupported bit count: %d", bitsPerPixel)
+ }
+
+ img.Set(x, y, color.RGBA{R: r, G: g, B: b, A: a})
+ }
+ }
+
+ imageBytes, err := ImageToBytes(img)
+ if err != nil {
+ return nil, "", err
+ }
+ return imageBytes, "", nil
+}
+
+func bytesToString(b []byte) string {
+ i := bytes.IndexByte(b, 0)
+ if i == -1 {
+ return string(b)
+ }
+ return string(b[:i])
+}
diff --git a/packages/tui/internal/tui/image/images.go b/packages/tui/internal/tui/image/images.go
new file mode 100644
index 000000000..f476b201c
--- /dev/null
+++ b/packages/tui/internal/tui/image/images.go
@@ -0,0 +1,85 @@
+package image
+
+import (
+ "bytes"
+ "fmt"
+ "image"
+ "image/png"
+ "os"
+ "strings"
+
+ "github.com/charmbracelet/lipgloss"
+ "github.com/disintegration/imaging"
+ "github.com/lucasb-eyer/go-colorful"
+ _ "golang.org/x/image/webp"
+)
+
+func ValidateFileSize(filePath string, sizeLimit int64) (bool, error) {
+ fileInfo, err := os.Stat(filePath)
+ if err != nil {
+ return false, fmt.Errorf("error getting file info: %w", err)
+ }
+
+ if fileInfo.Size() > sizeLimit {
+ return true, nil
+ }
+
+ return false, nil
+}
+
+func ToString(width int, img image.Image) string {
+ img = imaging.Resize(img, width, 0, imaging.Lanczos)
+ b := img.Bounds()
+ imageWidth := b.Max.X
+ h := b.Max.Y
+ str := strings.Builder{}
+
+ for heightCounter := 0; heightCounter < h; heightCounter += 2 {
+ for x := range imageWidth {
+ c1, _ := colorful.MakeColor(img.At(x, heightCounter))
+ color1 := lipgloss.Color(c1.Hex())
+
+ var color2 lipgloss.Color
+ if heightCounter+1 < h {
+ c2, _ := colorful.MakeColor(img.At(x, heightCounter+1))
+ color2 = lipgloss.Color(c2.Hex())
+ } else {
+ color2 = color1
+ }
+
+ str.WriteString(lipgloss.NewStyle().Foreground(color1).
+ Background(color2).Render("▀"))
+ }
+
+ str.WriteString("\n")
+ }
+
+ return str.String()
+}
+
+func ImagePreview(width int, filename string) (string, error) {
+ imageContent, err := os.Open(filename)
+ if err != nil {
+ return "", err
+ }
+ defer imageContent.Close()
+
+ img, _, err := image.Decode(imageContent)
+ if err != nil {
+ return "", err
+ }
+
+ imageString := ToString(width, img)
+
+ return imageString, nil
+}
+
+func ImageToBytes(image image.Image) ([]byte, error) {
+ buf := new(bytes.Buffer)
+ err := png.Encode(buf, image)
+ if err != nil {
+ return nil, err
+ }
+
+ return buf.Bytes(), nil
+}
diff --git a/packages/tui/internal/tui/layout/container.go b/packages/tui/internal/tui/layout/container.go
new file mode 100644
index 000000000..b5bdca20a
--- /dev/null
+++ b/packages/tui/internal/tui/layout/container.go
@@ -0,0 +1,230 @@
+package layout
+
+import (
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/tui/theme"
+)
+
+type Container interface {
+ tea.Model
+ Sizeable
+ Bindings
+ Focus()
+ Blur()
+}
+
+type container struct {
+ width int
+ height int
+
+ content tea.Model
+
+ paddingTop int
+ paddingRight int
+ paddingBottom int
+ paddingLeft int
+
+ borderTop bool
+ borderRight bool
+ borderBottom bool
+ borderLeft bool
+ borderStyle lipgloss.Border
+
+ focused bool
+}
+
+func (c *container) Init() tea.Cmd {
+ return c.content.Init()
+}
+
+func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ u, cmd := c.content.Update(msg)
+ c.content = u
+ return c, cmd
+}
+
+func (c *container) View() string {
+ t := theme.CurrentTheme()
+ style := lipgloss.NewStyle()
+ width := c.width
+ height := c.height
+
+ style = style.Background(t.Background())
+
+ // Apply border if any side is enabled
+ if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
+ // Adjust width and height for borders
+ if c.borderTop {
+ height--
+ }
+ if c.borderBottom {
+ height--
+ }
+ if c.borderLeft {
+ width--
+ }
+ if c.borderRight {
+ width--
+ }
+ style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
+
+ // Use primary color for border if focused
+ if c.focused {
+ style = style.BorderBackground(t.Background()).BorderForeground(t.Primary())
+ } else {
+ style = style.BorderBackground(t.Background()).BorderForeground(t.BorderNormal())
+ }
+ }
+ style = style.
+ Width(width).
+ Height(height).
+ PaddingTop(c.paddingTop).
+ PaddingRight(c.paddingRight).
+ PaddingBottom(c.paddingBottom).
+ PaddingLeft(c.paddingLeft)
+
+ return style.Render(c.content.View())
+}
+
+func (c *container) SetSize(width, height int) tea.Cmd {
+ c.width = width
+ c.height = height
+
+ // If the content implements Sizeable, adjust its size to account for padding and borders
+ if sizeable, ok := c.content.(Sizeable); ok {
+ // Calculate horizontal space taken by padding and borders
+ horizontalSpace := c.paddingLeft + c.paddingRight
+ if c.borderLeft {
+ horizontalSpace++
+ }
+ if c.borderRight {
+ horizontalSpace++
+ }
+
+ // Calculate vertical space taken by padding and borders
+ verticalSpace := c.paddingTop + c.paddingBottom
+ if c.borderTop {
+ verticalSpace++
+ }
+ if c.borderBottom {
+ verticalSpace++
+ }
+
+ // Set content size with adjusted dimensions
+ contentWidth := max(0, width-horizontalSpace)
+ contentHeight := max(0, height-verticalSpace)
+ return sizeable.SetSize(contentWidth, contentHeight)
+ }
+ return nil
+}
+
+func (c *container) GetSize() (int, int) {
+ return c.width, c.height
+}
+
+func (c *container) BindingKeys() []key.Binding {
+ if b, ok := c.content.(Bindings); ok {
+ return b.BindingKeys()
+ }
+ return []key.Binding{}
+}
+
+// Focus sets the container as focused
+func (c *container) Focus() {
+ c.focused = true
+ // Pass focus to content if it supports it
+ if focusable, ok := c.content.(interface{ Focus() }); ok {
+ focusable.Focus()
+ }
+}
+
+// Blur removes focus from the container
+func (c *container) Blur() {
+ c.focused = false
+ // Remove focus from content if it supports it
+ if blurable, ok := c.content.(interface{ Blur() }); ok {
+ blurable.Blur()
+ }
+}
+
+type ContainerOption func(*container)
+
+func NewContainer(content tea.Model, options ...ContainerOption) Container {
+ c := &container{
+ content: content,
+ borderStyle: lipgloss.NormalBorder(),
+ }
+ for _, option := range options {
+ option(c)
+ }
+ return c
+}
+
+// Padding options
+func WithPadding(top, right, bottom, left int) ContainerOption {
+ return func(c *container) {
+ c.paddingTop = top
+ c.paddingRight = right
+ c.paddingBottom = bottom
+ c.paddingLeft = left
+ }
+}
+
+func WithPaddingAll(padding int) ContainerOption {
+ return WithPadding(padding, padding, padding, padding)
+}
+
+func WithPaddingHorizontal(padding int) ContainerOption {
+ return func(c *container) {
+ c.paddingLeft = padding
+ c.paddingRight = padding
+ }
+}
+
+func WithPaddingVertical(padding int) ContainerOption {
+ return func(c *container) {
+ c.paddingTop = padding
+ c.paddingBottom = padding
+ }
+}
+
+func WithBorder(top, right, bottom, left bool) ContainerOption {
+ return func(c *container) {
+ c.borderTop = top
+ c.borderRight = right
+ c.borderBottom = bottom
+ c.borderLeft = left
+ }
+}
+
+func WithBorderAll() ContainerOption {
+ return WithBorder(true, true, true, true)
+}
+
+func WithBorderHorizontal() ContainerOption {
+ return WithBorder(true, false, true, false)
+}
+
+func WithBorderVertical() ContainerOption {
+ return WithBorder(false, true, false, true)
+}
+
+func WithBorderStyle(style lipgloss.Border) ContainerOption {
+ return func(c *container) {
+ c.borderStyle = style
+ }
+}
+
+func WithRoundedBorder() ContainerOption {
+ return WithBorderStyle(lipgloss.RoundedBorder())
+}
+
+func WithThickBorder() ContainerOption {
+ return WithBorderStyle(lipgloss.ThickBorder())
+}
+
+func WithDoubleBorder() ContainerOption {
+ return WithBorderStyle(lipgloss.DoubleBorder())
+}
diff --git a/packages/tui/internal/tui/layout/layout.go b/packages/tui/internal/tui/layout/layout.go
new file mode 100644
index 000000000..495a3fbc5
--- /dev/null
+++ b/packages/tui/internal/tui/layout/layout.go
@@ -0,0 +1,35 @@
+package layout
+
+import (
+ "reflect"
+
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+type Focusable interface {
+ Focus() tea.Cmd
+ Blur() tea.Cmd
+ IsFocused() bool
+}
+
+type Sizeable interface {
+ SetSize(width, height int) tea.Cmd
+ GetSize() (int, int)
+}
+
+type Bindings interface {
+ BindingKeys() []key.Binding
+}
+
+func KeyMapToSlice(t any) (bindings []key.Binding) {
+ typ := reflect.TypeOf(t)
+ if typ.Kind() != reflect.Struct {
+ return nil
+ }
+ for i := range typ.NumField() {
+ v := reflect.ValueOf(t).Field(i)
+ bindings = append(bindings, v.Interface().(key.Binding))
+ }
+ return
+}
diff --git a/packages/tui/internal/tui/layout/overlay.go b/packages/tui/internal/tui/layout/overlay.go
new file mode 100644
index 000000000..64836463d
--- /dev/null
+++ b/packages/tui/internal/tui/layout/overlay.go
@@ -0,0 +1,169 @@
+package layout
+
+import (
+ "strings"
+
+ "github.com/charmbracelet/lipgloss"
+ chAnsi "github.com/charmbracelet/x/ansi"
+ "github.com/muesli/ansi"
+ "github.com/muesli/reflow/truncate"
+ "github.com/muesli/termenv"
+ "github.com/sst/opencode/internal/tui/styles"
+ "github.com/sst/opencode/internal/tui/theme"
+ "github.com/sst/opencode/internal/tui/util"
+)
+
+// Most of this code is borrowed from
+// https://github.com/charmbracelet/lipgloss/pull/102
+// as well as the lipgloss library, with some modification for what I needed.
+
+// Split a string into lines, additionally returning the size of the widest
+// line.
+func getLines(s string) (lines []string, widest int) {
+ lines = strings.Split(s, "\n")
+
+ for _, l := range lines {
+ w := ansi.PrintableRuneWidth(l)
+ if widest < w {
+ widest = w
+ }
+ }
+
+ return lines, widest
+}
+
+// PlaceOverlay places fg on top of bg.
+func PlaceOverlay(
+ x, y int,
+ fg, bg string,
+ shadow bool, opts ...WhitespaceOption,
+) string {
+ fgLines, fgWidth := getLines(fg)
+ bgLines, bgWidth := getLines(bg)
+ bgHeight := len(bgLines)
+ fgHeight := len(fgLines)
+
+ if shadow {
+ t := theme.CurrentTheme()
+ baseStyle := styles.BaseStyle()
+
+ var shadowbg string = ""
+ shadowchar := lipgloss.NewStyle().
+ Background(t.BackgroundDarker()).
+ Foreground(t.Background()).
+ Render("░")
+ bgchar := baseStyle.Render(" ")
+ for i := 0; i <= fgHeight; i++ {
+ if i == 0 {
+ shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n"
+ } else {
+ shadowbg += bgchar + strings.Repeat(shadowchar, fgWidth) + "\n"
+ }
+ }
+
+ fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...)
+ fgLines, fgWidth = getLines(fg)
+ fgHeight = len(fgLines)
+ }
+
+ if fgWidth >= bgWidth && fgHeight >= bgHeight {
+ // FIXME: return fg or bg?
+ return fg
+ }
+ // TODO: allow placement outside of the bg box?
+ x = util.Clamp(x, 0, bgWidth-fgWidth)
+ y = util.Clamp(y, 0, bgHeight-fgHeight)
+
+ ws := &whitespace{}
+ for _, opt := range opts {
+ opt(ws)
+ }
+
+ var b strings.Builder
+ for i, bgLine := range bgLines {
+ if i > 0 {
+ b.WriteByte('\n')
+ }
+ if i < y || i >= y+fgHeight {
+ b.WriteString(bgLine)
+ continue
+ }
+
+ pos := 0
+ if x > 0 {
+ left := truncate.String(bgLine, uint(x))
+ pos = ansi.PrintableRuneWidth(left)
+ b.WriteString(left)
+ if pos < x {
+ b.WriteString(ws.render(x - pos))
+ pos = x
+ }
+ }
+
+ fgLine := fgLines[i-y]
+ b.WriteString(fgLine)
+ pos += ansi.PrintableRuneWidth(fgLine)
+
+ right := cutLeft(bgLine, pos)
+ bgWidth := ansi.PrintableRuneWidth(bgLine)
+ rightWidth := ansi.PrintableRuneWidth(right)
+ if rightWidth <= bgWidth-pos {
+ b.WriteString(ws.render(bgWidth - rightWidth - pos))
+ }
+
+ b.WriteString(right)
+ }
+
+ return b.String()
+}
+
+// cutLeft cuts printable characters from the left.
+// This function is heavily based on muesli's ansi and truncate packages.
+func cutLeft(s string, cutWidth int) string {
+ return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
+}
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
+
+type whitespace struct {
+ style termenv.Style
+ chars string
+}
+
+// Render whitespaces.
+func (w whitespace) render(width int) string {
+ if w.chars == "" {
+ w.chars = " "
+ }
+
+ r := []rune(w.chars)
+ j := 0
+ b := strings.Builder{}
+
+ // Cycle through runes and print them into the whitespace.
+ for i := 0; i < width; {
+ b.WriteRune(r[j])
+ j++
+ if j >= len(r) {
+ j = 0
+ }
+ i += ansi.PrintableRuneWidth(string(r[j]))
+ }
+
+ // Fill any extra gaps white spaces. This might be necessary if any runes
+ // are more than one cell wide, which could leave a one-rune gap.
+ short := width - ansi.PrintableRuneWidth(b.String())
+ if short > 0 {
+ b.WriteString(strings.Repeat(" ", short))
+ }
+
+ return w.style.Styled(b.String())
+}
+
+// WhitespaceOption sets a styling rule for rendering whitespace.
+type WhitespaceOption func(*whitespace)
diff --git a/packages/tui/internal/tui/layout/split.go b/packages/tui/internal/tui/layout/split.go
new file mode 100644
index 000000000..81e159517
--- /dev/null
+++ b/packages/tui/internal/tui/layout/split.go
@@ -0,0 +1,283 @@
+package layout
+
+import (
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/tui/theme"
+)
+
+type SplitPaneLayout interface {
+ tea.Model
+ Sizeable
+ Bindings
+ SetLeftPanel(panel Container) tea.Cmd
+ SetRightPanel(panel Container) tea.Cmd
+ SetBottomPanel(panel Container) tea.Cmd
+
+ ClearLeftPanel() tea.Cmd
+ ClearRightPanel() tea.Cmd
+ ClearBottomPanel() tea.Cmd
+}
+
+type splitPaneLayout struct {
+ width int
+ height int
+ ratio float64
+ verticalRatio float64
+
+ rightPanel Container
+ leftPanel Container
+ bottomPanel Container
+}
+
+type SplitPaneOption func(*splitPaneLayout)
+
+func (s *splitPaneLayout) Init() tea.Cmd {
+ var cmds []tea.Cmd
+
+ if s.leftPanel != nil {
+ cmds = append(cmds, s.leftPanel.Init())
+ }
+
+ if s.rightPanel != nil {
+ cmds = append(cmds, s.rightPanel.Init())
+ }
+
+ if s.bottomPanel != nil {
+ cmds = append(cmds, s.bottomPanel.Init())
+ }
+
+ return tea.Batch(cmds...)
+}
+
+func (s *splitPaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ return s, s.SetSize(msg.Width, msg.Height)
+ }
+
+ if s.rightPanel != nil {
+ u, cmd := s.rightPanel.Update(msg)
+ s.rightPanel = u.(Container)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ }
+
+ if s.leftPanel != nil {
+ u, cmd := s.leftPanel.Update(msg)
+ s.leftPanel = u.(Container)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ }
+
+ if s.bottomPanel != nil {
+ u, cmd := s.bottomPanel.Update(msg)
+ s.bottomPanel = u.(Container)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ }
+
+ return s, tea.Batch(cmds...)
+}
+
+func (s *splitPaneLayout) View() string {
+ var topSection string
+
+ if s.leftPanel != nil && s.rightPanel != nil {
+ leftView := s.leftPanel.View()
+ rightView := s.rightPanel.View()
+ topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView, rightView)
+ } else if s.leftPanel != nil {
+ topSection = s.leftPanel.View()
+ } else if s.rightPanel != nil {
+ topSection = s.rightPanel.View()
+ } else {
+ topSection = ""
+ }
+
+ var finalView string
+
+ if s.bottomPanel != nil && topSection != "" {
+ bottomView := s.bottomPanel.View()
+ finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView)
+ } else if s.bottomPanel != nil {
+ finalView = s.bottomPanel.View()
+ } else {
+ finalView = topSection
+ }
+
+ if finalView != "" {
+ t := theme.CurrentTheme()
+
+ style := lipgloss.NewStyle().
+ Width(s.width).
+ Height(s.height).
+ Background(t.Background())
+
+ return style.Render(finalView)
+ }
+
+ return finalView
+}
+
+func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
+ s.width = width
+ s.height = height
+
+ var topHeight, bottomHeight int
+ if s.bottomPanel != nil {
+ topHeight = int(float64(height) * s.verticalRatio)
+ bottomHeight = height - topHeight
+ } else {
+ topHeight = height
+ bottomHeight = 0
+ }
+
+ var leftWidth, rightWidth int
+ if s.leftPanel != nil && s.rightPanel != nil {
+ leftWidth = int(float64(width) * s.ratio)
+ rightWidth = width - leftWidth
+ } else if s.leftPanel != nil {
+ leftWidth = width
+ rightWidth = 0
+ } else if s.rightPanel != nil {
+ leftWidth = 0
+ rightWidth = width
+ }
+
+ var cmds []tea.Cmd
+ if s.leftPanel != nil {
+ cmd := s.leftPanel.SetSize(leftWidth, topHeight)
+ cmds = append(cmds, cmd)
+ }
+
+ if s.rightPanel != nil {
+ cmd := s.rightPanel.SetSize(rightWidth, topHeight)
+ cmds = append(cmds, cmd)
+ }
+
+ if s.bottomPanel != nil {
+ cmd := s.bottomPanel.SetSize(width, bottomHeight)
+ cmds = append(cmds, cmd)
+ }
+ return tea.Batch(cmds...)
+}
+
+func (s *splitPaneLayout) GetSize() (int, int) {
+ return s.width, s.height
+}
+
+func (s *splitPaneLayout) SetLeftPanel(panel Container) tea.Cmd {
+ s.leftPanel = panel
+ if s.width > 0 && s.height > 0 {
+ return s.SetSize(s.width, s.height)
+ }
+ return nil
+}
+
+func (s *splitPaneLayout) SetRightPanel(panel Container) tea.Cmd {
+ s.rightPanel = panel
+ if s.width > 0 && s.height > 0 {
+ return s.SetSize(s.width, s.height)
+ }
+ return nil
+}
+
+func (s *splitPaneLayout) SetBottomPanel(panel Container) tea.Cmd {
+ s.bottomPanel = panel
+ if s.width > 0 && s.height > 0 {
+ return s.SetSize(s.width, s.height)
+ }
+ return nil
+}
+
+func (s *splitPaneLayout) ClearLeftPanel() tea.Cmd {
+ s.leftPanel = nil
+ if s.width > 0 && s.height > 0 {
+ return s.SetSize(s.width, s.height)
+ }
+ return nil
+}
+
+func (s *splitPaneLayout) ClearRightPanel() tea.Cmd {
+ s.rightPanel = nil
+ if s.width > 0 && s.height > 0 {
+ return s.SetSize(s.width, s.height)
+ }
+ return nil
+}
+
+func (s *splitPaneLayout) ClearBottomPanel() tea.Cmd {
+ s.bottomPanel = nil
+ if s.width > 0 && s.height > 0 {
+ return s.SetSize(s.width, s.height)
+ }
+ return nil
+}
+
+func (s *splitPaneLayout) BindingKeys() []key.Binding {
+ keys := []key.Binding{}
+ if s.leftPanel != nil {
+ if b, ok := s.leftPanel.(Bindings); ok {
+ keys = append(keys, b.BindingKeys()...)
+ }
+ }
+ if s.rightPanel != nil {
+ if b, ok := s.rightPanel.(Bindings); ok {
+ keys = append(keys, b.BindingKeys()...)
+ }
+ }
+ if s.bottomPanel != nil {
+ if b, ok := s.bottomPanel.(Bindings); ok {
+ keys = append(keys, b.BindingKeys()...)
+ }
+ }
+ return keys
+}
+
+func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
+
+ layout := &splitPaneLayout{
+ ratio: 0.7,
+ verticalRatio: 0.9, // Default 90% for top section, 10% for bottom
+ }
+ for _, option := range options {
+ option(layout)
+ }
+ return layout
+}
+
+func WithLeftPanel(panel Container) SplitPaneOption {
+ return func(s *splitPaneLayout) {
+ s.leftPanel = panel
+ }
+}
+
+func WithRightPanel(panel Container) SplitPaneOption {
+ return func(s *splitPaneLayout) {
+ s.rightPanel = panel
+ }
+}
+
+func WithRatio(ratio float64) SplitPaneOption {
+ return func(s *splitPaneLayout) {
+ s.ratio = ratio
+ }
+}
+
+func WithBottomPanel(panel Container) SplitPaneOption {
+ return func(s *splitPaneLayout) {
+ s.bottomPanel = panel
+ }
+}
+
+func WithVerticalRatio(ratio float64) SplitPaneOption {
+ return func(s *splitPaneLayout) {
+ s.verticalRatio = ratio
+ }
+}
diff --git a/packages/tui/internal/tui/page/chat.go b/packages/tui/internal/tui/page/chat.go
new file mode 100644
index 000000000..bf30193f5
--- /dev/null
+++ b/packages/tui/internal/tui/page/chat.go
@@ -0,0 +1,233 @@
+package page
+
+import (
+ "context"
+ "strings"
+
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/completions"
+ "github.com/sst/opencode/internal/status"
+ "github.com/sst/opencode/internal/tui/app"
+ "github.com/sst/opencode/internal/tui/components/chat"
+ "github.com/sst/opencode/internal/tui/components/dialog"
+ "github.com/sst/opencode/internal/tui/layout"
+ "github.com/sst/opencode/internal/tui/state"
+ "github.com/sst/opencode/internal/tui/util"
+ "github.com/sst/opencode/pkg/client"
+)
+
+var ChatPage PageID = "chat"
+
+type chatPage struct {
+ app *app.App
+ editor layout.Container
+ messages layout.Container
+ layout layout.SplitPaneLayout
+ completionDialog dialog.CompletionDialog
+ showCompletionDialog bool
+}
+
+type ChatKeyMap struct {
+ NewSession key.Binding
+ Cancel key.Binding
+ ToggleTools key.Binding
+ ShowCompletionDialog key.Binding
+}
+
+var keyMap = ChatKeyMap{
+ NewSession: key.NewBinding(
+ key.WithKeys("ctrl+n"),
+ key.WithHelp("ctrl+n", "new session"),
+ ),
+ Cancel: key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "cancel"),
+ ),
+ ToggleTools: key.NewBinding(
+ key.WithKeys("ctrl+h"),
+ key.WithHelp("ctrl+h", "toggle tools"),
+ ),
+ ShowCompletionDialog: key.NewBinding(
+ key.WithKeys("/"),
+ key.WithHelp("/", "Complete"),
+ ),
+}
+
+func (p *chatPage) Init() tea.Cmd {
+ cmds := []tea.Cmd{
+ p.layout.Init(),
+ }
+ cmds = append(cmds, p.completionDialog.Init())
+ return tea.Batch(cmds...)
+}
+
+func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ cmd := p.layout.SetSize(msg.Width, msg.Height)
+ cmds = append(cmds, cmd)
+ case chat.SendMsg:
+ cmd := p.sendMessage(msg.Text, msg.Attachments)
+ if cmd != nil {
+ return p, cmd
+ }
+ case dialog.CommandRunCustomMsg:
+ // Check if the agent is busy before executing custom commands
+ if p.app.PrimaryAgentOLD.IsBusy() {
+ status.Warn("Agent is busy, please wait before executing a command...")
+ return p, nil
+ }
+
+ // Process the command content with arguments if any
+ content := msg.Content
+ if msg.Args != nil {
+ // Replace all named arguments with their values
+ for name, value := range msg.Args {
+ placeholder := "$" + name
+ content = strings.ReplaceAll(content, placeholder, value)
+ }
+ }
+
+ // Handle custom command execution
+ cmd := p.sendMessage(content, nil)
+ if cmd != nil {
+ return p, cmd
+ }
+ case state.SessionSelectedMsg:
+ cmd := p.setSidebar()
+ cmds = append(cmds, cmd)
+ case state.SessionClearedMsg:
+ cmd := p.setSidebar()
+ cmds = append(cmds, cmd)
+
+ case dialog.CompletionDialogCloseMsg:
+ p.showCompletionDialog = false
+ p.app.SetCompletionDialogOpen(false)
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, keyMap.ShowCompletionDialog):
+ p.showCompletionDialog = true
+ p.app.SetCompletionDialogOpen(true)
+ // Continue sending keys to layout->chat
+ case key.Matches(msg, keyMap.NewSession):
+ p.app.Session = &client.SessionInfo{}
+ p.app.Messages = []client.MessageInfo{}
+ return p, tea.Batch(
+ p.clearSidebar(),
+ util.CmdHandler(state.SessionClearedMsg{}),
+ )
+ case key.Matches(msg, keyMap.Cancel):
+ if p.app.Session.Id != "" {
+ // Cancel the current session's generation process
+ // This allows users to interrupt long-running operations
+ // p.app.PrimaryAgentOLD.Cancel(p.app.CurrentSessionOLD.ID)
+ return p, nil
+ }
+ case key.Matches(msg, keyMap.ToggleTools):
+ return p, util.CmdHandler(chat.ToggleToolMessagesMsg{})
+ }
+ }
+ if p.showCompletionDialog {
+ context, contextCmd := p.completionDialog.Update(msg)
+ p.completionDialog = context.(dialog.CompletionDialog)
+ cmds = append(cmds, contextCmd)
+
+ // Doesn't forward event if enter key is pressed
+ if keyMsg, ok := msg.(tea.KeyMsg); ok {
+ if keyMsg.String() == "enter" {
+ return p, tea.Batch(cmds...)
+ }
+ }
+ }
+
+ u, cmd := p.layout.Update(msg)
+ cmds = append(cmds, cmd)
+ p.layout = u.(layout.SplitPaneLayout)
+ return p, tea.Batch(cmds...)
+}
+
+func (p *chatPage) setSidebar() tea.Cmd {
+ sidebarContainer := layout.NewContainer(
+ chat.NewSidebarCmp(p.app),
+ layout.WithPadding(1, 1, 1, 1),
+ )
+ return tea.Batch(p.layout.SetRightPanel(sidebarContainer), sidebarContainer.Init())
+}
+
+func (p *chatPage) clearSidebar() tea.Cmd {
+ return p.layout.ClearRightPanel()
+}
+
+func (p *chatPage) sendMessage(text string, attachments []app.Attachment) tea.Cmd {
+ var cmds []tea.Cmd
+ cmd := p.app.SendChatMessage(context.Background(), text, attachments)
+ cmds = append(cmds, cmd)
+ cmd = p.setSidebar()
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ return tea.Batch(cmds...)
+}
+
+func (p *chatPage) SetSize(width, height int) tea.Cmd {
+ return p.layout.SetSize(width, height)
+}
+
+func (p *chatPage) GetSize() (int, int) {
+ return p.layout.GetSize()
+}
+
+func (p *chatPage) View() string {
+ layoutView := p.layout.View()
+
+ if p.showCompletionDialog {
+ _, layoutHeight := p.layout.GetSize()
+ editorWidth, editorHeight := p.editor.GetSize()
+
+ p.completionDialog.SetWidth(editorWidth)
+ overlay := p.completionDialog.View()
+
+ layoutView = layout.PlaceOverlay(
+ 0,
+ layoutHeight-editorHeight-lipgloss.Height(overlay),
+ overlay,
+ layoutView,
+ false,
+ )
+ }
+
+ return layoutView
+}
+
+func (p *chatPage) BindingKeys() []key.Binding {
+ bindings := layout.KeyMapToSlice(keyMap)
+ bindings = append(bindings, p.messages.BindingKeys()...)
+ bindings = append(bindings, p.editor.BindingKeys()...)
+ return bindings
+}
+
+func NewChatPage(app *app.App) tea.Model {
+ cg := completions.NewFileAndFolderContextGroup()
+ completionDialog := dialog.NewCompletionDialogCmp(cg)
+ messagesContainer := layout.NewContainer(
+ chat.NewMessagesCmp(app),
+ layout.WithPadding(1, 1, 0, 1),
+ )
+ editorContainer := layout.NewContainer(
+ chat.NewEditorCmp(app),
+ layout.WithBorder(true, false, false, false),
+ )
+ return &chatPage{
+ app: app,
+ editor: editorContainer,
+ messages: messagesContainer,
+ completionDialog: completionDialog,
+ layout: layout.NewSplitPane(
+ layout.WithLeftPanel(messagesContainer),
+ layout.WithBottomPanel(editorContainer),
+ ),
+ }
+}
diff --git a/packages/tui/internal/tui/page/page.go b/packages/tui/internal/tui/page/page.go
new file mode 100644
index 000000000..482df5fd7
--- /dev/null
+++ b/packages/tui/internal/tui/page/page.go
@@ -0,0 +1,8 @@
+package page
+
+type PageID string
+
+// PageChangeMsg is used to change the current page
+type PageChangeMsg struct {
+ ID PageID
+}
diff --git a/packages/tui/internal/tui/state/state.go b/packages/tui/internal/tui/state/state.go
new file mode 100644
index 000000000..6b117518e
--- /dev/null
+++ b/packages/tui/internal/tui/state/state.go
@@ -0,0 +1,19 @@
+package state
+
+import (
+ "github.com/sst/opencode/pkg/client"
+)
+
+type SessionSelectedMsg = *client.SessionInfo
+type ModelSelectedMsg struct {
+ Provider client.ProviderInfo
+ Model client.ProviderModel
+}
+
+type SessionClearedMsg struct{}
+type CompactSessionMsg struct{}
+
+// TODO: remove
+type StateUpdatedMsg struct {
+ State map[string]any
+}
diff --git a/packages/tui/internal/tui/styles/background.go b/packages/tui/internal/tui/styles/background.go
new file mode 100644
index 000000000..2fbb34efb
--- /dev/null
+++ b/packages/tui/internal/tui/styles/background.go
@@ -0,0 +1,123 @@
+package styles
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/charmbracelet/lipgloss"
+)
+
+var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m")
+
+func getColorRGB(c lipgloss.TerminalColor) (uint8, uint8, uint8) {
+ r, g, b, a := c.RGBA()
+
+ // Un-premultiply alpha if needed
+ if a > 0 && a < 0xffff {
+ r = (r * 0xffff) / a
+ g = (g * 0xffff) / a
+ b = (b * 0xffff) / a
+ }
+
+ // Convert from 16-bit to 8-bit color
+ return uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)
+}
+
+// ForceReplaceBackgroundWithLipgloss replaces any ANSI background color codes
+// in `input` with a single 24‑bit background (48;2;R;G;B).
+func ForceReplaceBackgroundWithLipgloss(input string, newBgColor lipgloss.TerminalColor) string {
+ // Precompute our new-bg sequence once
+ r, g, b := getColorRGB(newBgColor)
+ newBg := fmt.Sprintf("48;2;%d;%d;%d", r, g, b)
+
+ return ansiEscape.ReplaceAllStringFunc(input, func(seq string) string {
+ const (
+ escPrefixLen = 2 // "\x1b["
+ escSuffixLen = 1 // "m"
+ )
+
+ raw := seq
+ start := escPrefixLen
+ end := len(raw) - escSuffixLen
+
+ var sb strings.Builder
+ // reserve enough space: original content minus bg codes + our newBg
+ sb.Grow((end - start) + len(newBg) + 2)
+
+ // scan from start..end, token by token
+ for i := start; i < end; {
+ // find the next ';' or end
+ j := i
+ for j < end && raw[j] != ';' {
+ j++
+ }
+ token := raw[i:j]
+
+ // fast‑path: skip "48;5;N" or "48;2;R;G;B"
+ if len(token) == 2 && token[0] == '4' && token[1] == '8' {
+ k := j + 1
+ if k < end {
+ // find next token
+ l := k
+ for l < end && raw[l] != ';' {
+ l++
+ }
+ next := raw[k:l]
+ if next == "5" {
+ // skip "48;5;N"
+ m := l + 1
+ for m < end && raw[m] != ';' {
+ m++
+ }
+ i = m + 1
+ continue
+ } else if next == "2" {
+ // skip "48;2;R;G;B"
+ m := l + 1
+ for count := 0; count < 3 && m < end; count++ {
+ for m < end && raw[m] != ';' {
+ m++
+ }
+ m++
+ }
+ i = m
+ continue
+ }
+ }
+ }
+
+ // decide whether to keep this token
+ // manually parse ASCII digits to int
+ isNum := true
+ val := 0
+ for p := i; p < j; p++ {
+ c := raw[p]
+ if c < '0' || c > '9' {
+ isNum = false
+ break
+ }
+ val = val*10 + int(c-'0')
+ }
+ keep := !isNum ||
+ ((val < 40 || val > 47) && (val < 100 || val > 107) && val != 49)
+
+ if keep {
+ if sb.Len() > 0 {
+ sb.WriteByte(';')
+ }
+ sb.WriteString(token)
+ }
+ // advance past this token (and the semicolon)
+ i = j + 1
+ }
+
+ // append our new background
+ if sb.Len() > 0 {
+ sb.WriteByte(';')
+ }
+ sb.WriteString(newBg)
+
+ return "\x1b[" + sb.String() + "m"
+ })
+}
diff --git a/packages/tui/internal/tui/styles/icons.go b/packages/tui/internal/tui/styles/icons.go
new file mode 100644
index 000000000..8ff5fe8bf
--- /dev/null
+++ b/packages/tui/internal/tui/styles/icons.go
@@ -0,0 +1,12 @@
+package styles
+
+const (
+ OpenCodeIcon string = "◍"
+
+ ErrorIcon string = "ⓔ"
+ WarningIcon string = "ⓦ"
+ InfoIcon string = "ⓘ"
+ HintIcon string = "ⓗ"
+ SpinnerIcon string = "⟳"
+ DocumentIcon string = "🖼"
+)
diff --git a/packages/tui/internal/tui/styles/markdown.go b/packages/tui/internal/tui/styles/markdown.go
new file mode 100644
index 000000000..77fb51bae
--- /dev/null
+++ b/packages/tui/internal/tui/styles/markdown.go
@@ -0,0 +1,283 @@
+package styles
+
+import (
+ "github.com/charmbracelet/glamour"
+ "github.com/charmbracelet/glamour/ansi"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/tui/theme"
+)
+
+const defaultMargin = 1
+
+// Helper functions for style pointers
+func boolPtr(b bool) *bool { return &b }
+func stringPtr(s string) *string { return &s }
+func uintPtr(u uint) *uint { return &u }
+
+// returns a glamour TermRenderer configured with the current theme
+func GetMarkdownRenderer(width int) *glamour.TermRenderer {
+ r, _ := glamour.NewTermRenderer(
+ glamour.WithStyles(generateMarkdownStyleConfig()),
+ glamour.WithWordWrap(width),
+ )
+ return r
+}
+
+// creates an ansi.StyleConfig for markdown rendering
+// using adaptive colors from the provided theme.
+func generateMarkdownStyleConfig() ansi.StyleConfig {
+ t := theme.CurrentTheme()
+
+ return ansi.StyleConfig{
+ Document: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BlockPrefix: "",
+ BlockSuffix: "",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
+ },
+ Margin: uintPtr(defaultMargin),
+ },
+ BlockQuote: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownBlockQuote())),
+ Italic: boolPtr(true),
+ Prefix: "┃ ",
+ },
+ Indent: uintPtr(1),
+ IndentToken: stringPtr(BaseStyle().Render(" ")),
+ },
+ List: ansi.StyleList{
+ LevelIndent: defaultMargin,
+ StyleBlock: ansi.StyleBlock{
+ IndentToken: stringPtr(BaseStyle().Render(" ")),
+ StylePrimitive: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
+ },
+ },
+ },
+ Heading: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BlockSuffix: "\n",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
+ Bold: boolPtr(true),
+ },
+ },
+ H1: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "# ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
+ Bold: boolPtr(true),
+ },
+ },
+ H2: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "## ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
+ Bold: boolPtr(true),
+ },
+ },
+ H3: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "### ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
+ Bold: boolPtr(true),
+ },
+ },
+ H4: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "#### ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
+ Bold: boolPtr(true),
+ },
+ },
+ H5: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "##### ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
+ Bold: boolPtr(true),
+ },
+ },
+ H6: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "###### ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
+ Bold: boolPtr(true),
+ },
+ },
+ Strikethrough: ansi.StylePrimitive{
+ CrossedOut: boolPtr(true),
+ Color: stringPtr(adaptiveColorToString(t.TextMuted())),
+ },
+ Emph: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownEmph())),
+ Italic: boolPtr(true),
+ },
+ Strong: ansi.StylePrimitive{
+ Bold: boolPtr(true),
+ Color: stringPtr(adaptiveColorToString(t.MarkdownStrong())),
+ },
+ HorizontalRule: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHorizontalRule())),
+ Format: "\n─────────────────────────────────────────\n",
+ },
+ Item: ansi.StylePrimitive{
+ BlockPrefix: "• ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownListItem())),
+ },
+ Enumeration: ansi.StylePrimitive{
+ BlockPrefix: ". ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownListEnumeration())),
+ },
+ Task: ansi.StyleTask{
+ StylePrimitive: ansi.StylePrimitive{},
+ Ticked: "[✓] ",
+ Unticked: "[ ] ",
+ },
+ Link: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownLink())),
+ Underline: boolPtr(true),
+ },
+ LinkText: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownLinkText())),
+ Bold: boolPtr(true),
+ },
+ Image: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownImage())),
+ Underline: boolPtr(true),
+ Format: "🖼 {{.text}}",
+ },
+ ImageText: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownImageText())),
+ Format: "{{.text}}",
+ },
+ Code: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownCode())),
+ Prefix: "",
+ Suffix: "",
+ },
+ },
+ CodeBlock: ansi.StyleCodeBlock{
+ StyleBlock: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: " ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownCodeBlock())),
+ },
+ },
+ Chroma: &ansi.Chroma{
+ Text: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
+ },
+ Error: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.Error())),
+ },
+ Comment: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxComment())),
+ },
+ CommentPreproc: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
+ },
+ Keyword: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
+ },
+ KeywordReserved: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
+ },
+ KeywordNamespace: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
+ },
+ KeywordType: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxType())),
+ },
+ Operator: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxOperator())),
+ },
+ Punctuation: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxPunctuation())),
+ },
+ Name: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())),
+ },
+ NameBuiltin: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())),
+ },
+ NameTag: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
+ },
+ NameAttribute: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())),
+ },
+ NameClass: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxType())),
+ },
+ NameConstant: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())),
+ },
+ NameDecorator: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())),
+ },
+ NameFunction: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())),
+ },
+ LiteralNumber: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxNumber())),
+ },
+ LiteralString: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxString())),
+ },
+ LiteralStringEscape: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
+ },
+ GenericDeleted: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.DiffRemoved())),
+ },
+ GenericEmph: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownEmph())),
+ Italic: boolPtr(true),
+ },
+ GenericInserted: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.DiffAdded())),
+ },
+ GenericStrong: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownStrong())),
+ Bold: boolPtr(true),
+ },
+ GenericSubheading: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
+ },
+ },
+ },
+ Table: ansi.StyleTable{
+ StyleBlock: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BlockPrefix: "\n",
+ BlockSuffix: "\n",
+ },
+ },
+ CenterSeparator: stringPtr("┼"),
+ ColumnSeparator: stringPtr("│"),
+ RowSeparator: stringPtr("─"),
+ },
+ DefinitionDescription: ansi.StylePrimitive{
+ BlockPrefix: "\n ❯ ",
+ Color: stringPtr(adaptiveColorToString(t.MarkdownLinkText())),
+ },
+ Text: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
+ },
+ Paragraph: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
+ },
+ },
+ }
+}
+
+// adaptiveColorToString converts a lipgloss.AdaptiveColor to the appropriate
+// hex color string based on the current terminal background
+func adaptiveColorToString(color lipgloss.AdaptiveColor) string {
+ if lipgloss.HasDarkBackground() {
+ return color.Dark
+ }
+ return color.Light
+}
diff --git a/packages/tui/internal/tui/styles/styles.go b/packages/tui/internal/tui/styles/styles.go
new file mode 100644
index 000000000..91661a1dd
--- /dev/null
+++ b/packages/tui/internal/tui/styles/styles.go
@@ -0,0 +1,153 @@
+package styles
+
+import (
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/tui/theme"
+)
+
+// BaseStyle returns the base style with background and foreground colors
+func BaseStyle() lipgloss.Style {
+ t := theme.CurrentTheme()
+ return lipgloss.NewStyle().
+ Background(t.Background()).
+ Foreground(t.Text())
+}
+
+// Regular returns a basic unstyled lipgloss.Style
+func Regular() lipgloss.Style {
+ return lipgloss.NewStyle()
+}
+
+func Muted() lipgloss.Style {
+ return lipgloss.NewStyle().Foreground(theme.CurrentTheme().TextMuted())
+}
+
+// Bold returns a bold style
+func Bold() lipgloss.Style {
+ return Regular().Bold(true)
+}
+
+// Padded returns a style with horizontal padding
+func Padded() lipgloss.Style {
+ return Regular().Padding(0, 1)
+}
+
+// Border returns a style with a normal border
+func Border() lipgloss.Style {
+ t := theme.CurrentTheme()
+ return Regular().
+ Border(lipgloss.NormalBorder()).
+ BorderForeground(t.BorderNormal())
+}
+
+// ThickBorder returns a style with a thick border
+func ThickBorder() lipgloss.Style {
+ t := theme.CurrentTheme()
+ return Regular().
+ Border(lipgloss.ThickBorder()).
+ BorderForeground(t.BorderNormal())
+}
+
+// DoubleBorder returns a style with a double border
+func DoubleBorder() lipgloss.Style {
+ t := theme.CurrentTheme()
+ return Regular().
+ Border(lipgloss.DoubleBorder()).
+ BorderForeground(t.BorderNormal())
+}
+
+// FocusedBorder returns a style with a border using the focused border color
+func FocusedBorder() lipgloss.Style {
+ t := theme.CurrentTheme()
+ return Regular().
+ Border(lipgloss.NormalBorder()).
+ BorderForeground(t.BorderFocused())
+}
+
+// DimBorder returns a style with a border using the dim border color
+func DimBorder() lipgloss.Style {
+ t := theme.CurrentTheme()
+ return Regular().
+ Border(lipgloss.NormalBorder()).
+ BorderForeground(t.BorderDim())
+}
+
+// PrimaryColor returns the primary color from the current theme
+func PrimaryColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Primary()
+}
+
+// SecondaryColor returns the secondary color from the current theme
+func SecondaryColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Secondary()
+}
+
+// AccentColor returns the accent color from the current theme
+func AccentColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Accent()
+}
+
+// ErrorColor returns the error color from the current theme
+func ErrorColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Error()
+}
+
+// WarningColor returns the warning color from the current theme
+func WarningColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Warning()
+}
+
+// SuccessColor returns the success color from the current theme
+func SuccessColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Success()
+}
+
+// InfoColor returns the info color from the current theme
+func InfoColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Info()
+}
+
+// TextColor returns the text color from the current theme
+func TextColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Text()
+}
+
+// TextMutedColor returns the muted text color from the current theme
+func TextMutedColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().TextMuted()
+}
+
+// TextEmphasizedColor returns the emphasized text color from the current theme
+func TextEmphasizedColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().TextEmphasized()
+}
+
+// BackgroundColor returns the background color from the current theme
+func BackgroundColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().Background()
+}
+
+// BackgroundSecondaryColor returns the secondary background color from the current theme
+func BackgroundSecondaryColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().BackgroundSecondary()
+}
+
+// BackgroundDarkerColor returns the darker background color from the current theme
+func BackgroundDarkerColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().BackgroundDarker()
+}
+
+// BorderNormalColor returns the normal border color from the current theme
+func BorderNormalColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().BorderNormal()
+}
+
+// BorderFocusedColor returns the focused border color from the current theme
+func BorderFocusedColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().BorderFocused()
+}
+
+// BorderDimColor returns the dim border color from the current theme
+func BorderDimColor() lipgloss.AdaptiveColor {
+ return theme.CurrentTheme().BorderDim()
+}
diff --git a/packages/tui/internal/tui/theme/ayu.go b/packages/tui/internal/tui/theme/ayu.go
new file mode 100644
index 000000000..b3dfa870a
--- /dev/null
+++ b/packages/tui/internal/tui/theme/ayu.go
@@ -0,0 +1,280 @@
+package theme
+
+import (
+ "github.com/charmbracelet/lipgloss"
+)
+
+// AyuDarkTheme implements the Theme interface with Ayu Dark colors.
+type AyuDarkTheme struct {
+ BaseTheme
+}
+
+// AyuLightTheme implements the Theme interface with Ayu Light colors.
+type AyuLightTheme struct {
+ BaseTheme
+}
+
+// AyuMirageTheme implements the Theme interface with Ayu Mirage colors.
+type AyuMirageTheme struct {
+ BaseTheme
+}
+
+// NewAyuDarkTheme creates a new instance of the Ayu Dark theme.
+func NewAyuDarkTheme() *AyuDarkTheme {
+ // Ayu Dark color palette
+ darkBackground := "#0f1419"
+ darkCurrentLine := "#191f26"
+ darkSelection := "#253340"
+ darkForeground := "#b3b1ad"
+ darkComment := "#5c6773"
+ darkBlue := "#53bdfa"
+ darkCyan := "#90e1c6"
+ darkGreen := "#91b362"
+ darkOrange := "#f9af4f"
+ darkPurple := "#fae994"
+ darkRed := "#ea6c73"
+ darkBorder := "#253340"
+
+ // Light mode approximation for terminal compatibility
+ lightBackground := "#fafafa"
+ lightCurrentLine := "#f0f0f0"
+ lightSelection := "#d1d1d1"
+ lightForeground := "#5c6773"
+ lightComment := "#828c99"
+ lightBlue := "#3199e1"
+ lightCyan := "#46ba94"
+ lightGreen := "#7c9f32"
+ lightOrange := "#f29718"
+ lightPurple := "#9e75c7"
+ lightRed := "#f07171"
+ lightBorder := "#d1d1d1"
+
+ theme := &AyuDarkTheme{}
+
+ // Base colors
+ theme.PrimaryColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.SecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.AccentColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+
+ // Status colors
+ theme.ErrorColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.WarningColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.SuccessColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.InfoColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+
+ // Text colors
+ theme.TextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.TextMutedColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+
+ // Background colors
+ theme.BackgroundColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkCurrentLine,
+ Light: lightCurrentLine,
+ }
+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
+ Dark: "#0b0e14", // Darker than background
+ Light: "#ffffff", // Lighter than background
+ }
+
+ // Border colors
+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
+ Dark: darkBorder,
+ Light: lightBorder,
+ }
+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.BorderDimColor = lipgloss.AdaptiveColor{
+ Dark: darkSelection,
+ Light: lightSelection,
+ }
+
+ // Diff view colors
+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.DiffContextColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
+ Dark: "#91b362",
+ Light: "#a5d6a7",
+ }
+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
+ Dark: "#ea6c73",
+ Light: "#ef9a9a",
+ }
+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#1f2c1f",
+ Light: "#e8f5e9",
+ }
+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#2c1f1f",
+ Light: "#ffebee",
+ }
+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#1a261a",
+ Light: "#c8e6c9",
+ }
+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#261a1a",
+ Light: "#ffcdd2",
+ }
+
+ // Markdown colors
+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ // Syntax highlighting colors
+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ return theme
+}
+
+func init() {
+ // Register all three Ayu theme variants with the theme manager
+ RegisterTheme("ayu", NewAyuDarkTheme())
+}
diff --git a/packages/tui/internal/tui/theme/catppuccin.go b/packages/tui/internal/tui/theme/catppuccin.go
new file mode 100644
index 000000000..c3c32501e
--- /dev/null
+++ b/packages/tui/internal/tui/theme/catppuccin.go
@@ -0,0 +1,248 @@
+package theme
+
+import (
+ catppuccin "github.com/catppuccin/go"
+ "github.com/charmbracelet/lipgloss"
+)
+
+// CatppuccinTheme implements the Theme interface with Catppuccin colors.
+// It provides both dark (Mocha) and light (Latte) variants.
+type CatppuccinTheme struct {
+ BaseTheme
+}
+
+// NewCatppuccinTheme creates a new instance of the Catppuccin theme.
+func NewCatppuccinTheme() *CatppuccinTheme {
+ // Get the Catppuccin palettes
+ mocha := catppuccin.Mocha
+ latte := catppuccin.Latte
+
+ theme := &CatppuccinTheme{}
+
+ // Base colors
+ theme.PrimaryColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Blue().Hex,
+ Light: latte.Blue().Hex,
+ }
+ theme.SecondaryColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Mauve().Hex,
+ Light: latte.Mauve().Hex,
+ }
+ theme.AccentColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Peach().Hex,
+ Light: latte.Peach().Hex,
+ }
+
+ // Status colors
+ theme.ErrorColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Red().Hex,
+ Light: latte.Red().Hex,
+ }
+ theme.WarningColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Peach().Hex,
+ Light: latte.Peach().Hex,
+ }
+ theme.SuccessColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Green().Hex,
+ Light: latte.Green().Hex,
+ }
+ theme.InfoColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Blue().Hex,
+ Light: latte.Blue().Hex,
+ }
+
+ // Text colors
+ theme.TextColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Text().Hex,
+ Light: latte.Text().Hex,
+ }
+ theme.TextMutedColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Subtext0().Hex,
+ Light: latte.Subtext0().Hex,
+ }
+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Lavender().Hex,
+ Light: latte.Lavender().Hex,
+ }
+
+ // Background colors
+ theme.BackgroundColor = lipgloss.AdaptiveColor{
+ Dark: "#212121", // From existing styles
+ Light: "#EEEEEE", // Light equivalent
+ }
+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
+ Dark: "#2c2c2c", // From existing styles
+ Light: "#E0E0E0", // Light equivalent
+ }
+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
+ Dark: "#181818", // From existing styles
+ Light: "#F5F5F5", // Light equivalent
+ }
+
+ // Border colors
+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
+ Dark: "#4b4c5c", // From existing styles
+ Light: "#BDBDBD", // Light equivalent
+ }
+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Blue().Hex,
+ Light: latte.Blue().Hex,
+ }
+ theme.BorderDimColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Surface0().Hex,
+ Light: latte.Surface0().Hex,
+ }
+
+ // Diff view colors
+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
+ Dark: "#478247", // From existing diff.go
+ Light: "#2E7D32", // Light equivalent
+ }
+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
+ Dark: "#7C4444", // From existing diff.go
+ Light: "#C62828", // Light equivalent
+ }
+ theme.DiffContextColor = lipgloss.AdaptiveColor{
+ Dark: "#a0a0a0", // From existing diff.go
+ Light: "#757575", // Light equivalent
+ }
+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
+ Dark: "#a0a0a0", // From existing diff.go
+ Light: "#757575", // Light equivalent
+ }
+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
+ Dark: "#DAFADA", // From existing diff.go
+ Light: "#A5D6A7", // Light equivalent
+ }
+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
+ Dark: "#FADADD", // From existing diff.go
+ Light: "#EF9A9A", // Light equivalent
+ }
+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#303A30", // From existing diff.go
+ Light: "#E8F5E9", // Light equivalent
+ }
+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#3A3030", // From existing diff.go
+ Light: "#FFEBEE", // Light equivalent
+ }
+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
+ Dark: "#212121", // From existing diff.go
+ Light: "#F5F5F5", // Light equivalent
+ }
+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
+ Dark: "#888888", // From existing diff.go
+ Light: "#9E9E9E", // Light equivalent
+ }
+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#293229", // From existing diff.go
+ Light: "#C8E6C9", // Light equivalent
+ }
+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#332929", // From existing diff.go
+ Light: "#FFCDD2", // Light equivalent
+ }
+
+ // Markdown colors
+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Text().Hex,
+ Light: latte.Text().Hex,
+ }
+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Mauve().Hex,
+ Light: latte.Mauve().Hex,
+ }
+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Sky().Hex,
+ Light: latte.Sky().Hex,
+ }
+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Pink().Hex,
+ Light: latte.Pink().Hex,
+ }
+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Green().Hex,
+ Light: latte.Green().Hex,
+ }
+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Yellow().Hex,
+ Light: latte.Yellow().Hex,
+ }
+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Yellow().Hex,
+ Light: latte.Yellow().Hex,
+ }
+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Peach().Hex,
+ Light: latte.Peach().Hex,
+ }
+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Overlay0().Hex,
+ Light: latte.Overlay0().Hex,
+ }
+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Blue().Hex,
+ Light: latte.Blue().Hex,
+ }
+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Sky().Hex,
+ Light: latte.Sky().Hex,
+ }
+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Sapphire().Hex,
+ Light: latte.Sapphire().Hex,
+ }
+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Pink().Hex,
+ Light: latte.Pink().Hex,
+ }
+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Text().Hex,
+ Light: latte.Text().Hex,
+ }
+
+ // Syntax highlighting colors
+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Overlay1().Hex,
+ Light: latte.Overlay1().Hex,
+ }
+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Pink().Hex,
+ Light: latte.Pink().Hex,
+ }
+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Green().Hex,
+ Light: latte.Green().Hex,
+ }
+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Sky().Hex,
+ Light: latte.Sky().Hex,
+ }
+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Yellow().Hex,
+ Light: latte.Yellow().Hex,
+ }
+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Teal().Hex,
+ Light: latte.Teal().Hex,
+ }
+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Sky().Hex,
+ Light: latte.Sky().Hex,
+ }
+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Pink().Hex,
+ Light: latte.Pink().Hex,
+ }
+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
+ Dark: mocha.Text().Hex,
+ Light: latte.Text().Hex,
+ }
+
+ return theme
+}
+
+func init() {
+ // Register the Catppuccin theme with the theme manager
+ RegisterTheme("catppuccin", NewCatppuccinTheme())
+}
diff --git a/packages/tui/internal/tui/theme/dracula.go b/packages/tui/internal/tui/theme/dracula.go
new file mode 100644
index 000000000..29a1457d4
--- /dev/null
+++ b/packages/tui/internal/tui/theme/dracula.go
@@ -0,0 +1,274 @@
+package theme
+
+import (
+ "github.com/charmbracelet/lipgloss"
+)
+
+// DraculaTheme implements the Theme interface with Dracula colors.
+// It provides both dark and light variants, though Dracula is primarily a dark theme.
+type DraculaTheme struct {
+ BaseTheme
+}
+
+// NewDraculaTheme creates a new instance of the Dracula theme.
+func NewDraculaTheme() *DraculaTheme {
+ // Dracula color palette
+ // Official colors from https://draculatheme.com/
+ darkBackground := "#282a36"
+ darkCurrentLine := "#44475a"
+ darkSelection := "#44475a"
+ darkForeground := "#f8f8f2"
+ darkComment := "#6272a4"
+ darkCyan := "#8be9fd"
+ darkGreen := "#50fa7b"
+ darkOrange := "#ffb86c"
+ darkPink := "#ff79c6"
+ darkPurple := "#bd93f9"
+ darkRed := "#ff5555"
+ darkYellow := "#f1fa8c"
+ darkBorder := "#44475a"
+
+ // Light mode approximation (Dracula is primarily a dark theme)
+ lightBackground := "#f8f8f2"
+ lightCurrentLine := "#e6e6e6"
+ lightSelection := "#d8d8d8"
+ lightForeground := "#282a36"
+ lightComment := "#6272a4"
+ lightCyan := "#0097a7"
+ lightGreen := "#388e3c"
+ lightOrange := "#f57c00"
+ lightPink := "#d81b60"
+ lightPurple := "#7e57c2"
+ lightRed := "#e53935"
+ lightYellow := "#fbc02d"
+ lightBorder := "#d8d8d8"
+
+ theme := &DraculaTheme{}
+
+ // Base colors
+ theme.PrimaryColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.SecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkPink,
+ Light: lightPink,
+ }
+ theme.AccentColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+
+ // Status colors
+ theme.ErrorColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.WarningColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.SuccessColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.InfoColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+
+ // Text colors
+ theme.TextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.TextMutedColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+
+ // Background colors
+ theme.BackgroundColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkCurrentLine,
+ Light: lightCurrentLine,
+ }
+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
+ Dark: "#21222c", // Slightly darker than background
+ Light: "#ffffff", // Slightly lighter than background
+ }
+
+ // Border colors
+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
+ Dark: darkBorder,
+ Light: lightBorder,
+ }
+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.BorderDimColor = lipgloss.AdaptiveColor{
+ Dark: darkSelection,
+ Light: lightSelection,
+ }
+
+ // Diff view colors
+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.DiffContextColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
+ Dark: "#50fa7b",
+ Light: "#a5d6a7",
+ }
+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
+ Dark: "#ff5555",
+ Light: "#ef9a9a",
+ }
+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#2c3b2c",
+ Light: "#e8f5e9",
+ }
+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#3b2c2c",
+ Light: "#ffebee",
+ }
+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#253025",
+ Light: "#c8e6c9",
+ }
+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#302525",
+ Light: "#ffcdd2",
+ }
+
+ // Markdown colors
+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
+ Dark: darkPink,
+ Light: lightPink,
+ }
+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ // Syntax highlighting colors
+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
+ Dark: darkPink,
+ Light: lightPink,
+ }
+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
+ Dark: darkPink,
+ Light: lightPink,
+ }
+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ return theme
+}
+
+func init() {
+ // Register the Dracula theme with the theme manager
+ RegisterTheme("dracula", NewDraculaTheme())
+}
diff --git a/packages/tui/internal/tui/theme/flexoki.go b/packages/tui/internal/tui/theme/flexoki.go
new file mode 100644
index 000000000..5da5683c5
--- /dev/null
+++ b/packages/tui/internal/tui/theme/flexoki.go
@@ -0,0 +1,282 @@
+package theme
+
+import (
+ "github.com/charmbracelet/lipgloss"
+)
+
+// Flexoki color palette constants
+const (
+ // Base colors
+ flexokiPaper = "#FFFCF0" // Paper (lightest)
+ flexokiBase50 = "#F2F0E5" // bg-2 (light)
+ flexokiBase100 = "#E6E4D9" // ui (light)
+ flexokiBase150 = "#DAD8CE" // ui-2 (light)
+ flexokiBase200 = "#CECDC3" // ui-3 (light)
+ flexokiBase300 = "#B7B5AC" // tx-3 (light)
+ flexokiBase500 = "#878580" // tx-2 (light)
+ flexokiBase600 = "#6F6E69" // tx (light)
+ flexokiBase700 = "#575653" // tx-3 (dark)
+ flexokiBase800 = "#403E3C" // ui-3 (dark)
+ flexokiBase850 = "#343331" // ui-2 (dark)
+ flexokiBase900 = "#282726" // ui (dark)
+ flexokiBase950 = "#1C1B1A" // bg-2 (dark)
+ flexokiBlack = "#100F0F" // bg (darkest)
+
+ // Accent colors - Light theme (600)
+ flexokiRed600 = "#AF3029"
+ flexokiOrange600 = "#BC5215"
+ flexokiYellow600 = "#AD8301"
+ flexokiGreen600 = "#66800B"
+ flexokiCyan600 = "#24837B"
+ flexokiBlue600 = "#205EA6"
+ flexokiPurple600 = "#5E409D"
+ flexokiMagenta600 = "#A02F6F"
+
+ // Accent colors - Dark theme (400)
+ flexokiRed400 = "#D14D41"
+ flexokiOrange400 = "#DA702C"
+ flexokiYellow400 = "#D0A215"
+ flexokiGreen400 = "#879A39"
+ flexokiCyan400 = "#3AA99F"
+ flexokiBlue400 = "#4385BE"
+ flexokiPurple400 = "#8B7EC8"
+ flexokiMagenta400 = "#CE5D97"
+)
+
+// FlexokiTheme implements the Theme interface with Flexoki colors.
+// It provides both dark and light variants.
+type FlexokiTheme struct {
+ BaseTheme
+}
+
+// NewFlexokiTheme creates a new instance of the Flexoki theme.
+func NewFlexokiTheme() *FlexokiTheme {
+ theme := &FlexokiTheme{}
+
+ // Base colors
+ theme.PrimaryColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBlue400,
+ Light: flexokiBlue600,
+ }
+ theme.SecondaryColor = lipgloss.AdaptiveColor{
+ Dark: flexokiPurple400,
+ Light: flexokiPurple600,
+ }
+ theme.AccentColor = lipgloss.AdaptiveColor{
+ Dark: flexokiOrange400,
+ Light: flexokiOrange600,
+ }
+
+ // Status colors
+ theme.ErrorColor = lipgloss.AdaptiveColor{
+ Dark: flexokiRed400,
+ Light: flexokiRed600,
+ }
+ theme.WarningColor = lipgloss.AdaptiveColor{
+ Dark: flexokiYellow400,
+ Light: flexokiYellow600,
+ }
+ theme.SuccessColor = lipgloss.AdaptiveColor{
+ Dark: flexokiGreen400,
+ Light: flexokiGreen600,
+ }
+ theme.InfoColor = lipgloss.AdaptiveColor{
+ Dark: flexokiCyan400,
+ Light: flexokiCyan600,
+ }
+
+ // Text colors
+ theme.TextColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBase300,
+ Light: flexokiBase600,
+ }
+ theme.TextMutedColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBase700,
+ Light: flexokiBase500,
+ }
+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
+ Dark: flexokiYellow400,
+ Light: flexokiYellow600,
+ }
+
+ // Background colors
+ theme.BackgroundColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBlack,
+ Light: flexokiPaper,
+ }
+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBase950,
+ Light: flexokiBase50,
+ }
+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBase900,
+ Light: flexokiBase100,
+ }
+
+ // Border colors
+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBase900,
+ Light: flexokiBase100,
+ }
+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBlue400,
+ Light: flexokiBlue600,
+ }
+ theme.BorderDimColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBase850,
+ Light: flexokiBase150,
+ }
+
+ // Diff view colors
+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
+ Dark: flexokiGreen400,
+ Light: flexokiGreen600,
+ }
+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
+ Dark: flexokiRed400,
+ Light: flexokiRed600,
+ }
+ theme.DiffContextColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBase700,
+ Light: flexokiBase500,
+ }
+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBase700,
+ Light: flexokiBase500,
+ }
+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
+ Dark: flexokiGreen400,
+ Light: flexokiGreen600,
+ }
+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
+ Dark: flexokiRed400,
+ Light: flexokiRed600,
+ }
+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#1D2419", // Darker green background
+ Light: "#EFF2E2", // Light green background
+ }
+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#241919", // Darker red background
+ Light: "#F2E2E2", // Light red background
+ }
+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBlack,
+ Light: flexokiPaper,
+ }
+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBase700,
+ Light: flexokiBase500,
+ }
+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#1A2017", // Slightly darker green
+ Light: "#E5EBD9", // Light green
+ }
+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#201717", // Slightly darker red
+ Light: "#EBD9D9", // Light red
+ }
+
+ // Markdown colors
+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBase300,
+ Light: flexokiBase600,
+ }
+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
+ Dark: flexokiYellow400,
+ Light: flexokiYellow600,
+ }
+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
+ Dark: flexokiCyan400,
+ Light: flexokiCyan600,
+ }
+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
+ Dark: flexokiMagenta400,
+ Light: flexokiMagenta600,
+ }
+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
+ Dark: flexokiGreen400,
+ Light: flexokiGreen600,
+ }
+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
+ Dark: flexokiCyan400,
+ Light: flexokiCyan600,
+ }
+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
+ Dark: flexokiYellow400,
+ Light: flexokiYellow600,
+ }
+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
+ Dark: flexokiOrange400,
+ Light: flexokiOrange600,
+ }
+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBase800,
+ Light: flexokiBase200,
+ }
+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBlue400,
+ Light: flexokiBlue600,
+ }
+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBlue400,
+ Light: flexokiBlue600,
+ }
+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
+ Dark: flexokiPurple400,
+ Light: flexokiPurple600,
+ }
+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
+ Dark: flexokiMagenta400,
+ Light: flexokiMagenta600,
+ }
+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBase300,
+ Light: flexokiBase600,
+ }
+
+ // Syntax highlighting colors (based on Flexoki's mappings)
+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBase700, // tx-3
+ Light: flexokiBase300, // tx-3
+ }
+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
+ Dark: flexokiGreen400, // gr
+ Light: flexokiGreen600, // gr
+ }
+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
+ Dark: flexokiOrange400, // or
+ Light: flexokiOrange600, // or
+ }
+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBlue400, // bl
+ Light: flexokiBlue600, // bl
+ }
+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
+ Dark: flexokiCyan400, // cy
+ Light: flexokiCyan600, // cy
+ }
+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
+ Dark: flexokiPurple400, // pu
+ Light: flexokiPurple600, // pu
+ }
+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
+ Dark: flexokiYellow400, // ye
+ Light: flexokiYellow600, // ye
+ }
+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBase500, // tx-2
+ Light: flexokiBase500, // tx-2
+ }
+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
+ Dark: flexokiBase500, // tx-2
+ Light: flexokiBase500, // tx-2
+ }
+
+ return theme
+}
+
+func init() {
+ // Register the Flexoki theme with the theme manager
+ RegisterTheme("flexoki", NewFlexokiTheme())
+}
diff --git a/packages/tui/internal/tui/theme/gruvbox.go b/packages/tui/internal/tui/theme/gruvbox.go
new file mode 100644
index 000000000..51719faaa
--- /dev/null
+++ b/packages/tui/internal/tui/theme/gruvbox.go
@@ -0,0 +1,302 @@
+package theme
+
+import (
+ "github.com/charmbracelet/lipgloss"
+)
+
+// Gruvbox color palette constants
+const (
+ // Dark theme colors
+ gruvboxDarkBg0 = "#282828"
+ gruvboxDarkBg0Soft = "#32302f"
+ gruvboxDarkBg1 = "#3c3836"
+ gruvboxDarkBg2 = "#504945"
+ gruvboxDarkBg3 = "#665c54"
+ gruvboxDarkBg4 = "#7c6f64"
+ gruvboxDarkFg0 = "#fbf1c7"
+ gruvboxDarkFg1 = "#ebdbb2"
+ gruvboxDarkFg2 = "#d5c4a1"
+ gruvboxDarkFg3 = "#bdae93"
+ gruvboxDarkFg4 = "#a89984"
+ gruvboxDarkGray = "#928374"
+ gruvboxDarkRed = "#cc241d"
+ gruvboxDarkRedBright = "#fb4934"
+ gruvboxDarkGreen = "#98971a"
+ gruvboxDarkGreenBright = "#b8bb26"
+ gruvboxDarkYellow = "#d79921"
+ gruvboxDarkYellowBright = "#fabd2f"
+ gruvboxDarkBlue = "#458588"
+ gruvboxDarkBlueBright = "#83a598"
+ gruvboxDarkPurple = "#b16286"
+ gruvboxDarkPurpleBright = "#d3869b"
+ gruvboxDarkAqua = "#689d6a"
+ gruvboxDarkAquaBright = "#8ec07c"
+ gruvboxDarkOrange = "#d65d0e"
+ gruvboxDarkOrangeBright = "#fe8019"
+
+ // Light theme colors
+ gruvboxLightBg0 = "#fbf1c7"
+ gruvboxLightBg0Soft = "#f2e5bc"
+ gruvboxLightBg1 = "#ebdbb2"
+ gruvboxLightBg2 = "#d5c4a1"
+ gruvboxLightBg3 = "#bdae93"
+ gruvboxLightBg4 = "#a89984"
+ gruvboxLightFg0 = "#282828"
+ gruvboxLightFg1 = "#3c3836"
+ gruvboxLightFg2 = "#504945"
+ gruvboxLightFg3 = "#665c54"
+ gruvboxLightFg4 = "#7c6f64"
+ gruvboxLightGray = "#928374"
+ gruvboxLightRed = "#9d0006"
+ gruvboxLightRedBright = "#cc241d"
+ gruvboxLightGreen = "#79740e"
+ gruvboxLightGreenBright = "#98971a"
+ gruvboxLightYellow = "#b57614"
+ gruvboxLightYellowBright = "#d79921"
+ gruvboxLightBlue = "#076678"
+ gruvboxLightBlueBright = "#458588"
+ gruvboxLightPurple = "#8f3f71"
+ gruvboxLightPurpleBright = "#b16286"
+ gruvboxLightAqua = "#427b58"
+ gruvboxLightAquaBright = "#689d6a"
+ gruvboxLightOrange = "#af3a03"
+ gruvboxLightOrangeBright = "#d65d0e"
+)
+
+// GruvboxTheme implements the Theme interface with Gruvbox colors.
+// It provides both dark and light variants.
+type GruvboxTheme struct {
+ BaseTheme
+}
+
+// NewGruvboxTheme creates a new instance of the Gruvbox theme.
+func NewGruvboxTheme() *GruvboxTheme {
+ theme := &GruvboxTheme{}
+
+ // Base colors
+ theme.PrimaryColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkBlueBright,
+ Light: gruvboxLightBlueBright,
+ }
+ theme.SecondaryColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkPurpleBright,
+ Light: gruvboxLightPurpleBright,
+ }
+ theme.AccentColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkOrangeBright,
+ Light: gruvboxLightOrangeBright,
+ }
+
+ // Status colors
+ theme.ErrorColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkRedBright,
+ Light: gruvboxLightRedBright,
+ }
+ theme.WarningColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkYellowBright,
+ Light: gruvboxLightYellowBright,
+ }
+ theme.SuccessColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkGreenBright,
+ Light: gruvboxLightGreenBright,
+ }
+ theme.InfoColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkBlueBright,
+ Light: gruvboxLightBlueBright,
+ }
+
+ // Text colors
+ theme.TextColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkFg1,
+ Light: gruvboxLightFg1,
+ }
+ theme.TextMutedColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkFg4,
+ Light: gruvboxLightFg4,
+ }
+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkYellowBright,
+ Light: gruvboxLightYellowBright,
+ }
+
+ // Background colors
+ theme.BackgroundColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkBg0,
+ Light: gruvboxLightBg0,
+ }
+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkBg1,
+ Light: gruvboxLightBg1,
+ }
+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkBg0Soft,
+ Light: gruvboxLightBg0Soft,
+ }
+
+ // Border colors
+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkBg2,
+ Light: gruvboxLightBg2,
+ }
+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkBlueBright,
+ Light: gruvboxLightBlueBright,
+ }
+ theme.BorderDimColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkBg1,
+ Light: gruvboxLightBg1,
+ }
+
+ // Diff view colors
+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkGreenBright,
+ Light: gruvboxLightGreenBright,
+ }
+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkRedBright,
+ Light: gruvboxLightRedBright,
+ }
+ theme.DiffContextColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkFg4,
+ Light: gruvboxLightFg4,
+ }
+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkFg3,
+ Light: gruvboxLightFg3,
+ }
+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkGreenBright,
+ Light: gruvboxLightGreenBright,
+ }
+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkRedBright,
+ Light: gruvboxLightRedBright,
+ }
+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#3C4C3C", // Darker green background
+ Light: "#E8F5E9", // Light green background
+ }
+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#4C3C3C", // Darker red background
+ Light: "#FFEBEE", // Light red background
+ }
+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkBg0,
+ Light: gruvboxLightBg0,
+ }
+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkFg4,
+ Light: gruvboxLightFg4,
+ }
+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#32432F", // Slightly darker green
+ Light: "#C8E6C9", // Light green
+ }
+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#43322F", // Slightly darker red
+ Light: "#FFCDD2", // Light red
+ }
+
+ // Markdown colors
+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkFg1,
+ Light: gruvboxLightFg1,
+ }
+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkYellowBright,
+ Light: gruvboxLightYellowBright,
+ }
+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkBlueBright,
+ Light: gruvboxLightBlueBright,
+ }
+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkAquaBright,
+ Light: gruvboxLightAquaBright,
+ }
+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkGreenBright,
+ Light: gruvboxLightGreenBright,
+ }
+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkAquaBright,
+ Light: gruvboxLightAquaBright,
+ }
+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkYellowBright,
+ Light: gruvboxLightYellowBright,
+ }
+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkOrangeBright,
+ Light: gruvboxLightOrangeBright,
+ }
+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkBg3,
+ Light: gruvboxLightBg3,
+ }
+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkBlueBright,
+ Light: gruvboxLightBlueBright,
+ }
+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkBlueBright,
+ Light: gruvboxLightBlueBright,
+ }
+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkPurpleBright,
+ Light: gruvboxLightPurpleBright,
+ }
+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkAquaBright,
+ Light: gruvboxLightAquaBright,
+ }
+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkFg1,
+ Light: gruvboxLightFg1,
+ }
+
+ // Syntax highlighting colors
+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkGray,
+ Light: gruvboxLightGray,
+ }
+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkRedBright,
+ Light: gruvboxLightRedBright,
+ }
+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkGreenBright,
+ Light: gruvboxLightGreenBright,
+ }
+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkBlueBright,
+ Light: gruvboxLightBlueBright,
+ }
+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkYellowBright,
+ Light: gruvboxLightYellowBright,
+ }
+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkPurpleBright,
+ Light: gruvboxLightPurpleBright,
+ }
+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkYellow,
+ Light: gruvboxLightYellow,
+ }
+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkAquaBright,
+ Light: gruvboxLightAquaBright,
+ }
+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
+ Dark: gruvboxDarkFg1,
+ Light: gruvboxLightFg1,
+ }
+
+ return theme
+}
+
+func init() {
+ // Register the Gruvbox theme with the theme manager
+ RegisterTheme("gruvbox", NewGruvboxTheme())
+}
diff --git a/packages/tui/internal/tui/theme/manager.go b/packages/tui/internal/tui/theme/manager.go
new file mode 100644
index 000000000..5a5c791fb
--- /dev/null
+++ b/packages/tui/internal/tui/theme/manager.go
@@ -0,0 +1,265 @@
+package theme
+
+import (
+ "fmt"
+ "log/slog"
+ "slices"
+ "strings"
+ "sync"
+
+ "github.com/alecthomas/chroma/v2/styles"
+ "github.com/sst/opencode/internal/config"
+)
+
+// Manager handles theme registration, selection, and retrieval.
+// It maintains a registry of available themes and tracks the currently active theme.
+type Manager struct {
+ themes map[string]Theme
+ currentName string
+ mu sync.RWMutex
+}
+
+// Global instance of the theme manager
+var globalManager = &Manager{
+ themes: make(map[string]Theme),
+ currentName: "",
+}
+
+// Default theme instance for custom theme defaulting
+var defaultThemeColors = NewOpenCodeTheme()
+
+// RegisterTheme adds a new theme to the registry.
+// If this is the first theme registered, it becomes the default.
+func RegisterTheme(name string, theme Theme) {
+ globalManager.mu.Lock()
+ defer globalManager.mu.Unlock()
+
+ globalManager.themes[name] = theme
+
+ // If this is the first theme, make it the default
+ if globalManager.currentName == "" {
+ globalManager.currentName = name
+ }
+}
+
+// SetTheme changes the active theme to the one with the specified name.
+// Returns an error if the theme doesn't exist.
+func SetTheme(name string) error {
+ globalManager.mu.Lock()
+ defer globalManager.mu.Unlock()
+
+ delete(styles.Registry, "charm")
+
+ // Handle custom theme
+ if name == "custom" {
+ cfg := config.Get()
+ if cfg == nil || cfg.TUI.CustomTheme == nil || len(cfg.TUI.CustomTheme) == 0 {
+ return fmt.Errorf("custom theme selected but no custom theme colors defined in config")
+ }
+
+ customTheme, err := LoadCustomTheme(cfg.TUI.CustomTheme)
+ if err != nil {
+ return fmt.Errorf("failed to load custom theme: %w", err)
+ }
+
+ // Register the custom theme
+ globalManager.themes["custom"] = customTheme
+ } else if _, exists := globalManager.themes[name]; !exists {
+ return fmt.Errorf("theme '%s' not found", name)
+ }
+
+ globalManager.currentName = name
+
+ // Update the config file using viper
+ if err := updateConfigTheme(name); err != nil {
+ // Log the error but don't fail the theme change
+ slog.Warn("Warning: Failed to update config file with new theme", "err", err)
+ }
+
+ return nil
+}
+
+// CurrentTheme returns the currently active theme.
+// If no theme is set, it returns nil.
+func CurrentTheme() Theme {
+ globalManager.mu.RLock()
+ defer globalManager.mu.RUnlock()
+
+ if globalManager.currentName == "" {
+ return nil
+ }
+
+ return globalManager.themes[globalManager.currentName]
+}
+
+// CurrentThemeName returns the name of the currently active theme.
+func CurrentThemeName() string {
+ globalManager.mu.RLock()
+ defer globalManager.mu.RUnlock()
+
+ return globalManager.currentName
+}
+
+// AvailableThemes returns a list of all registered theme names.
+func AvailableThemes() []string {
+ globalManager.mu.RLock()
+ defer globalManager.mu.RUnlock()
+
+ names := make([]string, 0, len(globalManager.themes))
+ for name := range globalManager.themes {
+ names = append(names, name)
+ }
+ slices.SortFunc(names, func(a, b string) int {
+ if a == "opencode" {
+ return -1
+ } else if b == "opencode" {
+ return 1
+ }
+ return strings.Compare(a, b)
+ })
+ return names
+}
+
+// GetTheme returns a specific theme by name.
+// Returns nil if the theme doesn't exist.
+func GetTheme(name string) Theme {
+ globalManager.mu.RLock()
+ defer globalManager.mu.RUnlock()
+
+ return globalManager.themes[name]
+}
+
+// LoadCustomTheme creates a new theme instance based on the custom theme colors
+// defined in the configuration. It uses the default OpenCode theme as a base
+// and overrides colors that are specified in the customTheme map.
+func LoadCustomTheme(customTheme map[string]any) (Theme, error) {
+ // Create a new theme based on the default OpenCode theme
+ theme := NewOpenCodeTheme()
+
+ // Process each color in the custom theme map
+ for key, value := range customTheme {
+ adaptiveColor, err := ParseAdaptiveColor(value)
+ if err != nil {
+ slog.Warn("Invalid color definition in custom theme", "key", key, "error", err)
+ continue // Skip this color but continue processing others
+ }
+
+ // Set the color in the theme based on the key
+ switch strings.ToLower(key) {
+ case "primary":
+ theme.PrimaryColor = adaptiveColor
+ case "secondary":
+ theme.SecondaryColor = adaptiveColor
+ case "accent":
+ theme.AccentColor = adaptiveColor
+ case "error":
+ theme.ErrorColor = adaptiveColor
+ case "warning":
+ theme.WarningColor = adaptiveColor
+ case "success":
+ theme.SuccessColor = adaptiveColor
+ case "info":
+ theme.InfoColor = adaptiveColor
+ case "text":
+ theme.TextColor = adaptiveColor
+ case "textmuted":
+ theme.TextMutedColor = adaptiveColor
+ case "textemphasized":
+ theme.TextEmphasizedColor = adaptiveColor
+ case "background":
+ theme.BackgroundColor = adaptiveColor
+ case "backgroundsecondary":
+ theme.BackgroundSecondaryColor = adaptiveColor
+ case "backgrounddarker":
+ theme.BackgroundDarkerColor = adaptiveColor
+ case "bordernormal":
+ theme.BorderNormalColor = adaptiveColor
+ case "borderfocused":
+ theme.BorderFocusedColor = adaptiveColor
+ case "borderdim":
+ theme.BorderDimColor = adaptiveColor
+ case "diffadded":
+ theme.DiffAddedColor = adaptiveColor
+ case "diffremoved":
+ theme.DiffRemovedColor = adaptiveColor
+ case "diffcontext":
+ theme.DiffContextColor = adaptiveColor
+ case "diffhunkheader":
+ theme.DiffHunkHeaderColor = adaptiveColor
+ case "diffhighlightadded":
+ theme.DiffHighlightAddedColor = adaptiveColor
+ case "diffhighlightremoved":
+ theme.DiffHighlightRemovedColor = adaptiveColor
+ case "diffaddedbg":
+ theme.DiffAddedBgColor = adaptiveColor
+ case "diffremovedbg":
+ theme.DiffRemovedBgColor = adaptiveColor
+ case "diffcontextbg":
+ theme.DiffContextBgColor = adaptiveColor
+ case "difflinenumber":
+ theme.DiffLineNumberColor = adaptiveColor
+ case "diffaddedlinenumberbg":
+ theme.DiffAddedLineNumberBgColor = adaptiveColor
+ case "diffremovedlinenumberbg":
+ theme.DiffRemovedLineNumberBgColor = adaptiveColor
+ case "syntaxcomment":
+ theme.SyntaxCommentColor = adaptiveColor
+ case "syntaxkeyword":
+ theme.SyntaxKeywordColor = adaptiveColor
+ case "syntaxfunction":
+ theme.SyntaxFunctionColor = adaptiveColor
+ case "syntaxvariable":
+ theme.SyntaxVariableColor = adaptiveColor
+ case "syntaxstring":
+ theme.SyntaxStringColor = adaptiveColor
+ case "syntaxnumber":
+ theme.SyntaxNumberColor = adaptiveColor
+ case "syntaxtype":
+ theme.SyntaxTypeColor = adaptiveColor
+ case "syntaxoperator":
+ theme.SyntaxOperatorColor = adaptiveColor
+ case "syntaxpunctuation":
+ theme.SyntaxPunctuationColor = adaptiveColor
+ case "markdowntext":
+ theme.MarkdownTextColor = adaptiveColor
+ case "markdownheading":
+ theme.MarkdownHeadingColor = adaptiveColor
+ case "markdownlink":
+ theme.MarkdownLinkColor = adaptiveColor
+ case "markdownlinktext":
+ theme.MarkdownLinkTextColor = adaptiveColor
+ case "markdowncode":
+ theme.MarkdownCodeColor = adaptiveColor
+ case "markdownblockquote":
+ theme.MarkdownBlockQuoteColor = adaptiveColor
+ case "markdownemph":
+ theme.MarkdownEmphColor = adaptiveColor
+ case "markdownstrong":
+ theme.MarkdownStrongColor = adaptiveColor
+ case "markdownhorizontalrule":
+ theme.MarkdownHorizontalRuleColor = adaptiveColor
+ case "markdownlistitem":
+ theme.MarkdownListItemColor = adaptiveColor
+ case "markdownlistitemenum":
+ theme.MarkdownListEnumerationColor = adaptiveColor
+ case "markdownimage":
+ theme.MarkdownImageColor = adaptiveColor
+ case "markdownimagetext":
+ theme.MarkdownImageTextColor = adaptiveColor
+ case "markdowncodeblock":
+ theme.MarkdownCodeBlockColor = adaptiveColor
+ case "markdownlistenumeration":
+ theme.MarkdownListEnumerationColor = adaptiveColor
+ default:
+ slog.Warn("Unknown color key in custom theme", "key", key)
+ }
+ }
+
+ return theme, nil
+}
+
+// updateConfigTheme updates the theme setting in the configuration file
+func updateConfigTheme(themeName string) error {
+ // Use the config package to update the theme
+ return config.UpdateTheme(themeName)
+}
diff --git a/packages/tui/internal/tui/theme/monokai.go b/packages/tui/internal/tui/theme/monokai.go
new file mode 100644
index 000000000..7511d3333
--- /dev/null
+++ b/packages/tui/internal/tui/theme/monokai.go
@@ -0,0 +1,273 @@
+package theme
+
+import (
+ "github.com/charmbracelet/lipgloss"
+)
+
+// MonokaiProTheme implements the Theme interface with Monokai Pro colors.
+// It provides both dark and light variants.
+type MonokaiProTheme struct {
+ BaseTheme
+}
+
+// NewMonokaiProTheme creates a new instance of the Monokai Pro theme.
+func NewMonokaiProTheme() *MonokaiProTheme {
+ // Monokai Pro color palette (dark mode)
+ darkBackground := "#2d2a2e"
+ darkCurrentLine := "#403e41"
+ darkSelection := "#5b595c"
+ darkForeground := "#fcfcfa"
+ darkComment := "#727072"
+ darkRed := "#ff6188"
+ darkOrange := "#fc9867"
+ darkYellow := "#ffd866"
+ darkGreen := "#a9dc76"
+ darkCyan := "#78dce8"
+ darkBlue := "#ab9df2"
+ darkPurple := "#ab9df2"
+ darkBorder := "#403e41"
+
+ // Light mode colors (adapted from dark)
+ lightBackground := "#fafafa"
+ lightCurrentLine := "#f0f0f0"
+ lightSelection := "#e5e5e6"
+ lightForeground := "#2d2a2e"
+ lightComment := "#939293"
+ lightRed := "#f92672"
+ lightOrange := "#fd971f"
+ lightYellow := "#e6db74"
+ lightGreen := "#9bca65"
+ lightCyan := "#66d9ef"
+ lightBlue := "#7e75db"
+ lightPurple := "#ae81ff"
+ lightBorder := "#d3d3d3"
+
+ theme := &MonokaiProTheme{}
+
+ // Base colors
+ theme.PrimaryColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.SecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.AccentColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+
+ // Status colors
+ theme.ErrorColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.WarningColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.SuccessColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.InfoColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+
+ // Text colors
+ theme.TextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.TextMutedColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+
+ // Background colors
+ theme.BackgroundColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkCurrentLine,
+ Light: lightCurrentLine,
+ }
+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
+ Dark: "#221f22", // Slightly darker than background
+ Light: "#ffffff", // Slightly lighter than background
+ }
+
+ // Border colors
+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
+ Dark: darkBorder,
+ Light: lightBorder,
+ }
+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.BorderDimColor = lipgloss.AdaptiveColor{
+ Dark: darkSelection,
+ Light: lightSelection,
+ }
+
+ // Diff view colors
+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
+ Dark: "#a9dc76",
+ Light: "#9bca65",
+ }
+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
+ Dark: "#ff6188",
+ Light: "#f92672",
+ }
+ theme.DiffContextColor = lipgloss.AdaptiveColor{
+ Dark: "#a0a0a0",
+ Light: "#757575",
+ }
+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
+ Dark: "#a0a0a0",
+ Light: "#757575",
+ }
+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
+ Dark: "#c2e7a9",
+ Light: "#c5e0b4",
+ }
+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
+ Dark: "#ff8ca6",
+ Light: "#ffb3c8",
+ }
+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#3a4a35",
+ Light: "#e8f5e9",
+ }
+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#4a3439",
+ Light: "#ffebee",
+ }
+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
+ Dark: "#888888",
+ Light: "#9e9e9e",
+ }
+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#2d3a28",
+ Light: "#c8e6c9",
+ }
+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#3d2a2e",
+ Light: "#ffcdd2",
+ }
+
+ // Markdown colors
+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ // Syntax highlighting colors
+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ return theme
+}
+
+func init() {
+ // Register the Monokai Pro theme with the theme manager
+ RegisterTheme("monokai", NewMonokaiProTheme())
+}
diff --git a/packages/tui/internal/tui/theme/onedark.go b/packages/tui/internal/tui/theme/onedark.go
new file mode 100644
index 000000000..a2c1447ca
--- /dev/null
+++ b/packages/tui/internal/tui/theme/onedark.go
@@ -0,0 +1,274 @@
+package theme
+
+import (
+ "github.com/charmbracelet/lipgloss"
+)
+
+// OneDarkTheme implements the Theme interface with Atom's One Dark colors.
+// It provides both dark and light variants.
+type OneDarkTheme struct {
+ BaseTheme
+}
+
+// NewOneDarkTheme creates a new instance of the One Dark theme.
+func NewOneDarkTheme() *OneDarkTheme {
+ // One Dark color palette
+ // Dark mode colors from Atom One Dark
+ darkBackground := "#282c34"
+ darkCurrentLine := "#2c313c"
+ darkSelection := "#3e4451"
+ darkForeground := "#abb2bf"
+ darkComment := "#5c6370"
+ darkRed := "#e06c75"
+ darkOrange := "#d19a66"
+ darkYellow := "#e5c07b"
+ darkGreen := "#98c379"
+ darkCyan := "#56b6c2"
+ darkBlue := "#61afef"
+ darkPurple := "#c678dd"
+ darkBorder := "#3b4048"
+
+ // Light mode colors from Atom One Light
+ lightBackground := "#fafafa"
+ lightCurrentLine := "#f0f0f0"
+ lightSelection := "#e5e5e6"
+ lightForeground := "#383a42"
+ lightComment := "#a0a1a7"
+ lightRed := "#e45649"
+ lightOrange := "#da8548"
+ lightYellow := "#c18401"
+ lightGreen := "#50a14f"
+ lightCyan := "#0184bc"
+ lightBlue := "#4078f2"
+ lightPurple := "#a626a4"
+ lightBorder := "#d3d3d3"
+
+ theme := &OneDarkTheme{}
+
+ // Base colors
+ theme.PrimaryColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.SecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.AccentColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+
+ // Status colors
+ theme.ErrorColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.WarningColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.SuccessColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.InfoColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+
+ // Text colors
+ theme.TextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.TextMutedColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+
+ // Background colors
+ theme.BackgroundColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkCurrentLine,
+ Light: lightCurrentLine,
+ }
+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
+ Dark: "#21252b", // Slightly darker than background
+ Light: "#ffffff", // Slightly lighter than background
+ }
+
+ // Border colors
+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
+ Dark: darkBorder,
+ Light: lightBorder,
+ }
+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.BorderDimColor = lipgloss.AdaptiveColor{
+ Dark: darkSelection,
+ Light: lightSelection,
+ }
+
+ // Diff view colors
+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
+ Dark: "#478247",
+ Light: "#2E7D32",
+ }
+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
+ Dark: "#7C4444",
+ Light: "#C62828",
+ }
+ theme.DiffContextColor = lipgloss.AdaptiveColor{
+ Dark: "#a0a0a0",
+ Light: "#757575",
+ }
+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
+ Dark: "#a0a0a0",
+ Light: "#757575",
+ }
+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
+ Dark: "#DAFADA",
+ Light: "#A5D6A7",
+ }
+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
+ Dark: "#FADADD",
+ Light: "#EF9A9A",
+ }
+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#303A30",
+ Light: "#E8F5E9",
+ }
+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#3A3030",
+ Light: "#FFEBEE",
+ }
+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
+ Dark: "#888888",
+ Light: "#9E9E9E",
+ }
+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#293229",
+ Light: "#C8E6C9",
+ }
+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#332929",
+ Light: "#FFCDD2",
+ }
+
+ // Markdown colors
+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ // Syntax highlighting colors
+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ return theme
+}
+
+func init() {
+ // Register the One Dark theme with the theme manager
+ RegisterTheme("onedark", NewOneDarkTheme())
+}
diff --git a/packages/tui/internal/tui/theme/opencode.go b/packages/tui/internal/tui/theme/opencode.go
new file mode 100644
index 000000000..7ee6f15e5
--- /dev/null
+++ b/packages/tui/internal/tui/theme/opencode.go
@@ -0,0 +1,276 @@
+package theme
+
+import (
+ "github.com/charmbracelet/lipgloss"
+)
+
+// OpenCodeTheme implements the Theme interface with OpenCode brand colors.
+// It provides both dark and light variants.
+type OpenCodeTheme struct {
+ BaseTheme
+}
+
+// NewOpenCodeTheme creates a new instance of the OpenCode theme.
+func NewOpenCodeTheme() *OpenCodeTheme {
+ // OpenCode color palette
+ // Dark mode colors
+ darkBackground := "#212121"
+ darkCurrentLine := "#252525"
+ darkSelection := "#303030"
+ darkForeground := "#e0e0e0"
+ darkComment := "#6a6a6a"
+ darkPrimary := "#fab283" // Primary orange/gold
+ darkSecondary := "#5c9cf5" // Secondary blue
+ darkAccent := "#9d7cd8" // Accent purple
+ darkRed := "#e06c75" // Error red
+ darkOrange := "#f5a742" // Warning orange
+ darkGreen := "#7fd88f" // Success green
+ darkCyan := "#56b6c2" // Info cyan
+ darkYellow := "#e5c07b" // Emphasized text
+ darkBorder := "#4b4c5c" // Border color
+
+ // Light mode colors
+ lightBackground := "#f8f8f8"
+ lightCurrentLine := "#f0f0f0"
+ lightSelection := "#e5e5e6"
+ lightForeground := "#2a2a2a"
+ lightComment := "#8a8a8a"
+ lightPrimary := "#3b7dd8" // Primary blue
+ lightSecondary := "#7b5bb6" // Secondary purple
+ lightAccent := "#d68c27" // Accent orange/gold
+ lightRed := "#d1383d" // Error red
+ lightOrange := "#d68c27" // Warning orange
+ lightGreen := "#3d9a57" // Success green
+ lightCyan := "#318795" // Info cyan
+ lightYellow := "#b0851f" // Emphasized text
+ lightBorder := "#d3d3d3" // Border color
+
+ theme := &OpenCodeTheme{}
+
+ // Base colors
+ theme.PrimaryColor = lipgloss.AdaptiveColor{
+ Dark: darkPrimary,
+ Light: lightPrimary,
+ }
+ theme.SecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkSecondary,
+ Light: lightSecondary,
+ }
+ theme.AccentColor = lipgloss.AdaptiveColor{
+ Dark: darkAccent,
+ Light: lightAccent,
+ }
+
+ // Status colors
+ theme.ErrorColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.WarningColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.SuccessColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.InfoColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+
+ // Text colors
+ theme.TextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.TextMutedColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+
+ // Background colors
+ theme.BackgroundColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkCurrentLine,
+ Light: lightCurrentLine,
+ }
+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
+ Dark: "#121212", // Slightly darker than background
+ Light: "#ffffff", // Slightly lighter than background
+ }
+
+ // Border colors
+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
+ Dark: darkBorder,
+ Light: lightBorder,
+ }
+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
+ Dark: darkPrimary,
+ Light: lightPrimary,
+ }
+ theme.BorderDimColor = lipgloss.AdaptiveColor{
+ Dark: darkSelection,
+ Light: lightSelection,
+ }
+
+ // Diff view colors
+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
+ Dark: "#478247",
+ Light: "#2E7D32",
+ }
+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
+ Dark: "#7C4444",
+ Light: "#C62828",
+ }
+ theme.DiffContextColor = lipgloss.AdaptiveColor{
+ Dark: "#a0a0a0",
+ Light: "#757575",
+ }
+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
+ Dark: "#a0a0a0",
+ Light: "#757575",
+ }
+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
+ Dark: "#DAFADA",
+ Light: "#A5D6A7",
+ }
+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
+ Dark: "#FADADD",
+ Light: "#EF9A9A",
+ }
+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#303A30",
+ Light: "#E8F5E9",
+ }
+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#3A3030",
+ Light: "#FFEBEE",
+ }
+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
+ Dark: "#888888",
+ Light: "#9E9E9E",
+ }
+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#293229",
+ Light: "#C8E6C9",
+ }
+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#332929",
+ Light: "#FFCDD2",
+ }
+
+ // Markdown colors
+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
+ Dark: darkSecondary,
+ Light: lightSecondary,
+ }
+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
+ Dark: darkPrimary,
+ Light: lightPrimary,
+ }
+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
+ Dark: darkAccent,
+ Light: lightAccent,
+ }
+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
+ Dark: darkPrimary,
+ Light: lightPrimary,
+ }
+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
+ Dark: darkPrimary,
+ Light: lightPrimary,
+ }
+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ // Syntax highlighting colors
+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
+ Dark: darkSecondary,
+ Light: lightSecondary,
+ }
+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
+ Dark: darkPrimary,
+ Light: lightPrimary,
+ }
+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
+ Dark: darkAccent,
+ Light: lightAccent,
+ }
+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ return theme
+}
+
+func init() {
+ // Register the OpenCode theme with the theme manager
+ RegisterTheme("opencode", NewOpenCodeTheme())
+}
diff --git a/packages/tui/internal/tui/theme/theme.go b/packages/tui/internal/tui/theme/theme.go
new file mode 100644
index 000000000..c97b95478
--- /dev/null
+++ b/packages/tui/internal/tui/theme/theme.go
@@ -0,0 +1,290 @@
+package theme
+
+import (
+ "fmt"
+ "regexp"
+
+ "github.com/charmbracelet/lipgloss"
+)
+
+// Theme defines the interface for all UI themes in the application.
+// All colors must be defined as lipgloss.AdaptiveColor to support
+// both light and dark terminal backgrounds.
+type Theme interface {
+ // Base colors
+ Primary() lipgloss.AdaptiveColor
+ Secondary() lipgloss.AdaptiveColor
+ Accent() lipgloss.AdaptiveColor
+
+ // Status colors
+ Error() lipgloss.AdaptiveColor
+ Warning() lipgloss.AdaptiveColor
+ Success() lipgloss.AdaptiveColor
+ Info() lipgloss.AdaptiveColor
+
+ // Text colors
+ Text() lipgloss.AdaptiveColor
+ TextMuted() lipgloss.AdaptiveColor
+ TextEmphasized() lipgloss.AdaptiveColor
+
+ // Background colors
+ Background() lipgloss.AdaptiveColor
+ BackgroundSecondary() lipgloss.AdaptiveColor
+ BackgroundDarker() lipgloss.AdaptiveColor
+
+ // Border colors
+ BorderNormal() lipgloss.AdaptiveColor
+ BorderFocused() lipgloss.AdaptiveColor
+ BorderDim() lipgloss.AdaptiveColor
+
+ // Diff view colors
+ DiffAdded() lipgloss.AdaptiveColor
+ DiffRemoved() lipgloss.AdaptiveColor
+ DiffContext() lipgloss.AdaptiveColor
+ DiffHunkHeader() lipgloss.AdaptiveColor
+ DiffHighlightAdded() lipgloss.AdaptiveColor
+ DiffHighlightRemoved() lipgloss.AdaptiveColor
+ DiffAddedBg() lipgloss.AdaptiveColor
+ DiffRemovedBg() lipgloss.AdaptiveColor
+ DiffContextBg() lipgloss.AdaptiveColor
+ DiffLineNumber() lipgloss.AdaptiveColor
+ DiffAddedLineNumberBg() lipgloss.AdaptiveColor
+ DiffRemovedLineNumberBg() lipgloss.AdaptiveColor
+
+ // Markdown colors
+ MarkdownText() lipgloss.AdaptiveColor
+ MarkdownHeading() lipgloss.AdaptiveColor
+ MarkdownLink() lipgloss.AdaptiveColor
+ MarkdownLinkText() lipgloss.AdaptiveColor
+ MarkdownCode() lipgloss.AdaptiveColor
+ MarkdownBlockQuote() lipgloss.AdaptiveColor
+ MarkdownEmph() lipgloss.AdaptiveColor
+ MarkdownStrong() lipgloss.AdaptiveColor
+ MarkdownHorizontalRule() lipgloss.AdaptiveColor
+ MarkdownListItem() lipgloss.AdaptiveColor
+ MarkdownListEnumeration() lipgloss.AdaptiveColor
+ MarkdownImage() lipgloss.AdaptiveColor
+ MarkdownImageText() lipgloss.AdaptiveColor
+ MarkdownCodeBlock() lipgloss.AdaptiveColor
+
+ // Syntax highlighting colors
+ SyntaxComment() lipgloss.AdaptiveColor
+ SyntaxKeyword() lipgloss.AdaptiveColor
+ SyntaxFunction() lipgloss.AdaptiveColor
+ SyntaxVariable() lipgloss.AdaptiveColor
+ SyntaxString() lipgloss.AdaptiveColor
+ SyntaxNumber() lipgloss.AdaptiveColor
+ SyntaxType() lipgloss.AdaptiveColor
+ SyntaxOperator() lipgloss.AdaptiveColor
+ SyntaxPunctuation() lipgloss.AdaptiveColor
+}
+
+// BaseTheme provides a default implementation of the Theme interface
+// that can be embedded in concrete theme implementations.
+type BaseTheme struct {
+ // Base colors
+ PrimaryColor lipgloss.AdaptiveColor
+ SecondaryColor lipgloss.AdaptiveColor
+ AccentColor lipgloss.AdaptiveColor
+
+ // Status colors
+ ErrorColor lipgloss.AdaptiveColor
+ WarningColor lipgloss.AdaptiveColor
+ SuccessColor lipgloss.AdaptiveColor
+ InfoColor lipgloss.AdaptiveColor
+
+ // Text colors
+ TextColor lipgloss.AdaptiveColor
+ TextMutedColor lipgloss.AdaptiveColor
+ TextEmphasizedColor lipgloss.AdaptiveColor
+
+ // Background colors
+ BackgroundColor lipgloss.AdaptiveColor
+ BackgroundSecondaryColor lipgloss.AdaptiveColor
+ BackgroundDarkerColor lipgloss.AdaptiveColor
+
+ // Border colors
+ BorderNormalColor lipgloss.AdaptiveColor
+ BorderFocusedColor lipgloss.AdaptiveColor
+ BorderDimColor lipgloss.AdaptiveColor
+
+ // Diff view colors
+ DiffAddedColor lipgloss.AdaptiveColor
+ DiffRemovedColor lipgloss.AdaptiveColor
+ DiffContextColor lipgloss.AdaptiveColor
+ DiffHunkHeaderColor lipgloss.AdaptiveColor
+ DiffHighlightAddedColor lipgloss.AdaptiveColor
+ DiffHighlightRemovedColor lipgloss.AdaptiveColor
+ DiffAddedBgColor lipgloss.AdaptiveColor
+ DiffRemovedBgColor lipgloss.AdaptiveColor
+ DiffContextBgColor lipgloss.AdaptiveColor
+ DiffLineNumberColor lipgloss.AdaptiveColor
+ DiffAddedLineNumberBgColor lipgloss.AdaptiveColor
+ DiffRemovedLineNumberBgColor lipgloss.AdaptiveColor
+
+ // Markdown colors
+ MarkdownTextColor lipgloss.AdaptiveColor
+ MarkdownHeadingColor lipgloss.AdaptiveColor
+ MarkdownLinkColor lipgloss.AdaptiveColor
+ MarkdownLinkTextColor lipgloss.AdaptiveColor
+ MarkdownCodeColor lipgloss.AdaptiveColor
+ MarkdownBlockQuoteColor lipgloss.AdaptiveColor
+ MarkdownEmphColor lipgloss.AdaptiveColor
+ MarkdownStrongColor lipgloss.AdaptiveColor
+ MarkdownHorizontalRuleColor lipgloss.AdaptiveColor
+ MarkdownListItemColor lipgloss.AdaptiveColor
+ MarkdownListEnumerationColor lipgloss.AdaptiveColor
+ MarkdownImageColor lipgloss.AdaptiveColor
+ MarkdownImageTextColor lipgloss.AdaptiveColor
+ MarkdownCodeBlockColor lipgloss.AdaptiveColor
+
+ // Syntax highlighting colors
+ SyntaxCommentColor lipgloss.AdaptiveColor
+ SyntaxKeywordColor lipgloss.AdaptiveColor
+ SyntaxFunctionColor lipgloss.AdaptiveColor
+ SyntaxVariableColor lipgloss.AdaptiveColor
+ SyntaxStringColor lipgloss.AdaptiveColor
+ SyntaxNumberColor lipgloss.AdaptiveColor
+ SyntaxTypeColor lipgloss.AdaptiveColor
+ SyntaxOperatorColor lipgloss.AdaptiveColor
+ SyntaxPunctuationColor lipgloss.AdaptiveColor
+}
+
+// Implement the Theme interface for BaseTheme
+func (t *BaseTheme) Primary() lipgloss.AdaptiveColor { return t.PrimaryColor }
+func (t *BaseTheme) Secondary() lipgloss.AdaptiveColor { return t.SecondaryColor }
+func (t *BaseTheme) Accent() lipgloss.AdaptiveColor { return t.AccentColor }
+
+func (t *BaseTheme) Error() lipgloss.AdaptiveColor { return t.ErrorColor }
+func (t *BaseTheme) Warning() lipgloss.AdaptiveColor { return t.WarningColor }
+func (t *BaseTheme) Success() lipgloss.AdaptiveColor { return t.SuccessColor }
+func (t *BaseTheme) Info() lipgloss.AdaptiveColor { return t.InfoColor }
+
+func (t *BaseTheme) Text() lipgloss.AdaptiveColor { return t.TextColor }
+func (t *BaseTheme) TextMuted() lipgloss.AdaptiveColor { return t.TextMutedColor }
+func (t *BaseTheme) TextEmphasized() lipgloss.AdaptiveColor { return t.TextEmphasizedColor }
+
+func (t *BaseTheme) Background() lipgloss.AdaptiveColor { return t.BackgroundColor }
+func (t *BaseTheme) BackgroundSecondary() lipgloss.AdaptiveColor { return t.BackgroundSecondaryColor }
+func (t *BaseTheme) BackgroundDarker() lipgloss.AdaptiveColor { return t.BackgroundDarkerColor }
+
+func (t *BaseTheme) BorderNormal() lipgloss.AdaptiveColor { return t.BorderNormalColor }
+func (t *BaseTheme) BorderFocused() lipgloss.AdaptiveColor { return t.BorderFocusedColor }
+func (t *BaseTheme) BorderDim() lipgloss.AdaptiveColor { return t.BorderDimColor }
+
+func (t *BaseTheme) DiffAdded() lipgloss.AdaptiveColor { return t.DiffAddedColor }
+func (t *BaseTheme) DiffRemoved() lipgloss.AdaptiveColor { return t.DiffRemovedColor }
+func (t *BaseTheme) DiffContext() lipgloss.AdaptiveColor { return t.DiffContextColor }
+func (t *BaseTheme) DiffHunkHeader() lipgloss.AdaptiveColor { return t.DiffHunkHeaderColor }
+func (t *BaseTheme) DiffHighlightAdded() lipgloss.AdaptiveColor { return t.DiffHighlightAddedColor }
+func (t *BaseTheme) DiffHighlightRemoved() lipgloss.AdaptiveColor { return t.DiffHighlightRemovedColor }
+func (t *BaseTheme) DiffAddedBg() lipgloss.AdaptiveColor { return t.DiffAddedBgColor }
+func (t *BaseTheme) DiffRemovedBg() lipgloss.AdaptiveColor { return t.DiffRemovedBgColor }
+func (t *BaseTheme) DiffContextBg() lipgloss.AdaptiveColor { return t.DiffContextBgColor }
+func (t *BaseTheme) DiffLineNumber() lipgloss.AdaptiveColor { return t.DiffLineNumberColor }
+func (t *BaseTheme) DiffAddedLineNumberBg() lipgloss.AdaptiveColor {
+ return t.DiffAddedLineNumberBgColor
+}
+func (t *BaseTheme) DiffRemovedLineNumberBg() lipgloss.AdaptiveColor {
+ return t.DiffRemovedLineNumberBgColor
+}
+
+func (t *BaseTheme) MarkdownText() lipgloss.AdaptiveColor { return t.MarkdownTextColor }
+func (t *BaseTheme) MarkdownHeading() lipgloss.AdaptiveColor { return t.MarkdownHeadingColor }
+func (t *BaseTheme) MarkdownLink() lipgloss.AdaptiveColor { return t.MarkdownLinkColor }
+func (t *BaseTheme) MarkdownLinkText() lipgloss.AdaptiveColor { return t.MarkdownLinkTextColor }
+func (t *BaseTheme) MarkdownCode() lipgloss.AdaptiveColor { return t.MarkdownCodeColor }
+func (t *BaseTheme) MarkdownBlockQuote() lipgloss.AdaptiveColor { return t.MarkdownBlockQuoteColor }
+func (t *BaseTheme) MarkdownEmph() lipgloss.AdaptiveColor { return t.MarkdownEmphColor }
+func (t *BaseTheme) MarkdownStrong() lipgloss.AdaptiveColor { return t.MarkdownStrongColor }
+func (t *BaseTheme) MarkdownHorizontalRule() lipgloss.AdaptiveColor {
+ return t.MarkdownHorizontalRuleColor
+}
+func (t *BaseTheme) MarkdownListItem() lipgloss.AdaptiveColor { return t.MarkdownListItemColor }
+func (t *BaseTheme) MarkdownListEnumeration() lipgloss.AdaptiveColor {
+ return t.MarkdownListEnumerationColor
+}
+func (t *BaseTheme) MarkdownImage() lipgloss.AdaptiveColor { return t.MarkdownImageColor }
+func (t *BaseTheme) MarkdownImageText() lipgloss.AdaptiveColor { return t.MarkdownImageTextColor }
+func (t *BaseTheme) MarkdownCodeBlock() lipgloss.AdaptiveColor { return t.MarkdownCodeBlockColor }
+
+func (t *BaseTheme) SyntaxComment() lipgloss.AdaptiveColor { return t.SyntaxCommentColor }
+func (t *BaseTheme) SyntaxKeyword() lipgloss.AdaptiveColor { return t.SyntaxKeywordColor }
+func (t *BaseTheme) SyntaxFunction() lipgloss.AdaptiveColor { return t.SyntaxFunctionColor }
+func (t *BaseTheme) SyntaxVariable() lipgloss.AdaptiveColor { return t.SyntaxVariableColor }
+func (t *BaseTheme) SyntaxString() lipgloss.AdaptiveColor { return t.SyntaxStringColor }
+func (t *BaseTheme) SyntaxNumber() lipgloss.AdaptiveColor { return t.SyntaxNumberColor }
+func (t *BaseTheme) SyntaxType() lipgloss.AdaptiveColor { return t.SyntaxTypeColor }
+func (t *BaseTheme) SyntaxOperator() lipgloss.AdaptiveColor { return t.SyntaxOperatorColor }
+func (t *BaseTheme) SyntaxPunctuation() lipgloss.AdaptiveColor { return t.SyntaxPunctuationColor }
+
+// ParseAdaptiveColor parses a color value from the config file into a lipgloss.AdaptiveColor.
+// It accepts either a string (hex color) or a map with "dark" and "light" keys.
+func ParseAdaptiveColor(value any) (lipgloss.AdaptiveColor, error) {
+ // Regular expression to validate hex color format
+ hexColorRegex := regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
+
+ // Case 1: String value (same color for both dark and light modes)
+ if hexColor, ok := value.(string); ok {
+ if !hexColorRegex.MatchString(hexColor) {
+ return lipgloss.AdaptiveColor{}, fmt.Errorf("invalid hex color format: %s", hexColor)
+ }
+ return lipgloss.AdaptiveColor{
+ Dark: hexColor,
+ Light: hexColor,
+ }, nil
+ }
+
+ // Case 2: Int value between 0 and 255
+ if numericVal, ok := value.(float64); ok {
+ intVal := int(numericVal)
+ if intVal < 0 || intVal > 255 {
+ return lipgloss.AdaptiveColor{}, fmt.Errorf("invalid int color value (must be between 0 and 255): %d", intVal)
+ }
+ return lipgloss.AdaptiveColor{
+ Dark: fmt.Sprintf("%d", intVal),
+ Light: fmt.Sprintf("%d", intVal),
+ }, nil
+ }
+
+ // Case 3: Map with dark and light keys
+ if colorMap, ok := value.(map[string]any); ok {
+ darkVal, darkOk := colorMap["dark"]
+ lightVal, lightOk := colorMap["light"]
+
+ if !darkOk || !lightOk {
+ return lipgloss.AdaptiveColor{}, fmt.Errorf("color map must contain both 'dark' and 'light' keys")
+ }
+
+ darkHex, darkIsString := darkVal.(string)
+ lightHex, lightIsString := lightVal.(string)
+
+ if !darkIsString || !lightIsString {
+ darkVal, darkIsNumber := darkVal.(float64)
+ lightVal, lightIsNumber := lightVal.(float64)
+
+ if !darkIsNumber || !lightIsNumber {
+ return lipgloss.AdaptiveColor{}, fmt.Errorf("color map values must be strings or ints")
+ }
+
+ darkInt := int(darkVal)
+ lightInt := int(lightVal)
+
+ return lipgloss.AdaptiveColor{
+ Dark: fmt.Sprintf("%d", darkInt),
+ Light: fmt.Sprintf("%d", lightInt),
+ }, nil
+ }
+
+ if !hexColorRegex.MatchString(darkHex) || !hexColorRegex.MatchString(lightHex) {
+ return lipgloss.AdaptiveColor{}, fmt.Errorf("invalid hex color format")
+ }
+
+ return lipgloss.AdaptiveColor{
+ Dark: darkHex,
+ Light: lightHex,
+ }, nil
+ }
+
+ return lipgloss.AdaptiveColor{}, fmt.Errorf("color must be either a hex string or an object with dark/light keys")
+}
diff --git a/packages/tui/internal/tui/theme/theme_test.go b/packages/tui/internal/tui/theme/theme_test.go
new file mode 100644
index 000000000..790ee3aa8
--- /dev/null
+++ b/packages/tui/internal/tui/theme/theme_test.go
@@ -0,0 +1,89 @@
+package theme
+
+import (
+ "testing"
+)
+
+func TestThemeRegistration(t *testing.T) {
+ // Get list of available themes
+ availableThemes := AvailableThemes()
+
+ // Check if "catppuccin" theme is registered
+ catppuccinFound := false
+ for _, themeName := range availableThemes {
+ if themeName == "catppuccin" {
+ catppuccinFound = true
+ break
+ }
+ }
+
+ if !catppuccinFound {
+ t.Errorf("Catppuccin theme is not registered")
+ }
+
+ // Check if "gruvbox" theme is registered
+ gruvboxFound := false
+ for _, themeName := range availableThemes {
+ if themeName == "gruvbox" {
+ gruvboxFound = true
+ break
+ }
+ }
+
+ if !gruvboxFound {
+ t.Errorf("Gruvbox theme is not registered")
+ }
+
+ // Check if "monokai" theme is registered
+ monokaiFound := false
+ for _, themeName := range availableThemes {
+ if themeName == "monokai" {
+ monokaiFound = true
+ break
+ }
+ }
+
+ if !monokaiFound {
+ t.Errorf("Monokai theme is not registered")
+ }
+
+ // Try to get the themes and make sure they're not nil
+ catppuccin := GetTheme("catppuccin")
+ if catppuccin == nil {
+ t.Errorf("Catppuccin theme is nil")
+ }
+
+ gruvbox := GetTheme("gruvbox")
+ if gruvbox == nil {
+ t.Errorf("Gruvbox theme is nil")
+ }
+
+ monokai := GetTheme("monokai")
+ if monokai == nil {
+ t.Errorf("Monokai theme is nil")
+ }
+
+ // Test switching theme
+ originalTheme := CurrentThemeName()
+
+ err := SetTheme("gruvbox")
+ if err != nil {
+ t.Errorf("Failed to set theme to gruvbox: %v", err)
+ }
+
+ if CurrentThemeName() != "gruvbox" {
+ t.Errorf("Theme not properly switched to gruvbox")
+ }
+
+ err = SetTheme("monokai")
+ if err != nil {
+ t.Errorf("Failed to set theme to monokai: %v", err)
+ }
+
+ if CurrentThemeName() != "monokai" {
+ t.Errorf("Theme not properly switched to monokai")
+ }
+
+ // Switch back to original theme
+ _ = SetTheme(originalTheme)
+}
diff --git a/packages/tui/internal/tui/theme/tokyonight.go b/packages/tui/internal/tui/theme/tokyonight.go
new file mode 100644
index 000000000..a6499a25d
--- /dev/null
+++ b/packages/tui/internal/tui/theme/tokyonight.go
@@ -0,0 +1,274 @@
+package theme
+
+import (
+ "github.com/charmbracelet/lipgloss"
+)
+
+// TokyoNightTheme implements the Theme interface with Tokyo Night colors.
+// It provides both dark and light variants.
+type TokyoNightTheme struct {
+ BaseTheme
+}
+
+// NewTokyoNightTheme creates a new instance of the Tokyo Night theme.
+func NewTokyoNightTheme() *TokyoNightTheme {
+ // Tokyo Night color palette
+ // Dark mode colors
+ darkBackground := "#222436"
+ darkCurrentLine := "#1e2030"
+ darkSelection := "#2f334d"
+ darkForeground := "#c8d3f5"
+ darkComment := "#636da6"
+ darkRed := "#ff757f"
+ darkOrange := "#ff966c"
+ darkYellow := "#ffc777"
+ darkGreen := "#c3e88d"
+ darkCyan := "#86e1fc"
+ darkBlue := "#82aaff"
+ darkPurple := "#c099ff"
+ darkBorder := "#3b4261"
+
+ // Light mode colors (Tokyo Night Day)
+ lightBackground := "#e1e2e7"
+ lightCurrentLine := "#d5d6db"
+ lightSelection := "#c8c9ce"
+ lightForeground := "#3760bf"
+ lightComment := "#848cb5"
+ lightRed := "#f52a65"
+ lightOrange := "#b15c00"
+ lightYellow := "#8c6c3e"
+ lightGreen := "#587539"
+ lightCyan := "#007197"
+ lightBlue := "#2e7de9"
+ lightPurple := "#9854f1"
+ lightBorder := "#a8aecb"
+
+ theme := &TokyoNightTheme{}
+
+ // Base colors
+ theme.PrimaryColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.SecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.AccentColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+
+ // Status colors
+ theme.ErrorColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.WarningColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.SuccessColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.InfoColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+
+ // Text colors
+ theme.TextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.TextMutedColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+
+ // Background colors
+ theme.BackgroundColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkCurrentLine,
+ Light: lightCurrentLine,
+ }
+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
+ Dark: "#191B29", // Darker background from palette
+ Light: "#f0f0f5", // Slightly lighter than background
+ }
+
+ // Border colors
+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
+ Dark: darkBorder,
+ Light: lightBorder,
+ }
+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.BorderDimColor = lipgloss.AdaptiveColor{
+ Dark: darkSelection,
+ Light: lightSelection,
+ }
+
+ // Diff view colors
+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
+ Dark: "#4fd6be", // teal from palette
+ Light: "#1e725c",
+ }
+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
+ Dark: "#c53b53", // red1 from palette
+ Light: "#c53b53",
+ }
+ theme.DiffContextColor = lipgloss.AdaptiveColor{
+ Dark: "#828bb8", // fg_dark from palette
+ Light: "#7086b5",
+ }
+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
+ Dark: "#828bb8", // fg_dark from palette
+ Light: "#7086b5",
+ }
+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
+ Dark: "#b8db87", // git.add from palette
+ Light: "#4db380",
+ }
+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
+ Dark: "#e26a75", // git.delete from palette
+ Light: "#f52a65",
+ }
+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#20303b",
+ Light: "#d5e5d5",
+ }
+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#37222c",
+ Light: "#f7d8db",
+ }
+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
+ Dark: "#545c7e", // dark3 from palette
+ Light: "#848cb5",
+ }
+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#1b2b34",
+ Light: "#c5d5c5",
+ }
+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#2d1f26",
+ Light: "#e7c8cb",
+ }
+
+ // Markdown colors
+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ // Syntax highlighting colors
+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ return theme
+}
+
+func init() {
+ // Register the Tokyo Night theme with the theme manager
+ RegisterTheme("tokyonight", NewTokyoNightTheme())
+}
diff --git a/packages/tui/internal/tui/theme/tron.go b/packages/tui/internal/tui/theme/tron.go
new file mode 100644
index 000000000..c4997a6d1
--- /dev/null
+++ b/packages/tui/internal/tui/theme/tron.go
@@ -0,0 +1,276 @@
+package theme
+
+import (
+ "github.com/charmbracelet/lipgloss"
+)
+
+// TronTheme implements the Theme interface with Tron-inspired colors.
+// It provides both dark and light variants, though Tron is primarily a dark theme.
+type TronTheme struct {
+ BaseTheme
+}
+
+// NewTronTheme creates a new instance of the Tron theme.
+func NewTronTheme() *TronTheme {
+ // Tron color palette
+ // Inspired by the Tron movie's neon aesthetic
+ darkBackground := "#0c141f"
+ darkCurrentLine := "#1a2633"
+ darkSelection := "#1a2633"
+ darkForeground := "#caf0ff"
+ darkComment := "#4d6b87"
+ darkCyan := "#00d9ff"
+ darkBlue := "#007fff"
+ darkOrange := "#ff9000"
+ darkPink := "#ff00a0"
+ darkPurple := "#b73fff"
+ darkRed := "#ff3333"
+ darkYellow := "#ffcc00"
+ darkGreen := "#00ff8f"
+ darkBorder := "#1a2633"
+
+ // Light mode approximation
+ lightBackground := "#f0f8ff"
+ lightCurrentLine := "#e0f0ff"
+ lightSelection := "#d0e8ff"
+ lightForeground := "#0c141f"
+ lightComment := "#4d6b87"
+ lightCyan := "#0097b3"
+ lightBlue := "#0066cc"
+ lightOrange := "#cc7300"
+ lightPink := "#cc0080"
+ lightPurple := "#9932cc"
+ lightRed := "#cc2929"
+ lightYellow := "#cc9900"
+ lightGreen := "#00cc72"
+ lightBorder := "#d0e8ff"
+
+ theme := &TronTheme{}
+
+ // Base colors
+ theme.PrimaryColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.SecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.AccentColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+
+ // Status colors
+ theme.ErrorColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.WarningColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.SuccessColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.InfoColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+
+ // Text colors
+ theme.TextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.TextMutedColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+
+ // Background colors
+ theme.BackgroundColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkCurrentLine,
+ Light: lightCurrentLine,
+ }
+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
+ Dark: "#070d14", // Slightly darker than background
+ Light: "#ffffff", // Slightly lighter than background
+ }
+
+ // Border colors
+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
+ Dark: darkBorder,
+ Light: lightBorder,
+ }
+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.BorderDimColor = lipgloss.AdaptiveColor{
+ Dark: darkSelection,
+ Light: lightSelection,
+ }
+
+ // Diff view colors
+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.DiffContextColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
+ Dark: "#00ff8f",
+ Light: "#a5d6a7",
+ }
+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
+ Dark: "#ff3333",
+ Light: "#ef9a9a",
+ }
+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#0a2a1a",
+ Light: "#e8f5e9",
+ }
+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#2a0a0a",
+ Light: "#ffebee",
+ }
+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#082015",
+ Light: "#c8e6c9",
+ }
+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#200808",
+ Light: "#ffcdd2",
+ }
+
+ // Markdown colors
+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ // Syntax highlighting colors
+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
+ Dark: darkBlue,
+ Light: lightBlue,
+ }
+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
+ Dark: darkPurple,
+ Light: lightPurple,
+ }
+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
+ Dark: darkPink,
+ Light: lightPink,
+ }
+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ return theme
+}
+
+func init() {
+ // Register the Tron theme with the theme manager
+ RegisterTheme("tron", NewTronTheme())
+}
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
new file mode 100644
index 000000000..236d5e453
--- /dev/null
+++ b/packages/tui/internal/tui/tui.go
@@ -0,0 +1,988 @@
+package tui
+
+import (
+ "context"
+ "log/slog"
+ "strings"
+
+ "github.com/charmbracelet/bubbles/cursor"
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/spinner"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sst/opencode/internal/config"
+ "github.com/sst/opencode/internal/tui/app"
+
+ "github.com/sst/opencode/internal/status"
+ "github.com/sst/opencode/internal/tui/components/chat"
+ "github.com/sst/opencode/internal/tui/components/core"
+ "github.com/sst/opencode/internal/tui/components/dialog"
+ "github.com/sst/opencode/internal/tui/layout"
+ "github.com/sst/opencode/internal/tui/page"
+ "github.com/sst/opencode/internal/tui/state"
+ "github.com/sst/opencode/internal/tui/util"
+ "github.com/sst/opencode/pkg/client"
+)
+
+type keyMap struct {
+ Quit key.Binding
+ Help key.Binding
+ SwitchSession key.Binding
+ Commands key.Binding
+ Filepicker key.Binding
+ Models key.Binding
+ SwitchTheme key.Binding
+ Tools key.Binding
+}
+
+const (
+ quitKey = "q"
+)
+
+var keys = keyMap{
+ Quit: key.NewBinding(
+ key.WithKeys("ctrl+c"),
+ key.WithHelp("ctrl+c", "quit"),
+ ),
+ Help: key.NewBinding(
+ key.WithKeys("ctrl+_"),
+ key.WithHelp("ctrl+?", "toggle help"),
+ ),
+
+ SwitchSession: key.NewBinding(
+ key.WithKeys("ctrl+s"),
+ key.WithHelp("ctrl+s", "switch session"),
+ ),
+
+ Commands: key.NewBinding(
+ key.WithKeys("ctrl+k"),
+ key.WithHelp("ctrl+k", "commands"),
+ ),
+ Filepicker: key.NewBinding(
+ key.WithKeys("ctrl+f"),
+ key.WithHelp("ctrl+f", "select files to upload"),
+ ),
+ Models: key.NewBinding(
+ key.WithKeys("ctrl+o"),
+ key.WithHelp("ctrl+o", "model selection"),
+ ),
+
+ SwitchTheme: key.NewBinding(
+ key.WithKeys("ctrl+t"),
+ key.WithHelp("ctrl+t", "switch theme"),
+ ),
+
+ Tools: key.NewBinding(
+ key.WithKeys("f9"),
+ key.WithHelp("f9", "show available tools"),
+ ),
+}
+
+var helpEsc = key.NewBinding(
+ key.WithKeys("?"),
+ key.WithHelp("?", "toggle help"),
+)
+
+var returnKey = key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "close"),
+)
+
+type appModel struct {
+ width, height int
+ currentPage page.PageID
+ previousPage page.PageID
+ pages map[page.PageID]tea.Model
+ loadedPages map[page.PageID]bool
+ status core.StatusCmp
+ app *app.App
+
+ showPermissions bool
+ permissions dialog.PermissionDialogCmp
+
+ showHelp bool
+ help dialog.HelpCmp
+
+ showQuit bool
+ quit dialog.QuitDialog
+
+ showSessionDialog bool
+ sessionDialog dialog.SessionDialog
+
+ showCommandDialog bool
+ commandDialog dialog.CommandDialog
+ commands []dialog.Command
+
+ showModelDialog bool
+ modelDialog dialog.ModelDialog
+
+ showInitDialog bool
+ initDialog dialog.InitDialogCmp
+
+ showFilepicker bool
+ filepicker dialog.FilepickerCmp
+
+ showThemeDialog bool
+ themeDialog dialog.ThemeDialog
+
+ showMultiArgumentsDialog bool
+ multiArgumentsDialog dialog.MultiArgumentsDialogCmp
+
+ showToolsDialog bool
+ toolsDialog dialog.ToolsDialog
+}
+
+func (a appModel) Init() tea.Cmd {
+ var cmds []tea.Cmd
+ cmd := a.pages[a.currentPage].Init()
+ a.loadedPages[a.currentPage] = true
+ cmds = append(cmds, cmd)
+ cmd = a.status.Init()
+ cmds = append(cmds, cmd)
+ cmd = a.quit.Init()
+ cmds = append(cmds, cmd)
+ cmd = a.help.Init()
+ cmds = append(cmds, cmd)
+ cmd = a.sessionDialog.Init()
+ cmds = append(cmds, cmd)
+ cmd = a.commandDialog.Init()
+ cmds = append(cmds, cmd)
+ cmd = a.modelDialog.Init()
+ cmds = append(cmds, cmd)
+ cmd = a.initDialog.Init()
+ cmds = append(cmds, cmd)
+ cmd = a.filepicker.Init()
+ cmds = append(cmds, cmd)
+ cmd = a.themeDialog.Init()
+ cmds = append(cmds, cmd)
+ cmd = a.toolsDialog.Init()
+ cmds = append(cmds, cmd)
+
+ // Check if we should show the init dialog
+ cmds = append(cmds, func() tea.Msg {
+ shouldShow, err := config.ShouldShowInitDialog()
+ if err != nil {
+ status.Error("Failed to check init status: " + err.Error())
+ return nil
+ }
+ return dialog.ShowInitDialogMsg{Show: shouldShow}
+ })
+
+ // TODO: store last selected model somewhere
+ cmds = append(cmds, func() tea.Msg {
+ providers, _ := a.app.ListProviders(context.Background())
+ return state.ModelSelectedMsg{Provider: providers[0], Model: providers[0].Models[0]}
+ })
+
+ return tea.Batch(cmds...)
+}
+
+func (a appModel) updateAllPages(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+ var cmd tea.Cmd
+
+ for id := range a.pages {
+ a.pages[id], cmd = a.pages[id].Update(msg)
+ cmds = append(cmds, cmd)
+ }
+
+ s, cmd := a.status.Update(msg)
+ cmds = append(cmds, cmd)
+ a.status = s.(core.StatusCmp)
+
+ return a, tea.Batch(cmds...)
+}
+
+func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+ var cmd tea.Cmd
+ switch msg := msg.(type) {
+ case cursor.BlinkMsg:
+ return a.updateAllPages(msg)
+ case spinner.TickMsg:
+ return a.updateAllPages(msg)
+
+ case client.EventSessionUpdated:
+ if msg.Properties.Info.Id == a.app.Session.Id {
+ a.app.Session = &msg.Properties.Info
+ return a.updateAllPages(state.StateUpdatedMsg{State: nil})
+ }
+
+ case client.EventMessageUpdated:
+ if msg.Properties.Info.Metadata.SessionID == a.app.Session.Id {
+ for i, m := range a.app.Messages {
+ if m.Id == msg.Properties.Info.Id {
+ a.app.Messages[i] = msg.Properties.Info
+ return a.updateAllPages(state.StateUpdatedMsg{State: nil})
+ }
+ }
+ a.app.Messages = append(a.app.Messages, msg.Properties.Info)
+ return a.updateAllPages(state.StateUpdatedMsg{State: nil})
+ }
+
+ case tea.WindowSizeMsg:
+ msg.Height -= 2 // Make space for the status bar
+ a.width, a.height = msg.Width, msg.Height
+
+ s, _ := a.status.Update(msg)
+ a.status = s.(core.StatusCmp)
+ a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
+ cmds = append(cmds, cmd)
+
+ prm, permCmd := a.permissions.Update(msg)
+ a.permissions = prm.(dialog.PermissionDialogCmp)
+ cmds = append(cmds, permCmd)
+
+ help, helpCmd := a.help.Update(msg)
+ a.help = help.(dialog.HelpCmp)
+ cmds = append(cmds, helpCmd)
+
+ session, sessionCmd := a.sessionDialog.Update(msg)
+ a.sessionDialog = session.(dialog.SessionDialog)
+ cmds = append(cmds, sessionCmd)
+
+ command, commandCmd := a.commandDialog.Update(msg)
+ a.commandDialog = command.(dialog.CommandDialog)
+ cmds = append(cmds, commandCmd)
+
+ filepicker, filepickerCmd := a.filepicker.Update(msg)
+ a.filepicker = filepicker.(dialog.FilepickerCmp)
+ cmds = append(cmds, filepickerCmd)
+
+ a.initDialog.SetSize(msg.Width, msg.Height)
+
+ if a.showMultiArgumentsDialog {
+ a.multiArgumentsDialog.SetSize(msg.Width, msg.Height)
+ args, argsCmd := a.multiArgumentsDialog.Update(msg)
+ a.multiArgumentsDialog = args.(dialog.MultiArgumentsDialogCmp)
+ cmds = append(cmds, argsCmd, a.multiArgumentsDialog.Init())
+ }
+
+ return a, tea.Batch(cmds...)
+
+ // case pubsub.Event[permission.PermissionRequest]:
+ // a.showPermissions = true
+ // return a, a.permissions.SetPermissions(msg.Payload)
+
+ case dialog.PermissionResponseMsg:
+ // TODO: Permissions service not implemented in API yet
+ // var cmd tea.Cmd
+ // switch msg.Action {
+ // case dialog.PermissionAllow:
+ // a.app.Permissions.Grant(context.Background(), msg.Permission)
+ // case dialog.PermissionAllowForSession:
+ // a.app.Permissions.GrantPersistant(context.Background(), msg.Permission)
+ // case dialog.PermissionDeny:
+ // a.app.Permissions.Deny(context.Background(), msg.Permission)
+ // }
+ a.showPermissions = false
+ return a, nil
+
+ case page.PageChangeMsg:
+ return a, a.moveToPage(msg.ID)
+
+ case dialog.CloseQuitMsg:
+ a.showQuit = false
+ return a, nil
+
+ case dialog.CloseSessionDialogMsg:
+ a.showSessionDialog = false
+ if msg.Session != nil {
+ return a, util.CmdHandler(state.SessionSelectedMsg(msg.Session))
+ }
+ return a, nil
+
+ case state.SessionSelectedMsg:
+ a.app.Session = msg
+ a.app.Messages, _ = a.app.ListMessages(context.Background(), msg.Id)
+ return a.updateAllPages(msg)
+
+ case dialog.CloseModelDialogMsg:
+ a.showModelDialog = false
+ slog.Debug("closing model dialog", "msg", msg)
+ if msg.Provider != nil && msg.Model != nil {
+ return a, util.CmdHandler(state.ModelSelectedMsg{Provider: *msg.Provider, Model: *msg.Model})
+ }
+ return a, nil
+
+ case state.ModelSelectedMsg:
+ a.app.Provider = &msg.Provider
+ a.app.Model = &msg.Model
+ return a.updateAllPages(msg)
+
+ case dialog.CloseCommandDialogMsg:
+ a.showCommandDialog = false
+ return a, nil
+
+ case dialog.CloseThemeDialogMsg:
+ a.showThemeDialog = false
+ return a, nil
+
+ case dialog.CloseToolsDialogMsg:
+ a.showToolsDialog = false
+ return a, nil
+
+ case dialog.ShowToolsDialogMsg:
+ a.showToolsDialog = msg.Show
+ return a, nil
+
+ case dialog.ThemeChangedMsg:
+ a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
+ a.showThemeDialog = false
+ status.Info("Theme changed to: " + msg.ThemeName)
+ return a, cmd
+
+ case dialog.ShowInitDialogMsg:
+ a.showInitDialog = msg.Show
+ return a, nil
+
+ case dialog.CloseInitDialogMsg:
+ a.showInitDialog = false
+ if msg.Initialize {
+ // Run the initialization command
+ for _, cmd := range a.commands {
+ if cmd.ID == "init" {
+ // Mark the project as initialized
+ if err := config.MarkProjectInitialized(); err != nil {
+ status.Error(err.Error())
+ return a, nil
+ }
+ return a, cmd.Handler(cmd)
+ }
+ }
+ } else {
+ // Mark the project as initialized without running the command
+ if err := config.MarkProjectInitialized(); err != nil {
+ status.Error(err.Error())
+ return a, nil
+ }
+ }
+ return a, nil
+
+ case dialog.CommandSelectedMsg:
+ a.showCommandDialog = false
+ // Execute the command handler if available
+ if msg.Command.Handler != nil {
+ return a, msg.Command.Handler(msg.Command)
+ }
+ status.Info("Command selected: " + msg.Command.Title)
+ return a, nil
+
+ case dialog.ShowMultiArgumentsDialogMsg:
+ // Show multi-arguments dialog
+ a.multiArgumentsDialog = dialog.NewMultiArgumentsDialogCmp(msg.CommandID, msg.Content, msg.ArgNames)
+ a.showMultiArgumentsDialog = true
+ return a, a.multiArgumentsDialog.Init()
+
+ case dialog.CloseMultiArgumentsDialogMsg:
+ // Close multi-arguments dialog
+ a.showMultiArgumentsDialog = false
+
+ // If submitted, replace all named arguments and run the command
+ if msg.Submit {
+ content := msg.Content
+
+ // Replace each named argument with its value
+ for name, value := range msg.Args {
+ placeholder := "$" + name
+ content = strings.ReplaceAll(content, placeholder, value)
+ }
+
+ // Execute the command with arguments
+ return a, util.CmdHandler(dialog.CommandRunCustomMsg{
+ Content: content,
+ Args: msg.Args,
+ })
+ }
+ return a, nil
+
+ case tea.KeyMsg:
+ // If multi-arguments dialog is open, let it handle the key press first
+ if a.showMultiArgumentsDialog {
+ args, cmd := a.multiArgumentsDialog.Update(msg)
+ a.multiArgumentsDialog = args.(dialog.MultiArgumentsDialogCmp)
+ return a, cmd
+ }
+
+ switch {
+ case key.Matches(msg, keys.Quit):
+ a.showQuit = !a.showQuit
+ if a.showHelp {
+ a.showHelp = false
+ }
+ if a.showSessionDialog {
+ a.showSessionDialog = false
+ }
+ if a.showCommandDialog {
+ a.showCommandDialog = false
+ }
+ if a.showFilepicker {
+ a.showFilepicker = false
+ a.filepicker.ToggleFilepicker(a.showFilepicker)
+ a.app.SetFilepickerOpen(a.showFilepicker)
+ }
+ if a.showModelDialog {
+ a.showModelDialog = false
+ }
+ if a.showMultiArgumentsDialog {
+ a.showMultiArgumentsDialog = false
+ }
+ if a.showToolsDialog {
+ a.showToolsDialog = false
+ }
+ return a, nil
+ case key.Matches(msg, keys.SwitchSession):
+ if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showCommandDialog {
+ // Close other dialogs
+ a.showToolsDialog = false
+ a.showThemeDialog = false
+ a.showModelDialog = false
+ a.showFilepicker = false
+
+ // Load sessions and show the dialog
+ sessions, err := a.app.ListSessions(context.Background())
+ if err != nil {
+ status.Error(err.Error())
+ return a, nil
+ }
+ if len(sessions) == 0 {
+ status.Warn("No sessions available")
+ return a, nil
+ }
+ a.sessionDialog.SetSessions(sessions)
+ a.showSessionDialog = true
+ return a, nil
+ }
+ return a, nil
+ case key.Matches(msg, keys.Commands):
+ if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showThemeDialog && !a.showFilepicker {
+ // Close other dialogs
+ a.showToolsDialog = false
+ a.showModelDialog = false
+
+ // Show commands dialog
+ if len(a.commands) == 0 {
+ status.Warn("No commands available")
+ return a, nil
+ }
+ a.commandDialog.SetCommands(a.commands)
+ a.showCommandDialog = true
+ return a, nil
+ }
+ return a, nil
+ case key.Matches(msg, keys.Models):
+ if a.showModelDialog {
+ a.showModelDialog = false
+ return a, nil
+ }
+ if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
+ // Close other dialogs
+ a.showToolsDialog = false
+ a.showThemeDialog = false
+ a.showFilepicker = false
+
+ // Load providers and show the dialog
+ providers, err := a.app.ListProviders(context.Background())
+ if err != nil {
+ status.Error(err.Error())
+ return a, nil
+ }
+ if len(providers) == 0 {
+ status.Warn("No providers available")
+ return a, nil
+ }
+ a.modelDialog.SetProviders(providers)
+
+ a.showModelDialog = true
+ return a, nil
+ }
+ return a, nil
+ case key.Matches(msg, keys.SwitchTheme):
+ if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
+ // Close other dialogs
+ a.showToolsDialog = false
+ a.showModelDialog = false
+ a.showFilepicker = false
+
+ a.showThemeDialog = true
+ return a, a.themeDialog.Init()
+ }
+ return a, nil
+ case key.Matches(msg, keys.Tools):
+ // Check if any other dialog is open
+ if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions &&
+ !a.showSessionDialog && !a.showCommandDialog && !a.showThemeDialog &&
+ !a.showFilepicker && !a.showModelDialog && !a.showInitDialog &&
+ !a.showMultiArgumentsDialog {
+ // Toggle tools dialog
+ a.showToolsDialog = !a.showToolsDialog
+ if a.showToolsDialog {
+ // Get tool names dynamically
+ toolNames := getAvailableToolNames(a.app)
+ a.toolsDialog.SetTools(toolNames)
+ }
+ return a, nil
+ }
+ return a, nil
+ case key.Matches(msg, returnKey) || key.Matches(msg):
+ if !a.filepicker.IsCWDFocused() {
+ if a.showToolsDialog {
+ a.showToolsDialog = false
+ return a, nil
+ }
+ if a.showQuit {
+ a.showQuit = !a.showQuit
+ return a, nil
+ }
+ if a.showHelp {
+ a.showHelp = !a.showHelp
+ return a, nil
+ }
+ if a.showInitDialog {
+ a.showInitDialog = false
+ // Mark the project as initialized without running the command
+ if err := config.MarkProjectInitialized(); err != nil {
+ status.Error(err.Error())
+ return a, nil
+ }
+ return a, nil
+ }
+ if a.showFilepicker {
+ a.showFilepicker = false
+ a.filepicker.ToggleFilepicker(a.showFilepicker)
+ a.app.SetFilepickerOpen(a.showFilepicker)
+ return a, nil
+ }
+ }
+ case key.Matches(msg, keys.Help):
+ if a.showQuit {
+ return a, nil
+ }
+ a.showHelp = !a.showHelp
+
+ // Close other dialogs if opening help
+ if a.showHelp {
+ a.showToolsDialog = false
+ }
+ return a, nil
+ case key.Matches(msg, helpEsc):
+ if a.app.PrimaryAgentOLD.IsBusy() {
+ if a.showQuit {
+ return a, nil
+ }
+ a.showHelp = !a.showHelp
+ return a, nil
+ }
+ case key.Matches(msg, keys.Filepicker):
+ // Toggle filepicker
+ a.showFilepicker = !a.showFilepicker
+ a.filepicker.ToggleFilepicker(a.showFilepicker)
+ a.app.SetFilepickerOpen(a.showFilepicker)
+ // Close other dialogs if opening filepicker
+ if a.showFilepicker {
+ a.showToolsDialog = false
+ a.showThemeDialog = false
+ a.showModelDialog = false
+ a.showCommandDialog = false
+ a.showSessionDialog = false
+ }
+ return a, nil
+ }
+
+ default:
+ f, filepickerCmd := a.filepicker.Update(msg)
+ a.filepicker = f.(dialog.FilepickerCmp)
+ cmds = append(cmds, filepickerCmd)
+ }
+
+ if a.showFilepicker {
+ f, filepickerCmd := a.filepicker.Update(msg)
+ a.filepicker = f.(dialog.FilepickerCmp)
+ cmds = append(cmds, filepickerCmd)
+ // Only block key messages send all other messages down
+ if _, ok := msg.(tea.KeyMsg); ok {
+ return a, tea.Batch(cmds...)
+ }
+ }
+
+ if a.showQuit {
+ q, quitCmd := a.quit.Update(msg)
+ a.quit = q.(dialog.QuitDialog)
+ cmds = append(cmds, quitCmd)
+ // Only block key messages send all other messages down
+ if _, ok := msg.(tea.KeyMsg); ok {
+ return a, tea.Batch(cmds...)
+ }
+ }
+
+ if a.showPermissions {
+ d, permissionsCmd := a.permissions.Update(msg)
+ a.permissions = d.(dialog.PermissionDialogCmp)
+ cmds = append(cmds, permissionsCmd)
+ // Only block key messages send all other messages down
+ if _, ok := msg.(tea.KeyMsg); ok {
+ return a, tea.Batch(cmds...)
+ }
+ }
+
+ if a.showSessionDialog {
+ d, sessionCmd := a.sessionDialog.Update(msg)
+ a.sessionDialog = d.(dialog.SessionDialog)
+ cmds = append(cmds, sessionCmd)
+ // Only block key messages send all other messages down
+ if _, ok := msg.(tea.KeyMsg); ok {
+ return a, tea.Batch(cmds...)
+ }
+ }
+
+ if a.showCommandDialog {
+ d, commandCmd := a.commandDialog.Update(msg)
+ a.commandDialog = d.(dialog.CommandDialog)
+ cmds = append(cmds, commandCmd)
+ // Only block key messages send all other messages down
+ if _, ok := msg.(tea.KeyMsg); ok {
+ return a, tea.Batch(cmds...)
+ }
+ }
+
+ if a.showModelDialog {
+ d, modelCmd := a.modelDialog.Update(msg)
+ a.modelDialog = d.(dialog.ModelDialog)
+ cmds = append(cmds, modelCmd)
+ // Only block key messages send all other messages down
+ if _, ok := msg.(tea.KeyMsg); ok {
+ return a, tea.Batch(cmds...)
+ }
+ }
+
+ if a.showInitDialog {
+ d, initCmd := a.initDialog.Update(msg)
+ a.initDialog = d.(dialog.InitDialogCmp)
+ cmds = append(cmds, initCmd)
+ // Only block key messages send all other messages down
+ if _, ok := msg.(tea.KeyMsg); ok {
+ return a, tea.Batch(cmds...)
+ }
+ }
+
+ if a.showThemeDialog {
+ d, themeCmd := a.themeDialog.Update(msg)
+ a.themeDialog = d.(dialog.ThemeDialog)
+ cmds = append(cmds, themeCmd)
+ // Only block key messages send all other messages down
+ if _, ok := msg.(tea.KeyMsg); ok {
+ return a, tea.Batch(cmds...)
+ }
+ }
+
+ if a.showToolsDialog {
+ d, toolsCmd := a.toolsDialog.Update(msg)
+ a.toolsDialog = d.(dialog.ToolsDialog)
+ cmds = append(cmds, toolsCmd)
+ // Only block key messages send all other messages down
+ if _, ok := msg.(tea.KeyMsg); ok {
+ return a, tea.Batch(cmds...)
+ }
+ }
+
+ s, cmd := a.status.Update(msg)
+ cmds = append(cmds, cmd)
+ a.status = s.(core.StatusCmp)
+
+ a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
+ cmds = append(cmds, cmd)
+ return a, tea.Batch(cmds...)
+}
+
+// RegisterCommand adds a command to the command dialog
+func (a *appModel) RegisterCommand(cmd dialog.Command) {
+ a.commands = append(a.commands, cmd)
+}
+
+// getAvailableToolNames returns a list of all available tool names
+func getAvailableToolNames(_ *app.App) []string {
+ // TODO: Tools not implemented in API yet
+ return []string{"Tools not available in API mode"}
+}
+
+func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
+ var cmds []tea.Cmd
+ if _, ok := a.loadedPages[pageID]; !ok {
+ cmd := a.pages[pageID].Init()
+ cmds = append(cmds, cmd)
+ a.loadedPages[pageID] = true
+ }
+ a.previousPage = a.currentPage
+ a.currentPage = pageID
+ if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
+ cmd := sizable.SetSize(a.width, a.height)
+ cmds = append(cmds, cmd)
+ }
+
+ return tea.Batch(cmds...)
+}
+
+func (a appModel) View() string {
+ components := []string{
+ a.pages[a.currentPage].View(),
+ }
+
+ components = append(components, a.status.View())
+
+ appView := lipgloss.JoinVertical(lipgloss.Top, components...)
+
+ if a.showPermissions {
+ overlay := a.permissions.View()
+ row := lipgloss.Height(appView) / 2
+ row -= lipgloss.Height(overlay) / 2
+ col := lipgloss.Width(appView) / 2
+ col -= lipgloss.Width(overlay) / 2
+ appView = layout.PlaceOverlay(
+ col,
+ row,
+ overlay,
+ appView,
+ true,
+ )
+ }
+
+ if a.showFilepicker {
+ overlay := a.filepicker.View()
+ row := lipgloss.Height(appView) / 2
+ row -= lipgloss.Height(overlay) / 2
+ col := lipgloss.Width(appView) / 2
+ col -= lipgloss.Width(overlay) / 2
+ appView = layout.PlaceOverlay(
+ col,
+ row,
+ overlay,
+ appView,
+ true,
+ )
+
+ }
+
+ if !a.app.PrimaryAgentOLD.IsBusy() {
+ a.status.SetHelpWidgetMsg("ctrl+? help")
+ } else {
+ a.status.SetHelpWidgetMsg("? help")
+ }
+
+ if a.showHelp {
+ bindings := layout.KeyMapToSlice(keys)
+ if p, ok := a.pages[a.currentPage].(layout.Bindings); ok {
+ bindings = append(bindings, p.BindingKeys()...)
+ }
+ if a.showPermissions {
+ bindings = append(bindings, a.permissions.BindingKeys()...)
+ }
+ if !a.app.PrimaryAgentOLD.IsBusy() {
+ bindings = append(bindings, helpEsc)
+ }
+ a.help.SetBindings(bindings)
+
+ overlay := a.help.View()
+ row := lipgloss.Height(appView) / 2
+ row -= lipgloss.Height(overlay) / 2
+ col := lipgloss.Width(appView) / 2
+ col -= lipgloss.Width(overlay) / 2
+ appView = layout.PlaceOverlay(
+ col,
+ row,
+ overlay,
+ appView,
+ true,
+ )
+ }
+
+ if a.showQuit {
+ overlay := a.quit.View()
+ row := lipgloss.Height(appView) / 2
+ row -= lipgloss.Height(overlay) / 2
+ col := lipgloss.Width(appView) / 2
+ col -= lipgloss.Width(overlay) / 2
+ appView = layout.PlaceOverlay(
+ col,
+ row,
+ overlay,
+ appView,
+ true,
+ )
+ }
+
+ if a.showSessionDialog {
+ overlay := a.sessionDialog.View()
+ row := lipgloss.Height(appView) / 2
+ row -= lipgloss.Height(overlay) / 2
+ col := lipgloss.Width(appView) / 2
+ col -= lipgloss.Width(overlay) / 2
+ appView = layout.PlaceOverlay(
+ col,
+ row,
+ overlay,
+ appView,
+ true,
+ )
+ }
+
+ if a.showModelDialog {
+ overlay := a.modelDialog.View()
+ row := lipgloss.Height(appView) / 2
+ row -= lipgloss.Height(overlay) / 2
+ col := lipgloss.Width(appView) / 2
+ col -= lipgloss.Width(overlay) / 2
+ appView = layout.PlaceOverlay(
+ col,
+ row,
+ overlay,
+ appView,
+ true,
+ )
+ }
+
+ if a.showCommandDialog {
+ overlay := a.commandDialog.View()
+ row := lipgloss.Height(appView) / 2
+ row -= lipgloss.Height(overlay) / 2
+ col := lipgloss.Width(appView) / 2
+ col -= lipgloss.Width(overlay) / 2
+ appView = layout.PlaceOverlay(
+ col,
+ row,
+ overlay,
+ appView,
+ true,
+ )
+ }
+
+ if a.showInitDialog {
+ overlay := a.initDialog.View()
+ appView = layout.PlaceOverlay(
+ a.width/2-lipgloss.Width(overlay)/2,
+ a.height/2-lipgloss.Height(overlay)/2,
+ overlay,
+ appView,
+ true,
+ )
+ }
+
+ if a.showThemeDialog {
+ overlay := a.themeDialog.View()
+ row := lipgloss.Height(appView) / 2
+ row -= lipgloss.Height(overlay) / 2
+ col := lipgloss.Width(appView) / 2
+ col -= lipgloss.Width(overlay) / 2
+ appView = layout.PlaceOverlay(
+ col,
+ row,
+ overlay,
+ appView,
+ true,
+ )
+ }
+
+ if a.showMultiArgumentsDialog {
+ overlay := a.multiArgumentsDialog.View()
+ row := lipgloss.Height(appView) / 2
+ row -= lipgloss.Height(overlay) / 2
+ col := lipgloss.Width(appView) / 2
+ col -= lipgloss.Width(overlay) / 2
+ appView = layout.PlaceOverlay(
+ col,
+ row,
+ overlay,
+ appView,
+ true,
+ )
+ }
+
+ if a.showToolsDialog {
+ overlay := a.toolsDialog.View()
+ row := lipgloss.Height(appView) / 2
+ row -= lipgloss.Height(overlay) / 2
+ col := lipgloss.Width(appView) / 2
+ col -= lipgloss.Width(overlay) / 2
+ appView = layout.PlaceOverlay(
+ col,
+ row,
+ overlay,
+ appView,
+ true,
+ )
+ }
+
+ return appView
+}
+
+func New(app *app.App) tea.Model {
+ startPage := page.ChatPage
+ model := &appModel{
+ currentPage: startPage,
+ loadedPages: make(map[page.PageID]bool),
+ status: core.NewStatusCmp(app),
+ help: dialog.NewHelpCmp(),
+ quit: dialog.NewQuitCmp(),
+ sessionDialog: dialog.NewSessionDialogCmp(),
+ commandDialog: dialog.NewCommandDialogCmp(),
+ modelDialog: dialog.NewModelDialogCmp(app),
+ permissions: dialog.NewPermissionDialogCmp(),
+ initDialog: dialog.NewInitDialogCmp(),
+ themeDialog: dialog.NewThemeDialogCmp(),
+ toolsDialog: dialog.NewToolsDialogCmp(),
+ app: app,
+ commands: []dialog.Command{},
+ pages: map[page.PageID]tea.Model{
+ page.ChatPage: page.NewChatPage(app),
+ },
+ filepicker: dialog.NewFilepickerCmp(app),
+ }
+
+ model.RegisterCommand(dialog.Command{
+ ID: "init",
+ Title: "Initialize Project",
+ Description: "Create/Update the CONTEXT.md memory file",
+ Handler: func(cmd dialog.Command) tea.Cmd {
+ prompt := `Please analyze this codebase and create a CONTEXT.md file containing:
+1. Build/lint/test commands - especially for running a single test
+2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
+
+The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
+If there's already a CONTEXT.md, improve it.
+If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.`
+ return tea.Batch(
+ util.CmdHandler(chat.SendMsg{
+ Text: prompt,
+ }),
+ )
+ },
+ })
+
+ model.RegisterCommand(dialog.Command{
+ ID: "compact_conversation",
+ Title: "Compact Conversation",
+ Description: "Summarize the current session to save tokens",
+ Handler: func(cmd dialog.Command) tea.Cmd {
+ // Get the current session from the appModel
+ if model.currentPage != page.ChatPage {
+ status.Warn("Please navigate to a chat session first.")
+ return nil
+ }
+
+ // Return a message that will be handled by the chat page
+ status.Info("Compacting conversation...")
+ return util.CmdHandler(state.CompactSessionMsg{})
+ },
+ })
+
+ // Load custom commands
+ customCommands, err := dialog.LoadCustomCommands()
+ if err != nil {
+ slog.Warn("Failed to load custom commands", "error", err)
+ } else {
+ for _, cmd := range customCommands {
+ model.RegisterCommand(cmd)
+ }
+ }
+
+ return model
+}
diff --git a/packages/tui/internal/tui/util/util.go b/packages/tui/internal/tui/util/util.go
new file mode 100644
index 000000000..207382d1e
--- /dev/null
+++ b/packages/tui/internal/tui/util/util.go
@@ -0,0 +1,18 @@
+package util
+
+import (
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+func CmdHandler(msg tea.Msg) tea.Cmd {
+ return func() tea.Msg {
+ return msg
+ }
+}
+
+func Clamp(v, low, high int) int {
+ if high < low {
+ low, high = high, low
+ }
+ return min(high, max(low, v))
+}
diff --git a/packages/tui/internal/version/version.go b/packages/tui/internal/version/version.go
new file mode 100644
index 000000000..69fd5282b
--- /dev/null
+++ b/packages/tui/internal/version/version.go
@@ -0,0 +1,25 @@
+package version
+
+import "runtime/debug"
+
+// Build-time parameters set via -ldflags
+var Version = "unknown"
+
+// A user may install pug using `go install github.com/sst/opencode@latest`.
+// without -ldflags, in which case the version above is unset. As a workaround
+// we use the embedded build version that *is* set when using `go install` (and
+// is only set for `go install` and not for `go build`).
+func init() {
+ info, ok := debug.ReadBuildInfo()
+ if !ok {
+ // < go v1.18
+ return
+ }
+ mainVersion := info.Main.Version
+ if mainVersion == "" || mainVersion == "(devel)" {
+ // bin not built using `go install`
+ return
+ }
+ // bin built using `go install`
+ Version = mainVersion
+}
diff --git a/packages/tui/main.go b/packages/tui/main.go
new file mode 100644
index 000000000..d81e6f8f9
--- /dev/null
+++ b/packages/tui/main.go
@@ -0,0 +1,9 @@
+package main
+
+import (
+ "github.com/sst/opencode/cmd"
+)
+
+func main() {
+ cmd.Execute()
+}
diff --git a/packages/tui/pkg/client/.gitignore b/packages/tui/pkg/client/.gitignore
new file mode 100644
index 000000000..c56971e89
--- /dev/null
+++ b/packages/tui/pkg/client/.gitignore
@@ -0,0 +1,2 @@
+gen
+generated-*.go
diff --git a/packages/tui/pkg/client/client.go b/packages/tui/pkg/client/client.go
new file mode 100644
index 000000000..bf2d5038e
--- /dev/null
+++ b/packages/tui/pkg/client/client.go
@@ -0,0 +1,4 @@
+package client
+
+//go:generate bun run ../../js/src/index.ts generate
+//go:generate go tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --package=client --generate=types,client,models -o generated-client.go ./gen/openapi.json
diff --git a/packages/tui/pkg/client/event.go b/packages/tui/pkg/client/event.go
new file mode 100644
index 000000000..f5aab2d26
--- /dev/null
+++ b/packages/tui/pkg/client/event.go
@@ -0,0 +1,53 @@
+package client
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "net/http"
+ "strings"
+)
+
+func (c *Client) Event(ctx context.Context) (<-chan any, error) {
+ events := make(chan any)
+ req, err := http.NewRequestWithContext(ctx, "GET", c.Server+"event", nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ go func() {
+ defer close(events)
+ defer resp.Body.Close()
+
+ scanner := bufio.NewScanner(resp.Body)
+ for scanner.Scan() {
+ line := scanner.Text()
+ if strings.HasPrefix(line, "data: ") {
+ data := strings.TrimPrefix(line, "data: ")
+
+ var event Event
+ if err := json.Unmarshal([]byte(data), &event); err != nil {
+ continue
+ }
+
+ val, err := event.ValueByDiscriminator()
+ if err != nil {
+ continue
+ }
+
+ select {
+ case events <- val:
+ case <-ctx.Done():
+ return
+ }
+ }
+ }
+ }()
+
+ return events, nil
+}
diff --git a/packages/tui/pkg/tui/theme/opencode.go b/packages/tui/pkg/tui/theme/opencode.go
new file mode 100644
index 000000000..7ee6f15e5
--- /dev/null
+++ b/packages/tui/pkg/tui/theme/opencode.go
@@ -0,0 +1,276 @@
+package theme
+
+import (
+ "github.com/charmbracelet/lipgloss"
+)
+
+// OpenCodeTheme implements the Theme interface with OpenCode brand colors.
+// It provides both dark and light variants.
+type OpenCodeTheme struct {
+ BaseTheme
+}
+
+// NewOpenCodeTheme creates a new instance of the OpenCode theme.
+func NewOpenCodeTheme() *OpenCodeTheme {
+ // OpenCode color palette
+ // Dark mode colors
+ darkBackground := "#212121"
+ darkCurrentLine := "#252525"
+ darkSelection := "#303030"
+ darkForeground := "#e0e0e0"
+ darkComment := "#6a6a6a"
+ darkPrimary := "#fab283" // Primary orange/gold
+ darkSecondary := "#5c9cf5" // Secondary blue
+ darkAccent := "#9d7cd8" // Accent purple
+ darkRed := "#e06c75" // Error red
+ darkOrange := "#f5a742" // Warning orange
+ darkGreen := "#7fd88f" // Success green
+ darkCyan := "#56b6c2" // Info cyan
+ darkYellow := "#e5c07b" // Emphasized text
+ darkBorder := "#4b4c5c" // Border color
+
+ // Light mode colors
+ lightBackground := "#f8f8f8"
+ lightCurrentLine := "#f0f0f0"
+ lightSelection := "#e5e5e6"
+ lightForeground := "#2a2a2a"
+ lightComment := "#8a8a8a"
+ lightPrimary := "#3b7dd8" // Primary blue
+ lightSecondary := "#7b5bb6" // Secondary purple
+ lightAccent := "#d68c27" // Accent orange/gold
+ lightRed := "#d1383d" // Error red
+ lightOrange := "#d68c27" // Warning orange
+ lightGreen := "#3d9a57" // Success green
+ lightCyan := "#318795" // Info cyan
+ lightYellow := "#b0851f" // Emphasized text
+ lightBorder := "#d3d3d3" // Border color
+
+ theme := &OpenCodeTheme{}
+
+ // Base colors
+ theme.PrimaryColor = lipgloss.AdaptiveColor{
+ Dark: darkPrimary,
+ Light: lightPrimary,
+ }
+ theme.SecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkSecondary,
+ Light: lightSecondary,
+ }
+ theme.AccentColor = lipgloss.AdaptiveColor{
+ Dark: darkAccent,
+ Light: lightAccent,
+ }
+
+ // Status colors
+ theme.ErrorColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.WarningColor = lipgloss.AdaptiveColor{
+ Dark: darkOrange,
+ Light: lightOrange,
+ }
+ theme.SuccessColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.InfoColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+
+ // Text colors
+ theme.TextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.TextMutedColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+
+ // Background colors
+ theme.BackgroundColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
+ Dark: darkCurrentLine,
+ Light: lightCurrentLine,
+ }
+ theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
+ Dark: "#121212", // Slightly darker than background
+ Light: "#ffffff", // Slightly lighter than background
+ }
+
+ // Border colors
+ theme.BorderNormalColor = lipgloss.AdaptiveColor{
+ Dark: darkBorder,
+ Light: lightBorder,
+ }
+ theme.BorderFocusedColor = lipgloss.AdaptiveColor{
+ Dark: darkPrimary,
+ Light: lightPrimary,
+ }
+ theme.BorderDimColor = lipgloss.AdaptiveColor{
+ Dark: darkSelection,
+ Light: lightSelection,
+ }
+
+ // Diff view colors
+ theme.DiffAddedColor = lipgloss.AdaptiveColor{
+ Dark: "#478247",
+ Light: "#2E7D32",
+ }
+ theme.DiffRemovedColor = lipgloss.AdaptiveColor{
+ Dark: "#7C4444",
+ Light: "#C62828",
+ }
+ theme.DiffContextColor = lipgloss.AdaptiveColor{
+ Dark: "#a0a0a0",
+ Light: "#757575",
+ }
+ theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
+ Dark: "#a0a0a0",
+ Light: "#757575",
+ }
+ theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
+ Dark: "#DAFADA",
+ Light: "#A5D6A7",
+ }
+ theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
+ Dark: "#FADADD",
+ Light: "#EF9A9A",
+ }
+ theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#303A30",
+ Light: "#E8F5E9",
+ }
+ theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
+ Dark: "#3A3030",
+ Light: "#FFEBEE",
+ }
+ theme.DiffContextBgColor = lipgloss.AdaptiveColor{
+ Dark: darkBackground,
+ Light: lightBackground,
+ }
+ theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
+ Dark: "#888888",
+ Light: "#9E9E9E",
+ }
+ theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#293229",
+ Light: "#C8E6C9",
+ }
+ theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
+ Dark: "#332929",
+ Light: "#FFCDD2",
+ }
+
+ // Markdown colors
+ theme.MarkdownTextColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+ theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
+ Dark: darkSecondary,
+ Light: lightSecondary,
+ }
+ theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
+ Dark: darkPrimary,
+ Light: lightPrimary,
+ }
+ theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
+ Dark: darkAccent,
+ Light: lightAccent,
+ }
+ theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
+ Dark: darkPrimary,
+ Light: lightPrimary,
+ }
+ theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownImageColor = lipgloss.AdaptiveColor{
+ Dark: darkPrimary,
+ Light: lightPrimary,
+ }
+ theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ // Syntax highlighting colors
+ theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
+ Dark: darkComment,
+ Light: lightComment,
+ }
+ theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
+ Dark: darkSecondary,
+ Light: lightSecondary,
+ }
+ theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
+ Dark: darkPrimary,
+ Light: lightPrimary,
+ }
+ theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
+ Dark: darkRed,
+ Light: lightRed,
+ }
+ theme.SyntaxStringColor = lipgloss.AdaptiveColor{
+ Dark: darkGreen,
+ Light: lightGreen,
+ }
+ theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
+ Dark: darkAccent,
+ Light: lightAccent,
+ }
+ theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
+ Dark: darkYellow,
+ Light: lightYellow,
+ }
+ theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
+ Dark: darkCyan,
+ Light: lightCyan,
+ }
+ theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
+ Dark: darkForeground,
+ Light: lightForeground,
+ }
+
+ return theme
+}
+
+func init() {
+ // Register the OpenCode theme with the theme manager
+ RegisterTheme("opencode", NewOpenCodeTheme())
+}
diff --git a/packages/web/.gitignore b/packages/web/.gitignore
new file mode 100644
index 000000000..6240da8b1
--- /dev/null
+++ b/packages/web/.gitignore
@@ -0,0 +1,21 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
diff --git a/packages/web/README.md b/packages/web/README.md
new file mode 100644
index 000000000..f9f6d31c6
--- /dev/null
+++ b/packages/web/README.md
@@ -0,0 +1,54 @@
+# Starlight Starter Kit: Basics
+
+[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build)
+
+```
+npm create astro@latest -- --template starlight
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
+[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics)
+[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs)
+
+> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
+
+## 🚀 Project Structure
+
+Inside of your Astro + Starlight project, you'll see the following folders and files:
+
+```
+.
+├── public/
+├── src/
+│ ├── assets/
+│ ├── content/
+│ │ ├── docs/
+│ └── content.config.ts
+├── astro.config.mjs
+├── package.json
+└── tsconfig.json
+```
+
+Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
+
+Images can be added to `src/assets/` and embedded in Markdown with a relative link.
+
+Static assets, like favicons, can be placed in the `public/` directory.
+
+## 🧞 Commands
+
+All commands are run from the root of the project, from a terminal:
+
+| Command | Action |
+| :------------------------ | :----------------------------------------------- |
+| `npm install` | Installs dependencies |
+| `npm run dev` | Starts local dev server at `localhost:4321` |
+| `npm run build` | Build your production site to `./dist/` |
+| `npm run preview` | Preview your build locally, before deploying |
+| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
+| `npm run astro -- --help` | Get help using the Astro CLI |
+
+## 👀 Want to learn more?
+
+Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs
new file mode 100644
index 000000000..bfe18acd8
--- /dev/null
+++ b/packages/web/astro.config.mjs
@@ -0,0 +1,69 @@
+// @ts-check
+import { defineConfig } from "astro/config";
+import starlight from "@astrojs/starlight";
+import solidJs from "@astrojs/solid-js";
+import theme from "toolbeam-docs-theme";
+import { rehypeHeadingIds } from "@astrojs/markdown-remark";
+import rehypeAutolinkHeadings from "rehype-autolink-headings";
+
+const discord = "https://discord.gg/sst";
+const github = "https://github.com/sst/opencode";
+
+// https://astro.build/config
+export default defineConfig({
+ devToolbar: {
+ enabled: false,
+ },
+ markdown: {
+ rehypePlugins: [
+ rehypeHeadingIds,
+ [rehypeAutolinkHeadings, { behavior: "wrap" }],
+ ],
+ },
+ integrations: [
+ solidJs(),
+ starlight({
+ title: "OpenCode",
+ expressiveCode: { themes: ["github-light", "github-dark"] },
+ social: [
+ { icon: "discord", label: "Discord", href: discord },
+ { icon: "github", label: "GitHub", href: github },
+ ],
+ editLink: {
+ baseUrl: `${github}/edit/master/www/`,
+ },
+ markdown: {
+ headingLinks: false,
+ },
+ customCss: [
+ "./src/styles/custom.css",
+ ],
+ logo: {
+ light: "./src/assets/logo-light.svg",
+ dark: "./src/assets/logo-dark.svg",
+ replacesTitle: true,
+ },
+ sidebar: [
+ "docs",
+ "docs/cli",
+ "docs/config",
+ "docs/models",
+ "docs/themes",
+ "docs/shortcuts",
+ "docs/lsp-servers",
+ "docs/mcp-servers",
+ ],
+ components: {
+ Hero: "./src/components/Hero.astro",
+ Header: "./src/components/Header.astro",
+ },
+ plugins: [theme({
+ // Optionally, add your own header links
+ headerLinks: [
+ { name: "Home", url: "/" },
+ { name: "Docs", url: "/docs/" },
+ ],
+ })],
+ }),
+ ],
+});
diff --git a/packages/web/package.json b/packages/web/package.json
new file mode 100644
index 000000000..33b3dc4d2
--- /dev/null
+++ b/packages/web/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "@opencode/web",
+ "type": "module",
+ "version": "0.0.1",
+ "scripts": {
+ "dev": "astro dev",
+ "start": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/markdown-remark": "^6.3.1",
+ "@astrojs/solid-js": "^5.1.0",
+ "@astrojs/starlight": "^0.34.3",
+ "@fontsource/ibm-plex-mono": "^5.2.5",
+ "@shikijs/transformers": "^3.4.2",
+ "@types/luxon": "^3.6.2",
+ "ai": "^5.0.0-alpha.2",
+ "astro": "^5.7.13",
+ "diff": "^8.0.2",
+ "luxon": "^3.6.1",
+ "rehype-autolink-headings": "^7.1.0",
+ "sharp": "^0.32.5",
+ "shiki": "^3.4.2",
+ "solid-js": "^1.9.7",
+ "toolbeam-docs-theme": "^0.2.4"
+ }
+}
diff --git a/packages/web/public/favicon.svg b/packages/web/public/favicon.svg
new file mode 100644
index 000000000..8b011db3f
--- /dev/null
+++ b/packages/web/public/favicon.svg
@@ -0,0 +1,3 @@
+<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.184 28.512C12.144 28.512 10.26 28.152 8.532 27.432C6.804 26.736 5.304 25.752 4.032 24.48C2.76 23.184 1.764 21.672 1.044 19.944C0.348 18.216 0 16.332 0 14.292C0 12.228 0.36 10.332 1.08 8.604C1.8 6.852 2.796 5.34 4.068 4.068C5.364 2.772 6.876 1.776 8.604 1.08C10.332 0.36 12.192 0 14.184 0C16.224 0 18.108 0.36 19.836 1.08C21.588 1.8 23.112 2.808 24.408 4.104C25.704 5.4 26.712 6.924 27.432 8.676C28.152 10.404 28.512 12.276 28.512 14.292C28.512 16.284 28.152 18.144 27.432 19.872C26.736 21.6 25.74 23.112 24.444 24.408C23.172 25.704 21.66 26.712 19.908 27.432C18.18 28.152 16.272 28.512 14.184 28.512ZM13.104 25.74V2.772L11.016 3.168V25.416L13.104 25.74ZM15.3 25.74L17.388 25.416V3.168L15.3 2.772V25.74ZM8.82 24.516V3.996C8.076 4.404 7.392 4.884 6.768 5.436V23.148C7.104 23.364 7.44 23.592 7.776 23.832C8.136 24.048 8.484 24.276 8.82 24.516ZM19.548 24.552L21.636 23.148V5.364L19.548 3.996V24.552ZM23.868 20.736C25.14 18.888 25.776 16.74 25.776 14.292C25.776 11.964 25.14 9.816 23.868 7.848V20.736ZM4.608 20.736V7.884C3.384 9.756 2.772 11.892 2.772 14.292C2.772 15.468 2.916 16.596 3.204 17.676C3.516 18.732 3.984 19.752 4.608 20.736Z" fill="black"/>
+</svg>
diff --git a/packages/web/public/social-share.png b/packages/web/public/social-share.png
new file mode 100644
index 000000000..23905086a
--- /dev/null
+++ b/packages/web/public/social-share.png
Binary files differ
diff --git a/packages/web/src/assets/lander/check.svg b/packages/web/src/assets/lander/check.svg
new file mode 100644
index 000000000..22de6f2a8
--- /dev/null
+++ b/packages/web/src/assets/lander/check.svg
@@ -0,0 +1,2 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M9 16.17L5.53 12.7a.996.996 0 1 0-1.41 1.41l4.18 4.18c.39.39 1.02.39 1.41 0L20.29 7.71a.996.996 0 1 0-1.41-1.41z"/></svg>
+
diff --git a/packages/web/src/assets/lander/copy.svg b/packages/web/src/assets/lander/copy.svg
new file mode 100644
index 000000000..f1baac30a
--- /dev/null
+++ b/packages/web/src/assets/lander/copy.svg
@@ -0,0 +1,2 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512"><rect width="336" height="336" x="128" y="128" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32" rx="57" ry="57"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="m383.5 128l.5-24a56.16 56.16 0 0 0-56-56H112a64.19 64.19 0 0 0-64 64v216a56.16 56.16 0 0 0 56 56h24"/></svg>
+
diff --git a/packages/web/src/assets/logo-dark.svg b/packages/web/src/assets/logo-dark.svg
new file mode 100644
index 000000000..8fd212081
--- /dev/null
+++ b/packages/web/src/assets/logo-dark.svg
@@ -0,0 +1,11 @@
+<svg width="187" height="32" viewBox="0 0 187 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.184 28.512C12.144 28.512 10.26 28.152 8.532 27.432C6.804 26.736 5.304 25.752 4.032 24.48C2.76 23.184 1.764 21.672 1.044 19.944C0.348 18.216 0 16.332 0 14.292C0 12.228 0.36 10.332 1.08 8.604C1.8 6.852 2.796 5.34 4.068 4.068C5.364 2.772 6.876 1.776 8.604 1.08C10.332 0.36 12.192 0 14.184 0C16.224 0 18.108 0.36 19.836 1.08C21.588 1.8 23.112 2.808 24.408 4.104C25.704 5.4 26.712 6.924 27.432 8.676C28.152 10.404 28.512 12.276 28.512 14.292C28.512 16.284 28.152 18.144 27.432 19.872C26.736 21.6 25.74 23.112 24.444 24.408C23.172 25.704 21.66 26.712 19.908 27.432C18.18 28.152 16.272 28.512 14.184 28.512ZM13.104 25.74V2.772L11.016 3.168V25.416L13.104 25.74ZM15.3 25.74L17.388 25.416V3.168L15.3 2.772V25.74ZM8.82 24.516V3.996C8.076 4.404 7.392 4.884 6.768 5.436V23.148C7.104 23.364 7.44 23.592 7.776 23.832C8.136 24.048 8.484 24.276 8.82 24.516ZM19.548 24.552L21.636 23.148V5.364L19.548 3.996V24.552ZM23.868 20.736C25.14 18.888 25.776 16.74 25.776 14.292C25.776 11.964 25.14 9.816 23.868 7.848V20.736ZM4.608 20.736V7.884C3.384 9.756 2.772 11.892 2.772 14.292C2.772 15.468 2.916 16.596 3.204 17.676C3.516 18.732 3.984 19.752 4.608 20.736Z" fill="white"/>
+<path d="M45.628 25.432C42.556 25.432 40.252 24.688 38.716 23.2C37.204 21.688 36.448 19.552 36.448 16.792V9.62799C36.448 6.84399 37.204 4.70799 38.716 3.21999C40.252 1.73199 42.556 0.987986 45.628 0.987986C48.7 0.987986 50.992 1.73199 52.504 3.21999C54.04 4.70799 54.808 6.84399 54.808 9.62799V16.792C54.808 19.552 54.04 21.688 52.504 23.2C50.992 24.688 48.7 25.432 45.628 25.432ZM45.628 22.228C47.452 22.228 48.832 21.76 49.768 20.824C50.704 19.864 51.172 18.484 51.172 16.684V9.73599C51.172 7.91199 50.704 6.53199 49.768 5.59599C48.832 4.65999 47.452 4.19199 45.628 4.19199C43.828 4.19199 42.448 4.65999 41.488 5.59599C40.552 6.53199 40.084 7.91199 40.084 9.73599V16.684C40.084 18.484 40.552 19.864 41.488 20.824C42.448 21.76 43.828 22.228 45.628 22.228Z" fill="white"/>
+<path d="M67.0294 25.432C66.1654 25.432 65.2414 25.312 64.2574 25.072C63.2734 24.856 62.4694 24.544 61.8454 24.136L61.7734 21.184C62.4214 21.544 63.1414 21.832 63.9334 22.048C64.7254 22.24 65.4574 22.336 66.1294 22.336C67.4254 22.336 68.3614 21.964 68.9374 21.22C69.5374 20.452 69.8374 19.312 69.8374 17.8V13.912C69.8374 12.592 69.6094 11.632 69.1534 11.032C68.6974 10.408 68.0014 10.096 67.0654 10.096C66.2734 10.096 65.4694 10.336 64.6534 10.816C63.8374 11.272 62.8174 12.064 61.5934 13.192L61.4854 10.168C62.2534 9.44799 62.9854 8.84799 63.6814 8.36799C64.3774 7.88799 65.0854 7.52799 65.8054 7.28799C66.5254 7.04799 67.2934 6.92799 68.1094 6.92799C69.8614 6.92799 71.1814 7.46799 72.0694 8.54799C72.9814 9.62799 73.4374 11.272 73.4374 13.48V18.196C73.4374 20.572 72.8854 22.372 71.7814 23.596C70.7014 24.82 69.1174 25.432 67.0294 25.432ZM59.1094 31.66C58.8454 31.66 58.7134 31.528 58.7134 31.264V11.536C58.7134 10.936 58.6894 10.3 58.6414 9.62799C58.5934 8.95599 58.5334 8.35599 58.4614 7.82799C58.4374 7.51599 58.5694 7.35999 58.8574 7.35999H61.4134C61.6774 7.35999 61.8214 7.47999 61.8454 7.71999C61.8934 7.91199 61.9414 8.16399 61.9894 8.47599C62.0374 8.78799 62.0734 9.09999 62.0974 9.41199C62.1214 9.69999 62.1334 9.91599 62.1334 10.06L62.3134 11.968V31.264C62.3134 31.528 62.1814 31.66 61.9174 31.66H59.1094Z" fill="white"/>
+<path d="M84.0237 25.432C81.5757 25.432 79.7157 24.88 78.4437 23.776C77.1957 22.672 76.5717 21.028 76.5717 18.844V14.056C76.5717 11.728 77.1957 9.96399 78.4437 8.76399C79.6917 7.53999 81.5397 6.92799 83.9877 6.92799C86.4117 6.92799 88.2477 7.51599 89.4957 8.69199C90.7437 9.86799 91.3677 11.584 91.3677 13.84V16.648C91.3677 16.912 91.2477 17.044 91.0077 17.044H80.1717V18.196C80.1717 19.66 80.4837 20.728 81.1077 21.4C81.7317 22.072 82.7397 22.408 84.1317 22.408C85.1877 22.408 86.0037 22.24 86.5797 21.904C87.1557 21.568 87.4437 21.052 87.4437 20.356C87.4437 20.092 87.5877 19.96 87.8757 19.96H90.6477C90.8637 19.96 90.9957 20.08 91.0437 20.32C91.1397 21.928 90.5637 23.188 89.3157 24.1C88.0677 24.988 86.3037 25.432 84.0237 25.432ZM80.1717 14.308H87.8037V14.128C87.8037 12.688 87.4917 11.632 86.8677 10.96C86.2437 10.288 85.2957 9.95199 84.0237 9.95199C82.7277 9.95199 81.7557 10.3 81.1077 10.996C80.4837 11.692 80.1717 12.736 80.1717 14.128V14.308Z" fill="white"/>
+<path d="M106.647 25C106.383 25 106.251 24.868 106.251 24.604V13.3C106.251 12.196 106.023 11.392 105.567 10.888C105.135 10.36 104.451 10.096 103.515 10.096C102.627 10.096 101.751 10.36 100.887 10.888C100.023 11.392 98.9906 12.244 97.7906 13.444L97.7186 10.528C98.4866 9.71199 99.2306 9.03999 99.9506 8.51199C100.671 7.98399 101.403 7.58799 102.147 7.32399C102.915 7.05999 103.719 6.92799 104.559 6.92799C106.287 6.92799 107.595 7.43199 108.483 8.43999C109.395 9.42399 109.851 10.912 109.851 12.904V24.604C109.851 24.868 109.731 25 109.491 25H106.647ZM95.2346 25C94.9706 25 94.8386 24.868 94.8386 24.604V11.824C94.8386 11.152 94.8026 10.444 94.7306 9.69999C94.6826 8.93199 94.6346 8.31999 94.5866 7.86399C94.5386 7.52799 94.6706 7.35999 94.9826 7.35999H97.5386C97.7786 7.35999 97.9226 7.47999 97.9706 7.71999C98.0186 7.93599 98.0666 8.23599 98.1146 8.61999C98.1626 9.00399 98.2106 9.41199 98.2586 9.84399C98.3066 10.276 98.3306 10.648 98.3306 10.96L98.4386 12.112V24.604C98.4386 24.868 98.3066 25 98.0426 25H95.2346Z" fill="white"/>
+<path d="M122.87 25.432C119.894 25.432 117.626 24.7 116.066 23.236C114.506 21.748 113.726 19.6 113.726 16.792V9.66399C113.726 6.83199 114.506 4.68399 116.066 3.21999C117.65 1.73199 119.93 0.987986 122.906 0.987986C124.85 0.987986 126.542 1.31199 127.982 1.95999C129.422 2.60799 130.49 3.54399 131.186 4.76799C131.906 5.96799 132.146 7.40799 131.906 9.08799C131.882 9.23199 131.846 9.35199 131.798 9.44799C131.75 9.54399 131.654 9.59199 131.51 9.59199H128.63C128.366 9.59199 128.246 9.45999 128.27 9.19599C128.342 7.61199 127.922 6.37599 127.01 5.48799C126.098 4.59999 124.742 4.15599 122.942 4.15599C121.142 4.15599 119.762 4.62399 118.802 5.55999C117.842 6.49599 117.362 7.86399 117.362 9.66399V16.756C117.362 18.556 117.842 19.924 118.802 20.86C119.762 21.796 121.142 22.264 122.942 22.264C124.766 22.264 126.146 21.82 127.082 20.932C128.042 20.02 128.438 18.784 128.27 17.224C128.246 16.96 128.366 16.828 128.63 16.828H131.474C131.714 16.828 131.858 16.996 131.906 17.332C132.05 18.964 131.762 20.392 131.042 21.616C130.346 22.816 129.29 23.752 127.874 24.424C126.458 25.096 124.79 25.432 122.87 25.432Z" fill="white"/>
+<path d="M142.558 25.432C140.086 25.432 138.19 24.832 136.87 23.632C135.55 22.432 134.89 20.62 134.89 18.196V14.164C134.89 11.74 135.538 9.92799 136.834 8.72799C138.154 7.52799 140.062 6.92799 142.558 6.92799C145.03 6.92799 146.914 7.52799 148.21 8.72799C149.53 9.92799 150.19 11.74 150.19 14.164V18.196C150.19 20.62 149.542 22.432 148.246 23.632C146.95 24.832 145.054 25.432 142.558 25.432ZM142.558 22.3C143.974 22.3 144.994 21.94 145.618 21.22C146.266 20.5 146.59 19.384 146.59 17.872V14.488C146.59 13 146.266 11.896 145.618 11.176C144.994 10.432 143.974 10.06 142.558 10.06C141.118 10.06 140.074 10.432 139.426 11.176C138.802 11.896 138.49 13 138.49 14.488V17.872C138.49 19.384 138.802 20.5 139.426 21.22C140.074 21.94 141.118 22.3 142.558 22.3Z" fill="white"/>
+<path d="M158.473 25.432C156.793 25.432 155.509 24.892 154.621 23.812C153.733 22.708 153.289 21.064 153.289 18.88V14.2C153.289 11.848 153.853 10.048 154.981 8.79999C156.133 7.55199 157.729 6.92799 159.769 6.92799C160.657 6.92799 161.605 7.04799 162.613 7.28799C163.645 7.52799 164.449 7.83999 165.025 8.22399L165.169 11.212C164.449 10.828 163.657 10.54 162.793 10.348C161.953 10.132 161.209 10.024 160.561 10.024C159.337 10.024 158.413 10.408 157.789 11.176C157.189 11.92 156.889 13.048 156.889 14.56V18.772C156.889 19.948 157.117 20.836 157.573 21.436C158.029 22.012 158.713 22.3 159.625 22.3C160.201 22.3 160.765 22.192 161.317 21.976C161.869 21.736 162.469 21.376 163.117 20.896C163.765 20.392 164.497 19.72 165.313 18.88L165.349 21.832C164.581 22.6 163.837 23.26 163.117 23.812C162.397 24.34 161.665 24.736 160.921 25C160.177 25.288 159.361 25.432 158.473 25.432ZM165.601 25C165.313 25 165.157 24.868 165.133 24.604C165.085 24.244 165.025 23.8 164.953 23.272C164.881 22.72 164.857 22.252 164.881 21.868L164.629 20.104V0.807986C164.629 0.543987 164.761 0.411987 165.025 0.411987H167.833C168.097 0.411987 168.229 0.543987 168.229 0.807986V20.824C168.229 21.184 168.241 21.604 168.265 22.084C168.289 22.54 168.313 22.984 168.337 23.416C168.385 23.848 168.421 24.208 168.445 24.496C168.517 24.832 168.397 25 168.085 25H165.601Z" fill="white"/>
+<path d="M179.171 25.432C176.723 25.432 174.863 24.88 173.591 23.776C172.343 22.672 171.719 21.028 171.719 18.844V14.056C171.719 11.728 172.343 9.96399 173.591 8.76399C174.839 7.53999 176.687 6.92799 179.135 6.92799C181.559 6.92799 183.395 7.51599 184.643 8.69199C185.891 9.86799 186.515 11.584 186.515 13.84V16.648C186.515 16.912 186.395 17.044 186.155 17.044H175.319V18.196C175.319 19.66 175.631 20.728 176.255 21.4C176.879 22.072 177.887 22.408 179.279 22.408C180.335 22.408 181.151 22.24 181.727 21.904C182.303 21.568 182.591 21.052 182.591 20.356C182.591 20.092 182.735 19.96 183.023 19.96H185.795C186.011 19.96 186.143 20.08 186.191 20.32C186.287 21.928 185.711 23.188 184.463 24.1C183.215 24.988 181.451 25.432 179.171 25.432ZM175.319 14.308H182.951V14.128C182.951 12.688 182.639 11.632 182.015 10.96C181.391 10.288 180.443 9.95199 179.171 9.95199C177.875 9.95199 176.903 10.3 176.255 10.996C175.631 11.692 175.319 12.736 175.319 14.128V14.308Z" fill="white"/>
+</svg>
diff --git a/packages/web/src/assets/logo-light.svg b/packages/web/src/assets/logo-light.svg
new file mode 100644
index 000000000..0a9007e1a
--- /dev/null
+++ b/packages/web/src/assets/logo-light.svg
@@ -0,0 +1,11 @@
+<svg width="187" height="32" viewBox="0 0 187 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.184 28.512C12.144 28.512 10.26 28.152 8.532 27.432C6.804 26.736 5.304 25.752 4.032 24.48C2.76 23.184 1.764 21.672 1.044 19.944C0.348 18.216 0 16.332 0 14.292C0 12.228 0.36 10.332 1.08 8.604C1.8 6.852 2.796 5.34 4.068 4.068C5.364 2.772 6.876 1.776 8.604 1.08C10.332 0.36 12.192 0 14.184 0C16.224 0 18.108 0.36 19.836 1.08C21.588 1.8 23.112 2.808 24.408 4.104C25.704 5.4 26.712 6.924 27.432 8.676C28.152 10.404 28.512 12.276 28.512 14.292C28.512 16.284 28.152 18.144 27.432 19.872C26.736 21.6 25.74 23.112 24.444 24.408C23.172 25.704 21.66 26.712 19.908 27.432C18.18 28.152 16.272 28.512 14.184 28.512ZM13.104 25.74V2.772L11.016 3.168V25.416L13.104 25.74ZM15.3 25.74L17.388 25.416V3.168L15.3 2.772V25.74ZM8.82 24.516V3.996C8.076 4.404 7.392 4.884 6.768 5.436V23.148C7.104 23.364 7.44 23.592 7.776 23.832C8.136 24.048 8.484 24.276 8.82 24.516ZM19.548 24.552L21.636 23.148V5.364L19.548 3.996V24.552ZM23.868 20.736C25.14 18.888 25.776 16.74 25.776 14.292C25.776 11.964 25.14 9.816 23.868 7.848V20.736ZM4.608 20.736V7.884C3.384 9.756 2.772 11.892 2.772 14.292C2.772 15.468 2.916 16.596 3.204 17.676C3.516 18.732 3.984 19.752 4.608 20.736Z" fill="black"/>
+<path d="M45.628 25.432C42.556 25.432 40.252 24.688 38.716 23.2C37.204 21.688 36.448 19.552 36.448 16.792V9.62799C36.448 6.84399 37.204 4.70799 38.716 3.21999C40.252 1.73199 42.556 0.987986 45.628 0.987986C48.7 0.987986 50.992 1.73199 52.504 3.21999C54.04 4.70799 54.808 6.84399 54.808 9.62799V16.792C54.808 19.552 54.04 21.688 52.504 23.2C50.992 24.688 48.7 25.432 45.628 25.432ZM45.628 22.228C47.452 22.228 48.832 21.76 49.768 20.824C50.704 19.864 51.172 18.484 51.172 16.684V9.73599C51.172 7.91199 50.704 6.53199 49.768 5.59599C48.832 4.65999 47.452 4.19199 45.628 4.19199C43.828 4.19199 42.448 4.65999 41.488 5.59599C40.552 6.53199 40.084 7.91199 40.084 9.73599V16.684C40.084 18.484 40.552 19.864 41.488 20.824C42.448 21.76 43.828 22.228 45.628 22.228Z" fill="black"/>
+<path d="M67.0294 25.432C66.1654 25.432 65.2414 25.312 64.2574 25.072C63.2734 24.856 62.4694 24.544 61.8454 24.136L61.7734 21.184C62.4214 21.544 63.1414 21.832 63.9334 22.048C64.7254 22.24 65.4574 22.336 66.1294 22.336C67.4254 22.336 68.3614 21.964 68.9374 21.22C69.5374 20.452 69.8374 19.312 69.8374 17.8V13.912C69.8374 12.592 69.6094 11.632 69.1534 11.032C68.6974 10.408 68.0014 10.096 67.0654 10.096C66.2734 10.096 65.4694 10.336 64.6534 10.816C63.8374 11.272 62.8174 12.064 61.5934 13.192L61.4854 10.168C62.2534 9.44799 62.9854 8.84799 63.6814 8.36799C64.3774 7.88799 65.0854 7.52799 65.8054 7.28799C66.5254 7.04799 67.2934 6.92799 68.1094 6.92799C69.8614 6.92799 71.1814 7.46799 72.0694 8.54799C72.9814 9.62799 73.4374 11.272 73.4374 13.48V18.196C73.4374 20.572 72.8854 22.372 71.7814 23.596C70.7014 24.82 69.1174 25.432 67.0294 25.432ZM59.1094 31.66C58.8454 31.66 58.7134 31.528 58.7134 31.264V11.536C58.7134 10.936 58.6894 10.3 58.6414 9.62799C58.5934 8.95599 58.5334 8.35599 58.4614 7.82799C58.4374 7.51599 58.5694 7.35999 58.8574 7.35999H61.4134C61.6774 7.35999 61.8214 7.47999 61.8454 7.71999C61.8934 7.91199 61.9414 8.16399 61.9894 8.47599C62.0374 8.78799 62.0734 9.09999 62.0974 9.41199C62.1214 9.69999 62.1334 9.91599 62.1334 10.06L62.3134 11.968V31.264C62.3134 31.528 62.1814 31.66 61.9174 31.66H59.1094Z" fill="black"/>
+<path d="M84.0237 25.432C81.5757 25.432 79.7157 24.88 78.4437 23.776C77.1957 22.672 76.5717 21.028 76.5717 18.844V14.056C76.5717 11.728 77.1957 9.96399 78.4437 8.76399C79.6917 7.53999 81.5397 6.92799 83.9877 6.92799C86.4117 6.92799 88.2477 7.51599 89.4957 8.69199C90.7437 9.86799 91.3677 11.584 91.3677 13.84V16.648C91.3677 16.912 91.2477 17.044 91.0077 17.044H80.1717V18.196C80.1717 19.66 80.4837 20.728 81.1077 21.4C81.7317 22.072 82.7397 22.408 84.1317 22.408C85.1877 22.408 86.0037 22.24 86.5797 21.904C87.1557 21.568 87.4437 21.052 87.4437 20.356C87.4437 20.092 87.5877 19.96 87.8757 19.96H90.6477C90.8637 19.96 90.9957 20.08 91.0437 20.32C91.1397 21.928 90.5637 23.188 89.3157 24.1C88.0677 24.988 86.3037 25.432 84.0237 25.432ZM80.1717 14.308H87.8037V14.128C87.8037 12.688 87.4917 11.632 86.8677 10.96C86.2437 10.288 85.2957 9.95199 84.0237 9.95199C82.7277 9.95199 81.7557 10.3 81.1077 10.996C80.4837 11.692 80.1717 12.736 80.1717 14.128V14.308Z" fill="black"/>
+<path d="M106.647 25C106.383 25 106.251 24.868 106.251 24.604V13.3C106.251 12.196 106.023 11.392 105.567 10.888C105.135 10.36 104.451 10.096 103.515 10.096C102.627 10.096 101.751 10.36 100.887 10.888C100.023 11.392 98.9906 12.244 97.7906 13.444L97.7186 10.528C98.4866 9.71199 99.2306 9.03999 99.9506 8.51199C100.671 7.98399 101.403 7.58799 102.147 7.32399C102.915 7.05999 103.719 6.92799 104.559 6.92799C106.287 6.92799 107.595 7.43199 108.483 8.43999C109.395 9.42399 109.851 10.912 109.851 12.904V24.604C109.851 24.868 109.731 25 109.491 25H106.647ZM95.2346 25C94.9706 25 94.8386 24.868 94.8386 24.604V11.824C94.8386 11.152 94.8026 10.444 94.7306 9.69999C94.6826 8.93199 94.6346 8.31999 94.5866 7.86399C94.5386 7.52799 94.6706 7.35999 94.9826 7.35999H97.5386C97.7786 7.35999 97.9226 7.47999 97.9706 7.71999C98.0186 7.93599 98.0666 8.23599 98.1146 8.61999C98.1626 9.00399 98.2106 9.41199 98.2586 9.84399C98.3066 10.276 98.3306 10.648 98.3306 10.96L98.4386 12.112V24.604C98.4386 24.868 98.3066 25 98.0426 25H95.2346Z" fill="black"/>
+<path d="M122.87 25.432C119.894 25.432 117.626 24.7 116.066 23.236C114.506 21.748 113.726 19.6 113.726 16.792V9.66399C113.726 6.83199 114.506 4.68399 116.066 3.21999C117.65 1.73199 119.93 0.987986 122.906 0.987986C124.85 0.987986 126.542 1.31199 127.982 1.95999C129.422 2.60799 130.49 3.54399 131.186 4.76799C131.906 5.96799 132.146 7.40799 131.906 9.08799C131.882 9.23199 131.846 9.35199 131.798 9.44799C131.75 9.54399 131.654 9.59199 131.51 9.59199H128.63C128.366 9.59199 128.246 9.45999 128.27 9.19599C128.342 7.61199 127.922 6.37599 127.01 5.48799C126.098 4.59999 124.742 4.15599 122.942 4.15599C121.142 4.15599 119.762 4.62399 118.802 5.55999C117.842 6.49599 117.362 7.86399 117.362 9.66399V16.756C117.362 18.556 117.842 19.924 118.802 20.86C119.762 21.796 121.142 22.264 122.942 22.264C124.766 22.264 126.146 21.82 127.082 20.932C128.042 20.02 128.438 18.784 128.27 17.224C128.246 16.96 128.366 16.828 128.63 16.828H131.474C131.714 16.828 131.858 16.996 131.906 17.332C132.05 18.964 131.762 20.392 131.042 21.616C130.346 22.816 129.29 23.752 127.874 24.424C126.458 25.096 124.79 25.432 122.87 25.432Z" fill="black"/>
+<path d="M142.558 25.432C140.086 25.432 138.19 24.832 136.87 23.632C135.55 22.432 134.89 20.62 134.89 18.196V14.164C134.89 11.74 135.538 9.92799 136.834 8.72799C138.154 7.52799 140.062 6.92799 142.558 6.92799C145.03 6.92799 146.914 7.52799 148.21 8.72799C149.53 9.92799 150.19 11.74 150.19 14.164V18.196C150.19 20.62 149.542 22.432 148.246 23.632C146.95 24.832 145.054 25.432 142.558 25.432ZM142.558 22.3C143.974 22.3 144.994 21.94 145.618 21.22C146.266 20.5 146.59 19.384 146.59 17.872V14.488C146.59 13 146.266 11.896 145.618 11.176C144.994 10.432 143.974 10.06 142.558 10.06C141.118 10.06 140.074 10.432 139.426 11.176C138.802 11.896 138.49 13 138.49 14.488V17.872C138.49 19.384 138.802 20.5 139.426 21.22C140.074 21.94 141.118 22.3 142.558 22.3Z" fill="black"/>
+<path d="M158.473 25.432C156.793 25.432 155.509 24.892 154.621 23.812C153.733 22.708 153.289 21.064 153.289 18.88V14.2C153.289 11.848 153.853 10.048 154.981 8.79999C156.133 7.55199 157.729 6.92799 159.769 6.92799C160.657 6.92799 161.605 7.04799 162.613 7.28799C163.645 7.52799 164.449 7.83999 165.025 8.22399L165.169 11.212C164.449 10.828 163.657 10.54 162.793 10.348C161.953 10.132 161.209 10.024 160.561 10.024C159.337 10.024 158.413 10.408 157.789 11.176C157.189 11.92 156.889 13.048 156.889 14.56V18.772C156.889 19.948 157.117 20.836 157.573 21.436C158.029 22.012 158.713 22.3 159.625 22.3C160.201 22.3 160.765 22.192 161.317 21.976C161.869 21.736 162.469 21.376 163.117 20.896C163.765 20.392 164.497 19.72 165.313 18.88L165.349 21.832C164.581 22.6 163.837 23.26 163.117 23.812C162.397 24.34 161.665 24.736 160.921 25C160.177 25.288 159.361 25.432 158.473 25.432ZM165.601 25C165.313 25 165.157 24.868 165.133 24.604C165.085 24.244 165.025 23.8 164.953 23.272C164.881 22.72 164.857 22.252 164.881 21.868L164.629 20.104V0.807986C164.629 0.543987 164.761 0.411987 165.025 0.411987H167.833C168.097 0.411987 168.229 0.543987 168.229 0.807986V20.824C168.229 21.184 168.241 21.604 168.265 22.084C168.289 22.54 168.313 22.984 168.337 23.416C168.385 23.848 168.421 24.208 168.445 24.496C168.517 24.832 168.397 25 168.085 25H165.601Z" fill="black"/>
+<path d="M179.171 25.432C176.723 25.432 174.863 24.88 173.591 23.776C172.343 22.672 171.719 21.028 171.719 18.844V14.056C171.719 11.728 172.343 9.96399 173.591 8.76399C174.839 7.53999 176.687 6.92799 179.135 6.92799C181.559 6.92799 183.395 7.51599 184.643 8.69199C185.891 9.86799 186.515 11.584 186.515 13.84V16.648C186.515 16.912 186.395 17.044 186.155 17.044H175.319V18.196C175.319 19.66 175.631 20.728 176.255 21.4C176.879 22.072 177.887 22.408 179.279 22.408C180.335 22.408 181.151 22.24 181.727 21.904C182.303 21.568 182.591 21.052 182.591 20.356C182.591 20.092 182.735 19.96 183.023 19.96H185.795C186.011 19.96 186.143 20.08 186.191 20.32C186.287 21.928 185.711 23.188 184.463 24.1C183.215 24.988 181.451 25.432 179.171 25.432ZM175.319 14.308H182.951V14.128C182.951 12.688 182.639 11.632 182.015 10.96C181.391 10.288 180.443 9.95199 179.171 9.95199C177.875 9.95199 176.903 10.3 176.255 10.996C175.631 11.692 175.319 12.736 175.319 14.128V14.308Z" fill="black"/>
+</svg>
diff --git a/packages/web/src/components/CodeBlock.tsx b/packages/web/src/components/CodeBlock.tsx
new file mode 100644
index 000000000..17559ece1
--- /dev/null
+++ b/packages/web/src/components/CodeBlock.tsx
@@ -0,0 +1,47 @@
+import {
+ type JSX,
+ onCleanup,
+ splitProps,
+ createEffect,
+ createResource,
+} from "solid-js"
+import { codeToHtml } from "shiki"
+import { transformerNotationDiff } from '@shikijs/transformers'
+
+interface CodeBlockProps extends JSX.HTMLAttributes<HTMLDivElement> {
+ code: string
+ lang?: string
+}
+function CodeBlock(props: CodeBlockProps) {
+ const [local, rest] = splitProps(props, ["code", "lang"])
+ let containerRef!: HTMLDivElement
+
+ const [html] = createResource(async () => {
+ return (await codeToHtml(local.code, {
+ lang: local.lang || "text",
+ themes: {
+ light: 'github-light',
+ dark: 'github-dark',
+ },
+ transformers: [
+ transformerNotationDiff(),
+ ],
+ })) as string
+ })
+
+ onCleanup(() => {
+ if (containerRef) containerRef.innerHTML = ""
+ })
+
+ createEffect(() => {
+ if (html() && containerRef) {
+ containerRef.innerHTML = html() as string
+ }
+ })
+
+ return (
+ <div ref={containerRef} {...rest}></div>
+ )
+}
+
+export default CodeBlock
diff --git a/packages/web/src/components/DiffView.tsx b/packages/web/src/components/DiffView.tsx
new file mode 100644
index 000000000..44feef140
--- /dev/null
+++ b/packages/web/src/components/DiffView.tsx
@@ -0,0 +1,73 @@
+import { type Component, createSignal, onMount } from "solid-js"
+import { diffLines } from "diff"
+import CodeBlock from "./CodeBlock"
+import styles from "./diffview.module.css"
+
+type DiffRow = {
+ left: string
+ right: string
+ type: "added" | "removed" | "unchanged"
+}
+
+interface DiffViewProps {
+ oldCode: string
+ newCode: string
+ lang?: string
+ class?: string
+}
+
+const DiffView: Component<DiffViewProps> = (props) => {
+ const [rows, setRows] = createSignal<DiffRow[]>([])
+
+ onMount(() => {
+ const chunks = diffLines(props.oldCode, props.newCode)
+ const diffRows: DiffRow[] = []
+
+ for (const chunk of chunks) {
+ const lines = chunk.value.split(/\r?\n/)
+ if (lines.at(-1) === "") lines.pop()
+
+ for (const line of lines) {
+ diffRows.push({
+ left: chunk.removed ? line : chunk.added ? "" : line,
+ right: chunk.added ? line : chunk.removed ? "" : line,
+ type: chunk.added
+ ? "added"
+ : chunk.removed
+ ? "removed"
+ : "unchanged",
+ })
+ }
+ }
+
+ setRows(diffRows)
+ })
+
+ return (
+ <div class={`${styles.diff} ${props.class ?? ""}`}>
+ <div class={styles.column}>
+ {rows().map((r) => (
+ <CodeBlock
+ code={r.left}
+ lang={props.lang}
+ data-section="cell"
+ data-diff-type={r.type === "removed" ? "removed" : ""}
+ />
+ ))}
+ </div>
+
+ <div class={styles.column}>
+ {rows().map((r) => (
+ <CodeBlock
+ code={r.right}
+ lang={props.lang}
+ data-section="cell"
+ data-diff-type={r.type === "added" ? "added" : ""}
+ />
+ ))}
+ </div>
+ </div>
+ )
+}
+
+export default DiffView
diff --git a/packages/web/src/components/Header.astro b/packages/web/src/components/Header.astro
new file mode 100644
index 000000000..a45899ff8
--- /dev/null
+++ b/packages/web/src/components/Header.astro
@@ -0,0 +1,62 @@
+---
+import config from 'virtual:starlight/user-config';
+import { Icon } from '@astrojs/starlight/components';
+import { HeaderLinks } from 'toolbeam-docs-theme/components';
+import Default from 'toolbeam-docs-theme/overrides/Header.astro';
+import SiteTitle from '@astrojs/starlight/components/SiteTitle.astro';
+
+const path = Astro.url.pathname;
+
+const links = config.social || [];
+---
+
+{ path.startsWith("/share")
+ ? <div class="header sl-flex">
+ <div class="title-wrapper sl-flex">
+ <SiteTitle {...Astro.props} />
+ </div>
+ <div class="middle-group sl-flex">
+ <HeaderLinks {...Astro.props} />
+ </div>
+ </div>
+ : <Default {...Astro.props}><slot /></Default>
+}
+
+<style>
+ .header {
+ justify-content: space-between;
+ align-items: center;
+ height: 100%;
+ }
+
+ .title-wrapper {
+ /* Prevent long titles overflowing and covering the search and menu buttons on narrow viewports. */
+ overflow: clip;
+ /* Avoid clipping focus ring around link inside title wrapper. */
+ padding: calc(0.25rem + 2px) 0.25rem calc(0.25rem - 2px);
+ margin: -0.25rem;
+ }
+
+ .middle-group {
+ justify-content: flex-end;
+ gap: var(--sl-nav-gap);
+ }
+ @media (max-width: 50rem) {
+ :global(:root[data-has-sidebar]) {
+ .middle-group {
+ display: none;
+ }
+ }
+ }
+ @media (min-width: 50rem) {
+ .middle-group {
+ display: flex;
+ }
+ }
+</style>
+<style is:global>
+body > div.page > header {
+ border-color: var(--sl-color-divider);
+}
+</style>
+
diff --git a/packages/web/src/components/Hero.astro b/packages/web/src/components/Hero.astro
new file mode 100644
index 000000000..f80f85266
--- /dev/null
+++ b/packages/web/src/components/Hero.astro
@@ -0,0 +1,11 @@
+---
+import Default from '@astrojs/starlight/components/Hero.astro';
+import Lander from './Lander.astro';
+
+const { slug } = Astro.locals.starlightRoute.entry;
+---
+
+{ slug === ""
+ ? <Lander {...Astro.props} />
+ : <Default {...Astro.props}><slot /></Default>
+}
diff --git a/packages/web/src/components/Lander.astro b/packages/web/src/components/Lander.astro
new file mode 100644
index 000000000..d27358f8f
--- /dev/null
+++ b/packages/web/src/components/Lander.astro
@@ -0,0 +1,269 @@
+---
+import { Image } from 'astro:assets';
+import config from "virtual:starlight/user-config";
+import type { Props } from '@astrojs/starlight/props';
+
+import CopyIcon from "../assets/lander/copy.svg";
+import CheckIcon from "../assets/lander/check.svg";
+
+const { data } = Astro.locals.starlightRoute.entry;
+const { title = data.title, tagline, image, actions = [] } = data.hero || {};
+
+const imageAttrs = {
+ loading: 'eager' as const,
+ decoding: 'async' as const,
+ width: 400,
+ alt: image?.alt || '',
+};
+
+const github = config.social.filter(s => s.icon === 'github')[0];
+
+const command = "npm i -g";
+const pkg = "opencode";
+
+let darkImage: ImageMetadata | undefined;
+let lightImage: ImageMetadata | undefined;
+let rawHtml: string | undefined;
+if (image) {
+ if ('file' in image) {
+ darkImage = image.file;
+ } else if ('dark' in image) {
+ darkImage = image.dark;
+ lightImage = image.light;
+ } else {
+ rawHtml = image.html;
+ }
+}
+---
+<div class="hero">
+ <section class="top">
+ <div class="logo">
+ <Image
+ src={darkImage}
+ {...imageAttrs}
+ class:list={{ 'light:sl-hidden': Boolean(lightImage) }}
+ />
+ <Image src={lightImage} {...imageAttrs} class="dark:sl-hidden" />
+ </div>
+ <h1>The AI coding agent built for the terminal.</h1>
+ </section>
+
+ <section class="cta">
+ <div class="col1">
+ <a href="/docs">View the docs</a>
+ </div>
+ <div class="col2">
+ <button class="command" data-command={`${command} ${pkg}`}>
+ <code>{command}&nbsp;<span class="highlight">{pkg}</span></code>
+ <span class="copy">
+ <CopyIcon />
+ <CheckIcon />
+ </span>
+ </button>
+ </div>
+ <div class="col3">
+ <a href={github.href}>Star on GitHub</a>
+ </div>
+ </section>
+
+ <section class="content">
+ <ul>
+ <li><b>Native TUI</b>: A native terminal UI for a smoother, snappier experience.</li>
+ <li><b>LSP enabled</b>: Loads the right LSPs for your codebase. Helps the LLM make fewer mistakes.</li>
+ <li><b>Multi-session</b>: Start multiple conversations in a project to have agents working in parallel.</li>
+ <li><b>Use any model</b>: Supports all the models from OpenAI, Anthropic, Google, OpenRouter, and more.</li>
+ <li><b>Change tracking</b>: View the file changes from the current conversation in the sidebar.</li>
+ <li><b>Edit with Vim</b>: Use Vim as an external editor to compose longer messages.</li>
+ </ul>
+ </section>
+
+ <section class="footer">
+ <div class="col1">
+ <span>Version: Beta</span>
+ </div>
+ <div class="col2">
+ <span>Author: <a href="https://sst.dev">SST</a></span>
+ </div>
+ </section>
+</div>
+
+<style>
+.hero {
+ --padding: 3rem;
+ --vertical-padding: 2rem;
+ --heading-font-size: var(--sl-text-3xl);
+
+ margin: 1rem;
+ border: 2px solid var(--sl-color-white);
+}
+@media (max-width: 30rem) {
+ .hero {
+ --padding: 1rem;
+ --vertical-padding: 1rem;
+ --heading-font-size: var(--sl-text-2xl);
+
+ margin: 0.5rem;
+ }
+}
+
+section.top {
+ padding: var(--padding);
+
+ h1 {
+ margin-top: calc(var(--vertical-padding) / 8);
+ font-size: var(--heading-font-size);
+ line-height: 1.25;
+ text-transform: uppercase;
+ }
+
+ img {
+ height: auto;
+ width: clamp(200px, 70vw, 400px);
+ }
+}
+
+section.cta {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ border-top: 2px solid var(--sl-color-white);
+
+ & > div {
+ flex: 1;
+ line-height: 1.4;
+ padding: calc(var(--padding) / 2) 0.5rem;
+ }
+ & > div:not(.col2) {
+ text-align: center;
+ text-transform: uppercase;
+ }
+
+ @media (max-width: 30rem) {
+ & > div {
+ padding-bottom: calc(var(--padding) / 2 + 4px);
+ }
+ }
+
+ & > div + div {
+ border-left: 2px solid var(--sl-color-white);
+ }
+
+ .command {
+ all: unset;
+ display: flex;
+ align-items: center;
+ gap: 0.625rem;
+ justify-content: center;
+ cursor: pointer;
+ width: 100%;
+
+ code {
+ color: var(--sl-color-text-secondary);
+ font-size: 1.125rem;
+ }
+ code .highlight {
+ color: var(--sl-color-text);
+ font-weight: 500;
+ }
+
+ .copy {
+ line-height: 1;
+ padding: 0;
+ }
+ .copy svg {
+ width: 1rem;
+ height: 1rem;
+ vertical-align: middle;
+ }
+ .copy svg:first-child {
+ color: var(--sl-color-text-dimmed);
+ }
+ .copy svg:last-child {
+ color: var(--sl-color-text);
+ display: none;
+ }
+ &.success .copy {
+ pointer-events: none;
+ }
+ &.success .copy svg:first-child {
+ display: none;
+ }
+ &.success .copy svg:last-child {
+ display: inline;
+ }
+ }
+}
+
+section.content {
+ border-top: 2px solid var(--sl-color-white);
+ padding: var(--padding);
+
+ ul {
+ padding-left: 1rem;
+
+ li + li {
+ margin-top: calc(var(--vertical-padding) / 2);
+ }
+
+ li b {
+ text-transform: uppercase;
+ }
+ }
+}
+
+section.approach {
+ border-top: 2px solid var(--sl-color-white);
+ padding: var(--padding);
+
+ p + p {
+ margin-top: var(--vertical-padding);
+ }
+}
+
+section.footer {
+ border-top: 2px solid var(--sl-color-white);
+ display: flex;
+ flex-direction: row;
+
+ & > div {
+ flex: 1;
+ text-align: center;
+ text-transform: uppercase;
+ padding: calc(var(--padding) / 2) 0.5rem;
+ }
+
+ & > div + div {
+ border-left: 2px solid var(--sl-color-white);
+ }
+}
+</style>
+
+<style is:global>
+:root[data-has-hero] {
+ header.header {
+ display: none;
+ }
+ .main-frame {
+ padding-top: 0;
+
+ .main-pane > main {
+ padding: 0;
+ }
+ }
+ main > .content-panel .sl-markdown-content {
+ margin-top: 0;
+ }
+}
+</style>
+
+<script>
+ const button = document.querySelector("button.command") as HTMLButtonElement;
+
+ button?.addEventListener("click", () => {
+ navigator.clipboard.writeText(button.dataset.command!);
+ button.classList.toggle("success");
+ setTimeout(() => {
+ button.classList.toggle("success");
+ }, 1500);
+ });
+</script>
diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx
new file mode 100644
index 000000000..ac75a3cf7
--- /dev/null
+++ b/packages/web/src/components/Share.tsx
@@ -0,0 +1,772 @@
+import { type JSX } from "solid-js"
+import {
+ For,
+ Show,
+ Match,
+ Switch,
+ onMount,
+ onCleanup,
+ splitProps,
+ createMemo,
+ createEffect,
+ createSignal,
+} from "solid-js"
+import { DateTime } from "luxon"
+import {
+ IconOpenAI,
+ IconGemini,
+ IconAnthropic,
+} from "./icons/custom"
+import {
+ IconCpuChip,
+ IconSparkles,
+ IconUserCircle,
+ IconChevronDown,
+ IconChevronRight,
+ IconPencilSquare,
+ IconWrenchScrewdriver,
+} from "./icons"
+import DiffView from "./DiffView"
+import styles from "./share.module.css"
+import { type UIMessage } from "ai"
+import { createStore, reconcile } from "solid-js/store"
+
+type Status = "disconnected" | "connecting" | "connected" | "error" | "reconnecting"
+
+
+type SessionMessage = UIMessage<{
+ time: {
+ created: number
+ completed?: number
+ }
+ assistant?: {
+ modelID: string;
+ providerID: string;
+ cost: number;
+ tokens: {
+ input: number;
+ output: number;
+ reasoning: number;
+ };
+ };
+ sessionID: string
+ tool: Record<string, {
+ properties: Record<string, any>
+ time: {
+ start: number
+ end: number
+ }
+ }>
+}>
+
+type SessionInfo = {
+ title: string
+ cost?: number
+}
+
+function getFileType(path: string) {
+ return path.split('.').pop()
+}
+
+// Converts `{a:{b:{c:1}}` to `[['a.b.c', 1]]`
+function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> {
+ const entries: Array<[string, any]> = [];
+
+ for (const [key, value] of Object.entries(obj)) {
+ const path = prefix ? `${prefix}.${key}` : key;
+
+ if (
+ value !== null &&
+ typeof value === "object" &&
+ !Array.isArray(value)
+ ) {
+ entries.push(...flattenToolArgs(value, path));
+ }
+ else {
+ entries.push([path, value]);
+ }
+ }
+
+ return entries;
+}
+
+function getStatusText(status: [Status, string?]): string {
+ switch (status[0]) {
+ case "connected": return "Connected"
+ case "connecting": return "Connecting..."
+ case "disconnected": return "Disconnected"
+ case "reconnecting": return "Reconnecting..."
+ case "error": return status[1] || "Error"
+ default: return "Unknown"
+ }
+}
+
+function ProviderIcon(props: { provider: string, size?: number }) {
+ const size = props.size || 16
+ return (
+ <Switch fallback={
+ <IconSparkles width={size} height={size} />
+ }>
+ <Match when={props.provider === "openai"}>
+ <IconOpenAI width={size} height={size} />
+ </Match>
+ <Match when={props.provider === "anthropic"}>
+ <IconAnthropic width={size} height={size} />
+ </Match>
+ <Match when={props.provider === "gemini"}>
+ <IconGemini width={size} height={size} />
+ </Match>
+ </Switch>
+ )
+}
+
+interface ResultsButtonProps extends JSX.HTMLAttributes<HTMLButtonElement> {
+ results: boolean
+}
+function ResultsButton(props: ResultsButtonProps) {
+ const [local, rest] = splitProps(props, ["results"])
+ return (
+ <button
+ type="button"
+ data-element-button-text
+ data-element-button-more
+ {...rest}
+ >
+ <span>
+ {local.results ? "Hide results" : "Show results"}
+ </span>
+ <span data-button-icon>
+ <Show
+ when={local.results}
+ fallback={
+ <IconChevronRight width={10} height={10} />
+ }
+ >
+ <IconChevronDown width={10} height={10} />
+ </Show>
+ </span>
+ </button>
+ )
+}
+
+interface TextPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
+ text: string
+ expand?: boolean
+ highlight?: boolean
+}
+function TextPart(props: TextPartProps) {
+ const [local, rest] = splitProps(props, ["text", "expand", "highlight"])
+ const [expanded, setExpanded] = createSignal(false)
+ const [overflowed, setOverflowed] = createSignal(false)
+ let preEl: HTMLPreElement | undefined
+
+ function checkOverflow() {
+ if (preEl && !local.expand) {
+ setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1)
+ }
+ }
+
+ onMount(() => {
+ checkOverflow()
+ window.addEventListener("resize", checkOverflow)
+ })
+
+ createEffect(() => {
+ local.text
+ setTimeout(checkOverflow, 0)
+ })
+
+ onCleanup(() => {
+ window.removeEventListener("resize", checkOverflow)
+ })
+
+ return (
+ <div
+ data-element-message-text
+ data-highlight={local.highlight}
+ data-expanded={expanded() || local.expand === true}
+ {...rest}
+ >
+ <pre ref={el => (preEl = el)}>{local.text}</pre>
+ {overflowed() &&
+ <button
+ type="button"
+ data-element-button-text
+ onClick={() => setExpanded(e => !e)}
+ >
+ {expanded() ? "Show less" : "Show more"}
+ </button>
+ }
+ </div>
+ )
+}
+
+function PartFooter(props: { time: number }) {
+ return (
+ <span
+ data-part-footer
+ title={
+ DateTime.fromMillis(props.time).toLocaleString(
+ DateTime.DATETIME_FULL_WITH_SECONDS
+ )
+ }
+ >
+ {DateTime.fromMillis(props.time).toLocaleString(DateTime.TIME_WITH_SECONDS)}
+ </span>
+ )
+}
+
+export default function Share(props: { api: string }) {
+ let params = new URLSearchParams(document.location.search)
+ const id = params.get("id")
+
+ const [store, setStore] = createStore<{
+ info?: SessionInfo
+ messages: Record<string, SessionMessage>
+ }>({
+ messages: {},
+ })
+ const messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)))
+ const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"])
+
+ onMount(() => {
+ const apiUrl = props.api
+
+ if (!id) {
+ setConnectionStatus(["error", "id not found"])
+ return
+ }
+
+ if (!apiUrl) {
+ console.error("API URL not found in environment variables")
+ setConnectionStatus(["error", "API URL not found"])
+ return
+ }
+
+ let reconnectTimer: number | undefined
+ let socket: WebSocket | null = null
+
+ // Function to create and set up WebSocket with auto-reconnect
+ const setupWebSocket = () => {
+ // Close any existing connection
+ if (socket) {
+ socket.close()
+ }
+
+ setConnectionStatus(["connecting"])
+
+ // Always use secure WebSocket protocol (wss)
+ const wsBaseUrl = apiUrl.replace(/^https?:\/\//, "wss://")
+ const wsUrl = `${wsBaseUrl}/share_poll?id=${id}`
+ console.log("Connecting to WebSocket URL:", wsUrl)
+
+ // Create WebSocket connection
+ socket = new WebSocket(wsUrl)
+
+ // Handle connection opening
+ socket.onopen = () => {
+ setConnectionStatus(["connected"])
+ console.log("WebSocket connection established")
+ }
+
+ // Handle incoming messages
+ socket.onmessage = (event) => {
+ console.log("WebSocket message received")
+ try {
+ const data = JSON.parse(event.data)
+ const [root, type, ...splits] = data.key.split("/")
+ if (root !== "session") return
+ if (type === "info") {
+ setStore("info", reconcile(data.content))
+ return
+ }
+ if (type === "message") {
+ const [, messageID] = splits
+ setStore("messages", messageID, reconcile(data.content))
+ }
+ } catch (error) {
+ console.error("Error parsing WebSocket message:", error)
+ }
+ }
+
+ // Handle errors
+ socket.onerror = (error) => {
+ console.error("WebSocket error:", error)
+ setConnectionStatus(["error", "Connection failed"])
+ }
+
+ // Handle connection close and reconnection
+ socket.onclose = (event) => {
+ console.log(`WebSocket closed: ${event.code} ${event.reason}`)
+ setConnectionStatus(["reconnecting"])
+
+ // Try to reconnect after 2 seconds
+ clearTimeout(reconnectTimer)
+ reconnectTimer = window.setTimeout(
+ setupWebSocket,
+ 2000,
+ ) as unknown as number
+ }
+ }
+
+ // Initial connection
+ setupWebSocket()
+
+ // Clean up on component unmount
+ onCleanup(() => {
+ console.log("Cleaning up WebSocket connection")
+ if (socket) {
+ socket.close()
+ }
+ clearTimeout(reconnectTimer)
+ })
+ })
+
+ const models = createMemo(() => {
+ const result: string[][] = []
+ for (const msg of messages()) {
+ if (msg.role === "assistant" && msg.metadata?.assistant) {
+ result.push([msg.metadata.assistant.providerID, msg.metadata.assistant.modelID])
+ }
+ }
+ return result
+ })
+
+ const metrics = createMemo(() => {
+ const result = {
+ cost: 0,
+ tokens: {
+ input: 0,
+ output: 0,
+ reasoning: 0,
+ }
+ }
+ for (const msg of messages()) {
+ const assistant = msg.metadata?.assistant
+ if (!assistant) continue
+ result.cost += assistant.cost
+ result.tokens.input += assistant.tokens.input
+ result.tokens.output += assistant.tokens.output
+ result.tokens.reasoning += assistant.tokens.reasoning
+ }
+ return result
+ })
+
+ return (
+ <main class={`${styles.root} not-content`}>
+ <div class={styles.header}>
+ <div data-section="title">
+ <h1>{store.info?.title}</h1>
+ <p>
+ <span data-status={connectionStatus()[0]}>&#9679;</span>
+ <span data-element-label>{getStatusText(connectionStatus())}</span>
+ </p>
+ </div>
+ <div data-section="row">
+ <ul data-section="stats">
+ <li>
+ <span data-element-label>Cost</span>
+ {metrics().cost !== undefined ?
+ <span>${metrics().cost.toFixed(2)}</span>
+ :
+ <span data-placeholder>&mdash;</span>
+ }
+ </li>
+ <li>
+ <span data-element-label>Input Tokens</span>
+ {metrics().tokens.input ?
+ <span>{metrics().tokens.input}</span>
+ :
+ <span data-placeholder>&mdash;</span>
+ }
+ </li>
+ <li>
+ <span data-element-label>Output Tokens</span>
+ {metrics().tokens.output ?
+ <span>{metrics().tokens.output}</span>
+ :
+ <span data-placeholder>&mdash;</span>
+ }
+ </li>
+ <li>
+ <span data-element-label>Reasoning Tokens</span>
+ {metrics().tokens.reasoning ?
+ <span>{metrics().tokens.reasoning}</span>
+ :
+ <span data-placeholder>&mdash;</span>
+ }
+ </li>
+ </ul>
+ <ul data-section="stats" data-section-models>
+ {models().length > 0 ?
+ <For each={Array.from(models())}>
+ {([provider, model]) => (
+ <li>
+ <div data-stat-model-icon title={provider}>
+ <ProviderIcon provider={provider} />
+ </div>
+ <span data-stat-model>{model}</span>
+ </li>
+ )}
+ </For>
+ :
+ <li>
+ <span data-element-label>Models</span>
+ <span data-placeholder>&mdash;</span>
+ </li>
+ }
+ </ul>
+ <div data-section="date">
+ {messages().length > 0 && messages()[0].metadata?.time.created ?
+ <span title={
+ DateTime.fromMillis(
+ messages()[0].metadata?.time.created || 0
+ ).toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)
+ }>
+ {DateTime.fromMillis(
+ messages()[0].metadata?.time.created || 0
+ ).toLocaleString(DateTime.DATE_MED)}
+ </span>
+ :
+ <span data-element-label data-placeholder>Started at &mdash;</span>
+ }
+ </div>
+ </div>
+ </div>
+
+ <div>
+ <Show
+ when={messages().length > 0}
+ fallback={<p>Waiting for messages...</p>}
+ >
+ <div class={styles.parts}>
+ <For each={messages()}>
+ {(msg, msgIndex) => (
+ <For each={msg.parts}>
+ {(part, partIndex) => {
+ if (part.type === "step-start" && (partIndex() > 0 || !msg.metadata?.assistant)) return null
+
+ const [results, showResults] = createSignal(false)
+ const isLastPart = createMemo(() =>
+ (messages().length === msgIndex() + 1)
+ && (msg.parts.length === partIndex() + 1)
+ )
+ const time = msg.metadata?.time.completed
+ || msg.metadata?.time.created
+ || 0
+ return (
+ <Switch>
+ { /* User text */}
+ <Match when={
+ msg.role === "user" && part.type === "text" && part
+ }>
+ {part =>
+ <div
+ data-section="part"
+ data-part-type="user-text"
+ >
+ <div data-section="decoration">
+ <div>
+ <IconUserCircle width={18} height={18} />
+ </div>
+ <div></div>
+ </div>
+ <div data-section="content">
+ <TextPart
+ highlight
+ text={part().text}
+ expand={isLastPart()}
+ />
+ <PartFooter time={time} />
+ </div>
+ </div>
+ }
+ </Match>
+ { /* AI text */}
+ <Match when={
+ msg.role === "assistant"
+ && part.type === "text"
+ && part
+ }>
+ {part =>
+ <div
+ data-section="part"
+ data-part-type="ai-text"
+ >
+ <div data-section="decoration">
+ <div><IconSparkles width={18} height={18} /></div>
+ <div></div>
+ </div>
+ <div data-section="content">
+ <TextPart
+ text={part().text}
+ expand={isLastPart()}
+ />
+ <PartFooter time={time} />
+ </div>
+ </div>
+ }
+ </Match>
+ { /* AI model */}
+ <Match when={
+ msg.role === "assistant"
+ && part.type === "step-start"
+ && msg.metadata?.assistant
+ }>
+ {assistant =>
+ <div
+ data-section="part"
+ data-part-type="ai-model"
+ >
+ <div data-section="decoration">
+ <div>
+ <ProviderIcon
+ size={18}
+ provider={assistant().providerID}
+ />
+ </div>
+ <div></div>
+ </div>
+ <div data-section="content">
+ <div data-part-tool-body>
+ <span
+ data-size="md"
+ data-part-title
+ data-element-label
+ >
+ {assistant().providerID}
+ </span>
+ <span data-part-model>
+ {assistant().modelID}
+ </span>
+ </div>
+ </div>
+ </div>
+ }
+ </Match>
+ { /* System text */}
+ <Match when={
+ msg.role === "system"
+ && part.type === "text"
+ && part
+ }>
+ {part =>
+ <div
+ data-section="part"
+ data-part-type="system-text"
+ >
+ <div data-section="decoration">
+ <div>
+ <IconCpuChip width={18} height={18} />
+ </div>
+ <div></div>
+ </div>
+ <div data-section="content">
+ <div data-part-tool-body>
+ <span data-element-label data-part-title>
+ System
+ </span>
+ <TextPart
+ data-size="sm"
+ text={part().text}
+ data-color="dimmed"
+ />
+ </div>
+ <PartFooter time={time} />
+ </div>
+ </div>
+ }
+ </Match>
+ { /* Edit tool */}
+ <Match when={
+ msg.role === "assistant"
+ && part.type === "tool-invocation"
+ && part.toolInvocation.toolName === "edit"
+ && part
+ }>
+ {part => {
+ const args = part().toolInvocation.args
+ const filePath = args.filePath
+ return (
+ <div
+ data-section="part"
+ data-part-type="tool-edit"
+ >
+ <div data-section="decoration">
+ <div>
+ <IconPencilSquare width={18} height={18} />
+ </div>
+ <div></div>
+ </div>
+ <div data-section="content">
+ <div data-part-tool-body>
+ <span data-part-title data-size="md">
+ Edit {filePath}
+ </span>
+ <div data-part-tool-edit>
+ <DiffView
+ class={styles["code-block"]}
+ oldCode={args.oldString}
+ newCode={args.newString}
+ lang={getFileType(filePath)}
+ />
+ </div>
+ </div>
+ <PartFooter time={time} />
+ </div>
+ </div>
+ )
+ }}
+ </Match>
+ { /* Tool call */}
+ <Match when={
+ msg.role === "assistant"
+ && part.type === "tool-invocation"
+ && part
+ }>
+ {part =>
+ <div
+ data-section="part"
+ data-part-type="tool-fallback"
+ >
+ <div data-section="decoration">
+ <div>
+ <IconWrenchScrewdriver width={18} height={18} />
+ </div>
+ <div></div>
+ </div>
+ <div data-section="content">
+ <div data-part-tool-body>
+ <span data-part-title data-size="md">
+ {part().toolInvocation.toolName}
+ </span>
+ <div data-part-tool-args>
+ <For each={
+ flattenToolArgs(part().toolInvocation.args)
+ }>
+ {([name, value]) =>
+ <>
+ <div></div>
+ <div>{name}</div>
+ <div>{value}</div>
+ </>
+ }
+ </For>
+ </div>
+ <Switch>
+ <Match when={
+ part().toolInvocation.state === "result"
+ && part().toolInvocation.result
+ }>
+ <div data-part-tool-result>
+ <ResultsButton
+ results={results()}
+ onClick={() => showResults(e => !e)}
+ />
+ <Show when={results()}>
+ <TextPart
+ expand
+ data-size="sm"
+ data-color="dimmed"
+ text={part().toolInvocation.result}
+ />
+ </Show>
+ </div>
+ </Match>
+ <Match when={
+ part().toolInvocation.state === "call"
+ }>
+ <TextPart
+ data-size="sm"
+ data-color="dimmed"
+ text="Calling..."
+ />
+ </Match>
+ </Switch>
+ </div>
+ <PartFooter time={time} />
+ </div>
+ </div>
+ }
+ </Match>
+ { /* Fallback */}
+ <Match when={true}>
+ <div
+ data-section="part"
+ data-part-type="fallback"
+ >
+ <div data-section="decoration">
+ <div>
+ <Switch fallback={
+ <IconWrenchScrewdriver width={16} height={16} />
+ }>
+ <Match when={msg.role === "assistant" && part.type !== "tool-invocation"}>
+ <IconSparkles width={18} height={18} />
+ </Match>
+ <Match when={msg.role === "system"}>
+ <IconCpuChip width={18} height={18} />
+ </Match>
+ <Match when={msg.role === "user"}>
+ <IconUserCircle width={18} height={18} />
+ </Match>
+ </Switch>
+ </div>
+ <div></div>
+ </div>
+ <div data-section="content">
+ <div data-part-tool-body>
+ <span data-element-label data-part-title>
+ {part.type}
+ </span>
+ <TextPart text={JSON.stringify(part, null, 2)} />
+ </div>
+ <PartFooter time={time} />
+ </div>
+ </div>
+ </Match>
+ </Switch>
+ )
+ }}
+ </For>
+ )}
+ </For>
+ </div>
+ </Show>
+ </div>
+
+ <div style={{ margin: "2rem 0" }}>
+ <div
+ style={{
+ border: "1px solid #ccc",
+ padding: "1rem",
+ "overflow-y": "auto",
+ }}
+ >
+ <Show
+ when={messages().length > 0}
+ fallback={<p>Waiting for messages...</p>}
+ >
+ <ul style={{ "list-style-type": "none", padding: 0 }}>
+ <For each={messages()}>
+ {(msg) => (
+ <li
+ style={{
+ padding: "0.75rem",
+ margin: "0.75rem 0",
+ "box-shadow": "0 1px 3px rgba(0,0,0,0.1)",
+ }}
+ >
+ <div>
+ <strong>Key:</strong> {msg.id}
+ </div>
+ <pre>{JSON.stringify(msg, null, 2)}</pre>
+ </li>
+ )}
+ </For>
+ </ul>
+ </Show>
+ </div>
+ </div>
+ </main >
+ )
+}
diff --git a/packages/web/src/components/diffview.module.css b/packages/web/src/components/diffview.module.css
new file mode 100644
index 000000000..1a0e6c523
--- /dev/null
+++ b/packages/web/src/components/diffview.module.css
@@ -0,0 +1,80 @@
+.diff {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ border: 1px solid var(--sl-color-divider);
+ background-color: var(--sl-color-bg-surface);
+ border-radius: 0.25rem;
+}
+
+.column {
+ display: flex;
+ flex-direction: column;
+ overflow-x: auto;
+ min-width: 0;
+ align-items: flex-start;
+
+ &:first-child {
+ border-right: 1px solid var(--sl-color-divider);
+ }
+
+ & > [data-section="cell"]:first-child {
+ padding-top: 0.5rem;
+ }
+ & > [data-section="cell"]:last-child {
+ padding-bottom: 0.5rem;
+ }
+}
+
+[data-section="cell"] {
+ position: relative;
+ flex: none;
+ width: max-content;
+ padding: 0.1875rem 0.5rem 0.1875rem 1.8ch;
+ margin: 0;
+
+ pre {
+ background-color: var(--sl-color-bg-surface) !important;
+ white-space: pre;
+
+ code > span:empty::before {
+ content: "\00a0";
+ white-space: pre;
+ display: inline-block;
+ width: 0;
+ }
+ }
+}
+
+[data-diff-type="removed"] {
+ background-color: var(--sl-color-red-low);
+ min-width: 100%;
+
+ pre {
+ background-color: var(--sl-color-red-low) !important;
+ }
+
+ &::before {
+ content: "-";
+ position: absolute;
+ left: 0.5ch;
+ user-select: none;
+ color: var(--sl-color-red-high);
+ }
+}
+
+[data-diff-type="added"] {
+ background-color: var(--sl-color-green-low);
+ min-width: 100%;
+
+ pre {
+ background-color: var(--sl-color-green-low) !important;
+ }
+
+ &::before {
+ content: "+";
+ position: absolute;
+ left: 0.6ch;
+ user-select: none;
+ color: var(--sl-color-green-high);
+ }
+}
diff --git a/packages/web/src/components/icons/custom.tsx b/packages/web/src/components/icons/custom.tsx
new file mode 100644
index 000000000..f016b83cf
--- /dev/null
+++ b/packages/web/src/components/icons/custom.tsx
@@ -0,0 +1,22 @@
+import { type JSX } from "solid-js"
+
+// https://icones.js.org/collection/ri?s=openai&icon=ri:openai-fill
+export function IconOpenAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M20.562 10.188c.25-.688.313-1.376.25-2.063c-.062-.687-.312-1.375-.625-2c-.562-.937-1.375-1.687-2.312-2.125c-1-.437-2.063-.562-3.125-.312c-.5-.5-1.063-.938-1.688-1.25S11.687 2 11 2a5.17 5.17 0 0 0-3 .938c-.875.624-1.5 1.5-1.813 2.5c-.75.187-1.375.5-2 .875c-.562.437-1 1-1.375 1.562c-.562.938-.75 2-.625 3.063a5.44 5.44 0 0 0 1.25 2.874a4.7 4.7 0 0 0-.25 2.063c.063.688.313 1.375.625 2c.563.938 1.375 1.688 2.313 2.125c1 .438 2.062.563 3.125.313c.5.5 1.062.937 1.687 1.25S12.312 22 13 22a5.17 5.17 0 0 0 3-.937c.875-.625 1.5-1.5 1.812-2.5a4.54 4.54 0 0 0 1.938-.875c.562-.438 1.062-.938 1.375-1.563c.562-.937.75-2 .625-3.062c-.125-1.063-.5-2.063-1.188-2.876m-7.5 10.5c-1 0-1.75-.313-2.437-.875c0 0 .062-.063.125-.063l4-2.312a.5.5 0 0 0 .25-.25a.57.57 0 0 0 .062-.313V11.25l1.688 1v4.625a3.685 3.685 0 0 1-3.688 3.813M5 17.25c-.438-.75-.625-1.625-.438-2.5c0 0 .063.063.125.063l4 2.312a.56.56 0 0 0 .313.063c.125 0 .25 0 .312-.063l4.875-2.812v1.937l-4.062 2.375A3.7 3.7 0 0 1 7.312 19c-1-.25-1.812-.875-2.312-1.75M3.937 8.563a3.8 3.8 0 0 1 1.938-1.626v4.751c0 .124 0 .25.062.312a.5.5 0 0 0 .25.25l4.875 2.813l-1.687 1l-4-2.313a3.7 3.7 0 0 1-1.75-2.25c-.25-.937-.188-2.062.312-2.937M17.75 11.75l-4.875-2.812l1.687-1l4 2.312c.625.375 1.125.875 1.438 1.5s.5 1.313.437 2.063a3.7 3.7 0 0 1-.75 1.937c-.437.563-1 1-1.687 1.25v-4.75c0-.125 0-.25-.063-.312c0 0-.062-.126-.187-.188m1.687-2.5s-.062-.062-.125-.062l-4-2.313c-.125-.062-.187-.062-.312-.062s-.25 0-.313.062L9.812 9.688V7.75l4.063-2.375c.625-.375 1.312-.5 2.062-.5c.688 0 1.375.25 2 .688c.563.437 1.063 1 1.313 1.625s.312 1.375.187 2.062m-10.5 3.5l-1.687-1V7.063c0-.688.187-1.438.562-2C8.187 4.438 8.75 4 9.375 3.688a3.37 3.37 0 0 1 2.062-.313c.688.063 1.375.375 1.938.813c0 0-.063.062-.125.062l-4 2.313a.5.5 0 0 0-.25.25c-.063.125-.063.187-.063.312zm.875-2L12 9.5l2.187 1.25v2.5L12 14.5l-2.188-1.25z" /></svg>
+ )
+}
+
+// https://icones.js.org/collection/ri?s=anthropic&icon=ri:anthropic-fill
+export function IconAnthropic(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M16.765 5h-3.308l5.923 15h3.23zM7.226 5L1.38 20h3.308l1.307-3.154h6.154l1.23 3.077h3.309L10.688 5zm-.308 9.077l2-5.308l2.077 5.308z" /></svg>
+ )
+}
+
+// https://icones.js.org/collection/ri?s=gemini&icon=ri:gemini-fill
+export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M24 12.024c-6.437.388-11.59 5.539-11.977 11.976h-.047C11.588 17.563 6.436 12.412 0 12.024v-.047C6.437 11.588 11.588 6.437 11.976 0h.047c.388 6.437 5.54 11.588 11.977 11.977z" /></svg>
+ )
+}
diff --git a/packages/web/src/components/icons/index.tsx b/packages/web/src/components/icons/index.tsx
new file mode 100644
index 000000000..9603925d5
--- /dev/null
+++ b/packages/web/src/components/icons/index.tsx
@@ -0,0 +1,6101 @@
+import { type JSX } from "solid-js"
+// heroicons
+
+export function IconAcademicCap(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M4.25933 10.1468C3.98688 12.2308 3.82139 14.3485 3.76853 16.4941C6.66451 17.7032 9.41893 19.1836 12 20.9037C14.5811 19.1836 17.3355 17.7032 20.2315 16.4941C20.1786 14.3485 20.0131 12.2308 19.7407 10.1468M4.25933 10.1468C3.38362 9.85242 2.49729 9.5812 1.60107 9.33382C4.84646 7.05899 8.32741 5.09732 12 3.49268C15.6727 5.09732 19.1536 7.059 22.399 9.33383C21.5028 9.58122 20.6164 9.85245 19.7407 10.1468M4.25933 10.1468C6.94656 11.05 9.5338 12.171 12.0001 13.4888C14.4663 12.171 17.0535 11.0501 19.7407 10.1468M6.75 15.0001C7.16421 15.0001 7.5 14.6643 7.5 14.2501C7.5 13.8359 7.16421 13.5001 6.75 13.5001C6.33579 13.5001 6 13.8359 6 14.2501C6 14.6643 6.33579 15.0001 6.75 15.0001ZM6.75 15.0001V11.3246C8.44147 10.2736 10.1936 9.31107 12 8.44342M4.99264 19.9928C6.16421 18.8212 6.75 17.2857 6.75 15.7501V14.2501"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconAdjustmentsHorizontal(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M10.5 6L20.25 6M10.5 6C10.5 6.82843 9.82843 7.5 9 7.5C8.17157 7.5 7.5 6.82843 7.5 6M10.5 6C10.5 5.17157 9.82843 4.5 9 4.5C8.17157 4.5 7.5 5.17157 7.5 6M3.75 6H7.5M10.5 18H20.25M10.5 18C10.5 18.8284 9.82843 19.5 9 19.5C8.17157 19.5 7.5 18.8284 7.5 18M10.5 18C10.5 17.1716 9.82843 16.5 9 16.5C8.17157 16.5 7.5 17.1716 7.5 18M3.75 18L7.5 18M16.5 12L20.25 12M16.5 12C16.5 12.8284 15.8284 13.5 15 13.5C14.1716 13.5 13.5 12.8284 13.5 12M16.5 12C16.5 11.1716 15.8284 10.5 15 10.5C14.1716 10.5 13.5 11.1716 13.5 12M3.75 12H13.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconAdjustmentsVertical(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M6 13.5L6 3.75M6 13.5C6.82843 13.5 7.5 14.1716 7.5 15C7.5 15.8284 6.82843 16.5 6 16.5M6 13.5C5.17157 13.5 4.5 14.1716 4.5 15C4.5 15.8284 5.17157 16.5 6 16.5M6 20.25L6 16.5M18 13.5V3.75M18 13.5C18.8284 13.5 19.5 14.1716 19.5 15C19.5 15.8284 18.8284 16.5 18 16.5M18 13.5C17.1716 13.5 16.5 14.1716 16.5 15C16.5 15.8284 17.1716 16.5 18 16.5M18 20.25L18 16.5M12 7.5V3.75M12 7.5C12.8284 7.5 13.5 8.17157 13.5 9C13.5 9.82843 12.8284 10.5 12 10.5M12 7.5C11.1716 7.5 10.5 8.17157 10.5 9C10.5 9.82843 11.1716 10.5 12 10.5M12 20.25V10.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArchiveBoxArrowDown(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M20.25 7.5L19.6246 18.1321C19.5546 19.3214 18.5698 20.25 17.3785 20.25H6.62154C5.43022 20.25 4.44538 19.3214 4.37542 18.1321L3.75 7.5M12 10.5V17.25M12 17.25L9 14.25M12 17.25L15 14.25M3.375 7.5H20.625C21.2463 7.5 21.75 6.99632 21.75 6.375V4.875C21.75 4.25368 21.2463 3.75 20.625 3.75H3.375C2.75368 3.75 2.25 4.25368 2.25 4.875V6.375C2.25 6.99632 2.75368 7.5 3.375 7.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArchiveBoxXMark(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M20.25 7.5L19.6246 18.1321C19.5546 19.3214 18.5698 20.25 17.3785 20.25H6.62154C5.43022 20.25 4.44538 19.3214 4.37542 18.1321L3.75 7.5M9.75 11.625L12 13.875M12 13.875L14.25 16.125M12 13.875L14.25 11.625M12 13.875L9.75 16.125M3.375 7.5H20.625C21.2463 7.5 21.75 6.99632 21.75 6.375V4.875C21.75 4.25368 21.2463 3.75 20.625 3.75H3.375C2.75368 3.75 2.25 4.25368 2.25 4.875V6.375C2.25 6.99632 2.75368 7.5 3.375 7.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArchiveBox(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M20.25 7.5L19.6246 18.1321C19.5546 19.3214 18.5698 20.25 17.3785 20.25H6.62154C5.43022 20.25 4.44538 19.3214 4.37542 18.1321L3.75 7.5M9.99976 11.25H13.9998M3.375 7.5H20.625C21.2463 7.5 21.75 6.99632 21.75 6.375V4.875C21.75 4.25368 21.2463 3.75 20.625 3.75H3.375C2.75368 3.75 2.25 4.25368 2.25 4.875V6.375C2.25 6.99632 2.75368 7.5 3.375 7.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowDownCircle(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 12.75L12 15.75M12 15.75L15 12.75M12 15.75L12 8.25M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowDownLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 4.5L4.5 19.5M4.5 19.5L15.75 19.5M4.5 19.5L4.5 8.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowDownOnSquareStack(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M7.5 7.5H6.75C5.50736 7.5 4.5 8.50736 4.5 9.75V17.25C4.5 18.4926 5.50736 19.5 6.75 19.5H14.25C15.4926 19.5 16.5 18.4926 16.5 17.25V9.75C16.5 8.50736 15.4926 7.5 14.25 7.5H13.5M7.5 11.25L10.5 14.25M10.5 14.25L13.5 11.25M10.5 14.25L10.5 1.5M16.5 10.5H17.25C18.4926 10.5 19.5 11.5074 19.5 12.75V20.25C19.5 21.4926 18.4926 22.5 17.25 22.5H9.75C8.50736 22.5 7.5 21.4926 7.5 20.25V19.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowDownOnSquare(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 8.25H7.5C6.25736 8.25 5.25 9.25736 5.25 10.5V19.5C5.25 20.7426 6.25736 21.75 7.5 21.75H16.5C17.7426 21.75 18.75 20.7426 18.75 19.5V10.5C18.75 9.25736 17.7426 8.25 16.5 8.25H15M9 12L12 15M12 15L15 12M12 15L12 2.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowDownRight(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M4.5 4.5L19.5 19.5M19.5 19.5V8.25M19.5 19.5H8.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowDownTray(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3 16.5V18.75C3 19.9926 4.00736 21 5.25 21H18.75C19.9926 21 21 19.9926 21 18.75V16.5M16.5 12L12 16.5M12 16.5L7.5 12M12 16.5V3"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowDown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 13.5L12 21M12 21L4.5 13.5M12 21L12 3"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowLeftCircle(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M11.25 9L8.25 12M8.25 12L11.25 15M8.25 12H15.75M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowLeftOnRectangle(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.75 9V5.25C15.75 4.00736 14.7426 3 13.5 3L7.5 3C6.25736 3 5.25 4.00736 5.25 5.25L5.25 18.75C5.25 19.9926 6.25736 21 7.5 21H13.5C14.7426 21 15.75 19.9926 15.75 18.75V15M12 9L9 12M9 12L12 15M9 12L21.75 12"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M10.5 19.5L3 12M3 12L10.5 4.5M3 12H21"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowLongDown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.75 17.25L12 21M12 21L8.25 17.25M12 21L12 3"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowLongLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M6.75 15.75L3 12M3 12L6.75 8.25M3 12H21"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowLongRight(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M17.25 8.25L21 12M21 12L17.25 15.75M21 12H3"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowLongUp(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M8.25 6.75L12 3M12 3L15.75 6.75M12 3V21"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowPathRoundedSquare(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 12C19.5 10.7681 19.4536 9.54699 19.3624 8.3384C19.2128 6.35425 17.6458 4.78724 15.6616 4.63757C14.453 4.54641 13.2319 4.5 12 4.5C10.7681 4.5 9.54699 4.54641 8.3384 4.63757C6.35425 4.78724 4.78724 6.35425 4.63757 8.3384C4.62097 8.55852 4.60585 8.77906 4.59222 9M19.5 12L22.5 9M19.5 12L16.5 9M4.5 12C4.5 13.2319 4.54641 14.453 4.63757 15.6616C4.78724 17.6458 6.35425 19.2128 8.3384 19.3624C9.54699 19.4536 10.7681 19.5 12 19.5C13.2319 19.5 14.453 19.4536 15.6616 19.3624C17.6458 19.2128 19.2128 17.6458 19.3624 15.6616C19.379 15.4415 19.3941 15.2209 19.4078 15M4.5 12L7.5 15M4.5 12L1.5 15"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowPath(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M16.0228 9.34841H21.0154V9.34663M2.98413 19.6444V14.6517M2.98413 14.6517L7.97677 14.6517M2.98413 14.6517L6.16502 17.8347C7.15555 18.8271 8.41261 19.58 9.86436 19.969C14.2654 21.1483 18.7892 18.5364 19.9685 14.1353M4.03073 9.86484C5.21 5.46374 9.73377 2.85194 14.1349 4.03121C15.5866 4.4202 16.8437 5.17312 17.8342 6.1655L21.0154 9.34663M21.0154 4.3558V9.34663"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowRightCircle(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12.75 15L15.75 12M15.75 12L12.75 9M15.75 12L8.25 12M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowRightOnRectangle(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.75 9V5.25C15.75 4.00736 14.7426 3 13.5 3L7.5 3C6.25736 3 5.25 4.00736 5.25 5.25L5.25 18.75C5.25 19.9926 6.25736 21 7.5 21H13.5C14.7426 21 15.75 19.9926 15.75 18.75V15M18.75 15L21.75 12M21.75 12L18.75 9M21.75 12L9 12"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowRight(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M13.5 4.5L21 12M21 12L13.5 19.5M21 12H3"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowSmallDown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 4.5V19.5M12 19.5L18.75 12.75M12 19.5L5.25 12.75"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowSmallLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 12L4.5 12M4.5 12L11.25 18.75M4.5 12L11.25 5.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowSmallRight(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M4.5 12L19.5 12M19.5 12L12.75 5.25M19.5 12L12.75 18.75"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowSmallUp(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 19.5L12 4.5M12 4.5L5.25 11.25M12 4.5L18.75 11.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowTopRightOnSquare(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M13.5 6H5.25C4.00736 6 3 7.00736 3 8.25V18.75C3 19.9926 4.00736 21 5.25 21H15.75C16.9926 21 18 19.9926 18 18.75V10.5M7.5 16.5L21 3M21 3L15.75 3M21 3V8.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowTrendingDown(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.25 6L9 12.75L13.2862 8.46383C15.3217 10.0166 16.8781 12.23 17.5919 14.8941L18.3684 17.7919M18.3684 17.7919L21.5504 12.2806M18.3684 17.7919L12.857 14.6099"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowTrendingUp(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.25 18.0004L9 11.2504L13.3064 15.5568C14.5101 13.1885 16.5042 11.2027 19.1203 10.038L21.8609 8.81775M21.8609 8.81775L15.9196 6.53711M21.8609 8.81775L19.5802 14.759"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowUpCircle(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15 11.25L12 8.25M12 8.25L9 11.25M12 8.25L12 15.75M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowUpLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 19.5L4.5 4.5M4.5 4.5L4.5 15.75M4.5 4.5L15.75 4.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowUpOnSquareStack(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M7.5 7.5H6.75C5.50736 7.5 4.5 8.50736 4.5 9.75V17.25C4.5 18.4926 5.50736 19.5 6.75 19.5H14.25C15.4926 19.5 16.5 18.4926 16.5 17.25V9.75C16.5 8.50736 15.4926 7.5 14.25 7.5H13.5M13.5 4.5L10.5 1.5M10.5 1.5L7.5 4.5M10.5 1.5L10.5 12.75M16.5 10.5H17.25C18.4926 10.5 19.5 11.5074 19.5 12.75V20.25C19.5 21.4926 18.4926 22.5 17.25 22.5H9.75C8.50736 22.5 7.5 21.4926 7.5 20.25V19.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowUpOnSquare(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 8.25H7.5C6.25736 8.25 5.25 9.25736 5.25 10.5V19.5C5.25 20.7426 6.25736 21.75 7.5 21.75H16.5C17.7426 21.75 18.75 20.7426 18.75 19.5V10.5C18.75 9.25736 17.7426 8.25 16.5 8.25H15M15 5.25L12 2.25M12 2.25L9 5.25M12 2.25L12 15"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowUpRight(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M4.5 19.5L19.5 4.5M19.5 4.5L8.25 4.5M19.5 4.5V15.75"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowUpTray(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3 16.5V18.75C3 19.9926 4.00736 21 5.25 21H18.75C19.9926 21 21 19.9926 21 18.75V16.5M7.5 7.5L12 3M12 3L16.5 7.5M12 3L12 16.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowUp(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M4.5 10.5L12 3M12 3L19.5 10.5M12 3V21"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowUturnDown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15 15L9 21M9 21L3 15M9 21V9C9 5.68629 11.6863 3 15 3C18.3137 3 21 5.68629 21 9V12"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowUturnLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 15L3 9M3 9L9 3M3 9H15C18.3137 9 21 11.6863 21 15C21 18.3137 18.3137 21 15 21H12"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowUturnRight(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15 15L21 9M21 9L15 3M21 9H9C5.68629 9 3 11.6863 3 15C3 18.3137 5.68629 21 9 21H12"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowUturnUp(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 9L15 3M15 3L21 9M15 3L15 15C15 18.3137 12.3137 21 9 21C5.68629 21 3 18.3137 3 15L3 12"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowsPointingIn(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 9L9 4.5M9 9L4.5 9M9 9L3.75 3.75M9 15L9 19.5M9 15L4.5 15M9 15L3.75 20.25M15 9H19.5M15 9V4.5M15 9L20.25 3.75M15 15H19.5M15 15L15 19.5M15 15L20.25 20.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowsPointingOut(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75 3.75V8.25M3.75 3.75H8.25M3.75 3.75L9 9M3.75 20.25V15.75M3.75 20.25H8.25M3.75 20.25L9 15M20.25 3.75L15.75 3.75M20.25 3.75V8.25M20.25 3.75L15 9M20.25 20.25H15.75M20.25 20.25V15.75M20.25 20.25L15 15"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowsRightLeft(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M7.5 21L3 16.5M3 16.5L7.5 12M3 16.5H16.5M16.5 3L21 7.5M21 7.5L16.5 12M21 7.5L7.5 7.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconArrowsUpDown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3 7.5L7.5 3M7.5 3L12 7.5M7.5 3V16.5M21 16.5L16.5 21M16.5 21L12 16.5M16.5 21L16.5 7.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconAtSymbol(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M16.5 12C16.5 14.4853 14.4853 16.5 12 16.5C9.51472 16.5 7.5 14.4853 7.5 12C7.5 9.51472 9.51472 7.5 12 7.5C14.4853 7.5 16.5 9.51472 16.5 12ZM16.5 12C16.5 13.6569 17.5074 15 18.75 15C19.9926 15 21 13.6569 21 12C21 9.69671 20.1213 7.3934 18.364 5.63604C14.8492 2.12132 9.15076 2.12132 5.63604 5.63604C2.12132 9.15076 2.12132 14.8492 5.63604 18.364C9.15076 21.8787 14.8492 21.8787 18.364 18.364M16.5 12V8.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ />
+ </svg>
+ )
+}
+export function IconBackspace(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 9.75L14.25 12M14.25 12L16.5 14.25M14.25 12L16.5 9.75M14.25 12L12 14.25M9.42049 19.1705L3.04549 12.7955C2.60615 12.3562 2.60615 11.6438 3.04549 11.2045L9.42049 4.82951C9.63147 4.61853 9.91762 4.5 10.216 4.5L19.5 4.5C20.7426 4.5 21.75 5.50736 21.75 6.75V17.25C21.75 18.4926 20.7426 19.5 19.5 19.5H10.216C9.91762 19.5 9.63147 19.3815 9.42049 19.1705Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBackward(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21 16.811C21 17.6748 20.0668 18.2164 19.3168 17.7878L12.2094 13.7264C11.4536 13.2945 11.4536 12.2047 12.2094 11.7728L19.3168 7.71141C20.0668 7.28285 21 7.82439 21 8.68819V16.811Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M11.25 16.811C11.25 17.6748 10.3168 18.2164 9.56684 17.7878L2.45935 13.7264C1.70356 13.2945 1.70356 12.2047 2.45935 11.7728L9.56684 7.71141C10.3168 7.28285 11.25 7.82439 11.25 8.68819L11.25 16.811Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBanknotes(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.25 18.75C7.71719 18.75 13.0136 19.4812 18.0468 20.8512C18.7738 21.0491 19.5 20.5086 19.5 19.7551V18.75M3.75 4.5V5.25C3.75 5.66421 3.41421 6 3 6H2.25M2.25 6V5.625C2.25 5.00368 2.75368 4.5 3.375 4.5H20.25M2.25 6V15M20.25 4.5V5.25C20.25 5.66421 20.5858 6 21 6H21.75M20.25 4.5H20.625C21.2463 4.5 21.75 5.00368 21.75 5.625V15.375C21.75 15.9963 21.2463 16.5 20.625 16.5H20.25M21.75 15H21C20.5858 15 20.25 15.3358 20.25 15.75V16.5M20.25 16.5H3.75M3.75 16.5H3.375C2.75368 16.5 2.25 15.9963 2.25 15.375V15M3.75 16.5V15.75C3.75 15.3358 3.41421 15 3 15H2.25M15 10.5C15 12.1569 13.6569 13.5 12 13.5C10.3431 13.5 9 12.1569 9 10.5C9 8.84315 10.3431 7.5 12 7.5C13.6569 7.5 15 8.84315 15 10.5ZM18 10.5H18.0075V10.5075H18V10.5ZM6 10.5H6.0075V10.5075H6V10.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBars2(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75 9H20.25M3.75 15.75H20.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBars3BottomLeft(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75 6.75H20.25M3.75 12H20.25M3.75 17.25H12"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBars3BottomRight(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75 6.75H20.25M3.75 12H20.25M12 17.25H20.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBars3CenterLeft(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75 6.75H20.25M3.75 12H12M3.75 17.25H20.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBars3(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75 6.75H20.25M3.75 12H20.25M3.75 17.25H20.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBars4(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75 5.25H20.25M3.75 9.75H20.25M3.75 14.25H20.25M3.75 18.75H20.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBarsArrowDown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3 4.5H17.25M3 9H12.75M3 13.5H12.75M17.25 9V21M17.25 21L13.5 17.25M17.25 21L21 17.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBarsArrowUp(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3 4.5H17.25M3 9H12.75M3 13.5H8.25M13.5 12.75L17.25 9M17.25 9L21 12.75M17.25 9V21"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBattery0(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21 10.5H21.375C21.9963 10.5 22.5 11.0037 22.5 11.625V13.875C22.5 14.4963 21.9963 15 21.375 15H21M3.75 18H18.75C19.9926 18 21 16.9926 21 15.75V9.75C21 8.50736 19.9926 7.5 18.75 7.5H3.75C2.50736 7.5 1.5 8.50736 1.5 9.75V15.75C1.5 16.9926 2.50736 18 3.75 18Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBattery100(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21 10.5H21.375C21.9963 10.5 22.5 11.0037 22.5 11.625V13.875C22.5 14.4963 21.9963 15 21.375 15H21M4.5 10.5H18V15H4.5V10.5ZM3.75 18H18.75C19.9926 18 21 16.9926 21 15.75V9.75C21 8.50736 19.9926 7.5 18.75 7.5H3.75C2.50736 7.5 1.5 8.50736 1.5 9.75V15.75C1.5 16.9926 2.50736 18 3.75 18Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBattery50(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21 10.5H21.375C21.9963 10.5 22.5 11.0037 22.5 11.625V13.875C22.5 14.4963 21.9963 15 21.375 15H21M4.5 10.5H11.25V15H4.5V10.5ZM3.75 18H18.75C19.9926 18 21 16.9926 21 15.75V9.75C21 8.50736 19.9926 7.5 18.75 7.5H3.75C2.50736 7.5 1.5 8.50736 1.5 9.75V15.75C1.5 16.9926 2.50736 18 3.75 18Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBeaker(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9.75001 3.10408V8.81802C9.75001 9.41476 9.51295 9.98705 9.091 10.409L5.00001 14.5M9.75001 3.10408C9.49886 3.12743 9.24884 3.15465 9.00001 3.18568M9.75001 3.10408C10.4908 3.03521 11.2413 3 12 3C12.7587 3 13.5093 3.03521 14.25 3.10408M14.25 3.10408V8.81802C14.25 9.41476 14.4871 9.98705 14.909 10.409L19.8 15.3M14.25 3.10408C14.5011 3.12743 14.7512 3.15465 15 3.18568M19.8 15.3L18.2299 15.6925C16.1457 16.2136 13.9216 15.9608 12 15C10.0784 14.0392 7.85435 13.7864 5.7701 14.3075L5.00001 14.5M19.8 15.3L21.2022 16.7022C22.4341 17.9341 21.8527 20.0202 20.1354 20.3134C17.4911 20.7649 14.773 21 12 21C9.227 21 6.50891 20.7649 3.86459 20.3134C2.14728 20.0202 1.56591 17.9341 2.7978 16.7022L5.00001 14.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBellAlert(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M14.8569 17.0817C16.7514 16.857 18.5783 16.4116 20.3111 15.7719C18.8743 14.177 17.9998 12.0656 17.9998 9.75V9.04919C17.9999 9.03281 18 9.01641 18 9C18 5.68629 15.3137 3 12 3C8.6863 3 6.00001 5.68629 6.00001 9L5.99982 9.75C5.99982 12.0656 5.12529 14.177 3.68849 15.7719C5.42142 16.4116 7.24845 16.857 9.14315 17.0818M14.8569 17.0817C13.92 17.1928 12.9666 17.25 11.9998 17.25C11.0332 17.25 10.0799 17.1929 9.14315 17.0818M14.8569 17.0817C14.9498 17.3711 15 17.6797 15 18C15 19.6569 13.6569 21 12 21C10.3432 21 9.00001 19.6569 9.00001 18C9.00001 17.6797 9.0502 17.3712 9.14315 17.0818M3.12445 7.5C3.41173 5.78764 4.18254 4.23924 5.29169 3M18.7083 3C19.8175 4.23924 20.5883 5.78764 20.8756 7.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBellSlash(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9.14314 17.0818C10.0799 17.1929 11.0332 17.25 11.9998 17.25C12.3306 17.25 12.6599 17.2433 12.9874 17.2301M9.14314 17.0818C7.24843 16.857 5.4214 16.4116 3.68848 15.7719C5.02539 14.2879 5.87549 12.3567 5.98723 10.2299M9.14314 17.0818C9.05019 17.3712 9 17.6797 9 18C9 19.6569 10.3431 21 12 21C13.2864 21 14.3837 20.1903 14.8101 19.0527M16.7749 16.7749L21 21M16.7749 16.7749C17.9894 16.5298 19.1706 16.1929 20.3111 15.7719C18.8743 14.177 17.9998 12.0656 17.9998 9.75V9.04919L18 9C18 5.68629 15.3137 3 12 3C9.5667 3 7.47171 4.44849 6.53026 6.53026M16.7749 16.7749L6.53026 6.53026M3 3L6.53026 6.53026"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBellSnooze(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M14.8569 17.0817C16.7514 16.857 18.5783 16.4116 20.3111 15.7719C18.8743 14.177 17.9998 12.0656 17.9998 9.75V9.04919C17.9999 9.03281 18 9.01641 18 9C18 5.68629 15.3137 3 12 3C8.68629 3 6 5.68629 6 9L5.9998 9.75C5.9998 12.0656 5.12527 14.177 3.68848 15.7719C5.4214 16.4116 7.24843 16.857 9.14314 17.0818M14.8569 17.0817C13.92 17.1928 12.9666 17.25 11.9998 17.25C11.0332 17.25 10.0799 17.1929 9.14314 17.0818M14.8569 17.0817C14.9498 17.3711 15 17.6797 15 18C15 19.6569 13.6569 21 12 21C10.3431 21 9 19.6569 9 18C9 17.6797 9.05019 17.3712 9.14314 17.0818M10.5 8.25H13.5L10.5 12.75H13.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBell(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M14.8569 17.0817C16.7514 16.857 18.5783 16.4116 20.3111 15.7719C18.8743 14.177 17.9998 12.0656 17.9998 9.75V9.04919C17.9999 9.03281 18 9.01641 18 9C18 5.68629 15.3137 3 12 3C8.68629 3 6 5.68629 6 9L5.9998 9.75C5.9998 12.0656 5.12527 14.177 3.68848 15.7719C5.4214 16.4116 7.24843 16.857 9.14314 17.0818M14.8569 17.0817C13.92 17.1928 12.9666 17.25 11.9998 17.25C11.0332 17.25 10.0799 17.1929 9.14314 17.0818M14.8569 17.0817C14.9498 17.3711 15 17.6797 15 18C15 19.6569 13.6569 21 12 21C10.3431 21 9 19.6569 9 18C9 17.6797 9.05019 17.3712 9.14314 17.0818"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBoltSlash(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M11.4123 15.6549L9.75 21.75L13.4949 17.7376M9.25736 13.5H3.75L6.40873 10.6514M8.4569 8.4569L14.25 2.25L12 10.5H20.25L15.5431 15.5431M8.4569 8.4569L3 3M8.4569 8.4569L15.5431 15.5431M15.5431 15.5431L21 21"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBolt(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75 13.5L14.25 2.25L12 10.5H20.25L9.75 21.75L12 13.5H3.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+
+export function IconBoltSolid(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 24 24"
+ fill="currentColor"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M14.615 1.595a.75.75 0 01.359.852L12.982 9.75h7.268a.75.75 0 01.548 1.262l-10.5 11.25a.75.75 0 01-1.272-.71l1.992-7.302H3.75a.75.75 0 01-.548-1.262l10.5-11.25a.75.75 0 01.913-.143z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ )
+}
+export function IconBookOpen(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 6.04168C10.4077 4.61656 8.30506 3.75 6 3.75C4.94809 3.75 3.93834 3.93046 3 4.26212V18.5121C3.93834 18.1805 4.94809 18 6 18C8.30506 18 10.4077 18.8666 12 20.2917M12 6.04168C13.5923 4.61656 15.6949 3.75 18 3.75C19.0519 3.75 20.0617 3.93046 21 4.26212V18.5121C20.0617 18.1805 19.0519 18 18 18C15.6949 18 13.5923 18.8666 12 20.2917M12 6.04168V20.2917"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBookmarkSlash(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3 3L4.66365 4.66365M21 21L19.5 19.5M14.0153 18.2576L12 17.25L4.5 21V8.74237M4.66365 4.66365C4.95294 3.94962 5.60087 3.41593 6.40668 3.32241C8.24156 3.10947 10.108 3 12 3C13.892 3 15.7584 3.10947 17.5933 3.32241C18.6939 3.45014 19.5 4.399 19.5 5.50699V19.5M4.66365 4.66365L19.5 19.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBookmarkSquare(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M16.5 3.75V16.5L12 14.25L7.5 16.5V3.75M16.5 3.75H18C19.2426 3.75 20.25 4.75736 20.25 6V18C20.25 19.2426 19.2426 20.25 18 20.25H6C4.75736 20.25 3.75 19.2426 3.75 18V6C3.75 4.75736 4.75736 3.75 6 3.75H7.5M16.5 3.75H7.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBookmark(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M17.5933 3.32241C18.6939 3.45014 19.5 4.399 19.5 5.50699V21L12 17.25L4.5 21V5.50699C4.5 4.399 5.30608 3.45014 6.40668 3.32241C8.24156 3.10947 10.108 3 12 3C13.892 3 15.7584 3.10947 17.5933 3.32241Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBriefcase(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M20.25 14.1499V18.4C20.25 19.4944 19.4631 20.4359 18.3782 20.58C16.2915 20.857 14.1624 21 12 21C9.83757 21 7.70854 20.857 5.62185 20.58C4.5369 20.4359 3.75 19.4944 3.75 18.4V14.1499M20.25 14.1499C20.7219 13.7476 21 13.1389 21 12.4889V8.70569C21 7.62475 20.2321 6.69082 19.1631 6.53086C18.0377 6.36247 16.8995 6.23315 15.75 6.14432M20.25 14.1499C20.0564 14.315 19.8302 14.4453 19.5771 14.5294C17.1953 15.3212 14.6477 15.75 12 15.75C9.35229 15.75 6.80469 15.3212 4.42289 14.5294C4.16984 14.4452 3.94361 14.3149 3.75 14.1499M3.75 14.1499C3.27808 13.7476 3 13.1389 3 12.4889V8.70569C3 7.62475 3.7679 6.69082 4.83694 6.53086C5.96233 6.36247 7.10049 6.23315 8.25 6.14432M15.75 6.14432V5.25C15.75 4.00736 14.7426 3 13.5 3H10.5C9.25736 3 8.25 4.00736 8.25 5.25V6.14432M15.75 6.14432C14.5126 6.0487 13.262 6 12 6C10.738 6 9.48744 6.0487 8.25 6.14432"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M12 12.75H12.0075V12.7575H12V12.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBugAnt(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M11.9997 12.75C13.1482 12.75 14.2778 12.8307 15.3833 12.9867C16.4196 13.1329 17.2493 13.9534 17.2493 15C17.2493 18.7279 14.8988 21.75 11.9993 21.75C9.09977 21.75 6.74927 18.7279 6.74927 15C6.74927 13.9535 7.57879 13.1331 8.61502 12.9868C9.72081 12.8307 10.8508 12.75 11.9997 12.75ZM11.9997 12.75C14.8825 12.75 17.6469 13.2583 20.2075 14.1901C20.083 16.2945 19.6873 18.3259 19.0549 20.25M11.9997 12.75C9.11689 12.75 6.35312 13.2583 3.79248 14.1901C3.91702 16.2945 4.31272 18.3259 4.94512 20.25M11.9997 12.75C13.2423 12.75 14.2498 11.7426 14.2498 10.5C14.2498 10.4652 14.249 10.4306 14.2475 10.3961M11.9997 12.75C10.757 12.75 9.74979 11.7426 9.74979 10.5C9.74979 10.4652 9.75058 10.4306 9.75214 10.3961M12.0002 8.25C12.995 8.25 13.971 8.16929 14.922 8.01406C15.3246 7.94835 15.6628 7.65623 15.7168 7.25196C15.7388 7.08776 15.7502 6.92021 15.7502 6.75C15.7502 6.11844 15.594 5.52335 15.3183 5.00121M12.0002 8.25C11.0053 8.25 10.0293 8.16929 9.0783 8.01406C8.67576 7.94835 8.33754 7.65623 8.28346 7.25196C8.26149 7.08777 8.25015 6.92021 8.25015 6.75C8.25015 6.1175 8.40675 5.52157 8.68327 4.99887M12.0002 8.25C10.7923 8.25 9.80641 9.20171 9.75214 10.3961M12.0002 8.25C13.208 8.25 14.1932 9.20171 14.2475 10.3961M8.68327 4.99887C8.25654 4.71496 7.86824 4.37787 7.52783 3.99707C7.59799 3.36615 7.7986 2.7746 8.10206 2.25M8.68327 4.99887C9.31221 3.81004 10.5616 3 12.0002 3C13.4397 3 14.6897 3.8111 15.3183 5.00121M15.3183 5.00121C15.7445 4.71804 16.1325 4.38184 16.4728 4.00201C16.4031 3.36924 16.2023 2.77597 15.898 2.25M4.92097 6C4.71594 7.08086 4.58339 8.18738 4.52856 9.3143C6.19671 9.86025 7.94538 10.2283 9.75214 10.3961M19.0786 6C19.2836 7.08086 19.4162 8.18738 19.471 9.3143C17.8029 9.86024 16.0542 10.2283 14.2475 10.3961"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBuildingLibrary(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 21V12.75M15.75 21V12.75M8.25 21V12.75M3 9L12 3L21 9M19.5 21V10.3325C17.0563 9.94906 14.5514 9.75 12 9.75C9.44861 9.75 6.94372 9.94906 4.5 10.3325V21M3 21H21M12 6.75H12.0075V6.7575H12V6.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBuildingOffice2(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.25 21H21.75M3.75 3V21M14.25 3V21M20.25 7.5V21M6.75 6.75H7.5M6.75 9.75H7.5M6.75 12.75H7.5M10.5 6.75H11.25M10.5 9.75H11.25M10.5 12.75H11.25M6.75 21V17.625C6.75 17.0037 7.25368 16.5 7.875 16.5H10.125C10.7463 16.5 11.25 17.0037 11.25 17.625V21M3 3H15M14.25 7.5H21M17.25 11.25H17.2575V11.2575H17.25V11.25ZM17.25 14.25H17.2575V14.2575H17.25V14.25ZM17.25 17.25H17.2575V17.2575H17.25V17.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBuildingOffice(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75 21H20.25M4.5 3H19.5M5.25 3V21M18.75 3V21M9 6.75H10.5M9 9.75H10.5M9 12.75H10.5M13.5 6.75H15M13.5 9.75H15M13.5 12.75H15M9 21V17.625C9 17.0037 9.50368 16.5 10.125 16.5H13.875C14.4963 16.5 15 17.0037 15 17.625V21"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconBuildingStorefront(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M13.5 20.9999V13.4999C13.5 13.0856 13.8358 12.7499 14.25 12.7499H17.25C17.6642 12.7499 18 13.0856 18 13.4999V20.9999M13.5 20.9999H2.36088M13.5 20.9999H18M18 20.9999H21.6391M20.25 20.9999V9.34863M3.75 20.9999V9.34888M3.75 9.34888C4.89729 10.012 6.38977 9.85281 7.37132 8.87127C7.41594 8.82665 7.45886 8.78097 7.50008 8.73432C8.04979 9.35708 8.85402 9.74986 9.75 9.74986C10.646 9.74986 11.4503 9.35704 12 8.73423C12.5497 9.35704 13.354 9.74986 14.25 9.74986C15.1459 9.74986 15.9501 9.35713 16.4998 8.73443C16.541 8.78101 16.5838 8.82662 16.6284 8.87118C17.61 9.85281 19.1027 10.012 20.25 9.34863M3.75 9.34888C3.52788 9.2205 3.31871 9.06129 3.12868 8.87127C1.95711 7.69969 1.95711 5.8002 3.12868 4.62863L4.31797 3.43934C4.59927 3.15804 4.9808 3 5.37863 3H18.6212C19.019 3 19.4005 3.15804 19.6818 3.43934L20.871 4.62854C22.0426 5.80011 22.0426 7.69961 20.871 8.87118C20.6811 9.06113 20.472 9.22028 20.25 9.34863M6.75 17.9999H10.5C10.9142 17.9999 11.25 17.6641 11.25 17.2499V13.4999C11.25 13.0856 10.9142 12.7499 10.5 12.7499H6.75C6.33579 12.7499 6 13.0856 6 13.4999V17.2499C6 17.6641 6.33579 17.9999 6.75 17.9999Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCake(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 8.25006V6.75006M12 8.25006C10.6448 8.25006 9.30281 8.30622 7.97608 8.41633C6.84499 8.51021 6 9.47329 6 10.6083V13.1214M12 8.25006C13.3552 8.25006 14.6972 8.30622 16.0239 8.41633C17.155 8.51021 18 9.47329 18 10.6083V13.1214M15 8.25006V6.75006M9 8.25006V6.75006M21 16.5001L19.5 17.2501C18.5557 17.7222 17.4443 17.7222 16.5 17.2501C15.5557 16.7779 14.4443 16.7779 13.5 17.2501C12.5557 17.7222 11.4443 17.7222 10.5 17.2501C9.55573 16.7779 8.44427 16.7779 7.5 17.2501C6.55573 17.7222 5.44427 17.7222 4.5 17.2501L3 16.5001M18 13.1214C16.0344 12.8763 14.032 12.7501 12 12.7501C9.96804 12.7501 7.96557 12.8763 6 13.1214M18 13.1214C18.3891 13.1699 18.7768 13.2231 19.163 13.2809C20.2321 13.4409 21 14.3748 21 15.4557V20.6251C21 21.2464 20.4963 21.7501 19.875 21.7501H4.125C3.50368 21.7501 3 21.2464 3 20.6251V15.4557C3 14.3748 3.76793 13.4409 4.83697 13.2809C5.22316 13.2231 5.61086 13.1699 6 13.1214M12.2652 3.10989C12.4117 3.25634 12.4117 3.49378 12.2652 3.64022C12.1188 3.78667 11.8813 3.78667 11.7349 3.64022C11.5884 3.49378 11.5884 3.25634 11.7349 3.10989C11.8104 3.03435 12.0001 2.84473 12.0001 2.84473C12.0001 2.84473 12.1943 3.039 12.2652 3.10989ZM9.26522 3.10989C9.41167 3.25634 9.41167 3.49378 9.26522 3.64022C9.11878 3.78667 8.88134 3.78667 8.73489 3.64022C8.58844 3.49378 8.58844 3.25634 8.73489 3.10989C8.81044 3.03435 9.00005 2.84473 9.00005 2.84473C9.00005 2.84473 9.19432 3.039 9.26522 3.10989ZM15.2652 3.10989C15.4117 3.25634 15.4117 3.49378 15.2652 3.64022C15.1188 3.78667 14.8813 3.78667 14.7349 3.64022C14.5884 3.49378 14.5884 3.25634 14.7349 3.10989C14.8104 3.03435 15.0001 2.84473 15.0001 2.84473C15.0001 2.84473 15.1943 3.039 15.2652 3.10989Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCalculator(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.75 15.75V18M8.25 11.25H8.2575V11.2575H8.25V11.25ZM8.25 13.5H8.2575V13.5075H8.25V13.5ZM8.25 15.75H8.2575V15.7575H8.25V15.75ZM8.25 18H8.2575V18.0075H8.25V18ZM10.7476 11.25H10.7551V11.2575H10.7476V11.25ZM10.7476 13.5H10.7551V13.5075H10.7476V13.5ZM10.7476 15.75H10.7551V15.7575H10.7476V15.75ZM10.7476 18H10.7551V18.0075H10.7476V18ZM13.2524 11.25H13.2599V11.2575H13.2524V11.25ZM13.2524 13.5H13.2599V13.5075H13.2524V13.5ZM13.2524 15.75H13.2599V15.7575H13.2524V15.75ZM13.2524 18H13.2599V18.0075H13.2524V18ZM15.75 11.25H15.7575V11.2575H15.75V11.25ZM15.75 13.5H15.7575V13.5075H15.75V13.5ZM8.25 6H15.75V8.25H8.25V6ZM12 2.25C10.108 2.25 8.24156 2.35947 6.40668 2.57241C5.30608 2.70014 4.5 3.649 4.5 4.75699V19.5C4.5 20.7426 5.50736 21.75 6.75 21.75H17.25C18.4926 21.75 19.5 20.7426 19.5 19.5V4.75699C19.5 3.649 18.6939 2.70014 17.5933 2.57241C15.7584 2.35947 13.892 2.25 12 2.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCalendarDays(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M6.75 3V5.25M17.25 3V5.25M3 18.75V7.5C3 6.25736 4.00736 5.25 5.25 5.25H18.75C19.9926 5.25 21 6.25736 21 7.5V18.75M3 18.75C3 19.9926 4.00736 21 5.25 21H18.75C19.9926 21 21 19.9926 21 18.75M3 18.75V11.25C3 10.0074 4.00736 9 5.25 9H18.75C19.9926 9 21 10.0074 21 11.25V18.75M12 12.75H12.0075V12.7575H12V12.75ZM12 15H12.0075V15.0075H12V15ZM12 17.25H12.0075V17.2575H12V17.25ZM9.75 15H9.7575V15.0075H9.75V15ZM9.75 17.25H9.7575V17.2575H9.75V17.25ZM7.5 15H7.5075V15.0075H7.5V15ZM7.5 17.25H7.5075V17.2575H7.5V17.25ZM14.25 12.75H14.2575V12.7575H14.25V12.75ZM14.25 15H14.2575V15.0075H14.25V15ZM14.25 17.25H14.2575V17.2575H14.25V17.25ZM16.5 12.75H16.5075V12.7575H16.5V12.75ZM16.5 15H16.5075V15.0075H16.5V15Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCalendar(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M6.75 3V5.25M17.25 3V5.25M3 18.75V7.5C3 6.25736 4.00736 5.25 5.25 5.25H18.75C19.9926 5.25 21 6.25736 21 7.5V18.75M3 18.75C3 19.9926 4.00736 21 5.25 21H18.75C19.9926 21 21 19.9926 21 18.75M3 18.75V11.25C3 10.0074 4.00736 9 5.25 9H18.75C19.9926 9 21 10.0074 21 11.25V18.75"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCamera(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M6.82689 6.1749C6.46581 6.75354 5.86127 7.13398 5.186 7.22994C4.80655 7.28386 4.42853 7.34223 4.05199 7.40497C2.99912 7.58042 2.25 8.50663 2.25 9.57402V18C2.25 19.2426 3.25736 20.25 4.5 20.25H19.5C20.7426 20.25 21.75 19.2426 21.75 18V9.57403C21.75 8.50664 21.0009 7.58043 19.948 7.40498C19.5715 7.34223 19.1934 7.28387 18.814 7.22995C18.1387 7.13398 17.5342 6.75354 17.1731 6.17491L16.3519 4.85889C15.9734 4.25237 15.3294 3.85838 14.6155 3.82005C13.7496 3.77355 12.8775 3.75 12 3.75C11.1225 3.75 10.2504 3.77355 9.3845 3.82005C8.6706 3.85838 8.02658 4.25237 7.64809 4.85889L6.82689 6.1749Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M16.5 12.75C16.5 15.2353 14.4853 17.25 12 17.25C9.51472 17.25 7.5 15.2353 7.5 12.75C7.5 10.2647 9.51472 8.25 12 8.25C14.4853 8.25 16.5 10.2647 16.5 12.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M18.75 10.5H18.7575V10.5075H18.75V10.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChartBarSquare(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M7.5 14.25V16.5M10.5 12V16.5M13.5 9.75V16.5M16.5 7.5V16.5M6 20.25H18C19.2426 20.25 20.25 19.2426 20.25 18V6C20.25 4.75736 19.2426 3.75 18 3.75H6C4.75736 3.75 3.75 4.75736 3.75 6V18C3.75 19.2426 4.75736 20.25 6 20.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChartBar(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3 13.125C3 12.5037 3.50368 12 4.125 12H6.375C6.99632 12 7.5 12.5037 7.5 13.125V19.875C7.5 20.4963 6.99632 21 6.375 21H4.125C3.50368 21 3 20.4963 3 19.875V13.125Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M9.75 8.625C9.75 8.00368 10.2537 7.5 10.875 7.5H13.125C13.7463 7.5 14.25 8.00368 14.25 8.625V19.875C14.25 20.4963 13.7463 21 13.125 21H10.875C10.2537 21 9.75 20.4963 9.75 19.875V8.625Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M16.5 4.125C16.5 3.50368 17.0037 3 17.625 3H19.875C20.4963 3 21 3.50368 21 4.125V19.875C21 20.4963 20.4963 21 19.875 21H17.625C17.0037 21 16.5 20.4963 16.5 19.875V4.125Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChartPie(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M10.5 6C6.35786 6 3 9.35786 3 13.5C3 17.6421 6.35786 21 10.5 21C14.6421 21 18 17.6421 18 13.5H10.5V6Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M13.5 10.5H21C21 6.35786 17.6421 3 13.5 3V10.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChatBubbleBottomCenterText(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M7.5 8.25H16.5M7.5 11.25H12M2.25 12.7593C2.25 14.3604 3.37341 15.754 4.95746 15.987C6.08596 16.1529 7.22724 16.2796 8.37985 16.3655C8.73004 16.3916 9.05017 16.5753 9.24496 16.8674L12 21L14.755 16.8675C14.9498 16.5753 15.2699 16.3917 15.6201 16.3656C16.7727 16.2796 17.914 16.153 19.0425 15.9871C20.6266 15.7542 21.75 14.3606 21.75 12.7595V6.74056C21.75 5.13946 20.6266 3.74583 19.0425 3.51293C16.744 3.17501 14.3926 3 12.0003 3C9.60776 3 7.25612 3.17504 4.95747 3.51302C3.37342 3.74593 2.25 5.13956 2.25 6.74064V12.7593Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChatBubbleBottomCenter(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.25 12.7593C2.25 14.3604 3.37341 15.754 4.95746 15.987C6.02548 16.144 7.10495 16.2659 8.19464 16.3513C8.66142 16.388 9.08828 16.6324 9.348 17.022L12 21L14.652 17.0221C14.9117 16.6325 15.3386 16.388 15.8053 16.3514C16.895 16.2659 17.9745 16.1441 19.0425 15.9871C20.6266 15.7542 21.75 14.3606 21.75 12.7595V6.74056C21.75 5.13946 20.6266 3.74583 19.0425 3.51293C16.744 3.17501 14.3926 3 12.0003 3C9.60776 3 7.25612 3.17504 4.95747 3.51302C3.37342 3.74593 2.25 5.13956 2.25 6.74064V12.7593Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChatBubbleLeftEllipsis(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M8.625 9.75C8.625 9.95711 8.45711 10.125 8.25 10.125C8.04289 10.125 7.875 9.95711 7.875 9.75C7.875 9.54289 8.04289 9.375 8.25 9.375C8.45711 9.375 8.625 9.54289 8.625 9.75ZM8.625 9.75H8.25M12.375 9.75C12.375 9.95711 12.2071 10.125 12 10.125C11.7929 10.125 11.625 9.95711 11.625 9.75C11.625 9.54289 11.7929 9.375 12 9.375C12.2071 9.375 12.375 9.54289 12.375 9.75ZM12.375 9.75H12M16.125 9.75C16.125 9.95711 15.9571 10.125 15.75 10.125C15.5429 10.125 15.375 9.95711 15.375 9.75C15.375 9.54289 15.5429 9.375 15.75 9.375C15.9571 9.375 16.125 9.54289 16.125 9.75ZM16.125 9.75H15.75M2.25 12.7593C2.25 14.3604 3.37341 15.754 4.95746 15.987C6.04357 16.1467 7.14151 16.27 8.25 16.3556V21L12.4335 16.8165C12.6402 16.6098 12.9193 16.4923 13.2116 16.485C15.1872 16.4361 17.1331 16.2678 19.0425 15.9871C20.6266 15.7542 21.75 14.3606 21.75 12.7595V6.74056C21.75 5.13946 20.6266 3.74583 19.0425 3.51293C16.744 3.17501 14.3926 3 12.0003 3C9.60776 3 7.25612 3.17504 4.95747 3.51302C3.37342 3.74593 2.25 5.13956 2.25 6.74064V12.7593Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChatBubbleLeftRight(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M20.25 8.51104C21.1341 8.79549 21.75 9.6392 21.75 10.6082V14.8938C21.75 16.0304 20.9026 16.9943 19.7697 17.0867C19.4308 17.1144 19.0909 17.1386 18.75 17.1592V20.25L15.75 17.25C14.3963 17.25 13.0556 17.1948 11.7302 17.0866C11.4319 17.0623 11.1534 16.9775 10.9049 16.8451M20.25 8.51104C20.0986 8.46232 19.9393 8.43 19.7739 8.41628C18.4472 8.30616 17.1051 8.25 15.75 8.25C14.3948 8.25 13.0528 8.30616 11.7261 8.41627C10.595 8.51015 9.75 9.47323 9.75 10.6082V14.8937C9.75 15.731 10.2099 16.4746 10.9049 16.8451M20.25 8.51104V6.63731C20.25 5.01589 19.0983 3.61065 17.4903 3.40191C15.4478 3.13676 13.365 3 11.2503 3C9.13533 3 7.05233 3.13678 5.00963 3.40199C3.40173 3.61074 2.25 5.01598 2.25 6.63738V12.8626C2.25 14.484 3.40173 15.8893 5.00964 16.098C5.58661 16.1729 6.16679 16.2376 6.75 16.2918V21L10.9049 16.8451"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChatBubbleLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.25 12.7593C2.25 14.3604 3.37341 15.754 4.95746 15.987C6.04357 16.1467 7.14151 16.27 8.25 16.3556V21L12.326 16.924C12.6017 16.6483 12.9738 16.4919 13.3635 16.481C15.2869 16.4274 17.1821 16.2606 19.0425 15.9871C20.6266 15.7542 21.75 14.3606 21.75 12.7595V6.74056C21.75 5.13946 20.6266 3.74583 19.0425 3.51293C16.744 3.17501 14.3926 3 12.0003 3C9.60776 3 7.25612 3.17504 4.95747 3.51302C3.37342 3.74593 2.25 5.13956 2.25 6.74064V12.7593Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChatBubbleOvalLeftEllipsis(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M8.625 12C8.625 12.2071 8.45711 12.375 8.25 12.375C8.04289 12.375 7.875 12.2071 7.875 12C7.875 11.7929 8.04289 11.625 8.25 11.625C8.45711 11.625 8.625 11.7929 8.625 12ZM8.625 12H8.25M12.375 12C12.375 12.2071 12.2071 12.375 12 12.375C11.7929 12.375 11.625 12.2071 11.625 12C11.625 11.7929 11.7929 11.625 12 11.625C12.2071 11.625 12.375 11.7929 12.375 12ZM12.375 12H12M16.125 12C16.125 12.2071 15.9571 12.375 15.75 12.375C15.5429 12.375 15.375 12.2071 15.375 12C15.375 11.7929 15.5429 11.625 15.75 11.625C15.9571 11.625 16.125 11.7929 16.125 12ZM16.125 12H15.75M21 12C21 16.5563 16.9706 20.25 12 20.25C11.1125 20.25 10.2551 20.1323 9.44517 19.9129C8.47016 20.5979 7.28201 21 6 21C5.80078 21 5.60376 20.9903 5.40967 20.9713C5.25 20.9558 5.0918 20.9339 4.93579 20.906C5.41932 20.3353 5.76277 19.6427 5.91389 18.8808C6.00454 18.4238 5.7807 17.9799 5.44684 17.6549C3.9297 16.1782 3 14.1886 3 12C3 7.44365 7.02944 3.75 12 3.75C16.9706 3.75 21 7.44365 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChatBubbleOvalLeft(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 20.25C16.9706 20.25 21 16.5563 21 12C21 7.44365 16.9706 3.75 12 3.75C7.02944 3.75 3 7.44365 3 12C3 14.1036 3.85891 16.0234 5.2728 17.4806C5.70538 17.9265 6.01357 18.5192 5.85933 19.121C5.68829 19.7883 5.368 20.3959 4.93579 20.906C5.0918 20.9339 5.25 20.9558 5.40967 20.9713C5.60376 20.9903 5.80078 21 6 21C7.28201 21 8.47016 20.5979 9.44517 19.9129C10.2551 20.1323 11.1125 20.25 12 20.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCheckBadge(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 12.75L11.25 15L15 9.75M21 12C21 13.2683 20.3704 14.3895 19.4067 15.0682C19.6081 16.2294 19.2604 17.4672 18.3637 18.3639C17.467 19.2606 16.2292 19.6083 15.068 19.4069C14.3893 20.3705 13.2682 21 12 21C10.7319 21 9.61072 20.3705 8.93204 19.407C7.77066 19.6086 6.53256 19.261 5.6357 18.3641C4.73886 17.4673 4.39125 16.2292 4.59286 15.0678C3.62941 14.3891 3 13.2681 3 12C3 10.7319 3.62946 9.61077 4.59298 8.93208C4.39147 7.77079 4.7391 6.53284 5.63587 5.63607C6.53265 4.73929 7.77063 4.39166 8.93194 4.59319C9.61061 3.62955 10.7318 3 12 3C13.2682 3 14.3893 3.6295 15.068 4.59307C16.2294 4.39145 17.4674 4.73906 18.3643 5.6359C19.2611 6.53274 19.6087 7.77081 19.4071 8.93218C20.3706 9.61087 21 10.7319 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCheckCircle(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 12.75L11.25 15L15 9.75M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M4.5 12.75L10.5 18.75L19.5 5.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChevronDoubleDown(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 5.25L12 12.75L4.5 5.25M19.5 11.25L12 18.75L4.5 11.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChevronDoubleLeft(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M18.75 19.5L11.25 12L18.75 4.5M12.75 19.5L5.25 12L12.75 4.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChevronDoubleRight(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M11.25 4.5L18.75 12L11.25 19.5M5.25 4.5L12.75 12L5.25 19.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChevronDoubleUp(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M4.5 12.75L12 5.25L19.5 12.75M4.5 18.75L12 11.25L19.5 18.75"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChevronDown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 8.25L12 15.75L4.5 8.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChevronLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.75 19.5L8.25 12L15.75 4.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChevronRight(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M8.25 4.5L15.75 12L8.25 19.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChevronUpDown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M8.25 15L12 18.75L15.75 15M8.25 9L12 5.25L15.75 9"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconChevronUp(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M4.5 15.75L12 8.25L19.5 15.75"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCircleStack(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M20.25 6.375C20.25 8.65317 16.5563 10.5 12 10.5C7.44365 10.5 3.75 8.65317 3.75 6.375M20.25 6.375C20.25 4.09683 16.5563 2.25 12 2.25C7.44365 2.25 3.75 4.09683 3.75 6.375M20.25 6.375V17.625C20.25 19.9032 16.5563 21.75 12 21.75C7.44365 21.75 3.75 19.9032 3.75 17.625V6.375M20.25 6.375V10.125M3.75 6.375V10.125M20.25 10.125V13.875C20.25 16.1532 16.5563 18 12 18C7.44365 18 3.75 16.1532 3.75 13.875V10.125M20.25 10.125C20.25 12.4032 16.5563 14.25 12 14.25C7.44365 14.25 3.75 12.4032 3.75 10.125"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconClipboardDocumentCheck(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M11.3495 3.83619C11.2848 4.04602 11.25 4.26894 11.25 4.5C11.25 4.91421 11.5858 5.25 12 5.25H16.5C16.9142 5.25 17.25 4.91421 17.25 4.5C17.25 4.26894 17.2152 4.04602 17.1505 3.83619M11.3495 3.83619C11.6328 2.91757 12.4884 2.25 13.5 2.25H15C16.0116 2.25 16.8672 2.91757 17.1505 3.83619M11.3495 3.83619C10.9739 3.85858 10.5994 3.88529 10.2261 3.91627C9.09499 4.01015 8.25 4.97324 8.25 6.10822V8.25M17.1505 3.83619C17.5261 3.85858 17.9006 3.88529 18.2739 3.91627C19.405 4.01015 20.25 4.97324 20.25 6.10822V16.5C20.25 17.7426 19.2426 18.75 18 18.75H15.75M8.25 8.25H4.875C4.25368 8.25 3.75 8.75368 3.75 9.375V20.625C3.75 21.2463 4.25368 21.75 4.875 21.75H14.625C15.2463 21.75 15.75 21.2463 15.75 20.625V18.75M8.25 8.25H14.625C15.2463 8.25 15.75 8.75368 15.75 9.375V18.75M7.5 15.75L9 17.25L12 13.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconClipboardDocumentList(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 12H12.75M9 15H12.75M9 18H12.75M15.75 18.75H18C19.2426 18.75 20.25 17.7426 20.25 16.5V6.10822C20.25 4.97324 19.405 4.01015 18.2739 3.91627C17.9006 3.88529 17.5261 3.85858 17.1505 3.83619M11.3495 3.83619C11.2848 4.04602 11.25 4.26894 11.25 4.5C11.25 4.91421 11.5858 5.25 12 5.25H16.5C16.9142 5.25 17.25 4.91421 17.25 4.5C17.25 4.26894 17.2152 4.04602 17.1505 3.83619M11.3495 3.83619C11.6328 2.91757 12.4884 2.25 13.5 2.25H15C16.0116 2.25 16.8672 2.91757 17.1505 3.83619M11.3495 3.83619C10.9739 3.85858 10.5994 3.88529 10.2261 3.91627C9.09499 4.01015 8.25 4.97324 8.25 6.10822V8.25M8.25 8.25H4.875C4.25368 8.25 3.75 8.75368 3.75 9.375V20.625C3.75 21.2463 4.25368 21.75 4.875 21.75H14.625C15.2463 21.75 15.75 21.2463 15.75 20.625V9.375C15.75 8.75368 15.2463 8.25 14.625 8.25H8.25ZM6.75 12H6.7575V12.0075H6.75V12ZM6.75 15H6.7575V15.0075H6.75V15ZM6.75 18H6.7575V18.0075H6.75V18Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconClipboardDocument(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M8.25 7.5V6.10822C8.25 4.97324 9.09499 4.01015 10.2261 3.91627C10.5994 3.88529 10.9739 3.85858 11.3495 3.83619M15.75 18H18C19.2426 18 20.25 16.9926 20.25 15.75V6.10822C20.25 4.97324 19.405 4.01015 18.2739 3.91627C17.9006 3.88529 17.5261 3.85858 17.1505 3.83619M15.75 18.75V16.875C15.75 15.011 14.239 13.5 12.375 13.5H10.875C10.2537 13.5 9.75 12.9963 9.75 12.375V10.875C9.75 9.01104 8.23896 7.5 6.375 7.5H5.25M17.1505 3.83619C16.8672 2.91757 16.0116 2.25 15 2.25H13.5C12.4884 2.25 11.6328 2.91757 11.3495 3.83619M17.1505 3.83619C17.2152 4.04602 17.25 4.26894 17.25 4.5V5.25H11.25V4.5C11.25 4.26894 11.2848 4.04602 11.3495 3.83619M6.75 7.5H4.875C4.25368 7.5 3.75 8.00368 3.75 8.625V20.625C3.75 21.2463 4.25368 21.75 4.875 21.75H14.625C15.2463 21.75 15.75 21.2463 15.75 20.625V16.5C15.75 11.5294 11.7206 7.5 6.75 7.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconClipboard(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.6657 3.88789C15.3991 2.94272 14.5305 2.25 13.5 2.25H10.5C9.46954 2.25 8.60087 2.94272 8.33426 3.88789M15.6657 3.88789C15.7206 4.0825 15.75 4.28782 15.75 4.5V4.5C15.75 4.91421 15.4142 5.25 15 5.25H9C8.58579 5.25 8.25 4.91421 8.25 4.5V4.5C8.25 4.28782 8.27937 4.0825 8.33426 3.88789M15.6657 3.88789C16.3119 3.93668 16.9545 3.99828 17.5933 4.07241C18.6939 4.20014 19.5 5.149 19.5 6.25699V19.5C19.5 20.7426 18.4926 21.75 17.25 21.75H6.75C5.50736 21.75 4.5 20.7426 4.5 19.5V6.25699C4.5 5.149 5.30608 4.20014 6.40668 4.07241C7.04547 3.99828 7.68808 3.93668 8.33426 3.88789"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconClock(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 6V12H16.5M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCloudArrowDown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 9.75V16.5M12 16.5L9 13.5M12 16.5L15 13.5M6.75 19.5C4.26472 19.5 2.25 17.4853 2.25 15C2.25 13.0071 3.54555 11.3167 5.3404 10.7252C5.28105 10.4092 5.25 10.0832 5.25 9.75C5.25 6.85051 7.60051 4.5 10.5 4.5C12.9312 4.5 14.9765 6.1526 15.5737 8.39575C15.8654 8.30113 16.1767 8.25 16.5 8.25C18.1569 8.25 19.5 9.59315 19.5 11.25C19.5 11.5981 19.4407 11.9324 19.3316 12.2433C20.7453 12.7805 21.75 14.1479 21.75 15.75C21.75 17.8211 20.0711 19.5 18 19.5H6.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCloudArrowUp(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 16.5L12 9.75M12 9.75L15 12.75M12 9.75L9 12.75M6.75 19.5C4.26472 19.5 2.25 17.4853 2.25 15C2.25 13.0071 3.54555 11.3167 5.3404 10.7252C5.28105 10.4092 5.25 10.0832 5.25 9.75C5.25 6.85051 7.60051 4.5 10.5 4.5C12.9312 4.5 14.9765 6.1526 15.5737 8.39575C15.8654 8.30113 16.1767 8.25 16.5 8.25C18.1569 8.25 19.5 9.59315 19.5 11.25C19.5 11.5981 19.4407 11.9324 19.3316 12.2433C20.7453 12.7805 21.75 14.1479 21.75 15.75C21.75 17.8211 20.0711 19.5 18 19.5H6.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCloud(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.25 15C2.25 17.4853 4.26472 19.5 6.75 19.5H18C20.0711 19.5 21.75 17.8211 21.75 15.75C21.75 14.1479 20.7453 12.7805 19.3316 12.2433C19.4407 11.9324 19.5 11.5981 19.5 11.25C19.5 9.59315 18.1569 8.25 16.5 8.25C16.1767 8.25 15.8654 8.30113 15.5737 8.39575C14.9765 6.1526 12.9312 4.5 10.5 4.5C7.6005 4.5 5.25 6.85051 5.25 9.75C5.25 10.0832 5.28105 10.4092 5.3404 10.7252C3.54555 11.3167 2.25 13.0071 2.25 15Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCodeBracketSquare(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M14.25 9.75L16.5 12L14.25 14.25M9.75 14.25L7.5 12L9.75 9.75M6 20.25H18C19.2426 20.25 20.25 19.2426 20.25 18V6C20.25 4.75736 19.2426 3.75 18 3.75H6C4.75736 3.75 3.75 4.75736 3.75 6V18C3.75 19.2426 4.75736 20.25 6 20.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCodeBracket(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M17.25 6.75L22.5 12L17.25 17.25M6.75 17.25L1.5 12L6.75 6.75M14.25 3.75L9.75 20.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCog6Tooth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9.59353 3.94005C9.68394 3.39759 10.1533 3 10.7032 3H13.2972C13.8471 3 14.3165 3.39759 14.4069 3.94005L14.6204 5.2211C14.6827 5.59514 14.9327 5.90671 15.2645 6.09036C15.3386 6.13142 15.412 6.17383 15.4844 6.21757C15.8094 6.41384 16.2048 6.47486 16.5603 6.34166L17.7772 5.88578C18.2922 5.69284 18.8712 5.90051 19.1461 6.37677L20.4431 8.62321C20.7181 9.09948 20.6084 9.70473 20.1839 10.0543L19.1795 10.8811C18.887 11.1219 18.742 11.4937 18.749 11.8725C18.7498 11.9149 18.7502 11.9574 18.7502 12C18.7502 12.0426 18.7498 12.0851 18.749 12.1275C18.742 12.5063 18.887 12.8781 19.1795 13.1189L20.1839 13.9457C20.6084 14.2953 20.7181 14.9005 20.4431 15.3768L19.1461 17.6232C18.8712 18.0995 18.2922 18.3071 17.7772 18.1142L16.5603 17.6583C16.2048 17.5251 15.8094 17.5862 15.4844 17.7824C15.412 17.8262 15.3386 17.8686 15.2645 17.9096C14.9327 18.0933 14.6827 18.4049 14.6204 18.7789L14.4069 20.0599C14.3165 20.6024 13.8471 21 13.2972 21H10.7032C10.1533 21 9.68394 20.6024 9.59353 20.0599L9.38002 18.7789C9.31768 18.4049 9.06771 18.0933 8.73594 17.9096C8.66176 17.8686 8.58844 17.8262 8.51601 17.7824C8.19098 17.5862 7.79565 17.5251 7.44008 17.6583L6.22322 18.1142C5.70822 18.3072 5.12923 18.0995 4.85426 17.6232L3.55728 15.3768C3.28231 14.9005 3.39196 14.2953 3.81654 13.9457L4.82089 13.1189C5.1134 12.8781 5.2584 12.5063 5.25138 12.1275C5.2506 12.0851 5.2502 12.0426 5.2502 12C5.2502 11.9574 5.2506 11.9149 5.25138 11.8725C5.2584 11.4937 5.1134 11.1219 4.82089 10.8811L3.81654 10.0543C3.39196 9.70475 3.28231 9.09949 3.55728 8.62323L4.85426 6.37679C5.12923 5.90052 5.70822 5.69286 6.22321 5.88579L7.44007 6.34167C7.79563 6.47487 8.19096 6.41385 8.516 6.21758C8.58843 6.17384 8.66176 6.13142 8.73594 6.09036C9.06771 5.90671 9.31768 5.59514 9.38002 5.2211L9.59353 3.94005Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M15 11.9999C15 13.6568 13.6568 14.9999 12 14.9999C10.3431 14.9999 8.99997 13.6568 8.99997 11.9999C8.99997 10.3431 10.3431 8.99992 12 8.99992C13.6568 8.99992 15 10.3431 15 11.9999Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCog8Tooth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M10.3427 3.94005C10.4331 3.39759 10.9024 3 11.4523 3H12.5463C13.0963 3 13.5656 3.39759 13.656 3.94005L13.805 4.83386C13.8757 5.25813 14.1886 5.59838 14.5859 5.76332C14.9833 5.92832 15.4396 5.90629 15.7898 5.65617L16.5273 5.12933C16.9749 4.80969 17.5879 4.86042 17.9767 5.24929L18.7503 6.02284C19.1392 6.41171 19.1899 7.02472 18.8702 7.47223L18.3432 8.21007C18.0932 8.56012 18.0711 9.01633 18.2361 9.41363C18.401 9.81078 18.7411 10.1236 19.1653 10.1943L20.0593 10.3433C20.6017 10.4337 20.9993 10.9031 20.9993 11.453V12.547C20.9993 13.0969 20.6017 13.5663 20.0593 13.6567L19.1655 13.8056C18.7412 13.8764 18.4009 14.1893 18.236 14.5865C18.071 14.9839 18.0931 15.4403 18.3432 15.7904L18.8699 16.5278C19.1895 16.9753 19.1388 17.5884 18.7499 17.9772L17.9764 18.7508C17.5875 19.1396 16.9745 19.1904 16.527 18.8707L15.7894 18.3439C15.4393 18.0938 14.9831 18.0718 14.5857 18.2367C14.1886 18.4016 13.8757 18.7418 13.805 19.166L13.656 20.0599C13.5656 20.6024 13.0963 21 12.5463 21H11.4523C10.9024 21 10.4331 20.6024 10.3427 20.0599L10.1937 19.1661C10.123 18.7419 9.81005 18.4016 9.41282 18.2367C9.01541 18.0717 8.55908 18.0937 8.20893 18.3438L7.47131 18.8707C7.0238 19.1904 6.41079 19.1396 6.02192 18.7507L5.24837 17.9772C4.8595 17.5883 4.80877 16.9753 5.12842 16.5278L5.65545 15.79C5.90549 15.4399 5.92754 14.9837 5.76258 14.5864C5.5977 14.1892 5.25752 13.8764 4.83335 13.8057L3.93938 13.6567C3.39692 13.5663 2.99933 13.0969 2.99933 12.547V11.453C2.99933 10.9031 3.39692 10.4337 3.93938 10.3433L4.83319 10.1944C5.25746 10.1236 5.59771 9.81071 5.76265 9.41347C5.92766 9.01605 5.90562 8.5597 5.6555 8.20954L5.12881 7.47216C4.80916 7.02465 4.85989 6.41164 5.24876 6.02277L6.02231 5.24922C6.41118 4.86036 7.02419 4.80962 7.4717 5.12927L8.2093 5.65613C8.55937 5.90618 9.01561 5.92822 9.41293 5.76326C9.8101 5.59837 10.123 5.25819 10.1937 4.834L10.3427 3.94005Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M15 12C15 13.6569 13.6569 15 12 15C10.3432 15 9.00003 13.6569 9.00003 12C9.00003 10.3432 10.3432 9.00002 12 9.00002C13.6569 9.00002 15 10.3432 15 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCog(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M4.50073 11.9993C4.50073 16.1414 7.8586 19.4993 12.0007 19.4993C16.1429 19.4993 19.5007 16.1414 19.5007 11.9993M4.50073 11.9993C4.50073 7.85712 7.8586 4.49925 12.0007 4.49925C16.1429 4.49926 19.5007 7.85712 19.5007 11.9993M4.50073 11.9993L3.00073 11.9993M19.5007 11.9993L21.0007 11.9993M19.5007 11.9993L12.0007 11.9993M3.54329 15.0774L4.95283 14.5644M19.0482 9.43411L20.4578 8.92108M5.1062 17.785L6.25527 16.8208M17.7459 7.17897L18.895 6.21479M7.50064 19.7943L8.25064 18.4952M15.7506 5.50484L16.5006 4.2058M10.4378 20.8633L10.6983 19.386M13.303 4.61393L13.5635 3.13672M13.5635 20.8633L13.303 19.3861M10.6983 4.61397L10.4378 3.13676M16.5007 19.7941L15.7507 18.4951M7.50068 4.20565L12.0007 11.9993M18.8952 17.7843L17.7461 16.8202M6.25542 7.17835L5.10635 6.21417M20.458 15.0776L19.0485 14.5646M4.95308 9.43426L3.54354 8.92123M12.0007 11.9993L8.25073 18.4944"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCommandLine(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M6.75 7.5L9.75 9.75L6.75 12M11.25 12H14.25M5.25 20.25H18.75C19.9926 20.25 21 19.2426 21 18V6C21 4.75736 19.9926 3.75 18.75 3.75H5.25C4.00736 3.75 3 4.75736 3 6V18C3 19.2426 4.00736 20.25 5.25 20.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconComputerDesktop(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 17.25V18.2574C9 19.053 8.68393 19.8161 8.12132 20.3787L7.5 21H16.5L15.8787 20.3787C15.3161 19.8161 15 19.053 15 18.2574V17.25M21 5.25V15C21 16.2426 19.9926 17.25 18.75 17.25H5.25C4.00736 17.25 3 16.2426 3 15V5.25M21 5.25C21 4.00736 19.9926 3 18.75 3H5.25C4.00736 3 3 4.00736 3 5.25M21 5.25V12C21 13.2426 19.9926 14.25 18.75 14.25H5.25C4.00736 14.25 3 13.2426 3 12V5.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCpuChip(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M8.25 3V4.5M4.5 8.25H3M21 8.25H19.5M4.5 12H3M21 12H19.5M4.5 15.75H3M21 15.75H19.5M8.25 19.5V21M12 3V4.5M12 19.5V21M15.75 3V4.5M15.75 19.5V21M6.75 19.5H17.25C18.4926 19.5 19.5 18.4926 19.5 17.25V6.75C19.5 5.50736 18.4926 4.5 17.25 4.5H6.75C5.50736 4.5 4.5 5.50736 4.5 6.75V17.25C4.5 18.4926 5.50736 19.5 6.75 19.5ZM7.5 7.5H16.5V16.5H7.5V7.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCreditCard(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.25 8.25H21.75M2.25 9H21.75M5.25 14.25H11.25M5.25 16.5H8.25M4.5 19.5H19.5C20.7426 19.5 21.75 18.4926 21.75 17.25V6.75C21.75 5.50736 20.7426 4.5 19.5 4.5H4.5C3.25736 4.5 2.25 5.50736 2.25 6.75V17.25C2.25 18.4926 3.25736 19.5 4.5 19.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCubeTransparent(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21 7.5L18.75 6.1875M21 7.5V9.75M21 7.5L18.75 8.8125M3 7.5L5.25 6.1875M3 7.5L5.25 8.8125M3 7.5V9.75M12 12.75L14.25 11.4375M12 12.75L9.75 11.4375M12 12.75V15M12 21.75L14.25 20.4375M12 21.75V19.5M12 21.75L9.75 20.4375M9.75 3.5625L12 2.25L14.25 3.5625M21 14.25V16.5L18.75 17.8125M5.25 17.8125L3 16.5V14.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCube(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21 7.5L12 2.25L3 7.5M21 7.5L12 12.75M21 7.5V16.5L12 21.75M3 7.5L12 12.75M3 7.5V16.5L12 21.75M12 12.75V21.75"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCurrencyBangladeshi(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M8.25 7.49997L8.66459 7.29267C9.16327 7.04333 9.75 7.40596 9.75 7.96349V10.5M9.75 10.5H15.75M9.75 10.5H8.25M9.75 10.5V15.9383C9.75 16.2921 9.91144 16.6351 10.2229 16.803C10.7518 17.0882 11.357 17.25 12 17.25C13.8142 17.25 15.3275 15.9617 15.675 14.25C15.7579 13.8414 15.412 13.5 14.995 13.5H14.25M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCurrencyDollar(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 6V18M9 15.1818L9.87887 15.841C11.0504 16.7197 12.9498 16.7197 14.1214 15.841C15.2929 14.9623 15.2929 13.5377 14.1214 12.659C13.5355 12.2196 12.7677 12 11.9999 12C11.275 12 10.5502 11.7804 9.99709 11.341C8.891 10.4623 8.891 9.03772 9.9971 8.15904C11.1032 7.28036 12.8965 7.28036 14.0026 8.15904L14.4175 8.48863M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCurrencyEuro(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M14.25 7.75625C12.667 7.19798 10.8341 7.5519 9.56802 8.81802C7.81066 10.5754 7.81066 13.4246 9.56802 15.182C10.8341 16.4481 12.667 16.802 14.25 16.2437M7.5 10.5H12.75M7.5 13.5H12.75M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCurrencyPound(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M14.1213 7.62877C12.9497 6.45719 11.0503 6.45719 9.87868 7.62877C9.37424 8.13321 9.08699 8.7726 9.01694 9.43073C8.9944 9.64251 9.01512 9.85582 9.04524 10.0667L9.5512 13.6084C9.68065 14.5146 9.5307 15.4386 9.12135 16.2573L9 16.5L10.5385 15.9872C11.0003 15.8332 11.4997 15.8332 11.9615 15.9872L12.6158 16.2053C13.182 16.394 13.7999 16.3501 14.3336 16.0832L15 15.75M8.25 12H12M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCurrencyRupee(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15 8.25L9 8.25M15 11.25H9M12 17.25L9 14.25H10.5C12.1569 14.25 13.5 12.9069 13.5 11.25C13.5 9.59315 12.1569 8.25 10.5 8.25M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCurrencyYen(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 7.5L12 12M12 12L15 7.5M12 12V17.25M15 12H9M15 15H9M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCursorArrowRays(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.0423 21.6718L13.6835 16.6007M13.6835 16.6007L11.1741 18.826L11.7425 9.35623L16.9697 17.2731L13.6835 16.6007ZM12 2.25V4.5M17.8336 4.66637L16.2426 6.25736M20.25 10.5H18M7.75736 14.7426L6.16637 16.3336M6 10.5H3.75M7.75736 6.25736L6.16637 4.66637"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconCursorArrowRipple(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.0423 21.6718L13.6835 16.6007M13.6835 16.6007L11.1741 18.826L11.7425 9.35623L16.9697 17.2731L13.6835 16.6007ZM6.16637 16.3336C2.94454 13.1118 2.94454 7.88819 6.16637 4.66637C9.38819 1.44454 14.6118 1.44454 17.8336 4.66637C19.4445 6.27724 20.25 8.38854 20.25 10.4999M8.28769 14.2123C6.23744 12.1621 6.23744 8.83794 8.28769 6.78769C10.3379 4.73744 13.6621 4.73744 15.7123 6.78769C16.7374 7.8128 17.25 9.15637 17.25 10.4999"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconDevicePhoneMobile(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M10.5 1.5H8.25C7.00736 1.5 6 2.50736 6 3.75V20.25C6 21.4926 7.00736 22.5 8.25 22.5H15.75C16.9926 22.5 18 21.4926 18 20.25V3.75C18 2.50736 16.9926 1.5 15.75 1.5H13.5M10.5 1.5V3H13.5V1.5M10.5 1.5H13.5M10.5 20.25H13.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconDeviceTablet(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M10.5 19.5H13.5M6.75 21.75H17.25C18.4926 21.75 19.5 20.7426 19.5 19.5V4.5C19.5 3.25736 18.4926 2.25 17.25 2.25H6.75C5.50736 2.25 4.5 3.25736 4.5 4.5V19.5C4.5 20.7426 5.50736 21.75 6.75 21.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconDocumentArrowDown(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 14.25V11.625C19.5 9.76104 17.989 8.25 16.125 8.25H14.625C14.0037 8.25 13.5 7.74632 13.5 7.125V5.625C13.5 3.76104 11.989 2.25 10.125 2.25H8.25M9 14.25L12 17.25M12 17.25L15 14.25M12 17.25L12 11.25M10.5 2.25H5.625C5.00368 2.25 4.5 2.75368 4.5 3.375V20.625C4.5 21.2463 5.00368 21.75 5.625 21.75H18.375C18.9963 21.75 19.5 21.2463 19.5 20.625V11.25C19.5 6.27944 15.4706 2.25 10.5 2.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconDocumentArrowUp(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 14.25V11.625C19.5 9.76104 17.989 8.25 16.125 8.25H14.625C14.0037 8.25 13.5 7.74632 13.5 7.125V5.625C13.5 3.76104 11.989 2.25 10.125 2.25H8.25M15 14.25L12 11.25M12 11.25L9 14.25M12 11.25L12 17.25M10.5 2.25H5.625C5.00368 2.25 4.5 2.75368 4.5 3.375V20.625C4.5 21.2463 5.00368 21.75 5.625 21.75H18.375C18.9963 21.75 19.5 21.2463 19.5 20.625V11.25C19.5 6.27944 15.4706 2.25 10.5 2.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconDocumentChartBar(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 14.25V11.625C19.5 9.76104 17.989 8.25 16.125 8.25H14.625C14.0037 8.25 13.5 7.74632 13.5 7.125V5.625C13.5 3.76104 11.989 2.25 10.125 2.25H8.25M9 16.5V17.25M12 14.25V17.25M15 12V17.25M10.5 2.25H5.625C5.00368 2.25 4.5 2.75368 4.5 3.375V20.625C4.5 21.2463 5.00368 21.75 5.625 21.75H18.375C18.9963 21.75 19.5 21.2463 19.5 20.625V11.25C19.5 6.27944 15.4706 2.25 10.5 2.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconDocumentCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M10.125 2.25H5.625C5.00368 2.25 4.5 2.75368 4.5 3.375V20.625C4.5 21.2463 5.00368 21.75 5.625 21.75H18.375C18.9963 21.75 19.5 21.2463 19.5 20.625V11.625M10.125 2.25H10.5C15.4706 2.25 19.5 6.27944 19.5 11.25V11.625M10.125 2.25C11.989 2.25 13.5 3.76104 13.5 5.625V7.125C13.5 7.74632 14.0037 8.25 14.625 8.25H16.125C17.989 8.25 19.5 9.76104 19.5 11.625M9 15L11.25 17.25L15 12"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconDocumentDuplicate(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.75 17.25V20.625C15.75 21.2463 15.2463 21.75 14.625 21.75H4.875C4.25368 21.75 3.75 21.2463 3.75 20.625V7.875C3.75 7.25368 4.25368 6.75 4.875 6.75H6.75C7.26107 6.75 7.76219 6.7926 8.25 6.87444M15.75 17.25H19.125C19.7463 17.25 20.25 16.7463 20.25 16.125V11.25C20.25 6.79051 17.0066 3.08855 12.75 2.37444C12.2622 2.2926 11.7611 2.25 11.25 2.25H9.375C8.75368 2.25 8.25 2.75368 8.25 3.375V6.87444M15.75 17.25H9.375C8.75368 17.25 8.25 16.7463 8.25 16.125V6.87444M20.25 13.5V11.625C20.25 9.76104 18.739 8.25 16.875 8.25H15.375C14.7537 8.25 14.25 7.74632 14.25 7.125V5.625C14.25 3.76104 12.739 2.25 10.875 2.25H9.75"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconDocumentMagnifyingGlass(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 14.25V11.625C19.5 9.76104 17.989 8.25 16.125 8.25H14.625C14.0037 8.25 13.5 7.74632 13.5 7.125V5.625C13.5 3.76104 11.989 2.25 10.125 2.25H8.25M13.4812 15.7312L15 17.25M10.5 2.25H5.625C5.00368 2.25 4.5 2.75368 4.5 3.375V19.875C4.5 20.4963 5.00368 21 5.625 21H18.375C18.9963 21 19.5 20.4963 19.5 19.875V11.25C19.5 6.27944 15.4706 2.25 10.5 2.25ZM14.25 13.875C14.25 15.3247 13.0747 16.5 11.625 16.5C10.1753 16.5 9 15.3247 9 13.875C9 12.4253 10.1753 11.25 11.625 11.25C13.0747 11.25 14.25 12.4253 14.25 13.875Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconDocumentMinus(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 14.25V11.625C19.5 9.76104 17.989 8.25 16.125 8.25H14.625C14.0037 8.25 13.5 7.74632 13.5 7.125V5.625C13.5 3.76104 11.989 2.25 10.125 2.25H8.25M15 14.25H9M10.5 2.25H5.625C5.00368 2.25 4.5 2.75368 4.5 3.375V20.625C4.5 21.2463 5.00368 21.75 5.625 21.75H18.375C18.9963 21.75 19.5 21.2463 19.5 20.625V11.25C19.5 6.27944 15.4706 2.25 10.5 2.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconDocumentPlus(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 14.25V11.625C19.5 9.76104 17.989 8.25 16.125 8.25H14.625C14.0037 8.25 13.5 7.74632 13.5 7.125V5.625C13.5 3.76104 11.989 2.25 10.125 2.25H8.25M12 11.25V17.25M15 14.25H9M10.5 2.25H5.625C5.00368 2.25 4.5 2.75368 4.5 3.375V20.625C4.5 21.2463 5.00368 21.75 5.625 21.75H18.375C18.9963 21.75 19.5 21.2463 19.5 20.625V11.25C19.5 6.27944 15.4706 2.25 10.5 2.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconDocumentText(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 14.25V11.625C19.5 9.76104 17.989 8.25 16.125 8.25H14.625C14.0037 8.25 13.5 7.74632 13.5 7.125V5.625C13.5 3.76104 11.989 2.25 10.125 2.25H8.25M8.25 15H15.75M8.25 18H12M10.5 2.25H5.625C5.00368 2.25 4.5 2.75368 4.5 3.375V20.625C4.5 21.2463 5.00368 21.75 5.625 21.75H18.375C18.9963 21.75 19.5 21.2463 19.5 20.625V11.25C19.5 6.27944 15.4706 2.25 10.5 2.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconDocument(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 14.25V11.625C19.5 9.76104 17.989 8.25 16.125 8.25H14.625C14.0037 8.25 13.5 7.74632 13.5 7.125V5.625C13.5 3.76104 11.989 2.25 10.125 2.25H8.25M10.5 2.25H5.625C5.00368 2.25 4.5 2.75368 4.5 3.375V20.625C4.5 21.2463 5.00368 21.75 5.625 21.75H18.375C18.9963 21.75 19.5 21.2463 19.5 20.625V11.25C19.5 6.27944 15.4706 2.25 10.5 2.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconEllipsisHorizontalCircle(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M8.625 12C8.625 12.2071 8.45711 12.375 8.25 12.375C8.04289 12.375 7.875 12.2071 7.875 12C7.875 11.7929 8.04289 11.625 8.25 11.625C8.45711 11.625 8.625 11.7929 8.625 12ZM8.625 12H8.25M12.375 12C12.375 12.2071 12.2071 12.375 12 12.375C11.7929 12.375 11.625 12.2071 11.625 12C11.625 11.7929 11.7929 11.625 12 11.625C12.2071 11.625 12.375 11.7929 12.375 12ZM12.375 12H12M16.125 12C16.125 12.2071 15.9571 12.375 15.75 12.375C15.5429 12.375 15.375 12.2071 15.375 12C15.375 11.7929 15.5429 11.625 15.75 11.625C15.9571 11.625 16.125 11.7929 16.125 12ZM16.125 12H15.75M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconEllipsisHorizontal(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M6.75 12C6.75 12.4142 6.41421 12.75 6 12.75C5.58579 12.75 5.25 12.4142 5.25 12C5.25 11.5858 5.58579 11.25 6 11.25C6.41421 11.25 6.75 11.5858 6.75 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M12.75 12C12.75 12.4142 12.4142 12.75 12 12.75C11.5858 12.75 11.25 12.4142 11.25 12C11.25 11.5858 11.5858 11.25 12 11.25C12.4142 11.25 12.75 11.5858 12.75 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M18.75 12C18.75 12.4142 18.4142 12.75 18 12.75C17.5858 12.75 17.25 12.4142 17.25 12C17.25 11.5858 17.5858 11.25 18 11.25C18.4142 11.25 18.75 11.5858 18.75 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconEllipsisVertical(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 6.75C11.5858 6.75 11.25 6.41421 11.25 6C11.25 5.58579 11.5858 5.25 12 5.25C12.4142 5.25 12.75 5.58579 12.75 6C12.75 6.41421 12.4142 6.75 12 6.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M12 12.75C11.5858 12.75 11.25 12.4142 11.25 12C11.25 11.5858 11.5858 11.25 12 11.25C12.4142 11.25 12.75 11.5858 12.75 12C12.75 12.4142 12.4142 12.75 12 12.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M12 18.75C11.5858 18.75 11.25 18.4142 11.25 18C11.25 17.5858 11.5858 17.25 12 17.25C12.4142 17.25 12.75 17.5858 12.75 18C12.75 18.4142 12.4142 18.75 12 18.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconEnvelopeOpen(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21.75 9.00021V9.9063C21.75 10.734 21.2955 11.4949 20.5667 11.8874L14.0893 15.3752M2.25 9.00021V9.9063C2.25 10.734 2.70448 11.4949 3.43328 11.8874L9.91074 15.3752M18.75 17.8849L14.0893 15.3752M14.0893 15.3752L13.0667 14.8246C12.4008 14.466 11.5992 14.466 10.9333 14.8246L9.91074 15.3752M9.91074 15.3752L5.25 17.8849M21.75 19.5002C21.75 20.7429 20.7426 21.7502 19.5 21.7502H4.5C3.25736 21.7502 2.25 20.7429 2.25 19.5002L2.25 8.84412C2.25 8.01639 2.70448 7.25549 3.43328 6.86307L10.9333 2.8246C11.5992 2.46602 12.4008 2.46602 13.0667 2.8246L20.5667 6.86307C21.2955 7.2555 21.75 8.01639 21.75 8.84413V19.5002Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconEnvelope(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21.75 6.75V17.25C21.75 18.4926 20.7426 19.5 19.5 19.5H4.5C3.25736 19.5 2.25 18.4926 2.25 17.25V6.75M21.75 6.75C21.75 5.50736 20.7426 4.5 19.5 4.5H4.5C3.25736 4.5 2.25 5.50736 2.25 6.75M21.75 6.75V6.99271C21.75 7.77405 21.3447 8.49945 20.6792 8.90894L13.1792 13.5243C12.4561 13.9694 11.5439 13.9694 10.8208 13.5243L3.32078 8.90894C2.65535 8.49945 2.25 7.77405 2.25 6.99271V6.75"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconEnvelopeSolid(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ {...props}
+ viewBox="0 0 24 24"
+ fill="currentColor"
+ >
+ <path d="M1.5 8.67v8.58a3 3 0 003 3h15a3 3 0 003-3V8.67l-8.928 5.493a3 3 0 01-3.144 0L1.5 8.67z" />
+ <path d="M22.5 6.908V6.75a3 3 0 00-3-3h-15a3 3 0 00-3 3v.158l9.714 5.978a1.5 1.5 0 001.572 0L22.5 6.908z" />
+ </svg>
+ )
+}
+export function IconExclamationCircle(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 9V12.75M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12ZM12 15.75H12.0075V15.7575H12V15.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconExclamationTriangle(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M11.9998 9.00006V12.7501M2.69653 16.1257C1.83114 17.6257 2.91371 19.5001 4.64544 19.5001H19.3541C21.0858 19.5001 22.1684 17.6257 21.303 16.1257L13.9487 3.37819C13.0828 1.87736 10.9167 1.87736 10.0509 3.37819L2.69653 16.1257ZM11.9998 15.7501H12.0073V15.7576H11.9998V15.7501Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconEyeDropper(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15 11.25L16.5 12.75L17.25 12V8.75798L19.5264 8.14802C20.019 8.01652 20.4847 7.75778 20.8712 7.37132C22.0428 6.19975 22.0428 4.30025 20.8712 3.12868C19.6996 1.95711 17.8001 1.95711 16.6286 3.12868C16.2421 3.51509 15.9832 3.98069 15.8517 4.47324L15.2416 6.74998H12L11.25 7.49998L12.75 8.99999M15 11.25L6.53033 19.7197C6.19077 20.0592 5.73022 20.25 5.25 20.25C4.76978 20.25 4.30924 20.4408 3.96967 20.7803L3 21.75L2.25 21L3.21967 20.0303C3.55923 19.6908 3.75 19.2302 3.75 18.75C3.75 18.2698 3.94077 17.8092 4.28033 17.4697L12.75 8.99999M15 11.25L12.75 8.99999"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconEyeSlash(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.9799 8.22257C3.05679 9.31382 2.35239 10.596 1.93433 12.0015C3.22562 16.338 7.24308 19.5 11.9991 19.5C12.9916 19.5 13.952 19.3623 14.8622 19.1049M6.2276 6.22763C7.88385 5.13558 9.86768 4.5 11.9999 4.5C16.7559 4.5 20.7734 7.66205 22.0647 11.9985C21.3528 14.3919 19.8105 16.4277 17.772 17.772M6.2276 6.22763L2.99997 3M6.2276 6.22763L9.87865 9.87868M17.772 17.772L21 21M17.772 17.772L14.1213 14.1213M14.1213 14.1213C14.6642 13.5784 15 12.8284 15 12C15 10.3431 13.6568 9 12 9C11.1715 9 10.4215 9.33579 9.87865 9.87868M14.1213 14.1213L9.87865 9.87868"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconEye(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.03556 12.3224C1.96648 12.1151 1.96642 11.8907 2.03538 11.6834C3.42374 7.50972 7.3608 4.5 12.0008 4.5C16.6387 4.5 20.5742 7.50692 21.9643 11.6776C22.0334 11.8849 22.0335 12.1093 21.9645 12.3166C20.5762 16.4903 16.6391 19.5 11.9991 19.5C7.36121 19.5 3.42565 16.4931 2.03556 12.3224Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M15 12C15 13.6569 13.6569 15 12 15C10.3432 15 9.00001 13.6569 9.00001 12C9.00001 10.3431 10.3432 9 12 9C13.6569 9 15 10.3431 15 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconFaceFrown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.1823 16.3179C14.3075 15.4432 13.1623 15.0038 12.0158 14.9999C10.859 14.996 9.70095 15.4353 8.81834 16.3179M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12ZM9.75 9.75C9.75 10.1642 9.58211 10.5 9.375 10.5C9.16789 10.5 9 10.1642 9 9.75C9 9.33579 9.16789 9 9.375 9C9.58211 9 9.75 9.33579 9.75 9.75ZM9.375 9.75H9.3825V9.765H9.375V9.75ZM15 9.75C15 10.1642 14.8321 10.5 14.625 10.5C14.4179 10.5 14.25 10.1642 14.25 9.75C14.25 9.33579 14.4179 9 14.625 9C14.8321 9 15 9.33579 15 9.75ZM14.625 9.75H14.6325V9.765H14.625V9.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconFaceSmile(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.182 15.182C13.4246 16.9393 10.5754 16.9393 8.81802 15.182M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12ZM9.75 9.75C9.75 10.1642 9.58211 10.5 9.375 10.5C9.16789 10.5 9 10.1642 9 9.75C9 9.33579 9.16789 9 9.375 9C9.58211 9 9.75 9.33579 9.75 9.75ZM9.375 9.75H9.3825V9.765H9.375V9.75ZM15 9.75C15 10.1642 14.8321 10.5 14.625 10.5C14.4179 10.5 14.25 10.1642 14.25 9.75C14.25 9.33579 14.4179 9 14.625 9C14.8321 9 15 9.33579 15 9.75ZM14.625 9.75H14.6325V9.765H14.625V9.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconFilm(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.375 19.5H20.625M3.375 19.5C2.75368 19.5 2.25 18.9963 2.25 18.375M3.375 19.5H4.875C5.49632 19.5 6 18.9963 6 18.375M2.25 18.375V5.625M2.25 18.375V16.875C2.25 16.2537 2.75368 15.75 3.375 15.75M21.75 18.375V5.625M21.75 18.375C21.75 18.9963 21.2463 19.5 20.625 19.5M21.75 18.375V16.875C21.75 16.2537 21.2463 15.75 20.625 15.75M20.625 19.5H19.125C18.5037 19.5 18 18.9963 18 18.375M20.625 4.5H3.375M20.625 4.5C21.2463 4.5 21.75 5.00368 21.75 5.625M20.625 4.5H19.125C18.5037 4.5 18 5.00368 18 5.625M21.75 5.625V7.125C21.75 7.74632 21.2463 8.25 20.625 8.25M3.375 4.5C2.75368 4.5 2.25 5.00368 2.25 5.625M3.375 4.5H4.875C5.49632 4.5 6 5.00368 6 5.625M2.25 5.625V7.125C2.25 7.74632 2.75368 8.25 3.375 8.25M3.375 8.25H4.875M3.375 8.25C2.75368 8.25 2.25 8.75368 2.25 9.375V10.875C2.25 11.4963 2.75368 12 3.375 12M4.875 8.25C5.49632 8.25 6 7.74632 6 7.125V5.625M4.875 8.25C5.49632 8.25 6 8.75368 6 9.375V10.875M6 5.625V10.875M6 5.625C6 5.00368 6.50368 4.5 7.125 4.5H16.875C17.4963 4.5 18 5.00368 18 5.625M19.125 8.25H20.625M19.125 8.25C18.5037 8.25 18 7.74632 18 7.125V5.625M19.125 8.25C18.5037 8.25 18 8.75368 18 9.375V10.875M20.625 8.25C21.2463 8.25 21.75 8.75368 21.75 9.375V10.875C21.75 11.4963 21.2463 12 20.625 12M18 5.625V10.875M7.125 12H16.875M7.125 12C6.50368 12 6 11.4963 6 10.875M7.125 12C6.50368 12 6 12.5037 6 13.125M6 10.875C6 11.4963 5.49632 12 4.875 12M18 10.875C18 11.4963 17.4963 12 16.875 12M18 10.875C18 11.4963 18.5037 12 19.125 12M16.875 12C17.4963 12 18 12.5037 18 13.125M6 18.375V13.125M6 18.375C6 18.9963 6.50368 19.5 7.125 19.5H16.875C17.4963 19.5 18 18.9963 18 18.375M6 18.375V16.875C6 16.2537 5.49632 15.75 4.875 15.75M18 18.375V13.125M18 18.375V16.875C18 16.2537 18.5037 15.75 19.125 15.75M18 13.125V14.625C18 15.2463 18.5037 15.75 19.125 15.75M18 13.125C18 12.5037 18.5037 12 19.125 12M6 13.125V14.625C6 15.2463 5.49632 15.75 4.875 15.75M6 13.125C6 12.5037 5.49632 12 4.875 12M3.375 12H4.875M3.375 12C2.75368 12 2.25 12.5037 2.25 13.125V14.625C2.25 15.2463 2.75368 15.75 3.375 15.75M19.125 12H20.625M20.625 12C21.2463 12 21.75 12.5037 21.75 13.125V14.625C21.75 15.2463 21.2463 15.75 20.625 15.75M3.375 15.75H4.875M19.125 15.75H20.625"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconFingerPrint(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M7.86391 4.24259C9.04956 3.45731 10.4714 3 12 3C16.1421 3 19.5 6.35786 19.5 10.5C19.5 13.4194 18.9443 16.2089 17.9324 18.7685M5.7426 6.36391C4.95732 7.54956 4.5 8.97138 4.5 10.5C4.5 11.9677 4.07875 13.3369 3.3501 14.4931M5.33889 18.052C7.14811 16.0555 8.25 13.4065 8.25 10.5C8.25 8.42893 9.92893 6.75 12 6.75C14.0711 6.75 15.75 8.42893 15.75 10.5C15.75 11.0269 15.7286 11.5487 15.686 12.0646M12.0003 10.5C12.0003 14.2226 10.6443 17.6285 8.39916 20.2506M15.033 15.6543C14.4852 17.5743 13.6391 19.3685 12.5479 20.9836"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconFire(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.3622 5.21385C18.2427 6.50093 20.25 9.391 20.25 12.7499C20.25 17.3062 16.5563 20.9999 12 20.9999C7.44365 20.9999 3.75 17.3062 3.75 12.7499C3.75 10.5378 4.62058 8.52914 6.03781 7.0477C6.8043 8.11811 7.82048 8.99755 9.00121 9.60089C9.04632 6.82521 10.348 4.35503 12.3621 2.73438C13.1255 3.75813 14.1379 4.61845 15.3622 5.21385Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M12 18.0003C14.0711 18.0003 15.75 16.3214 15.75 14.2503C15.75 12.347 14.3321 10.7749 12.4949 10.5327C11.4866 11.4372 10.7862 12.6781 10.5703 14.0789C9.78769 13.8876 9.06529 13.5428 8.43682 13.0782C8.31559 13.4469 8.25 13.841 8.25 14.2503C8.25 16.3214 9.92893 18.0003 12 18.0003Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconFlag(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3 3V4.5M3 21V15M3 15L5.77009 14.3075C7.85435 13.7864 10.0562 14.0281 11.9778 14.9889L12.0856 15.0428C13.9687 15.9844 16.1224 16.2359 18.1718 15.7537L21.2861 15.0209C21.097 13.2899 21 11.5313 21 9.75C21 7.98343 21.0954 6.23914 21.2814 4.52202L18.1718 5.25369C16.1224 5.73591 13.9687 5.48435 12.0856 4.54278L11.9778 4.48892C10.0562 3.52812 7.85435 3.28641 5.77009 3.80748L3 4.5M3 15V4.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconFolderArrowDown(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 13.5L12 16.5M12 16.5L15 13.5M12 16.5L12 10.5M13.0607 6.31066L10.9393 4.18934C10.658 3.90804 10.2765 3.75 9.87868 3.75H4.5C3.25736 3.75 2.25 4.75736 2.25 6V18C2.25 19.2426 3.25736 20.25 4.5 20.25H19.5C20.7426 20.25 21.75 19.2426 21.75 18V9C21.75 7.75736 20.7426 6.75 19.5 6.75H14.1213C13.7235 6.75 13.342 6.59197 13.0607 6.31066Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconFolderMinus(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15 13.5H9M13.0607 6.31066L10.9393 4.18934C10.658 3.90804 10.2765 3.75 9.87868 3.75H4.5C3.25736 3.75 2.25 4.75736 2.25 6V18C2.25 19.2426 3.25736 20.25 4.5 20.25H19.5C20.7426 20.25 21.75 19.2426 21.75 18V9C21.75 7.75736 20.7426 6.75 19.5 6.75H14.1213C13.7235 6.75 13.342 6.59197 13.0607 6.31066Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconFolderOpen(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75002 9.77602C3.86206 9.7589 3.97701 9.75 4.0943 9.75H19.9058C20.023 9.75 20.138 9.7589 20.25 9.77602M3.75002 9.77602C2.55402 9.9588 1.68986 11.0788 1.86691 12.3182L2.72405 18.3182C2.8824 19.4267 3.83173 20.25 4.95144 20.25H19.0486C20.1683 20.25 21.1176 19.4267 21.276 18.3182L22.1331 12.3182C22.3102 11.0788 21.446 9.9588 20.25 9.77602M3.75002 9.77602V6C3.75002 4.75736 4.75738 3.75 6.00002 3.75H9.8787C10.2765 3.75 10.6581 3.90804 10.9394 4.18934L13.0607 6.31066C13.342 6.59197 13.7235 6.75 14.1213 6.75H18C19.2427 6.75 20.25 7.75736 20.25 9V9.77602"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconFolderPlus(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 10.5V16.5M15 13.5H9M13.0607 6.31066L10.9393 4.18934C10.658 3.90804 10.2765 3.75 9.87868 3.75H4.5C3.25736 3.75 2.25 4.75736 2.25 6V18C2.25 19.2426 3.25736 20.25 4.5 20.25H19.5C20.7426 20.25 21.75 19.2426 21.75 18V9C21.75 7.75736 20.7426 6.75 19.5 6.75H14.1213C13.7235 6.75 13.342 6.59197 13.0607 6.31066Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconFolder(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.25 12.75V12C2.25 10.7574 3.25736 9.75 4.5 9.75H19.5C20.7426 9.75 21.75 10.7574 21.75 12V12.75M13.0607 6.31066L10.9393 4.18934C10.658 3.90804 10.2765 3.75 9.87868 3.75H4.5C3.25736 3.75 2.25 4.75736 2.25 6V18C2.25 19.2426 3.25736 20.25 4.5 20.25H19.5C20.7426 20.25 21.75 19.2426 21.75 18V9C21.75 7.75736 20.7426 6.75 19.5 6.75H14.1213C13.7235 6.75 13.342 6.59197 13.0607 6.31066Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconForward(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3 8.68819C3 7.82439 3.93317 7.28285 4.68316 7.71141L11.7906 11.7728C12.5464 12.2047 12.5464 13.2945 11.7906 13.7264L4.68316 17.7878C3.93317 18.2164 3 17.6748 3 16.811V8.68819Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M12.75 8.68819C12.75 7.82439 13.6832 7.28285 14.4332 7.71141L21.5406 11.7728C22.2964 12.2047 22.2964 13.2945 21.5406 13.7264L14.4332 17.7878C13.6832 18.2164 12.75 17.6748 12.75 16.811V8.68819Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconFunnel(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12.0001 3C14.7548 3 17.4552 3.23205 20.0831 3.67767C20.6159 3.76803 21 4.23355 21 4.77402V5.81802C21 6.41476 20.7629 6.98705 20.341 7.40901L14.909 12.841C14.4871 13.2629 14.25 13.8352 14.25 14.432V17.3594C14.25 18.2117 13.7685 18.9908 13.0062 19.3719L9.75 21V14.432C9.75 13.8352 9.51295 13.2629 9.09099 12.841L3.65901 7.40901C3.23705 6.98705 3 6.41476 3 5.81802V4.77404C3 4.23357 3.38408 3.76805 3.91694 3.67769C6.54479 3.23206 9.24533 3 12.0001 3Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconGif(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12.75 8.25V15.75M18.75 8.25H15.75V12M15.75 12V15.75M15.75 12H18M9.75 9.34835C8.72056 7.88388 7.05152 7.88388 6.02208 9.34835C4.99264 10.8128 4.99264 13.1872 6.02208 14.6517C7.05152 16.1161 8.72056 16.1161 9.75 14.6517V12H8.25M4.5 19.5H19.5C20.7426 19.5 21.75 18.4926 21.75 17.25V6.75C21.75 5.50736 20.7426 4.5 19.5 4.5H4.5C3.25736 4.5 2.25 5.50736 2.25 6.75V17.25C2.25 18.4926 3.25736 19.5 4.5 19.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconGiftTop(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 3.75V20.25M2.25 12H21.75M6.375 17.25C9.06739 17.25 11.25 15.0674 11.25 12.375V12M17.625 17.25C14.9326 17.25 12.75 15.0674 12.75 12.375V12M3.75 20.25H20.25C21.0784 20.25 21.75 19.5784 21.75 18.75V5.25C21.75 4.42157 21.0784 3.75 20.25 3.75H3.75C2.92157 3.75 2.25 4.42157 2.25 5.25V18.75C2.25 19.5784 2.92157 20.25 3.75 20.25ZM16.3713 10.8107C14.9623 12.2197 12.1286 11.8714 12.1286 11.8714C12.1286 11.8714 11.7803 9.03772 13.1893 7.62871C14.068 6.75003 15.4926 6.75003 16.3713 7.62871C17.2499 8.50739 17.2499 9.93201 16.3713 10.8107ZM10.773 7.62874C12.182 9.03775 11.8336 11.8714 11.8336 11.8714C11.8336 11.8714 9 12.2197 7.59099 10.8107C6.71231 9.93204 6.71231 8.50742 7.59099 7.62874C8.46967 6.75006 9.89429 6.75006 10.773 7.62874Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconGift(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21 11.25V19.5C21 20.3284 20.3284 21 19.5 21H5.25C4.42157 21 3.75 20.3284 3.75 19.5V11.25M12 4.875C12 3.42525 10.8247 2.25 9.375 2.25C7.92525 2.25 6.75 3.42525 6.75 4.875C6.75 6.32475 7.92525 7.5 9.375 7.5C10.1095 7.5 12 7.5 12 7.5M12 4.875C12 5.59024 12 7.5 12 7.5M12 4.875C12 3.42525 13.1753 2.25 14.625 2.25C16.0747 2.25 17.25 3.42525 17.25 4.875C17.25 6.32475 16.0747 7.5 14.625 7.5C13.8905 7.5 12 7.5 12 7.5M12 7.5V21M3.375 11.25H21.375C21.9963 11.25 22.5 10.7463 22.5 10.125V8.625C22.5 8.00368 21.9963 7.5 21.375 7.5H3.375C2.75368 7.5 2.25 8.00368 2.25 8.625V10.125C2.25 10.7463 2.75368 11.25 3.375 11.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconGlobeAlt(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 21C16.1926 21 19.7156 18.1332 20.7157 14.2529M12 21C7.80742 21 4.28442 18.1332 3.2843 14.2529M12 21C14.4853 21 16.5 16.9706 16.5 12C16.5 7.02944 14.4853 3 12 3M12 21C9.51472 21 7.5 16.9706 7.5 12C7.5 7.02944 9.51472 3 12 3M12 3C15.3652 3 18.299 4.84694 19.8431 7.58245M12 3C8.63481 3 5.70099 4.84694 4.15692 7.58245M19.8431 7.58245C17.7397 9.40039 14.9983 10.5 12 10.5C9.00172 10.5 6.26027 9.40039 4.15692 7.58245M19.8431 7.58245C20.5797 8.88743 21 10.3946 21 12C21 12.778 20.9013 13.5329 20.7157 14.2529M20.7157 14.2529C18.1334 15.6847 15.1619 16.5 12 16.5C8.8381 16.5 5.86662 15.6847 3.2843 14.2529M3.2843 14.2529C3.09871 13.5329 3 12.778 3 12C3 10.3946 3.42032 8.88743 4.15692 7.58245"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconGlobeAmericas(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M6.11507 5.19043L6.4339 7.10337C6.63948 8.33689 7.22535 9.47535 8.10962 10.3596L9.75 12L9.36262 12.7747C9.14607 13.2079 9.23096 13.731 9.57336 14.0734L10.9205 15.4205C11.1315 15.6315 11.25 15.9176 11.25 16.216V17.3047C11.25 17.7308 11.4908 18.1204 11.8719 18.3109L12.0247 18.3874C12.4579 18.6039 12.981 18.519 13.3234 18.1766L14.0461 17.4539C15.161 16.339 15.952 14.9419 16.3344 13.4122C16.4357 13.0073 16.2962 12.5802 15.9756 12.313L14.6463 11.2053C14.3947 10.9956 14.0642 10.906 13.7411 10.9598L12.5711 11.1548C12.2127 11.2146 11.8475 11.0975 11.5906 10.8406L11.2955 10.5455C10.8562 10.1062 10.8562 9.39384 11.2955 8.9545L11.4266 8.82336C11.769 8.48095 12.2921 8.39607 12.7253 8.61263L13.3292 8.91459C13.4415 8.97076 13.5654 9 13.691 9C14.2924 9 14.6835 8.3671 14.4146 7.82918L14.25 7.5L15.5057 6.66289C16.1573 6.22849 16.6842 5.63157 17.0344 4.93112L17.1803 4.63942M6.11507 5.19043C4.20716 6.84073 3 9.27939 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 8.95801 19.4908 6.26851 17.1803 4.63942M6.11507 5.19043C7.69292 3.82562 9.75004 3 12 3C13.9286 3 15.7155 3.6066 17.1803 4.63942"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconGlobeAsiaAustralia(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12.75 3.03081V3.59808C12.75 3.93196 12.8983 4.24858 13.1548 4.46233L14.2234 5.35284C14.6651 5.7209 14.7582 6.36275 14.4393 6.84112L13.9282 7.60766C13.6507 8.02398 13.2423 8.3359 12.7676 8.49413L12.6254 8.54154C11.9327 8.77243 11.6492 9.59877 12.0542 10.2063C12.4237 10.7605 12.2238 11.5131 11.6281 11.811L9 13.125L9.42339 14.1835C9.608 14.645 9.40803 15.171 8.96343 15.3933C8.5503 15.5999 8.04855 15.4814 7.77142 15.1119L7.09217 14.2062C6.59039 13.5372 5.55995 13.6301 5.18594 14.3781L4.5 15.75L3.88804 15.903M12.75 3.03081C12.5027 3.0104 12.2526 3 12 3C7.02944 3 3 7.02944 3 12C3 13.3984 3.31894 14.7223 3.88804 15.903M12.75 3.03081C17.3696 3.41192 21 7.282 21 12C21 13.8792 20.4241 15.6239 19.4391 17.0672M19.4391 17.0672L19.2628 16.5385C18.9566 15.6197 18.0968 15 17.1283 15H16.5L16.1756 14.6756C15.9031 14.4031 15.5335 14.25 15.1481 14.25C14.5977 14.25 14.0945 14.561 13.8484 15.0533L13.8119 15.1263C13.6131 15.5237 13.2567 15.8195 12.8295 15.9416L11.8408 16.2241C11.2906 16.3813 10.9461 16.9263 11.0401 17.4907L11.1131 17.9285C11.1921 18.4026 11.6022 18.75 12.0828 18.75C12.9291 18.75 13.6805 19.2916 13.9482 20.0945L14.1628 20.7384M19.4391 17.0672C18.2095 18.8688 16.3425 20.2007 14.1628 20.7384M14.1628 20.7384C13.47 20.9093 12.7456 21 12 21C8.42785 21 5.34177 18.9189 3.88804 15.903M15.7498 9C15.7498 9.89602 15.3569 10.7003 14.7341 11.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconGlobeEuropeAfrica(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M20.8929 13.3929L19.7582 12.2582C19.5872 12.0872 19.4449 11.8897 19.3367 11.6734L18.2567 9.5133C18.1304 9.26078 17.7938 9.20616 17.5942 9.4058C17.3818 9.61824 17.0708 9.69881 16.782 9.61627L15.5091 9.25259C15.0257 9.11447 14.5239 9.40424 14.402 9.892C14.3108 10.2566 14.4587 10.6392 14.7715 10.8476L15.3582 11.2388C15.9489 11.6326 16.0316 12.4684 15.5297 12.9703L15.3295 13.1705C15.1185 13.3815 15 13.6676 15 13.966V14.3768C15 14.7846 14.8892 15.1847 14.6794 15.5344L13.3647 17.7254C12.9834 18.3611 12.2964 18.75 11.5552 18.75C10.9724 18.75 10.5 18.2776 10.5 17.6948V16.5233C10.5 15.6033 9.93986 14.7759 9.08563 14.4343L8.43149 14.1726C7.44975 13.7799 6.8739 12.7566 7.04773 11.7136L7.05477 11.6714C7.10117 11.393 7.19956 11.1257 7.3448 10.8837L7.43421 10.7347C7.92343 9.91928 8.87241 9.49948 9.80483 9.68597L10.9827 9.92153C11.5574 10.0365 12.124 9.69096 12.285 9.12744L12.4935 8.39774C12.6422 7.87721 12.3991 7.32456 11.9149 7.08245L11.25 6.75L11.159 6.84099C10.7371 7.26295 10.1648 7.5 9.56802 7.5H9.38709C9.13924 7.5 8.90095 7.59905 8.7257 7.7743C8.44222 8.05778 8.00814 8.12907 7.64958 7.94979C7.16433 7.70716 6.98833 7.10278 7.26746 6.63757L8.67936 4.2844C8.82024 4.04961 8.91649 3.79207 8.96453 3.52474M20.8929 13.3929C20.9634 12.9389 21 12.4737 21 12C21 7.02944 16.9706 3 12 3C10.9348 3 9.91287 3.18504 8.96453 3.52474M20.8929 13.3929C20.2234 17.702 16.4968 21 12 21C7.02944 21 3 16.9706 3 12C3 8.09461 5.48749 4.77021 8.96453 3.52474"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconHandRaised(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M10.05 4.575C10.05 3.70515 9.34486 3 8.47501 3C7.60516 3 6.90001 3.70515 6.90001 4.575L6.9 7.575M10.05 4.575L10.05 3.075C10.05 2.20515 10.7552 1.5 11.625 1.5C12.4949 1.5 13.2 2.20515 13.2 3.075L13.2 4.575M10.05 4.575L10.125 10.5M13.2 11.25V4.575M13.2 4.575C13.2 3.70515 13.9052 3 14.775 3C15.6449 3 16.35 3.70515 16.35 4.575V15M6.9 7.575C6.9 6.70515 6.19485 6 5.325 6C4.45515 6 3.75 6.70515 3.75 7.575V15.75C3.75 19.4779 6.77208 22.5 10.5 22.5H12.5179C13.9103 22.5 15.2456 21.9469 16.2302 20.9623L17.9623 19.2302C18.9469 18.2456 19.5 16.9103 19.5 15.5179L19.5031 13.494C19.5046 13.3209 19.5701 13.1533 19.7007 13.0227C20.3158 12.4076 20.3158 11.4104 19.7007 10.7953C19.0857 10.1802 18.0884 10.1802 17.4733 10.7953C16.7315 11.5371 16.3578 12.5111 16.3531 13.4815M6.9 7.575V12M13.17 16.318C13.5599 15.9281 14.0035 15.6248 14.477 15.4079C15.0701 15.1362 15.71 15.0003 16.35 15M16.3519 15H16.35"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconHandThumbDown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M7.5 15L9.75 15M17.7745 5.25C17.7851 5.30001 17.802 5.34962 17.8258 5.3978C18.4175 6.59708 18.75 7.94722 18.75 9.375C18.75 10.8618 18.3895 12.2643 17.7511 13.5M17.7745 5.25C17.6975 4.88534 17.9575 4.5 18.3493 4.5L19.2571 4.5C20.1458 4.5 20.9701 5.01802 21.2294 5.86805C21.5679 6.97738 21.75 8.15493 21.75 9.375C21.75 10.9275 21.4552 12.4111 20.9185 13.7729C20.6135 14.547 19.8327 15 19.0006 15H17.9479C17.476 15 17.2027 14.4441 17.4477 14.0407C17.5548 13.8644 17.6561 13.684 17.7511 13.5M17.7745 5.25L16.4803 5.25C15.9966 5.25 15.5161 5.17203 15.0572 5.01908L11.9428 3.98093C11.4839 3.82798 11.0034 3.75 10.5198 3.75L6.50377 3.75C5.88581 3.75 5.2866 3.99749 4.899 4.47878C3.24188 6.53642 2.25 9.15238 2.25 12C2.25 12.4341 2.27306 12.8629 2.31801 13.2851C2.4267 14.306 3.34564 15 4.37227 15L7.49809 15C8.11638 15 8.48896 15.724 8.22337 16.2823C7.75956 17.2574 7.5 18.3484 7.5 19.5C7.5 20.7426 8.50736 21.75 9.75 21.75C10.1642 21.75 10.5 21.4142 10.5 21V20.3666C10.5 19.7941 10.6092 19.2269 10.8219 18.6954C11.1257 17.9357 11.7523 17.3644 12.4745 16.9798C13.5883 16.3866 14.5627 15.5662 15.3359 14.5803C15.8335 13.9458 16.5611 13.5 17.3674 13.5H17.7511"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconHandThumbUp(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M6.63257 10.5C7.43892 10.5 8.16648 10.0542 8.6641 9.41967C9.43726 8.43384 10.4117 7.6134 11.5255 7.02021C12.2477 6.63563 12.8743 6.06428 13.1781 5.30464C13.3908 4.7731 13.5 4.20587 13.5 3.63338V3C13.5 2.58579 13.8358 2.25 14.25 2.25C15.4926 2.25 16.5 3.25736 16.5 4.5C16.5 5.65163 16.2404 6.74263 15.7766 7.71771C15.511 8.27604 15.8836 9 16.5019 9H19.6277C20.6544 9 21.5733 9.69399 21.682 10.7149C21.7269 11.1371 21.75 11.5658 21.75 12C21.75 14.8476 20.7581 17.4636 19.101 19.5212C18.7134 20.0025 18.1142 20.25 17.4962 20.25H13.4802C12.9966 20.25 12.5161 20.172 12.0572 20.0191L8.94278 18.9809C8.48393 18.828 8.00342 18.75 7.51975 18.75H5.90421M14.25 9H16.5M5.90421 18.75C5.98702 18.9546 6.07713 19.1554 6.17423 19.3522C6.37137 19.7517 6.0962 20.25 5.65067 20.25H4.74289C3.85418 20.25 3.02991 19.732 2.77056 18.882C2.43208 17.7726 2.25 16.5951 2.25 15.375C2.25 13.8225 2.54481 12.3389 3.08149 10.9771C3.38655 10.203 4.16733 9.75 4.99936 9.75H6.05212C6.52404 9.75 6.7973 10.3059 6.5523 10.7093C5.72588 12.0698 5.25 13.6668 5.25 15.375C5.25 16.5685 5.48232 17.7078 5.90421 18.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconHashtag(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M5.25 8.25H20.25M3.75 15.75H18.75M16.95 2.25L13.05 21.75M10.9503 2.25L7.05029 21.75"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconHeart(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21 8.25C21 5.76472 18.9013 3.75 16.3125 3.75C14.3769 3.75 12.7153 4.87628 12 6.48342C11.2847 4.87628 9.62312 3.75 7.6875 3.75C5.09867 3.75 3 5.76472 3 8.25C3 15.4706 12 20.25 12 20.25C12 20.25 21 15.4706 21 8.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconHomeModern(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M8.25 21V16.125C8.25 15.5037 8.75368 15 9.375 15H11.625C12.2463 15 12.75 15.5037 12.75 16.125V21M12.75 21H17.25V3.54545M12.75 21H20.25V10.75M2.25 21H3.75M21.75 21H3.75M2.25 9L6.75 7.36364M18.75 3L17.25 3.54545M17.25 9.75L20.25 10.75M21.75 11.25L20.25 10.75M6.75 7.36364V3H3.75V21M6.75 7.36364L17.25 3.54545"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconHome(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.25 11.9998L11.2045 3.04533C11.6438 2.60599 12.3562 2.60599 12.7955 3.04532L21.75 11.9998M4.5 9.74983V19.8748C4.5 20.4961 5.00368 20.9998 5.625 20.9998H9.75V16.1248C9.75 15.5035 10.2537 14.9998 10.875 14.9998H13.125C13.7463 14.9998 14.25 15.5035 14.25 16.1248V20.9998H18.375C18.9963 20.9998 19.5 20.4962 19.5 19.8748V9.74983M8.25 20.9998H16.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconIdentification(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15 9H18.75M15 12H18.75M15 15H18.75M4.5 19.5H19.5C20.7426 19.5 21.75 18.4926 21.75 17.25V6.75C21.75 5.50736 20.7426 4.5 19.5 4.5H4.5C3.25736 4.5 2.25 5.50736 2.25 6.75V17.25C2.25 18.4926 3.25736 19.5 4.5 19.5ZM10.5 9.375C10.5 10.4105 9.66053 11.25 8.625 11.25C7.58947 11.25 6.75 10.4105 6.75 9.375C6.75 8.33947 7.58947 7.5 8.625 7.5C9.66053 7.5 10.5 8.33947 10.5 9.375ZM11.7939 15.7114C10.8489 16.2147 9.77021 16.5 8.62484 16.5C7.47948 16.5 6.40074 16.2147 5.45581 15.7114C5.92986 14.4207 7.16983 13.5 8.62484 13.5C10.0799 13.5 11.3198 14.4207 11.7939 15.7114Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconInboxArrowDown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 3.75H6.91179C5.92403 3.75 5.05178 4.39423 4.76129 5.33831L2.3495 13.1766C2.28354 13.391 2.25 13.614 2.25 13.8383V18C2.25 19.2426 3.25736 20.25 4.5 20.25H19.5C20.7426 20.25 21.75 19.2426 21.75 18V13.8383C21.75 13.614 21.7165 13.391 21.6505 13.1766L19.2387 5.33831C18.9482 4.39423 18.076 3.75 17.0882 3.75H15M2.25 13.5H6.10942C6.96166 13.5 7.74075 13.9815 8.12188 14.7438L8.37812 15.2562C8.75925 16.0185 9.53834 16.5 10.3906 16.5H13.6094C14.4617 16.5 15.2408 16.0185 15.6219 15.2562L15.8781 14.7438C16.2592 13.9815 17.0383 13.5 17.8906 13.5H21.75M12 3V11.25M12 11.25L9 8.25M12 11.25L15 8.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconInboxStack(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M7.875 14.25L9.08906 16.1925C9.50022 16.8504 10.2213 17.25 10.9971 17.25H13.0029C13.7787 17.25 14.4998 16.8504 14.9109 16.1925L16.125 14.25M2.40961 9H7.04584C7.79813 9 8.50065 9.37598 8.91795 10.0019L9.08205 10.2481C9.49935 10.874 10.2019 11.25 10.9542 11.25H13.0458C13.7981 11.25 14.5007 10.874 14.9179 10.2481L15.0821 10.0019C15.4993 9.37598 16.2019 9 16.9542 9H21.5904M2.40961 9C2.30498 9.2628 2.25 9.54503 2.25 9.83233V12C2.25 13.2426 3.25736 14.25 4.5 14.25H19.5C20.7426 14.25 21.75 13.2426 21.75 12V9.83233C21.75 9.54503 21.695 9.2628 21.5904 9M2.40961 9C2.50059 8.77151 2.62911 8.55771 2.79167 8.36805L6.07653 4.53572C6.50399 4.03702 7.12802 3.75 7.78485 3.75H16.2151C16.872 3.75 17.496 4.03702 17.9235 4.53572L21.2083 8.36805C21.3709 8.55771 21.4994 8.77151 21.5904 9M4.5 20.25H19.5C20.7426 20.25 21.75 19.2426 21.75 18V15.375C21.75 14.7537 21.2463 14.25 20.625 14.25H3.375C2.75368 14.25 2.25 14.7537 2.25 15.375V18C2.25 19.2426 3.25736 20.25 4.5 20.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconInbox(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.25 13.5H6.10942C6.96166 13.5 7.74075 13.9815 8.12188 14.7438L8.37812 15.2562C8.75925 16.0185 9.53834 16.5 10.3906 16.5H13.6094C14.4617 16.5 15.2408 16.0185 15.6219 15.2562L15.8781 14.7438C16.2592 13.9815 17.0383 13.5 17.8906 13.5H21.75M2.25 13.8383V18C2.25 19.2426 3.25736 20.25 4.5 20.25H19.5C20.7426 20.25 21.75 19.2426 21.75 18V13.8383C21.75 13.614 21.7165 13.391 21.6505 13.1766L19.2387 5.33831C18.9482 4.39423 18.076 3.75 17.0882 3.75H6.91179C5.92403 3.75 5.05178 4.39423 4.76129 5.33831L2.3495 13.1766C2.28354 13.391 2.25 13.614 2.25 13.8383Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconInformationCircle(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M11.25 11.25L11.2915 11.2293C11.8646 10.9427 12.5099 11.4603 12.3545 12.082L11.6455 14.918C11.4901 15.5397 12.1354 16.0573 12.7085 15.7707L12.75 15.75M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12ZM12 8.25H12.0075V8.2575H12V8.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconKey(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.75 5.25C17.4069 5.25 18.75 6.59315 18.75 8.25M21.75 8.25C21.75 11.5637 19.0637 14.25 15.75 14.25C15.3993 14.25 15.0555 14.2199 14.7213 14.1622C14.1583 14.0649 13.562 14.188 13.158 14.592L10.5 17.25H8.25V19.5H6V21.75H2.25V18.932C2.25 18.3352 2.48705 17.7629 2.90901 17.341L9.408 10.842C9.81202 10.438 9.93512 9.84172 9.83785 9.2787C9.7801 8.94446 9.75 8.60074 9.75 8.25C9.75 4.93629 12.4363 2.25 15.75 2.25C19.0637 2.25 21.75 4.93629 21.75 8.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconLanguage(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M10.5 21L15.75 9.75L21 21M12 18H19.5M3 5.62136C4.96557 5.37626 6.96804 5.25 9 5.25M9 5.25C10.1208 5.25 11.2326 5.28841 12.3343 5.364M9 5.25V3M12.3343 5.364C11.1763 10.6578 7.68868 15.0801 3 17.5023M12.3343 5.364C13.2298 5.42545 14.1186 5.51146 15 5.62136M10.4113 14.1162C8.78554 12.4619 7.47704 10.4949 6.58432 8.31366"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconLifebuoy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M16.7124 4.3299C17.2999 4.69153 17.8548 5.12691 18.364 5.63604C18.8731 6.14517 19.3085 6.70012 19.6701 7.28763M16.7124 4.3299L13.2636 8.46838M16.7124 4.3299C13.8316 2.5567 10.1684 2.5567 7.28763 4.3299M19.6701 7.28763L15.5316 10.7364M19.6701 7.28763C21.4433 10.1684 21.4433 13.8316 19.6701 16.7124M15.5316 10.7364C15.3507 10.2297 15.0574 9.75408 14.6517 9.34835C14.2459 8.94262 13.7703 8.6493 13.2636 8.46838M15.5316 10.7364C15.8228 11.5519 15.8228 12.4481 15.5316 13.2636M13.2636 8.46838C12.4481 8.17721 11.5519 8.17721 10.7364 8.46838M15.5316 13.2636C15.3507 13.7703 15.0574 14.2459 14.6517 14.6517C14.2459 15.0574 13.7703 15.3507 13.2636 15.5316M15.5316 13.2636L19.6701 16.7124M19.6701 16.7124C19.3085 17.2999 18.8731 17.8548 18.364 18.364C17.8548 18.8731 17.2999 19.3085 16.7124 19.6701M16.7124 19.6701L13.2636 15.5316M16.7124 19.6701C13.8316 21.4433 10.1684 21.4433 7.28763 19.6701M13.2636 15.5316C12.4481 15.8228 11.5519 15.8228 10.7364 15.5316M10.7364 15.5316C10.2297 15.3507 9.75408 15.0574 9.34835 14.6517C8.94262 14.2459 8.6493 13.7703 8.46838 13.2636M10.7364 15.5316L7.28763 19.6701M7.28763 19.6701C6.70012 19.3085 6.14517 18.8731 5.63604 18.364C5.12691 17.8548 4.69153 17.2999 4.3299 16.7124M4.3299 16.7124L8.46838 13.2636M4.3299 16.7124C2.5567 13.8316 2.5567 10.1684 4.3299 7.28763M8.46838 13.2636C8.17721 12.4481 8.17721 11.5519 8.46838 10.7364M8.46838 10.7364C8.6493 10.2297 8.94262 9.75408 9.34835 9.34835C9.75408 8.94262 10.2297 8.6493 10.7364 8.46838M8.46838 10.7364L4.3299 7.28763M10.7364 8.46838L7.28763 4.3299M7.28763 4.3299C6.70012 4.69153 6.14517 5.12691 5.63604 5.63604C5.12691 6.14517 4.69153 6.70013 4.3299 7.28763"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconLightBulb(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 18V12.75M12 12.75C12.5179 12.75 13.0206 12.6844 13.5 12.561M12 12.75C11.4821 12.75 10.9794 12.6844 10.5 12.561M14.25 20.0394C13.5212 20.1777 12.769 20.25 12 20.25C11.231 20.25 10.4788 20.1777 9.75 20.0394M13.5 22.422C13.007 22.4736 12.5066 22.5 12 22.5C11.4934 22.5 10.993 22.4736 10.5 22.422M14.25 18V17.8083C14.25 16.8254 14.9083 15.985 15.7585 15.4917C17.9955 14.1938 19.5 11.7726 19.5 9C19.5 4.85786 16.1421 1.5 12 1.5C7.85786 1.5 4.5 4.85786 4.5 9C4.5 11.7726 6.00446 14.1938 8.24155 15.4917C9.09173 15.985 9.75 16.8254 9.75 17.8083V18"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconLink(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M13.1903 8.68842C13.6393 8.90291 14.0601 9.19611 14.432 9.56802C16.1893 11.3254 16.1893 14.1746 14.432 15.932L9.93198 20.432C8.17462 22.1893 5.32538 22.1893 3.56802 20.432C1.81066 18.6746 1.81066 15.8254 3.56802 14.068L5.32499 12.311M18.675 11.689L20.432 9.93198C22.1893 8.17462 22.1893 5.32538 20.432 3.56802C18.6746 1.81066 15.8254 1.81066 14.068 3.56802L9.56802 8.06802C7.81066 9.82538 7.81066 12.6746 9.56802 14.432C9.93992 14.8039 10.3607 15.0971 10.8097 15.3116"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconListBullet(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M8.25 6.75H20.25M8.25 12H20.25M8.25 17.25H20.25M3.75 6.75H3.7575V6.7575H3.75V6.75ZM4.125 6.75C4.125 6.95711 3.95711 7.125 3.75 7.125C3.54289 7.125 3.375 6.95711 3.375 6.75C3.375 6.54289 3.54289 6.375 3.75 6.375C3.95711 6.375 4.125 6.54289 4.125 6.75ZM3.75 12H3.7575V12.0075H3.75V12ZM4.125 12C4.125 12.2071 3.95711 12.375 3.75 12.375C3.54289 12.375 3.375 12.2071 3.375 12C3.375 11.7929 3.54289 11.625 3.75 11.625C3.95711 11.625 4.125 11.7929 4.125 12ZM3.75 17.25H3.7575V17.2575H3.75V17.25ZM4.125 17.25C4.125 17.4571 3.95711 17.625 3.75 17.625C3.54289 17.625 3.375 17.4571 3.375 17.25C3.375 17.0429 3.54289 16.875 3.75 16.875C3.95711 16.875 4.125 17.0429 4.125 17.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconLockClosed(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M16.5 10.5V6.75C16.5 4.26472 14.4853 2.25 12 2.25C9.51472 2.25 7.5 4.26472 7.5 6.75V10.5M6.75 21.75H17.25C18.4926 21.75 19.5 20.7426 19.5 19.5V12.75C19.5 11.5074 18.4926 10.5 17.25 10.5H6.75C5.50736 10.5 4.5 11.5074 4.5 12.75V19.5C4.5 20.7426 5.50736 21.75 6.75 21.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconLockOpen(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M13.5 10.5V6.75C13.5 4.26472 15.5147 2.25 18 2.25C20.4853 2.25 22.5 4.26472 22.5 6.75V10.5M3.75 21.75H14.25C15.4926 21.75 16.5 20.7426 16.5 19.5V12.75C16.5 11.5074 15.4926 10.5 14.25 10.5H3.75C2.50736 10.5 1.5 11.5074 1.5 12.75V19.5C1.5 20.7426 2.50736 21.75 3.75 21.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconMagnifyingGlassCircle(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.75 15.75L13.2615 13.2615M13.2615 13.2615C13.8722 12.6507 14.25 11.807 14.25 10.875C14.25 9.01104 12.739 7.5 10.875 7.5C9.01104 7.5 7.5 9.01104 7.5 10.875C7.5 12.739 9.01104 14.25 10.875 14.25C11.807 14.25 12.6507 13.8722 13.2615 13.2615ZM21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconMagnifyingGlassMinus(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21 21L15.8033 15.8033M15.8033 15.8033C17.1605 14.4461 18 12.5711 18 10.5C18 6.35786 14.6421 3 10.5 3C6.35786 3 3 6.35786 3 10.5C3 14.6421 6.35786 18 10.5 18C12.5711 18 14.4461 17.1605 15.8033 15.8033ZM13.5 10.5H7.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconMagnifyingGlassPlus(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21 21L15.8033 15.8033M15.8033 15.8033C17.1605 14.4461 18 12.5711 18 10.5C18 6.35786 14.6421 3 10.5 3C6.35786 3 3 6.35786 3 10.5C3 14.6421 6.35786 18 10.5 18C12.5711 18 14.4461 17.1605 15.8033 15.8033ZM10.5 7.5V13.5M13.5 10.5H7.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconMagnifyingGlass(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21 21L15.8033 15.8033M15.8033 15.8033C17.1605 14.4461 18 12.5711 18 10.5C18 6.35786 14.6421 3 10.5 3C6.35786 3 3 6.35786 3 10.5C3 14.6421 6.35786 18 10.5 18C12.5711 18 14.4461 17.1605 15.8033 15.8033Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconMapPin(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15 10.5C15 12.1569 13.6569 13.5 12 13.5C10.3431 13.5 9 12.1569 9 10.5C9 8.84315 10.3431 7.5 12 7.5C13.6569 7.5 15 8.84315 15 10.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M19.5 10.5C19.5 17.6421 12 21.75 12 21.75C12 21.75 4.5 17.6421 4.5 10.5C4.5 6.35786 7.85786 3 12 3C16.1421 3 19.5 6.35786 19.5 10.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconMap(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 6.75002V15M15 9.00002V17.25M15.5031 20.7485L20.3781 18.311C20.7592 18.1204 21 17.7309 21 17.3047V4.82031C21 3.98401 20.1199 3.44007 19.3719 3.81408L15.5031 5.74847C15.1864 5.90683 14.8136 5.90683 14.4969 5.74847L9.50312 3.25158C9.1864 3.09322 8.8136 3.09322 8.49688 3.25158L3.62188 5.68908C3.24075 5.87965 3 6.26919 3 6.69531V19.1797C3 20.016 3.8801 20.56 4.62811 20.186L8.49688 18.2516C8.8136 18.0932 9.1864 18.0932 9.50312 18.2516L14.4969 20.7485C14.8136 20.9068 15.1864 20.9068 15.5031 20.7485Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconMegaphone(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M10.3404 15.8398C9.65153 15.7803 8.95431 15.75 8.25 15.75H7.5C5.01472 15.75 3 13.7353 3 11.25C3 8.76472 5.01472 6.75 7.5 6.75H8.25C8.95431 6.75 9.65153 6.71966 10.3404 6.66022M10.3404 15.8398C10.5933 16.8015 10.9237 17.7317 11.3246 18.6234C11.5721 19.1738 11.3842 19.8328 10.8616 20.1345L10.2053 20.5134C9.6539 20.8318 8.9456 20.6306 8.67841 20.0527C8.0518 18.6973 7.56541 17.2639 7.23786 15.771M10.3404 15.8398C9.95517 14.3745 9.75 12.8362 9.75 11.25C9.75 9.66379 9.95518 8.1255 10.3404 6.66022M10.3404 15.8398C13.5 16.1124 16.4845 16.9972 19.1747 18.3749M10.3404 6.66022C13.5 6.3876 16.4845 5.50283 19.1747 4.12509M19.1747 4.12509C19.057 3.74595 18.9302 3.37083 18.7944 3M19.1747 4.12509C19.7097 5.84827 20.0557 7.65462 20.1886 9.51991M19.1747 18.3749C19.057 18.7541 18.9302 19.1292 18.7944 19.5M19.1747 18.3749C19.7097 16.6517 20.0557 14.8454 20.1886 12.9801M20.1886 9.51991C20.6844 9.93264 21 10.5545 21 11.25C21 11.9455 20.6844 12.5674 20.1886 12.9801M20.1886 9.51991C20.2293 10.0913 20.25 10.6682 20.25 11.25C20.25 11.8318 20.2293 12.4087 20.1886 12.9801"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconMicrophone(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 18.75C15.3137 18.75 18 16.0637 18 12.75V11.25M12 18.75C8.68629 18.75 6 16.0637 6 12.75V11.25M12 18.75V22.5M8.25 22.5H15.75M12 15.75C10.3431 15.75 9 14.4069 9 12.75V4.5C9 2.84315 10.3431 1.5 12 1.5C13.6569 1.5 15 2.84315 15 4.5V12.75C15 14.4069 13.6569 15.75 12 15.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconMinusCircle(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15 12H9M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconMinusSmall(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M18 12L6 12"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconMinus(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.5 12L4.5 12"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconMoon(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21.7519 15.0019C20.597 15.4839 19.3296 15.75 18 15.75C12.6152 15.75 8.25 11.3848 8.25 5.99999C8.25 4.67039 8.51614 3.40296 8.99806 2.24805C5.47566 3.71785 3 7.19481 3 11.25C3 16.6348 7.36522 21 12.75 21C16.8052 21 20.2821 18.5243 21.7519 15.0019Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconMusicalNote(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 9L19.5 6M19.5 12.5528V16.3028C19.5 17.3074 18.834 18.1903 17.8681 18.4663L16.5481 18.8434C15.3964 19.1724 14.25 18.3077 14.25 17.1099C14.25 16.305 14.7836 15.5975 15.5576 15.3764L17.8681 14.7163C18.834 14.4403 19.5 13.5574 19.5 12.5528ZM19.5 12.5528V2.25L9 5.25V15.5528M9 15.5528V19.3028C9 20.3074 8.33405 21.1903 7.36812 21.4663L6.04814 21.8434C4.89645 22.1724 3.75 21.3077 3.75 20.1099C3.75 19.305 4.2836 18.5975 5.05757 18.3764L7.36812 17.7163C8.33405 17.4403 9 16.5574 9 15.5528Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconNewspaper(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 7.5H13.5M12 10.5H13.5M6 13.5H13.5M6 16.5H13.5M16.5 7.5H19.875C20.4963 7.5 21 8.00368 21 8.625V18C21 19.2426 19.9926 20.25 18.75 20.25M16.5 7.5V18C16.5 19.2426 17.5074 20.25 18.75 20.25M16.5 7.5V4.875C16.5 4.25368 15.9963 3.75 15.375 3.75H4.125C3.50368 3.75 3 4.25368 3 4.875V18C3 19.2426 4.00736 20.25 5.25 20.25H18.75M6 7.5H9V10.5H6V7.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconNoSymbol(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M18.364 18.364C21.8787 14.8492 21.8787 9.15076 18.364 5.63604C14.8492 2.12132 9.15076 2.12132 5.63604 5.63604M18.364 18.364C14.8492 21.8787 9.15076 21.8787 5.63604 18.364C2.12132 14.8492 2.12132 9.15076 5.63604 5.63604M18.364 18.364L5.63604 5.63604"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPaintBrush(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9.53086 16.1224C9.08517 15.0243 8.00801 14.25 6.75 14.25C5.09315 14.25 3.75 15.5931 3.75 17.25C3.75 18.4926 2.74262 19.5 1.49998 19.5C1.44928 19.5 1.39898 19.4983 1.34912 19.495C2.12648 20.8428 3.58229 21.75 5.24998 21.75C7.72821 21.75 9.73854 19.7467 9.74993 17.2711C9.74998 17.2641 9.75 17.2571 9.75 17.25C9.75 16.8512 9.67217 16.4705 9.53086 16.1224ZM9.53086 16.1224C10.7252 15.7153 11.8612 15.1705 12.9175 14.5028M7.875 14.4769C8.2823 13.2797 8.8281 12.1411 9.49724 11.0825M12.9175 14.5028C14.798 13.3141 16.4259 11.7362 17.6806 9.85406L21.5566 4.04006C21.6827 3.85093 21.75 3.6287 21.75 3.40139C21.75 2.76549 21.2345 2.25 20.5986 2.25C20.3713 2.25 20.1491 2.31729 19.9599 2.44338L14.1459 6.31937C12.2638 7.57413 10.6859 9.20204 9.49724 11.0825M12.9175 14.5028C12.2396 12.9833 11.0167 11.7604 9.49724 11.0825"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPaperAirplane(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M5.99972 12.0005L3.2688 3.125C9.88393 5.04665 16.0276 8.07649 21.4855 12.0002C16.0276 15.924 9.884 18.9539 3.26889 20.8757L5.99972 12.0005ZM5.99972 12.0005L13.5 12.0005"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPaperClip(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M18.375 12.739L10.682 20.432C8.92462 22.1893 6.07538 22.1893 4.31802 20.432C2.56066 18.6746 2.56066 15.8254 4.31802 14.068L15.2573 3.12868C16.4289 1.95711 18.3283 1.95711 19.4999 3.12868C20.6715 4.30025 20.6715 6.19975 19.4999 7.37132L8.55158 18.3197M8.56066 18.3107C8.55764 18.3137 8.55462 18.3167 8.55158 18.3197M14.2498 8.37865L6.43934 16.1893C5.85355 16.7751 5.85355 17.7249 6.43934 18.3107C7.02211 18.8934 7.9651 18.8964 8.55158 18.3197"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPauseCircle(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M14.25 9V15M9.75 15V9M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPause(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.75 5.25L15.75 18.75M8.25 5.25V18.75"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPencilSquare(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M16.8617 4.48667L18.5492 2.79917C19.2814 2.06694 20.4686 2.06694 21.2008 2.79917C21.9331 3.53141 21.9331 4.71859 21.2008 5.45083L10.5822 16.0695C10.0535 16.5981 9.40144 16.9868 8.68489 17.2002L6 18L6.79978 15.3151C7.01323 14.5986 7.40185 13.9465 7.93052 13.4178L16.8617 4.48667ZM16.8617 4.48667L19.5 7.12499M18 14V18.75C18 19.9926 16.9926 21 15.75 21H5.25C4.00736 21 3 19.9926 3 18.75V8.24999C3 7.00735 4.00736 5.99999 5.25 5.99999H10"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPencil(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M16.8617 4.48667L18.5492 2.79917C19.2814 2.06694 20.4686 2.06694 21.2008 2.79917C21.9331 3.53141 21.9331 4.71859 21.2008 5.45083L6.83218 19.8195C6.30351 20.3481 5.65144 20.7368 4.93489 20.9502L2.25 21.75L3.04978 19.0651C3.26323 18.3486 3.65185 17.6965 4.18052 17.1678L16.8617 4.48667ZM16.8617 4.48667L19.5 7.12499"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPhoneArrowDownLeft(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M14.25 9.75V5.25M14.25 9.75L18.75 9.75M14.25 9.75L20.25 3.75M17.25 21.75C8.96573 21.75 2.25 15.0343 2.25 6.75V4.5C2.25 3.25736 3.25736 2.25 4.5 2.25H5.87163C6.38785 2.25 6.83783 2.60133 6.96304 3.10215L8.06883 7.52533C8.17861 7.96445 8.01453 8.4266 7.65242 8.69818L6.3588 9.6684C5.98336 9.94998 5.81734 10.437 5.97876 10.8777C7.19015 14.1846 9.81539 16.8098 13.1223 18.0212C13.563 18.1827 14.05 18.0166 14.3316 17.6412L15.3018 16.3476C15.5734 15.9855 16.0355 15.8214 16.4747 15.9312L20.8979 17.037C21.3987 17.1622 21.75 17.6121 21.75 18.1284V19.5C21.75 20.7426 20.7426 21.75 19.5 21.75H17.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPhoneArrowUpRight(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M20.25 3.75V8.25M20.25 3.75H15.75M20.25 3.75L14.25 9.75M17.25 21.75C8.96573 21.75 2.25 15.0343 2.25 6.75V4.5C2.25 3.25736 3.25736 2.25 4.5 2.25H5.87163C6.38785 2.25 6.83783 2.60133 6.96304 3.10215L8.06883 7.52533C8.17861 7.96445 8.01453 8.4266 7.65242 8.69818L6.3588 9.6684C5.98336 9.94998 5.81734 10.437 5.97876 10.8777C7.19015 14.1846 9.81539 16.8098 13.1223 18.0212C13.563 18.1827 14.05 18.0166 14.3316 17.6412L15.3018 16.3476C15.5734 15.9855 16.0355 15.8214 16.4747 15.9312L20.8979 17.037C21.3987 17.1622 21.75 17.6121 21.75 18.1284V19.5C21.75 20.7426 20.7426 21.75 19.5 21.75H17.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPhoneXMark(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.75 3.75L18 6M18 6L20.25 8.25M18 6L20.25 3.75M18 6L15.75 8.25M17.25 21.75C8.96573 21.75 2.25 15.0343 2.25 6.75V4.5C2.25 3.25736 3.25736 2.25 4.5 2.25H5.87163C6.38785 2.25 6.83783 2.60133 6.96304 3.10215L8.06883 7.52533C8.17861 7.96445 8.01453 8.4266 7.65242 8.69818L6.3588 9.6684C5.98336 9.94998 5.81734 10.437 5.97876 10.8777C7.19015 14.1846 9.81539 16.8098 13.1223 18.0212C13.563 18.1827 14.05 18.0166 14.3316 17.6412L15.3018 16.3476C15.5734 15.9855 16.0355 15.8214 16.4747 15.9312L20.8979 17.037C21.3987 17.1622 21.75 17.6121 21.75 18.1284V19.5C21.75 20.7426 20.7426 21.75 19.5 21.75H17.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPhone(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.25 6.75C2.25 15.0343 8.96573 21.75 17.25 21.75H19.5C20.7426 21.75 21.75 20.7426 21.75 19.5V18.1284C21.75 17.6121 21.3987 17.1622 20.8979 17.037L16.4747 15.9312C16.0355 15.8214 15.5734 15.9855 15.3018 16.3476L14.3316 17.6412C14.05 18.0166 13.563 18.1827 13.1223 18.0212C9.81539 16.8098 7.19015 14.1846 5.97876 10.8777C5.81734 10.437 5.98336 9.94998 6.3588 9.6684L7.65242 8.69818C8.01453 8.4266 8.17861 7.96445 8.06883 7.52533L6.96304 3.10215C6.83783 2.60133 6.38785 2.25 5.87163 2.25H4.5C3.25736 2.25 2.25 3.25736 2.25 4.5V6.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPhoto(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.25 15.75L7.40901 10.591C8.28769 9.71231 9.71231 9.71231 10.591 10.591L15.75 15.75M14.25 14.25L15.659 12.841C16.5377 11.9623 17.9623 11.9623 18.841 12.841L21.75 15.75M3.75 19.5H20.25C21.0784 19.5 21.75 18.8284 21.75 18V6C21.75 5.17157 21.0784 4.5 20.25 4.5H3.75C2.92157 4.5 2.25 5.17157 2.25 6V18C2.25 18.8284 2.92157 19.5 3.75 19.5ZM14.25 8.25H14.2575V8.2575H14.25V8.25ZM14.625 8.25C14.625 8.45711 14.4571 8.625 14.25 8.625C14.0429 8.625 13.875 8.45711 13.875 8.25C13.875 8.04289 14.0429 7.875 14.25 7.875C14.4571 7.875 14.625 8.04289 14.625 8.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPlayCircle(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M15.9099 11.6722C16.1671 11.8151 16.1671 12.1849 15.9099 12.3278L10.3071 15.4405C10.0572 15.5794 9.75 15.3986 9.75 15.1127V8.88732C9.75 8.60139 10.0572 8.42065 10.3071 8.55951L15.9099 11.6722Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPlayPause(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21 7.5L21 18M15 7.5V18M3 16.8114V8.68858C3 7.82478 3.93317 7.28324 4.68316 7.7118L11.7906 11.7732C12.5464 12.2051 12.5464 13.2949 11.7906 13.7268L4.68316 17.7882C3.93317 18.2168 3 17.6752 3 16.8114Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPlay(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M5.25 5.65297C5.25 4.79729 6.1674 4.25486 6.91716 4.66723L18.4577 11.0145C19.2349 11.442 19.2349 12.5586 18.4577 12.986L6.91716 19.3334C6.1674 19.7457 5.25 19.2033 5.25 18.3476V5.65297Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPlusCircle(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 9V15M15 12H9M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPlusSmall(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 6V18M18 12L6 12"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPlus(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 4.5V19.5M19.5 12L4.5 12"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPower(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M5.63604 5.63604C2.12132 9.15076 2.12132 14.8492 5.63604 18.364C9.15076 21.8787 14.8492 21.8787 18.364 18.364C21.8787 14.8492 21.8787 9.15076 18.364 5.63604M12 3V12"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPresentationChartBar(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75 3V14.25C3.75 15.4926 4.75736 16.5 6 16.5H8.25M3.75 3H2.25M3.75 3H20.25M20.25 3H21.75M20.25 3V14.25C20.25 15.4926 19.2426 16.5 18 16.5H15.75M8.25 16.5H15.75M8.25 16.5L7.25 19.5M15.75 16.5L16.75 19.5M16.75 19.5L17.25 21M16.75 19.5H7.25M7.25 19.5L6.75 21M9 11.25V12.75M12 9V12.75M15 6.75V12.75"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPresentationChartLine(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75 3V14.25C3.75 15.4926 4.75736 16.5 6 16.5H8.25M3.75 3H2.25M3.75 3H20.25M20.25 3H21.75M20.25 3V14.25C20.25 15.4926 19.2426 16.5 18 16.5H15.75M8.25 16.5H15.75M8.25 16.5L7.25 19.5M15.75 16.5L16.75 19.5M16.75 19.5L17.25 21M16.75 19.5H7.25M7.25 19.5L6.75 21M7.5 12L10.5 9L12.6476 11.1476C13.6542 9.70301 14.9704 8.49023 16.5 7.60539"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPrinter(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M6.72012 13.8287C6.47944 13.8589 6.23939 13.8911 6 13.9253M6.72012 13.8287C8.44957 13.6118 10.2117 13.5 12 13.5C13.7883 13.5 15.5504 13.6118 17.2799 13.8287M6.72012 13.8287L6.34091 18M17.2799 13.8287C17.5206 13.8589 17.7606 13.8911 18 13.9253M17.2799 13.8287L17.6591 18M17.6591 18L17.8885 20.5231C17.9484 21.182 17.4296 21.75 16.7681 21.75H7.23191C6.57038 21.75 6.05164 21.182 6.11153 20.5231L6.34091 18M17.6591 18H18.75C19.9926 18 21 16.9926 21 15.75V9.45569C21 8.37475 20.2321 7.44082 19.1631 7.28086C18.5293 7.18604 17.8916 7.10361 17.25 7.03381M6.34091 18H5.25C4.00736 18 3 16.9926 3 15.75V9.45569C3 8.37475 3.7679 7.44082 4.83694 7.28086C5.47066 7.18604 6.10843 7.10361 6.75 7.03381M17.25 7.03381C15.5258 6.84625 13.7741 6.75 12 6.75C10.2259 6.75 8.47423 6.84625 6.75 7.03381M17.25 7.03381V3.375C17.25 2.75368 16.7463 2.25 16.125 2.25H7.875C7.25368 2.25 6.75 2.75368 6.75 3.375V7.03381M18 10.5H18.0075V10.5075H18V10.5ZM15 10.5H15.0075V10.5075H15V10.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconPuzzlePiece(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M14.25 6.08694C14.25 5.73178 14.4361 5.41076 14.6512 5.1282C14.8721 4.8381 15 4.494 15 4.125C15 3.08947 13.9926 2.25 12.75 2.25C11.5074 2.25 10.5 3.08947 10.5 4.125C10.5 4.494 10.6279 4.8381 10.8488 5.1282C11.064 5.41076 11.25 5.73178 11.25 6.08694V6.08694C11.25 6.44822 10.9542 6.73997 10.593 6.72957C9.18939 6.68914 7.80084 6.58845 6.42989 6.43C6.61626 8.04276 6.72269 9.67987 6.74511 11.3373C6.75007 11.7032 6.45293 12 6.08694 12V12C5.73178 12 5.41076 11.814 5.1282 11.5988C4.8381 11.3779 4.494 11.25 4.125 11.25C3.08947 11.25 2.25 12.2574 2.25 13.5C2.25 14.7426 3.08947 15.75 4.125 15.75C4.494 15.75 4.8381 15.6221 5.1282 15.4012C5.41076 15.186 5.73178 15 6.08694 15V15C6.39613 15 6.64157 15.2608 6.6189 15.5691C6.49306 17.2812 6.27742 18.9682 5.97668 20.6256C7.49458 20.8157 9.03451 20.9348 10.5931 20.9797C10.9542 20.9901 11.2501 20.6983 11.2501 20.337V20.337C11.2501 19.9818 11.0641 19.6608 10.8489 19.3782C10.628 19.0881 10.5001 18.744 10.5001 18.375C10.5001 17.3395 11.5075 16.5 12.7501 16.5C13.9928 16.5 15.0001 17.3395 15.0001 18.375C15.0001 18.744 14.8722 19.0881 14.6513 19.3782C14.4362 19.6608 14.2501 19.9818 14.2501 20.337V20.337C14.2501 20.6699 14.5281 20.9357 14.8605 20.9161C16.6992 20.8081 18.5102 20.5965 20.2876 20.2872C20.5571 18.7389 20.7523 17.1652 20.8696 15.5698C20.8923 15.2611 20.6466 15 20.3371 15V15C19.9819 15 19.6609 15.1861 19.3783 15.4013C19.0882 15.6221 18.7441 15.75 18.3751 15.75C17.3396 15.75 16.5001 14.7427 16.5001 13.5C16.5001 12.2574 17.3396 11.25 18.3751 11.25C18.7441 11.25 19.0882 11.378 19.3783 11.5988C19.6609 11.814 19.9819 12 20.3371 12V12C20.7034 12 21.0008 11.703 20.9959 11.3367C20.9713 9.52413 20.8463 7.73572 20.6261 5.97698C18.7403 6.31916 16.816 6.55115 14.8603 6.66605C14.528 6.68557 14.25 6.41979 14.25 6.08694V6.08694Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconQrCode(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75 4.875C3.75 4.25368 4.25368 3.75 4.875 3.75H9.375C9.99632 3.75 10.5 4.25368 10.5 4.875V9.375C10.5 9.99632 9.99632 10.5 9.375 10.5H4.875C4.25368 10.5 3.75 9.99632 3.75 9.375V4.875Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M3.75 14.625C3.75 14.0037 4.25368 13.5 4.875 13.5H9.375C9.99632 13.5 10.5 14.0037 10.5 14.625V19.125C10.5 19.7463 9.99632 20.25 9.375 20.25H4.875C4.25368 20.25 3.75 19.7463 3.75 19.125V14.625Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M13.5 4.875C13.5 4.25368 14.0037 3.75 14.625 3.75H19.125C19.7463 3.75 20.25 4.25368 20.25 4.875V9.375C20.25 9.99632 19.7463 10.5 19.125 10.5H14.625C14.0037 10.5 13.5 9.99632 13.5 9.375V4.875Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M6.75 6.75H7.5V7.5H6.75V6.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M6.75 16.5H7.5V17.25H6.75V16.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M16.5 6.75H17.25V7.5H16.5V6.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M13.5 13.5H14.25V14.25H13.5V13.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M13.5 19.5H14.25V20.25H13.5V19.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M19.5 13.5H20.25V14.25H19.5V13.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M19.5 19.5H20.25V20.25H19.5V19.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M16.5 16.5H17.25V17.25H16.5V16.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconQuestionMarkCircle(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9.87891 7.51884C11.0505 6.49372 12.95 6.49372 14.1215 7.51884C15.2931 8.54397 15.2931 10.206 14.1215 11.2312C13.9176 11.4096 13.6917 11.5569 13.4513 11.6733C12.7056 12.0341 12.0002 12.6716 12.0002 13.5V14.25M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12ZM12 17.25H12.0075V17.2575H12V17.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconQueueList(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75 12H20.25M3.75 15.75H20.25M3.75 19.5H20.25M5.625 4.5H18.375C19.4105 4.5 20.25 5.33947 20.25 6.375C20.25 7.41053 19.4105 8.25 18.375 8.25H5.625C4.58947 8.25 3.75 7.41053 3.75 6.375C3.75 5.33947 4.58947 4.5 5.625 4.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconRadio(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75 7.5L20.25 3.375M12 6.75C9.29246 6.75 6.63727 6.97417 4.05199 7.40497C2.99912 7.58042 2.25 8.50663 2.25 9.57402V18.75C2.25 19.9926 3.25736 21 4.5 21H19.5C20.7426 21 21.75 19.9926 21.75 18.75V9.57402C21.75 8.50663 21.0009 7.58042 19.948 7.40497C17.3627 6.97417 14.7075 6.75 12 6.75ZM10.3169 13.1931L10.3116 13.1984L10.3063 13.1931L10.3116 13.1878L10.3169 13.1931ZM10.3118 15.3195L10.3065 15.3142L10.3118 15.3089L10.3171 15.3142L10.3118 15.3195ZM8.1958 15.3144L8.1905 15.3197L8.18519 15.3144L8.1905 15.3091L8.1958 15.3144ZM8.19067 13.1982L8.18537 13.1929L8.19067 13.1876L8.19598 13.1929L8.19067 13.1982ZM9.25488 10.5V10.5075H9.24738V10.5H9.25488ZM12.5039 12.3801L12.4974 12.3839L12.4937 12.3774L12.5002 12.3736L12.5039 12.3801ZM11.1248 17.5063L11.121 17.4999L11.1275 17.4961L11.1313 17.5026L11.1248 17.5063ZM11.1313 11.0048L11.1276 11.0113L11.1211 11.0076L11.1249 11.0011L11.1313 11.0048ZM12.5002 16.1338L12.4937 16.13L12.4975 16.1235L12.504 16.1273L12.5002 16.1338ZM13.0049 14.2573H12.9974V14.2498H13.0049V14.2573ZM9.25488 18V18.0075H9.24738V18H9.25488ZM6.00879 16.1301L6.00229 16.1339L5.99854 16.1274L6.00504 16.1236L6.00879 16.1301ZM7.37476 11.0112L7.37101 11.0047L7.3775 11.001L7.38125 11.0075L7.37476 11.0112ZM7.38135 17.4999L7.3776 17.5064L7.3711 17.5027L7.37485 17.4962L7.38135 17.4999ZM6.00513 12.3838L5.99863 12.38L6.00238 12.3735L6.00888 12.3773L6.00513 12.3838ZM5.50488 14.2573H5.49738V14.2498H5.50488V14.2573ZM17.25 12.75C16.8358 12.75 16.5 12.4142 16.5 12C16.5 11.5858 16.8358 11.25 17.25 11.25C17.6642 11.25 18 11.5858 18 12C18 12.4142 17.6642 12.75 17.25 12.75ZM17.25 17.25C16.8358 17.25 16.5 16.9142 16.5 16.5C16.5 16.0858 16.8358 15.75 17.25 15.75C17.6642 15.75 18 16.0858 18 16.5C18 16.9142 17.6642 17.25 17.25 17.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconReceiptPercent(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 14.25L15 8.25M19.5 4.75699V21.75L15.75 20.25L12 21.75L8.25 20.25L4.5 21.75V4.75699C4.5 3.649 5.30608 2.70014 6.40668 2.57241C8.24156 2.35947 10.108 2.25 12 2.25C13.892 2.25 15.7584 2.35947 17.5933 2.57241C18.6939 2.70014 19.5 3.649 19.5 4.75699ZM9.75 9H9.7575V9.0075H9.75V9ZM10.125 9C10.125 9.20711 9.95711 9.375 9.75 9.375C9.54289 9.375 9.375 9.20711 9.375 9C9.375 8.79289 9.54289 8.625 9.75 8.625C9.95711 8.625 10.125 8.79289 10.125 9ZM14.25 13.5H14.2575V13.5075H14.25V13.5ZM14.625 13.5C14.625 13.7071 14.4571 13.875 14.25 13.875C14.0429 13.875 13.875 13.7071 13.875 13.5C13.875 13.2929 14.0429 13.125 14.25 13.125C14.4571 13.125 14.625 13.2929 14.625 13.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconReceiptRefund(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M8.25 9.75H13.125C14.5747 9.75 15.75 10.9253 15.75 12.375C15.75 13.8247 14.5747 15 13.125 15H12M8.25 9.75L10.5 7.5M8.25 9.75L10.5 12M19.5 4.75699V21.75L15.75 20.25L12 21.75L8.25 20.25L4.5 21.75V4.75699C4.5 3.649 5.30608 2.70014 6.40668 2.57241C8.24156 2.35947 10.108 2.25 12 2.25C13.892 2.25 15.7584 2.35947 17.5933 2.57241C18.6939 2.70014 19.5 3.649 19.5 4.75699Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconRectangleGroup(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.25 7.125C2.25 6.50368 2.75368 6 3.375 6H9.375C9.99632 6 10.5 6.50368 10.5 7.125V10.875C10.5 11.4963 9.99632 12 9.375 12H3.375C2.75368 12 2.25 11.4963 2.25 10.875V7.125Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M14.25 8.625C14.25 8.00368 14.7537 7.5 15.375 7.5H20.625C21.2463 7.5 21.75 8.00368 21.75 8.625V16.875C21.75 17.4963 21.2463 18 20.625 18H15.375C14.7537 18 14.25 17.4963 14.25 16.875V8.625Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M3.75 16.125C3.75 15.5037 4.25368 15 4.875 15H10.125C10.7463 15 11.25 15.5037 11.25 16.125V18.375C11.25 18.9963 10.7463 19.5 10.125 19.5H4.875C4.25368 19.5 3.75 18.9963 3.75 18.375V16.125Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconRectangleStack(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M6 6.87803V6C6 4.75736 7.00736 3.75 8.25 3.75H15.75C16.9926 3.75 18 4.75736 18 6V6.87803M6 6.87803C6.23458 6.79512 6.48702 6.75 6.75 6.75H17.25C17.513 6.75 17.7654 6.79512 18 6.87803M6 6.87803C5.12611 7.18691 4.5 8.02034 4.5 9V9.87803M18 6.87803C18.8739 7.18691 19.5 8.02034 19.5 9V9.87803M19.5 9.87803C19.2654 9.79512 19.013 9.75 18.75 9.75H5.25C4.98702 9.75 4.73458 9.79512 4.5 9.87803M19.5 9.87803C20.3739 10.1869 21 11.0203 21 12V18C21 19.2426 19.9926 20.25 18.75 20.25H5.25C4.00736 20.25 3 19.2426 3 18V12C3 11.0203 3.62611 10.1869 4.5 9.87803"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconRocketLaunch(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.5904 14.3696C15.6948 14.8128 15.75 15.275 15.75 15.75C15.75 19.0637 13.0637 21.75 9.75 21.75V16.9503M15.5904 14.3696C19.3244 11.6411 21.75 7.22874 21.75 2.25C16.7715 2.25021 12.3595 4.67586 9.63122 8.40975M15.5904 14.3696C13.8819 15.6181 11.8994 16.514 9.75 16.9503M9.63122 8.40975C9.18777 8.30528 8.72534 8.25 8.25 8.25C4.93629 8.25 2.25 10.9363 2.25 14.25H7.05072M9.63122 8.40975C8.38285 10.1183 7.48701 12.1007 7.05072 14.25M9.75 16.9503C9.64659 16.9713 9.54279 16.9912 9.43862 17.0101C8.53171 16.291 7.70991 15.4692 6.99079 14.5623C7.00969 14.4578 7.02967 14.3537 7.05072 14.25M4.81191 16.6408C3.71213 17.4612 3 18.7724 3 20.25C3 20.4869 3.0183 20.7195 3.05356 20.9464C3.28054 20.9817 3.51313 21 3.75 21C5.22758 21 6.53883 20.2879 7.35925 19.1881M16.5 9C16.5 9.82843 15.8284 10.5 15 10.5C14.1716 10.5 13.5 9.82843 13.5 9C13.5 8.17157 14.1716 7.5 15 7.5C15.8284 7.5 16.5 8.17157 16.5 9Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconRss(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12.75 19.5V18.75C12.75 14.6079 9.39214 11.25 5.25 11.25H4.5M4.5 4.5H5.25C13.1201 4.5 19.5 10.8799 19.5 18.75V19.5M6 18.75C6 19.1642 5.66421 19.5 5.25 19.5C4.83579 19.5 4.5 19.1642 4.5 18.75C4.5 18.3358 4.83579 18 5.25 18C5.66421 18 6 18.3358 6 18.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconScale(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 3V20.25M12 20.25C10.528 20.25 9.1179 20.515 7.81483 21M12 20.25C13.472 20.25 14.8821 20.515 16.1852 21M18.75 4.97089C16.5446 4.66051 14.291 4.5 12 4.5C9.70897 4.5 7.45542 4.66051 5.25 4.97089M18.75 4.97089C19.7604 5.1131 20.7608 5.28677 21.75 5.49087M18.75 4.97089L21.3704 15.6961C21.4922 16.1948 21.2642 16.7237 20.7811 16.8975C20.1468 17.1257 19.4629 17.25 18.75 17.25C18.0371 17.25 17.3532 17.1257 16.7189 16.8975C16.2358 16.7237 16.0078 16.1948 16.1296 15.6961L18.75 4.97089ZM2.25 5.49087C3.23922 5.28677 4.23956 5.1131 5.25 4.97089M5.25 4.97089L7.87036 15.6961C7.9922 16.1948 7.76419 16.7237 7.28114 16.8975C6.6468 17.1257 5.96292 17.25 5.25 17.25C4.53708 17.25 3.8532 17.1257 3.21886 16.8975C2.73581 16.7237 2.5078 16.1948 2.62964 15.6961L5.25 4.97089Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconScissors(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M7.84786 8.25007L9.38443 9.13721M7.84786 8.25007C7.01943 9.68494 5.18501 10.1765 3.75013 9.34809C2.31526 8.51966 1.82363 6.68489 2.65206 5.25001C3.48049 3.81513 5.31526 3.32351 6.75013 4.15194C8.18501 4.98036 8.67629 6.81519 7.84786 8.25007ZM9.38443 9.13721C10.043 9.51742 10.4538 10.2153 10.4666 10.9756C10.4725 11.3272 10.5207 11.6706 10.607 12.0001M9.38443 9.13721L11.4608 10.336M7.84786 15.7501L9.38443 14.863M7.84786 15.7501C8.67629 17.185 8.18501 19.0197 6.75013 19.8481C5.31526 20.6765 3.48049 20.1849 2.65206 18.75C1.82363 17.3151 2.31526 15.4804 3.75013 14.6519C5.18501 13.8235 7.01943 14.3153 7.84786 15.7501ZM9.38443 14.863C10.043 14.4828 10.4538 13.7849 10.4666 13.0246C10.4725 12.673 10.5207 12.3296 10.607 12.0001M9.38443 14.863L11.4608 13.6642M11.4608 10.336C11.9882 9.699 12.6991 9.21096 13.5294 8.95702L18.8541 7.32855C19.6606 7.08189 20.5202 7.06684 21.3348 7.28513L22.1373 7.50014L14.3431 12.0001M11.4608 10.336C11.062 10.8178 10.7681 11.3848 10.607 12.0001M14.3431 12.0001L22.1373 16.5001L21.3348 16.7151C20.5202 16.9334 19.6606 16.9183 18.8541 16.6717L13.5294 15.0432C12.6991 14.7892 11.9882 14.3012 11.4608 13.6642M14.3431 12.0001L11.4608 13.6642"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconServerStack(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M5.25 14.25H18.75M5.25 14.25C3.59315 14.25 2.25 12.9069 2.25 11.25M5.25 14.25C3.59315 14.25 2.25 15.5931 2.25 17.25C2.25 18.9069 3.59315 20.25 5.25 20.25H18.75C20.4069 20.25 21.75 18.9069 21.75 17.25C21.75 15.5931 20.4069 14.25 18.75 14.25M2.25 11.25C2.25 9.59315 3.59315 8.25 5.25 8.25H18.75C20.4069 8.25 21.75 9.59315 21.75 11.25M2.25 11.25C2.25 10.2763 2.5658 9.32893 3.15 8.55L5.7375 5.1C6.37488 4.25016 7.37519 3.75 8.4375 3.75H15.5625C16.6248 3.75 17.6251 4.25016 18.2625 5.1L20.85 8.55C21.4342 9.32893 21.75 10.2763 21.75 11.25M21.75 11.25C21.75 12.9069 20.4069 14.25 18.75 14.25M18.75 17.25H18.7575V17.2575H18.75V17.25ZM18.75 11.25H18.7575V11.2575H18.75V11.25ZM15.75 17.25H15.7575V17.2575H15.75V17.25ZM15.75 11.25H15.7575V11.2575H15.75V11.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconServer(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21.75 17.25V17.0223C21.75 16.6753 21.7099 16.3294 21.6304 15.9916L19.3622 6.35199C19.0035 4.82745 17.6431 3.75 16.077 3.75H7.92305C6.35688 3.75 4.99648 4.82745 4.63777 6.35199L2.36962 15.9916C2.29014 16.3294 2.25 16.6753 2.25 17.0223V17.25M21.75 17.25C21.75 18.9069 20.4069 20.25 18.75 20.25H5.25C3.59315 20.25 2.25 18.9069 2.25 17.25M21.75 17.25C21.75 15.5931 20.4069 14.25 18.75 14.25H5.25C3.59315 14.25 2.25 15.5931 2.25 17.25M18.75 17.25H18.7575V17.2575H18.75V17.25ZM15.75 17.25H15.7575V17.2575H15.75V17.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconShare(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M7.21721 10.9071C6.83295 10.2169 6.096 9.75 5.25 9.75C4.00736 9.75 3 10.7574 3 12C3 13.2426 4.00736 14.25 5.25 14.25C6.096 14.25 6.83295 13.7831 7.21721 13.0929M7.21721 10.9071C7.39737 11.2307 7.5 11.6034 7.5 12C7.5 12.3966 7.39737 12.7693 7.21721 13.0929M7.21721 10.9071L16.7828 5.5929M7.21721 13.0929L16.7828 18.4071M16.7828 18.4071C16.6026 18.7307 16.5 19.1034 16.5 19.5C16.5 20.7426 17.5074 21.75 18.75 21.75C19.9926 21.75 21 20.7426 21 19.5C21 18.2574 19.9926 17.25 18.75 17.25C17.904 17.25 17.1671 17.7169 16.7828 18.4071ZM16.7828 5.5929C17.1671 6.28309 17.904 6.75 18.75 6.75C19.9926 6.75 21 5.74264 21 4.5C21 3.25736 19.9926 2.25 18.75 2.25C17.5074 2.25 16.5 3.25736 16.5 4.5C16.5 4.89664 16.6026 5.26931 16.7828 5.5929Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconShieldCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 12.7498L11.25 14.9998L15 9.74985M12 2.71411C9.8495 4.75073 6.94563 5.99986 3.75 5.99986C3.69922 5.99986 3.64852 5.99955 3.59789 5.99892C3.2099 7.17903 3 8.43995 3 9.74991C3 15.3414 6.82432 20.0397 12 21.3719C17.1757 20.0397 21 15.3414 21 9.74991C21 8.43995 20.7901 7.17903 20.4021 5.99892C20.3515 5.99955 20.3008 5.99986 20.25 5.99986C17.0544 5.99986 14.1505 4.75073 12 2.71411Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconShieldExclamation(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 9.00009V12.7501M12 2.71436C9.8495 4.75098 6.94563 6.00011 3.75 6.00011C3.69922 6.00011 3.64852 5.99979 3.59789 5.99916C3.2099 7.17927 3 8.4402 3 9.75015C3 15.3417 6.82432 20.04 12 21.3721C17.1757 20.04 21 15.3417 21 9.75015C21 8.4402 20.7901 7.17927 20.4021 5.99916C20.3515 5.99979 20.3008 6.00011 20.25 6.00011C17.0544 6.00011 14.1505 4.75098 12 2.71436ZM12 15.7501H12.0075V15.7576H12V15.7501Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconShoppingBag(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.75 10.5V6C15.75 3.92893 14.0711 2.25 12 2.25C9.92893 2.25 8.25 3.92893 8.25 6V10.5M19.606 8.50723L20.8692 20.5072C20.9391 21.1715 20.4183 21.75 19.7504 21.75H4.24963C3.58172 21.75 3.06089 21.1715 3.13081 20.5072L4.39397 8.50723C4.45424 7.93466 4.93706 7.5 5.51279 7.5H18.4872C19.0629 7.5 19.5458 7.93466 19.606 8.50723ZM8.625 10.5C8.625 10.7071 8.4571 10.875 8.25 10.875C8.04289 10.875 7.875 10.7071 7.875 10.5C7.875 10.2929 8.04289 10.125 8.25 10.125C8.4571 10.125 8.625 10.2929 8.625 10.5ZM16.125 10.5C16.125 10.7071 15.9571 10.875 15.75 10.875C15.5429 10.875 15.375 10.7071 15.375 10.5C15.375 10.2929 15.5429 10.125 15.75 10.125C15.9571 10.125 16.125 10.2929 16.125 10.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconShoppingCart(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.25 3H3.63568C4.14537 3 4.59138 3.34265 4.7227 3.83513L5.1059 5.27209M7.5 14.25C5.84315 14.25 4.5 15.5931 4.5 17.25H20.25M7.5 14.25H18.7183C19.8394 11.9494 20.8177 9.56635 21.6417 7.1125C16.88 5.89646 11.8905 5.25 6.75 5.25C6.20021 5.25 5.65214 5.2574 5.1059 5.27209M7.5 14.25L5.1059 5.27209M6 20.25C6 20.6642 5.66421 21 5.25 21C4.83579 21 4.5 20.6642 4.5 20.25C4.5 19.8358 4.83579 19.5 5.25 19.5C5.66421 19.5 6 19.8358 6 20.25ZM18.75 20.25C18.75 20.6642 18.4142 21 18 21C17.5858 21 17.25 20.6642 17.25 20.25C17.25 19.8358 17.5858 19.5 18 19.5C18.4142 19.5 18.75 19.8358 18.75 20.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconSignalSlash(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3 3L11.7348 11.7348M11.7348 11.7348C11.8027 11.667 11.8964 11.625 12 11.625C12.2071 11.625 12.375 11.7929 12.375 12C12.375 12.1036 12.333 12.1973 12.2652 12.2652M11.7348 11.7348L12.2652 12.2652M12.2652 12.2652L21 21M14.6517 9.34835C16.1161 10.8128 16.1161 13.1872 14.6517 14.6516M16.773 7.22703C19.409 9.86307 19.409 14.1369 16.773 16.773M18.8943 5.10571C22.7019 8.91332 22.7019 15.0867 18.8943 18.8943M9.34835 14.6516C8.75129 14.0546 8.39765 13.3063 8.28743 12.5301M7.22703 16.773C5.35299 14.8989 4.81126 12.1971 5.60184 9.84448M5.10571 18.8943C2.03824 15.8268 1.44197 11.2239 3.3169 7.55955M12 12H12.0075V12.0075H12V12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconSignal(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9.34835 14.6514C7.88388 13.1869 7.88388 10.8126 9.34835 9.34811M14.6517 9.34811C16.1161 10.8126 16.1161 13.1869 14.6517 14.6514M7.22703 16.7727C4.59099 14.1367 4.59099 9.86283 7.22703 7.22679M16.773 7.22679C19.409 9.86283 19.409 14.1367 16.773 16.7727M5.10571 18.8941C1.2981 15.0864 1.2981 8.91308 5.10571 5.10547M18.8943 5.10547C22.7019 8.91308 22.7019 15.0864 18.8943 18.8941M12 11.9998H12.0075V12.0073H12V11.9998ZM12.375 11.9998C12.375 12.2069 12.2071 12.3748 12 12.3748C11.7929 12.3748 11.625 12.2069 11.625 11.9998C11.625 11.7927 11.7929 11.6248 12 11.6248C12.2071 11.6248 12.375 11.7927 12.375 11.9998Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconSparkles(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9.8132 15.9038L9 18.75L8.1868 15.9038C7.75968 14.4089 6.59112 13.2403 5.09619 12.8132L2.25 12L5.09619 11.1868C6.59113 10.7597 7.75968 9.59112 8.1868 8.09619L9 5.25L9.8132 8.09619C10.2403 9.59113 11.4089 10.7597 12.9038 11.1868L15.75 12L12.9038 12.8132C11.4089 13.2403 10.2403 14.4089 9.8132 15.9038Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M18.2589 8.71454L18 9.75L17.7411 8.71454C17.4388 7.50533 16.4947 6.56117 15.2855 6.25887L14.25 6L15.2855 5.74113C16.4947 5.43883 17.4388 4.49467 17.7411 3.28546L18 2.25L18.2589 3.28546C18.5612 4.49467 19.5053 5.43883 20.7145 5.74113L21.75 6L20.7145 6.25887C19.5053 6.56117 18.5612 7.50533 18.2589 8.71454Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M16.8942 20.5673L16.5 21.75L16.1058 20.5673C15.8818 19.8954 15.3546 19.3682 14.6827 19.1442L13.5 18.75L14.6827 18.3558C15.3546 18.1318 15.8818 17.6046 16.1058 16.9327L16.5 15.75L16.8942 16.9327C17.1182 17.6046 17.6454 18.1318 18.3173 18.3558L19.5 18.75L18.3173 19.1442C17.6454 19.3682 17.1182 19.8954 16.8942 20.5673Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconSpeakerWave(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19.114 5.63591C22.6287 9.15063 22.6287 14.8491 19.114 18.3638M16.4626 8.28765C18.5129 10.3379 18.5129 13.662 16.4626 15.7123M6.75 8.24993L11.4697 3.53026C11.9421 3.05778 12.75 3.39241 12.75 4.06059V19.9393C12.75 20.6074 11.9421 20.9421 11.4697 20.4696L6.75 15.7499H4.50905C3.62971 15.7499 2.8059 15.2435 2.57237 14.3957C2.36224 13.6329 2.25 12.8295 2.25 11.9999C2.25 11.1703 2.36224 10.367 2.57237 9.60416C2.8059 8.7564 3.62971 8.24993 4.50905 8.24993H6.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconSpeakerXMark(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M17.25 9.75041L19.5 12.0004M19.5 12.0004L21.75 14.2504M19.5 12.0004L21.75 9.75041M19.5 12.0004L17.25 14.2504M6.75 8.25041L11.4697 3.53074C11.9421 3.05827 12.75 3.3929 12.75 4.06107V19.9398C12.75 20.6079 11.9421 20.9426 11.4697 20.4701L6.75 15.7504H4.50905C3.62971 15.7504 2.8059 15.2439 2.57237 14.3962C2.36224 13.6334 2.25 12.83 2.25 12.0004C2.25 11.1708 2.36224 10.3675 2.57237 9.60465C2.8059 8.75689 3.62971 8.25041 4.50905 8.25041H6.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconSquare2Stack(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M16.5 8.25V6C16.5 4.75736 15.4926 3.75 14.25 3.75H6C4.75736 3.75 3.75 4.75736 3.75 6V14.25C3.75 15.4926 4.75736 16.5 6 16.5H8.25M16.5 8.25H18C19.2426 8.25 20.25 9.25736 20.25 10.5V18C20.25 19.2426 19.2426 20.25 18 20.25H10.5C9.25736 20.25 8.25 19.2426 8.25 18V16.5M16.5 8.25H10.5C9.25736 8.25 8.25 9.25736 8.25 10.5V16.5"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconSquare3Stack3d(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M6.42857 9.75L2.25 12L6.42857 14.25M6.42857 9.75L12 12.75L17.5714 9.75M6.42857 9.75L2.25 7.5L12 2.25L21.75 7.5L17.5714 9.75M17.5714 9.75L21.75 12L17.5714 14.25M17.5714 14.25L21.75 16.5L12 21.75L2.25 16.5L6.42857 14.25M17.5714 14.25L12 17.25L6.42857 14.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconSquares2x2(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.75 6C3.75 4.75736 4.75736 3.75 6 3.75H8.25C9.49264 3.75 10.5 4.75736 10.5 6V8.25C10.5 9.49264 9.49264 10.5 8.25 10.5H6C4.75736 10.5 3.75 9.49264 3.75 8.25V6Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M3.75 15.75C3.75 14.5074 4.75736 13.5 6 13.5H8.25C9.49264 13.5 10.5 14.5074 10.5 15.75V18C10.5 19.2426 9.49264 20.25 8.25 20.25H6C4.75736 20.25 3.75 19.2426 3.75 18V15.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M13.5 6C13.5 4.75736 14.5074 3.75 15.75 3.75H18C19.2426 3.75 20.25 4.75736 20.25 6V8.25C20.25 9.49264 19.2426 10.5 18 10.5H15.75C14.5074 10.5 13.5 9.49264 13.5 8.25V6Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M13.5 15.75C13.5 14.5074 14.5074 13.5 15.75 13.5H18C19.2426 13.5 20.25 14.5074 20.25 15.75V18C20.25 19.2426 19.2426 20.25 18 20.25H15.75C14.5074 20.25 13.5 19.2426 13.5 18V15.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconSquaresPlus(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M13.5 16.875H16.875M16.875 16.875H20.25M16.875 16.875V13.5M16.875 16.875V20.25M6 10.5H8.25C9.49264 10.5 10.5 9.49264 10.5 8.25V6C10.5 4.75736 9.49264 3.75 8.25 3.75H6C4.75736 3.75 3.75 4.75736 3.75 6V8.25C3.75 9.49264 4.75736 10.5 6 10.5ZM6 20.25H8.25C9.49264 20.25 10.5 19.2426 10.5 18V15.75C10.5 14.5074 9.49264 13.5 8.25 13.5H6C4.75736 13.5 3.75 14.5074 3.75 15.75V18C3.75 19.2426 4.75736 20.25 6 20.25ZM15.75 10.5H18C19.2426 10.5 20.25 9.49264 20.25 8.25V6C20.25 4.75736 19.2426 3.75 18 3.75H15.75C14.5074 3.75 13.5 4.75736 13.5 6V8.25C13.5 9.49264 14.5074 10.5 15.75 10.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconStar(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M11.4806 3.49883C11.6728 3.03685 12.3272 3.03685 12.5193 3.49883L14.6453 8.61028C14.7263 8.80504 14.9095 8.93811 15.1197 8.95497L20.638 9.39736C21.1367 9.43735 21.339 10.0598 20.959 10.3853L16.7546 13.9867C16.5945 14.1239 16.5245 14.3392 16.5734 14.5444L17.8579 19.9293C17.974 20.416 17.4446 20.8007 17.0176 20.5398L12.2932 17.6542C12.1132 17.5443 11.8868 17.5443 11.7068 17.6542L6.98238 20.5398C6.55539 20.8007 6.02594 20.416 6.14203 19.9293L7.42652 14.5444C7.47546 14.3392 7.4055 14.1239 7.24531 13.9867L3.04099 10.3853C2.661 10.0598 2.86323 9.43735 3.36197 9.39736L8.88022 8.95497C9.09048 8.93811 9.27363 8.80504 9.35464 8.61028L11.4806 3.49883Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconStopCircle(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M9 9.5625C9 9.25184 9.25184 9 9.5625 9H14.4375C14.7482 9 15 9.25184 15 9.5625V14.4375C15 14.7482 14.7482 15 14.4375 15H9.5625C9.25184 15 9 14.7482 9 14.4375V9.5625Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconStop(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M5.25 7.5C5.25 6.25736 6.25736 5.25 7.5 5.25H16.5C17.7426 5.25 18.75 6.25736 18.75 7.5V16.5C18.75 17.7426 17.7426 18.75 16.5 18.75H7.5C6.25736 18.75 5.25 17.7426 5.25 16.5V7.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconSun(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 3V5.25M18.364 5.63604L16.773 7.22703M21 12H18.75M18.364 18.364L16.773 16.773M12 18.75V21M7.22703 16.773L5.63604 18.364M5.25 12H3M7.22703 7.22703L5.63604 5.63604M15.75 12C15.75 14.0711 14.0711 15.75 12 15.75C9.92893 15.75 8.25 14.0711 8.25 12C8.25 9.92893 9.92893 8.25 12 8.25C14.0711 8.25 15.75 9.92893 15.75 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconSwatch(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M4.09835 19.9017C5.56282 21.3661 7.93719 21.3661 9.40165 19.9017L15.8033 13.5M6.75 21C4.67893 21 3 19.3211 3 17.25V4.125C3 3.50368 3.50368 3 4.125 3H9.375C9.99632 3 10.5 3.50368 10.5 4.125V8.1967M6.75 21C8.82107 21 10.5 19.3211 10.5 17.25V8.1967M6.75 21H19.875C20.4963 21 21 20.4963 21 19.875V14.625C21 14.0037 20.4963 13.5 19.875 13.5H15.8033M10.5 8.1967L13.3791 5.31757C13.8185 4.87823 14.5308 4.87823 14.9701 5.31757L18.6824 9.02988C19.1218 9.46922 19.1218 10.1815 18.6824 10.6209L15.8033 13.5M6.75 17.25H6.7575V17.2575H6.75V17.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconTableCells(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3.375 19.5H20.625M3.375 19.5C2.75368 19.5 2.25 18.9963 2.25 18.375M3.375 19.5H10.875C11.4963 19.5 12 18.9963 12 18.375M2.25 18.375V5.625M2.25 18.375V16.875C2.25 16.2537 2.75368 15.75 3.375 15.75M21.75 18.375V5.625M21.75 18.375C21.75 18.9963 21.2463 19.5 20.625 19.5M21.75 18.375V16.875C21.75 16.2537 21.2463 15.75 20.625 15.75M20.625 19.5H13.125C12.5037 19.5 12 18.9963 12 18.375M21.75 5.625C21.75 5.00368 21.2463 4.5 20.625 4.5H3.375C2.75368 4.5 2.25 5.00368 2.25 5.625M21.75 5.625V7.125C21.75 7.74632 21.2463 8.25 20.625 8.25M2.25 5.625V7.125C2.25 7.74632 2.75368 8.25 3.375 8.25M3.375 8.25H20.625M3.375 8.25H10.875C11.4963 8.25 12 8.75368 12 9.375M3.375 8.25C2.75368 8.25 2.25 8.75368 2.25 9.375V10.875C2.25 11.4963 2.75368 12 3.375 12M20.625 8.25H13.125C12.5037 8.25 12 8.75368 12 9.375M20.625 8.25C21.2463 8.25 21.75 8.75368 21.75 9.375V10.875C21.75 11.4963 21.2463 12 20.625 12M3.375 12H10.875M3.375 12C2.75368 12 2.25 12.5037 2.25 13.125V14.625C2.25 15.2463 2.75368 15.75 3.375 15.75M12 10.875V9.375M12 10.875C12 11.4963 11.4963 12 10.875 12M12 10.875C12 11.4963 12.5037 12 13.125 12M10.875 12C11.4963 12 12 12.5037 12 13.125M13.125 12H20.625M13.125 12C12.5037 12 12 12.5037 12 13.125M20.625 12C21.2463 12 21.75 12.5037 21.75 13.125V14.625C21.75 15.2463 21.2463 15.75 20.625 15.75M3.375 15.75H10.875M12 14.625V13.125M12 14.625C12 15.2463 11.4963 15.75 10.875 15.75M12 14.625C12 15.2463 12.5037 15.75 13.125 15.75M10.875 15.75C11.4963 15.75 12 16.2537 12 16.875M12 18.375V16.875M12 16.875C12 16.2537 12.5037 15.75 13.125 15.75M13.125 15.75H20.625"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconTag(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9.56802 3H5.25C4.00736 3 3 4.00736 3 5.25V9.56802C3 10.1648 3.23705 10.7371 3.65901 11.159L13.2401 20.7401C13.9388 21.4388 15.0199 21.6117 15.8465 21.0705C17.9271 19.7084 19.7084 17.9271 21.0705 15.8465C21.6117 15.0199 21.4388 13.9388 20.7401 13.2401L11.159 3.65901C10.7371 3.23705 10.1648 3 9.56802 3Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M6 6H6.0075V6.0075H6V6Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconTicket(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M16.5 6V6.75M16.5 9.75V10.5M16.5 13.5V14.25M16.5 17.25V18M7.5 12.75H12.75M7.5 15H10.5M3.375 5.25C2.75368 5.25 2.25 5.75368 2.25 6.375V9.40135C3.1467 9.92006 3.75 10.8896 3.75 12C3.75 13.1104 3.1467 14.0799 2.25 14.5987V17.625C2.25 18.2463 2.75368 18.75 3.375 18.75H20.625C21.2463 18.75 21.75 18.2463 21.75 17.625V14.5987C20.8533 14.0799 20.25 13.1104 20.25 12C20.25 10.8896 20.8533 9.92006 21.75 9.40135V6.375C21.75 5.75368 21.2463 5.25 20.625 5.25H3.375Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconTrash(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M14.7404 9L14.3942 18M9.60577 18L9.25962 9M19.2276 5.79057C19.5696 5.84221 19.9104 5.89747 20.25 5.95629M19.2276 5.79057L18.1598 19.6726C18.0696 20.8448 17.0921 21.75 15.9164 21.75H8.08357C6.90786 21.75 5.93037 20.8448 5.8402 19.6726L4.77235 5.79057M19.2276 5.79057C18.0812 5.61744 16.9215 5.48485 15.75 5.39432M3.75 5.95629C4.08957 5.89747 4.43037 5.84221 4.77235 5.79057M4.77235 5.79057C5.91878 5.61744 7.07849 5.48485 8.25 5.39432M15.75 5.39432V4.47819C15.75 3.29882 14.8393 2.31423 13.6606 2.27652C13.1092 2.25889 12.5556 2.25 12 2.25C11.4444 2.25 10.8908 2.25889 10.3394 2.27652C9.16065 2.31423 8.25 3.29882 8.25 4.47819V5.39432M15.75 5.39432C14.5126 5.2987 13.262 5.25 12 5.25C10.738 5.25 9.48744 5.2987 8.25 5.39432"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconTrophy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M16.5003 18.75H7.50026M16.5003 18.75C18.1571 18.75 19.5003 20.0931 19.5003 21.75H4.50026C4.50026 20.0931 5.8434 18.75 7.50026 18.75M16.5003 18.75V15.375C16.5003 14.7537 15.9966 14.25 15.3753 14.25H14.5036M7.50026 18.75V15.375C7.50026 14.7537 8.00394 14.25 8.62526 14.25H9.49689M14.5036 14.25H9.49689M14.5036 14.25C13.9563 13.3038 13.6097 12.227 13.5222 11.0777M9.49689 14.25C10.0442 13.3038 10.3908 12.227 10.4783 11.0777M5.25026 4.23636C4.26796 4.3792 3.29561 4.55275 2.33423 4.75601C2.78454 7.42349 4.99518 9.49282 7.72991 9.72775M5.25026 4.23636V4.5C5.25026 6.60778 6.21636 8.48992 7.72991 9.72775M5.25026 4.23636V2.72089C7.45568 2.41051 9.70922 2.25 12.0003 2.25C14.2913 2.25 16.5448 2.41051 18.7503 2.72089V4.23636M7.72991 9.72775C8.51748 10.3719 9.45329 10.8415 10.4783 11.0777M18.7503 4.23636V4.5C18.7503 6.60778 17.7842 8.48992 16.2706 9.72775M18.7503 4.23636C19.7326 4.3792 20.7049 4.55275 21.6663 4.75601C21.216 7.42349 19.0053 9.49282 16.2706 9.72775M16.2706 9.72775C15.483 10.3719 14.5472 10.8415 13.5222 11.0777M13.5222 11.0777C13.0331 11.1904 12.5236 11.25 12.0003 11.25C11.4769 11.25 10.9675 11.1904 10.4783 11.0777"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconTruck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M8.25 18.75C8.25 19.5784 7.57843 20.25 6.75 20.25C5.92157 20.25 5.25 19.5784 5.25 18.75M8.25 18.75C8.25 17.9216 7.57843 17.25 6.75 17.25C5.92157 17.25 5.25 17.9216 5.25 18.75M8.25 18.75H14.25M5.25 18.75H3.375C2.75368 18.75 2.25 18.2463 2.25 17.625V14.2504M19.5 18.75C19.5 19.5784 18.8284 20.25 18 20.25C17.1716 20.25 16.5 19.5784 16.5 18.75M19.5 18.75C19.5 17.9216 18.8284 17.25 18 17.25C17.1716 17.25 16.5 17.9216 16.5 18.75M19.5 18.75L20.625 18.75C21.2463 18.75 21.7537 18.2457 21.7154 17.6256C21.5054 14.218 20.3473 11.0669 18.5016 8.43284C18.1394 7.91592 17.5529 7.60774 16.9227 7.57315H14.25M16.5 18.75H14.25M14.25 7.57315V6.61479C14.25 6.0473 13.8275 5.56721 13.263 5.50863C11.6153 5.33764 9.94291 5.25 8.25 5.25C6.55709 5.25 4.88466 5.33764 3.23698 5.50863C2.67252 5.56721 2.25 6.0473 2.25 6.61479V14.2504M14.25 7.57315V14.2504M14.25 18.75V14.2504M14.25 14.2504H2.25"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconTv(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M6 20.25H18M10.5 17.25V20.25M13.5 17.25V20.25M3.375 17.25H20.625C21.2463 17.25 21.75 16.7463 21.75 16.125V4.875C21.75 4.25368 21.2463 3.75 20.625 3.75H3.375C2.75368 3.75 2.25 4.25368 2.25 4.875V16.125C2.25 16.7463 2.75368 17.25 3.375 17.25Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconUserCircle(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M17.9815 18.7248C16.6121 16.9175 14.4424 15.75 12 15.75C9.55761 15.75 7.38789 16.9175 6.01846 18.7248M17.9815 18.7248C19.8335 17.0763 21 14.6744 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 14.6744 4.1665 17.0763 6.01846 18.7248M17.9815 18.7248C16.3915 20.1401 14.2962 21 12 21C9.70383 21 7.60851 20.1401 6.01846 18.7248M15 9.75C15 11.4069 13.6569 12.75 12 12.75C10.3431 12.75 9 11.4069 9 9.75C9 8.09315 10.3431 6.75 12 6.75C13.6569 6.75 15 8.09315 15 9.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconUserGroup(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M17.9999 18.7191C18.2474 18.7396 18.4978 18.75 18.7506 18.75C19.7989 18.75 20.8054 18.5708 21.741 18.2413C21.7473 18.1617 21.7506 18.0812 21.7506 18C21.7506 16.3431 20.4074 15 18.7506 15C18.123 15 17.5403 15.1927 17.0587 15.5222M17.9999 18.7191C18 18.7294 18 18.7397 18 18.75C18 18.975 17.9876 19.1971 17.9635 19.4156C16.2067 20.4237 14.1707 21 12 21C9.82933 21 7.79327 20.4237 6.03651 19.4156C6.01238 19.1971 6 18.975 6 18.75C6 18.7397 6.00003 18.7295 6.00008 18.7192M17.9999 18.7191C17.994 17.5426 17.6494 16.4461 17.0587 15.5222M17.0587 15.5222C15.9928 13.8552 14.1255 12.75 12 12.75C9.87479 12.75 8.00765 13.8549 6.94169 15.5216M6.94169 15.5216C6.46023 15.1925 5.87796 15 5.25073 15C3.59388 15 2.25073 16.3431 2.25073 18C2.25073 18.0812 2.25396 18.1617 2.26029 18.2413C3.19593 18.5708 4.2024 18.75 5.25073 18.75C5.50307 18.75 5.75299 18.7396 6.00008 18.7192M6.94169 15.5216C6.35071 16.4457 6.00598 17.5424 6.00008 18.7192M15 6.75C15 8.40685 13.6569 9.75 12 9.75C10.3431 9.75 9 8.40685 9 6.75C9 5.09315 10.3431 3.75 12 3.75C13.6569 3.75 15 5.09315 15 6.75ZM21 9.75C21 10.9926 19.9926 12 18.75 12C17.5074 12 16.5 10.9926 16.5 9.75C16.5 8.50736 17.5074 7.5 18.75 7.5C19.9926 7.5 21 8.50736 21 9.75ZM7.5 9.75C7.5 10.9926 6.49264 12 5.25 12C4.00736 12 3 10.9926 3 9.75C3 8.50736 4.00736 7.5 5.25 7.5C6.49264 7.5 7.5 8.50736 7.5 9.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconUserMinus(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M22 10.5H16M13.75 6.375C13.75 8.23896 12.239 9.75 10.375 9.75C8.51104 9.75 7 8.23896 7 6.375C7 4.51104 8.51104 3 10.375 3C12.239 3 13.75 4.51104 13.75 6.375ZM4.00092 19.2343C4.00031 19.198 4 19.1615 4 19.125C4 15.6042 6.85418 12.75 10.375 12.75C13.8958 12.75 16.75 15.6042 16.75 19.125V19.1276C16.75 19.1632 16.7497 19.1988 16.7491 19.2343C14.8874 20.3552 12.7065 21 10.375 21C8.04353 21 5.86264 20.3552 4.00092 19.2343Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconUserPlus(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M19 7.5V10.5M19 10.5V13.5M19 10.5H22M19 10.5H16M13.75 6.375C13.75 8.23896 12.239 9.75 10.375 9.75C8.51104 9.75 7 8.23896 7 6.375C7 4.51104 8.51104 3 10.375 3C12.239 3 13.75 4.51104 13.75 6.375ZM4.00092 19.2343C4.00031 19.198 4 19.1615 4 19.125C4 15.6042 6.85418 12.75 10.375 12.75C13.8958 12.75 16.75 15.6042 16.75 19.125V19.1276C16.75 19.1632 16.7497 19.1988 16.7491 19.2343C14.8874 20.3552 12.7065 21 10.375 21C8.04353 21 5.86264 20.3552 4.00092 19.2343Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconUser(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.75 6C15.75 8.07107 14.071 9.75 12 9.75C9.9289 9.75 8.24996 8.07107 8.24996 6C8.24996 3.92893 9.9289 2.25 12 2.25C14.071 2.25 15.75 3.92893 15.75 6Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M4.5011 20.1182C4.5714 16.0369 7.90184 12.75 12 12.75C16.0982 12.75 19.4287 16.0371 19.4988 20.1185C17.216 21.166 14.6764 21.75 12.0003 21.75C9.32396 21.75 6.78406 21.1659 4.5011 20.1182Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconUsers(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15 19.1276C15.8329 19.37 16.7138 19.5 17.625 19.5C19.1037 19.5 20.5025 19.1576 21.7464 18.5478C21.7488 18.4905 21.75 18.4329 21.75 18.375C21.75 16.0968 19.9031 14.25 17.625 14.25C16.2069 14.25 14.956 14.9655 14.2136 16.0552M15 19.1276V19.125C15 18.0121 14.7148 16.9658 14.2136 16.0552M15 19.1276C15 19.1632 14.9997 19.1988 14.9991 19.2343C13.1374 20.3552 10.9565 21 8.625 21C6.29353 21 4.11264 20.3552 2.25092 19.2343C2.25031 19.198 2.25 19.1615 2.25 19.125C2.25 15.6042 5.10418 12.75 8.625 12.75C11.0329 12.75 13.129 14.085 14.2136 16.0552M12 6.375C12 8.23896 10.489 9.75 8.625 9.75C6.76104 9.75 5.25 8.23896 5.25 6.375C5.25 4.51104 6.76104 3 8.625 3C10.489 3 12 4.51104 12 6.375ZM20.25 8.625C20.25 10.0747 19.0747 11.25 17.625 11.25C16.1753 11.25 15 10.0747 15 8.625C15 7.17525 16.1753 6 17.625 6C19.0747 6 20.25 7.17525 20.25 8.625Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconVariable(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M4.74455 3C3.61952 5.77929 3 8.8173 3 12C3 15.1827 3.61952 18.2207 4.74455 21M19.5 3C20.4673 5.77929 21 8.8173 21 12C21 15.1827 20.4673 18.2207 19.5 21M8.25 8.88462L9.6945 7.99569C10.1061 7.74241 10.6463 7.93879 10.7991 8.39726L13.2009 15.6027C13.3537 16.0612 13.8939 16.2576 14.3055 16.0043L15.75 15.1154M7.5 15.8654L7.71335 15.9556C8.45981 16.2715 9.32536 16.012 9.77495 15.3376L14.225 8.66243C14.6746 7.98804 15.5402 7.72854 16.2867 8.04435L16.5 8.13462"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconVideoCameraSlash(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.75 10.5L20.4697 5.78033C20.9421 5.30786 21.75 5.64248 21.75 6.31066V17.6893C21.75 18.3575 20.9421 18.6921 20.4697 18.2197L15.75 13.5M12 18.75H4.5C3.25736 18.75 2.25 17.7426 2.25 16.5V9M15.091 18.091L16.5 19.5M15.091 18.091C15.4982 17.6838 15.75 17.1213 15.75 16.5V7.5C15.75 6.25736 14.7426 5.25 13.5 5.25H4.5C3.87868 5.25 3.31618 5.50184 2.90901 5.90901M15.091 18.091L2.90901 5.90901M1.5 4.5L2.90901 5.90901"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconVideoCamera(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M15.75 10.5L20.4697 5.78033C20.9421 5.30786 21.75 5.64248 21.75 6.31066V17.6893C21.75 18.3575 20.9421 18.6921 20.4697 18.2197L15.75 13.5M4.5 18.75H13.5C14.7426 18.75 15.75 17.7426 15.75 16.5V7.5C15.75 6.25736 14.7426 5.25 13.5 5.25H4.5C3.25736 5.25 2.25 6.25736 2.25 7.5V16.5C2.25 17.7426 3.25736 18.75 4.5 18.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ />
+ </svg>
+ )
+}
+export function IconViewColumns(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9 4.5V19.5M15 4.5V19.5M4.125 19.5H19.875C20.4963 19.5 21 18.9963 21 18.375V5.625C21 5.00368 20.4963 4.5 19.875 4.5H4.125C3.50368 4.5 3 5.00368 3 5.625V18.375C3 18.9963 3.50368 19.5 4.125 19.5Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconViewfinderCircle(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M7.5 3.75H6C4.75736 3.75 3.75 4.75736 3.75 6V7.5M16.5 3.75H18C19.2426 3.75 20.25 4.75736 20.25 6V7.5M20.25 16.5V18C20.25 19.2426 19.2426 20.25 18 20.25H16.5M7.5 20.25H6C4.75736 20.25 3.75 19.2426 3.75 18V16.5M15 12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconWallet(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21 12C21 10.7574 19.9926 9.75 18.75 9.75H15C15 11.4069 13.6569 12.75 12 12.75C10.3431 12.75 9 11.4069 9 9.75H5.25C4.00736 9.75 3 10.7574 3 12M21 12V18C21 19.2426 19.9926 20.25 18.75 20.25H5.25C4.00736 20.25 3 19.2426 3 18V12M21 12V9M3 12V9M21 9C21 7.75736 19.9926 6.75 18.75 6.75H5.25C4.00736 6.75 3 7.75736 3 9M21 9V6C21 4.75736 19.9926 3.75 18.75 3.75H5.25C4.00736 3.75 3 4.75736 3 6V9"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconWifi(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M8.28767 15.0378C10.3379 12.9875 13.662 12.9875 15.7123 15.0378M5.10569 11.8558C8.9133 8.04815 15.0867 8.04815 18.8943 11.8558M1.92371 8.67373C7.48868 3.10876 16.5113 3.10876 22.0762 8.67373M12.5303 18.2197L12 18.7501L11.4696 18.2197C11.7625 17.9268 12.2374 17.9268 12.5303 18.2197Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconWindow(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M3 8.25V18C3 19.2426 4.00736 20.25 5.25 20.25H18.75C19.9926 20.25 21 19.2426 21 18V8.25M3 8.25V6C3 4.75736 4.00736 3.75 5.25 3.75H18.75C19.9926 3.75 21 4.75736 21 6V8.25M3 8.25H21M5.25 6H5.2575V6.0075H5.25V6ZM7.5 6H7.5075V6.0075H7.5V6ZM9.75 6H9.7575V6.0075H9.75V6Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconWrenchScrewdriver(
+ props: JSX.SvgSVGAttributes<SVGSVGElement>
+) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M11.4194 15.1694L17.25 21C18.2855 22.0355 19.9645 22.0355 21 21C22.0355 19.9645 22.0355 18.2855 21 17.25L15.1233 11.3733M11.4194 15.1694L13.9155 12.1383C14.2315 11.7546 14.6542 11.5132 15.1233 11.3733M11.4194 15.1694L6.76432 20.8219C6.28037 21.4096 5.55897 21.75 4.79768 21.75C3.39064 21.75 2.25 20.6094 2.25 19.2023C2.25 18.441 2.59044 17.7196 3.1781 17.2357L10.0146 11.6056M15.1233 11.3733C15.6727 11.2094 16.2858 11.1848 16.8659 11.2338C16.9925 11.2445 17.1206 11.25 17.25 11.25C19.7353 11.25 21.75 9.23528 21.75 6.75C21.75 6.08973 21.6078 5.46268 21.3523 4.89779L18.0762 8.17397C16.9605 7.91785 16.0823 7.03963 15.8262 5.92397L19.1024 2.64774C18.5375 2.39223 17.9103 2.25 17.25 2.25C14.7647 2.25 12.75 4.26472 12.75 6.75C12.75 6.87938 12.7555 7.00749 12.7662 7.13411C12.8571 8.20956 12.6948 9.39841 11.8617 10.0845L11.7596 10.1686M10.0146 11.6056L5.90901 7.5H4.5L2.25 3.75L3.75 2.25L7.5 4.5V5.90901L11.7596 10.1686M10.0146 11.6056L11.7596 10.1686M18.375 18.375L15.75 15.75M4.86723 19.125H4.87473V19.1325H4.86723V19.125Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconWrench(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M21.75 6.75C21.75 9.23528 19.7353 11.25 17.25 11.25C17.1206 11.25 16.9925 11.2445 16.8659 11.2338C15.7904 11.1429 14.6016 11.3052 13.9155 12.1383L6.76432 20.8219C6.28037 21.4096 5.55897 21.75 4.79769 21.75C3.39064 21.75 2.25 20.6094 2.25 19.2023C2.25 18.441 2.59044 17.7196 3.1781 17.2357L11.8617 10.0845C12.6948 9.39841 12.8571 8.20956 12.7662 7.13411C12.7555 7.00749 12.75 6.87938 12.75 6.75C12.75 4.26472 14.7647 2.25 17.25 2.25C17.9103 2.25 18.5375 2.39223 19.1024 2.64774L15.8262 5.92397C16.0823 7.03963 16.9605 7.91785 18.0762 8.17397L21.3524 4.89779C21.6078 5.46268 21.75 6.08973 21.75 6.75Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ <path
+ d="M4.86723 19.125H4.87473V19.1325H4.86723V19.125Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconXCircle(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M9.75 9.75L14.25 14.25M14.25 9.75L9.75 14.25M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+export function IconXMark(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M6 18L18 6M6 6L18 18"
+ stroke="currentColor"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ )
+}
+// index
+export function IconCommand(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M3.17157 3.17157C3.92172 2.42143 4.93913 2 6 2C7.06087 2 8.07828 2.42143 8.82843 3.17157C9.57857 3.92172 10 4.93913 10 6V8H14V6C14 4.93913 14.4214 3.92172 15.1716 3.17157C15.9217 2.42143 16.9391 2 18 2C19.0609 2 20.0783 2.42143 20.8284 3.17157C21.5786 3.92172 22 4.93913 22 6C22 7.06087 21.5786 8.07828 20.8284 8.82843C20.0783 9.57857 19.0609 10 18 10H16V14H18C19.0609 14 20.0783 14.4214 20.8284 15.1716C21.5786 15.9217 22 16.9391 22 18C22 19.0609 21.5786 20.0783 20.8284 20.8284C20.0783 21.5786 19.0609 22 18 22C16.9391 22 15.9217 21.5786 15.1716 20.8284C14.4214 20.0783 14 19.0609 14 18V16H10V18C10 19.0609 9.57857 20.0783 8.82843 20.8284C8.07828 21.5786 7.06087 22 6 22C4.93913 22 3.92172 21.5786 3.17157 20.8284C2.42143 20.0783 2 19.0609 2 18C2 16.9391 2.42143 15.9217 3.17157 15.1716C3.92172 14.4214 4.93913 14 6 14H8V10H6C4.93913 10 3.92172 9.57857 3.17157 8.82843C2.42143 8.07828 2 7.06087 2 6C2 4.93913 2.42143 3.92172 3.17157 3.17157ZM8 8V6C8 5.46957 7.78929 4.96086 7.41421 4.58579C7.03914 4.21071 6.53043 4 6 4C5.46957 4 4.96086 4.21071 4.58579 4.58579C4.21071 4.96086 4 5.46957 4 6C4 6.53043 4.21071 7.03914 4.58579 7.41421C4.96086 7.78929 5.46957 8 6 8H8ZM10 10V14H14V10H10ZM8 16H6C5.46957 16 4.96086 16.2107 4.58579 16.5858C4.21071 16.9609 4 17.4696 4 18C4 18.5304 4.21071 19.0391 4.58579 19.4142C4.96086 19.7893 5.46957 20 6 20C6.53043 20 7.03914 19.7893 7.41421 19.4142C7.78929 19.0391 8 18.5304 8 18V16ZM16 16V18C16 18.5304 16.2107 19.0391 16.5858 19.4142C16.9609 19.7893 17.4696 20 18 20C18.5304 20 19.0391 19.7893 19.4142 19.4142C19.7893 19.0391 20 18.5304 20 18C20 17.4696 19.7893 16.9609 19.4142 16.5858C19.0391 16.2107 18.5304 16 18 16H16ZM16 8H18C18.5304 8 19.0391 7.78929 19.4142 7.41421C19.7893 7.03914 20 6.53043 20 6C20 5.46957 19.7893 4.96086 19.4142 4.58579C19.0391 4.21071 18.5304 4 18 4C17.4696 4 16.9609 4.21071 16.5858 4.58579C16.2107 4.96086 16 5.46957 16 6V8Z"
+ fill="#0D0E10"
+ />
+ </svg>
+ )
+}
+export function IconLetter(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M20.9172 20.49L13.0532 2.60368C12.6787 1.79879 11.3213 1.79879 10.9468 2.60368L3.08277 20.49C2.84873 21.0266 3.12958 21.6526 3.6913 21.9209C4.25301 22.1445 4.90835 21.8762 5.18921 21.3396L6.73392 17.7177H17.2661L18.8108 21.3396C18.998 21.7421 19.4193 21.9657 19.8874 21.9657C20.0278 21.9657 20.1683 21.9657 20.3087 21.9209C20.8704 21.6526 21.1513 21.0266 20.9172 20.49ZM7.71692 15.5713L12.0234 5.77851L16.2831 15.5266H7.71692V15.5713Z"
+ fill="#0D0E10"
+ />
+ </svg>
+ )
+}
+export function IconMultiSelect(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M2.00024 6.25002C2.00024 5.58595 2.54712 5.00002 3.25024 5.00002H18.2502C18.9143 5.00002 19.5002 5.58595 19.5002 6.25002C19.5002 6.95314 18.9143 7.50002 18.2502 7.50002H3.25024C2.54712 7.50002 2.00024 6.95314 2.00024 6.25002ZM4.50024 12.5C4.50024 11.836 5.04712 11.25 5.75024 11.25H20.7502C21.4143 11.25 22.0002 11.836 22.0002 12.5C22.0002 13.2031 21.4143 13.75 20.7502 13.75H5.75024C5.04712 13.75 4.50024 13.2031 4.50024 12.5ZM18.2502 20H3.25024C2.54712 20 2.00024 19.4531 2.00024 18.75C2.00024 18.086 2.54712 17.5 3.25024 17.5H18.2502C18.9143 17.5 19.5002 18.086 19.5002 18.75C19.5002 19.4531 18.9143 20 18.2502 20Z"
+ fill="#0D0E10"
+ />
+ </svg>
+ )
+}
+export function IconSettings(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ width="16"
+ height="16"
+ viewBox="0 0 16 16"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <g clip-path="url(#clip0_1256_16163)">
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M7.99967 6.66665C7.26329 6.66665 6.66634 7.2636 6.66634 7.99998C6.66634 8.73636 7.26329 9.33331 7.99967 9.33331C8.73605 9.33331 9.33301 8.73636 9.33301 7.99998C9.33301 7.2636 8.73605 6.66665 7.99967 6.66665ZM5.33301 7.99998C5.33301 6.52722 6.52692 5.33331 7.99967 5.33331C9.47243 5.33331 10.6663 6.52722 10.6663 7.99998C10.6663 9.47274 9.47243 10.6666 7.99967 10.6666C6.52692 10.6666 5.33301 9.47274 5.33301 7.99998Z"
+ fill="#5E5F61"
+ />
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M8 1.33333C7.82319 1.33333 7.65362 1.40357 7.5286 1.5286C7.40357 1.65362 7.33333 1.82319 7.33333 2V2.11599C7.33196 2.46053 7.22986 2.79715 7.03963 3.08441C6.84939 3.37167 6.5793 3.59703 6.26262 3.73276C6.20625 3.75692 6.14695 3.77313 6.08641 3.78104C5.79865 3.88181 5.48888 3.90665 5.18733 3.85197C4.84 3.789 4.5195 3.62341 4.26716 3.37658L4.2619 3.37143L4.22193 3.3314C4.16001 3.26942 4.08623 3.21999 4.00529 3.18644C3.92436 3.15289 3.83761 3.13562 3.75 3.13562C3.66239 3.13562 3.57564 3.15289 3.49471 3.18644C3.41377 3.21999 3.34025 3.26916 3.27833 3.33114L3.27781 3.33167C3.21582 3.39358 3.16665 3.46711 3.1331 3.54804C3.09955 3.62897 3.08228 3.71572 3.08228 3.80333C3.08228 3.89094 3.09955 3.97769 3.1331 4.05863C3.16665 4.13956 3.21582 4.21308 3.27781 4.275L3.32327 4.32046C3.57011 4.57281 3.73566 4.89334 3.79864 5.24067C3.86037 5.58109 3.82075 5.93198 3.68497 6.24985C3.56126 6.57422 3.34465 6.85511 3.06203 7.05725C2.7737 7.26348 2.42999 7.3782 2.07559 7.38648L2.06 7.38667H2C1.82319 7.38667 1.65362 7.4569 1.5286 7.58193C1.40357 7.70695 1.33333 7.87652 1.33333 8.05333C1.33333 8.23014 1.40357 8.39971 1.5286 8.52474C1.65362 8.64976 1.82319 8.72 2 8.72H2.11599C2.46053 8.72137 2.79715 8.82347 3.08441 9.01371C3.37067 9.20329 3.59547 9.47215 3.73134 9.78741C3.87272 10.1095 3.91474 10.4665 3.85197 10.8127C3.789 11.16 3.62341 11.4805 3.37658 11.7328L3.37143 11.7381L3.3314 11.7781C3.26942 11.84 3.21999 11.9138 3.18644 11.9947C3.15289 12.0756 3.13562 12.1624 3.13562 12.25C3.13562 12.3376 3.15289 12.4244 3.18644 12.5053C3.21999 12.5862 3.26916 12.6598 3.33114 12.7217L3.33167 12.7222C3.39358 12.7842 3.46711 12.8333 3.54804 12.8669C3.62897 12.9004 3.71572 12.9177 3.80333 12.9177C3.89094 12.9177 3.97769 12.9004 4.05863 12.8669C4.13956 12.8333 4.21309 12.7842 4.275 12.7222L4.32046 12.6767C4.57281 12.4299 4.89334 12.2643 5.24067 12.2014C5.58108 12.1396 5.93197 12.1792 6.24985 12.315C6.57422 12.4387 6.85511 12.6553 7.05725 12.938C7.26348 13.2263 7.3782 13.57 7.38648 13.9244L7.38667 13.94V14C7.38667 14.1768 7.4569 14.3464 7.58193 14.4714C7.70695 14.5964 7.87652 14.6667 8.05333 14.6667C8.23014 14.6667 8.39971 14.5964 8.52474 14.4714C8.64976 14.3464 8.72 14.1768 8.72 14V13.8867L8.72001 13.884C8.72138 13.5395 8.82347 13.2029 9.01371 12.9156C9.2033 12.6293 9.4722 12.4045 9.78749 12.2686C10.1096 12.1273 10.4665 12.0853 10.8127 12.148C11.16 12.211 11.4805 12.3766 11.7328 12.6234L11.7381 12.6286L11.7781 12.6686C11.84 12.7306 11.9138 12.78 11.9947 12.8136C12.0756 12.8471 12.1624 12.8644 12.25 12.8644C12.3376 12.8644 12.4244 12.8471 12.5053 12.8136C12.5862 12.78 12.6598 12.7308 12.7217 12.6689L12.7222 12.6683C12.7842 12.6064 12.8333 12.5329 12.8669 12.452C12.9004 12.371 12.9177 12.2843 12.9177 12.1967C12.9177 12.1091 12.9004 12.0223 12.8669 11.9414C12.8333 11.8604 12.7842 11.7869 12.7222 11.725L12.6767 11.6795C12.4299 11.4272 12.2643 11.1067 12.2014 10.7593C12.1386 10.4132 12.1806 10.0562 12.322 9.73416C12.4578 9.41886 12.6826 9.14997 12.9689 8.96038C13.2562 8.77014 13.5928 8.66805 13.9373 8.66667L13.94 8.66666L14 8.66667C14.1768 8.66667 14.3464 8.59643 14.4714 8.4714C14.5964 8.34638 14.6667 8.17681 14.6667 8C14.6667 7.82319 14.5964 7.65362 14.4714 7.5286C14.3464 7.40357 14.1768 7.33333 14 7.33333H13.8867L13.884 7.33333C13.5395 7.33195 13.2029 7.22986 12.9156 7.03963C12.6283 6.84939 12.403 6.5793 12.2672 6.26262C12.2431 6.20625 12.2269 6.14695 12.219 6.08641C12.1182 5.79865 12.0933 5.48888 12.148 5.18733C12.211 4.84 12.3766 4.5195 12.6234 4.26716L12.6286 4.2619L12.6686 4.22193C12.7306 4.16001 12.78 4.08622 12.8136 4.00529C12.8471 3.92436 12.8644 3.83761 12.8644 3.75C12.8644 3.66239 12.8471 3.57564 12.8136 3.49471C12.78 3.41378 12.7308 3.34025 12.6689 3.27833L12.6683 3.27781C12.6064 3.21582 12.5329 3.16665 12.452 3.1331C12.371 3.09955 12.2843 3.08228 12.1967 3.08228C12.1091 3.08228 12.0223 3.09955 11.9414 3.1331C11.8604 3.16665 11.7869 3.21583 11.725 3.27781L11.6795 3.32327C11.4272 3.57011 11.1067 3.73566 10.7593 3.79864C10.4132 3.86141 10.0562 3.81939 9.73408 3.67801C9.41882 3.54213 9.14995 3.31734 8.96038 3.03108C8.77014 2.74382 8.66805 2.4072 8.66667 2.06266L8.66667 2.06V2C8.66667 1.82319 8.59643 1.65362 8.4714 1.5286C8.34638 1.40357 8.17681 1.33333 8 1.33333ZM12.9333 10L13.5432 10.2692C13.5083 10.3484 13.4979 10.4363 13.5133 10.5215C13.5286 10.6057 13.5685 10.6835 13.6279 10.7451L13.6645 10.7817C13.6646 10.7818 13.6644 10.7816 13.6645 10.7817C13.8503 10.9674 13.998 11.1881 14.0986 11.4308C14.1992 11.6736 14.251 11.9338 14.251 12.1967C14.251 12.4595 14.1992 12.7198 14.0986 12.9625C13.9979 13.2053 13.8504 13.4259 13.6645 13.6117L13.1933 13.14L13.665 13.6111C13.4793 13.7971 13.2587 13.9446 13.0159 14.0453C12.7731 14.1459 12.5128 14.1977 12.25 14.1977C11.9872 14.1977 11.7269 14.1459 11.4841 14.0453C11.2414 13.9447 11.021 13.7972 10.8353 13.6114C10.8352 13.6113 10.8353 13.6115 10.8353 13.6114L10.7984 13.5746C10.7369 13.5151 10.659 13.4752 10.5748 13.46C10.4896 13.4445 10.4017 13.4549 10.3225 13.4899L10.316 13.4928C10.2383 13.5261 10.172 13.5813 10.1254 13.6518C10.0789 13.722 10.0539 13.8042 10.0533 13.8883V14C10.0533 14.5304 9.84262 15.0391 9.46755 15.4142C9.09247 15.7893 8.58377 16 8.05333 16C7.5229 16 7.01419 15.7893 6.63912 15.4142C6.26405 15.0391 6.05333 14.5304 6.05333 14V13.9496C6.05017 13.8648 6.02218 13.7827 5.97276 13.7136C5.92218 13.6429 5.85149 13.589 5.76988 13.559C5.7567 13.5542 5.74367 13.5489 5.73082 13.5432C5.65161 13.5083 5.56374 13.4979 5.47855 13.5133C5.39428 13.5286 5.31645 13.5685 5.25487 13.6279L5.21833 13.6645C5.21825 13.6646 5.21842 13.6644 5.21833 13.6645C5.03264 13.8503 4.8119 13.998 4.56921 14.0986C4.32642 14.1992 4.06617 14.251 3.80333 14.251C3.5405 14.251 3.28025 14.1992 3.03745 14.0986C2.79489 13.998 2.5745 13.8507 2.38886 13.665C2.2029 13.4793 2.05539 13.2587 1.95474 13.0159C1.85409 12.7731 1.80228 12.5128 1.80228 12.25C1.80228 11.9872 1.85409 11.7269 1.95474 11.4841C2.05539 11.2413 2.2029 11.0207 2.38886 10.835L2.42541 10.7985C2.48486 10.7369 2.52475 10.659 2.54003 10.5748C2.55548 10.4896 2.54505 10.4017 2.51009 10.3225L2.5072 10.316C2.47391 10.2383 2.41867 10.172 2.34821 10.1254C2.27803 10.0789 2.19583 10.0539 2.11167 10.0533H2C1.46957 10.0533 0.960859 9.84262 0.585786 9.46755C0.210714 9.09247 0 8.58377 0 8.05333C0 7.5229 0.210714 7.01419 0.585786 6.63912C0.960859 6.26405 1.46957 6.05333 2 6.05333H2.05043C2.13522 6.05017 2.21728 6.02218 2.28637 5.97276C2.35709 5.92218 2.41096 5.85149 2.44098 5.76988C2.44582 5.7567 2.45109 5.74367 2.45676 5.73082C2.49172 5.65161 2.50215 5.56374 2.4867 5.47855C2.47142 5.39428 2.43152 5.31645 2.37206 5.25487L2.33552 5.21833C2.14957 5.03259 2.00205 4.81201 1.90141 4.56921C1.80076 4.32642 1.74895 4.06616 1.74895 3.80333C1.74895 3.5405 1.80076 3.28025 1.90141 3.03745C2.00201 2.79477 2.14943 2.57429 2.33526 2.3886C2.52095 2.20277 2.74144 2.05534 2.98412 1.95474C3.22692 1.85409 3.48717 1.80228 3.75 1.80228C4.01283 1.80228 4.27308 1.85409 4.51588 1.95474C4.75868 2.05539 4.97925 2.2029 5.165 2.38886L5.20154 2.4254C5.26312 2.48485 5.34095 2.52475 5.42521 2.54003C5.51041 2.55548 5.59828 2.54505 5.67749 2.51009C5.72018 2.49125 5.76462 2.47699 5.81005 2.46748C5.85653 2.43593 5.89665 2.39549 5.92796 2.34821C5.97444 2.27803 5.99947 2.19583 6 2.11167V2C6 1.46957 6.21071 0.960859 6.58579 0.585786C6.96086 0.210714 7.46957 0 8 0C8.53043 0 9.03914 0.210714 9.41421 0.585786C9.78929 0.960859 10 1.46957 10 2V2.05833C10.0005 2.1425 10.0256 2.22469 10.072 2.29488C10.1187 2.36534 10.1849 2.42062 10.2626 2.45391L10.2692 2.45672C10.3484 2.49168 10.4363 2.50215 10.5215 2.4867C10.6057 2.47142 10.6835 2.43152 10.7451 2.37207L10.7817 2.33552C10.9674 2.14957 11.188 2.00205 11.4308 1.90141C11.6736 1.80076 11.9338 1.74895 12.1967 1.74895C12.4595 1.74895 12.7198 1.80076 12.9625 1.90141C13.2053 2.00205 13.4259 2.14957 13.6117 2.33552C13.7974 2.52116 13.9447 2.74155 14.0453 2.98412C14.1459 3.22691 14.1977 3.48717 14.1977 3.75C14.1977 4.01283 14.1459 4.27309 14.0453 4.51588C13.9447 4.75856 13.7972 4.97905 13.6114 5.16474C13.6113 5.16482 13.6115 5.16465 13.6114 5.16474L13.5746 5.20154C13.5151 5.26312 13.4752 5.34095 13.46 5.42521C13.4445 5.51041 13.4549 5.59828 13.4899 5.67749C13.5088 5.72018 13.523 5.76462 13.5325 5.81004C13.5641 5.85653 13.6045 5.89665 13.6518 5.92796C13.722 5.97444 13.8042 5.99947 13.8883 6H14C14.5304 6 15.0391 6.21071 15.4142 6.58579C15.7893 6.96086 16 7.46957 16 8C16 8.53043 15.7893 9.03914 15.4142 9.41421C15.0391 9.78929 14.5304 10 14 10H13.9417C13.8575 10.0005 13.7753 10.0256 13.7051 10.072C13.6347 10.1187 13.5794 10.1849 13.5461 10.2626L12.9333 10Z"
+ fill="#5E5F61"
+ />
+ </g>
+ <defs>
+ <clipPath id="clip0_1256_16163">
+ <rect width="16" height="16" fill="white" />
+ </clipPath>
+ </defs>
+ </svg>
+ )
+}
+export function IconSingleSelect(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg
+ {...props}
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ shape-rendering="geometricPrecision"
+ d="M12 2C6.45313 2 2 6.49219 2 12C2 17.5469 6.45313 22 12 22C17.5078 22 22 17.5469 22 12C22 6.49219 17.5078 2 12 2ZM17.2344 11.6484L12.8594 16.0234C12.625 16.2578 12.3125 16.375 12 16.375C11.6484 16.375 11.3359 16.2578 11.1016 16.0234L6.72656 11.6484C6.21875 11.1797 6.21875 10.3594 6.72656 9.89063C7.19531 9.38281 8.01563 9.38281 8.48438 9.89063L12 13.3672L15.4766 9.89063C15.9453 9.38281 16.7656 9.38281 17.2344 9.89063C17.7422 10.3594 17.7422 11.1797 17.2344 11.6484Z"
+ fill="#0D0E10"
+ />
+ </svg>
+ )
+}
diff --git a/packages/web/src/components/share.module.css b/packages/web/src/components/share.module.css
new file mode 100644
index 000000000..5d1dab1bf
--- /dev/null
+++ b/packages/web/src/components/share.module.css
@@ -0,0 +1,326 @@
+.root {
+ padding-top: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 2.5rem;
+ line-height: 1;
+}
+
+[data-element-button-text] {
+ cursor: pointer;
+ appearance: none;
+ background-color: transparent;
+ border: none;
+ padding: 0;
+ color: var(--sl-color-text-secondary);
+
+ &:hover {
+ color: var(--sl-color-text);
+ }
+}
+
+[data-element-button-text] {
+ cursor: pointer;
+ appearance: none;
+ background-color: transparent;
+ border: none;
+ padding: 0;
+ color: var(--sl-color-text-secondary);
+
+ &:hover {
+ color: var(--sl-color-text);
+ }
+
+ &[data-element-button-more] {
+ display: flex;
+ align-items: center;
+ gap: 0.125rem;
+
+ span[data-button-icon] {
+ line-height: 1;
+ opacity: 0.85;
+ svg {
+ display: block;
+ }
+ }
+ }
+}
+
+[data-element-label] {
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--sl-color-text-dimmed);
+}
+
+.header {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+
+ [data-section="title"] {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ [data-section="row"] {
+ display: flex;
+ flex-direction: column;
+ gap: 0.375rem;
+ }
+
+ h1 {
+ font-size: 1.75rem;
+ font-weight: 500;
+ line-height: 1.125;
+ letter-spacing: -0.05em;
+ }
+ p {
+ flex: 0 0 auto;
+ display: flex;
+ gap: 0.375rem;
+ font-size: 0.75rem;
+
+ span:first-child {
+ color: var(--sl-color-divider);
+
+ &[data-status="connected"] { color: var(--sl-color-green); }
+ &[data-status="connecting"] { color: var(--sl-color-orange); }
+ &[data-status="disconnected"] { color: var(--sl-color-divider); }
+ &[data-status="reconnecting"] { color: var(--sl-color-orange); }
+ &[data-status="error"] { color: var(--sl-color-red); }
+ }
+ }
+
+ [data-section="stats"] {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ gap: 1rem;
+
+ li {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.875rem;
+
+ span[data-placeholder] {
+ color: var(--sl-color-text-dimmed);
+ }
+ }
+ }
+
+ [data-section="stats"][data-section-models] {
+ li {
+ gap: 0.3125rem;
+
+ [data-stat-model-icon] {
+ flex: 0 0 auto;
+ color: var(--sl-color-text-dimmed);
+ opacity: 0.85;
+ svg {
+ display: block;
+ }
+ }
+
+ span[data-stat-model] {
+ color: var(sl-color-text);
+ }
+ }
+ }
+
+ [data-section="date"] {
+ span {
+ font-size: 0.875rem;
+ color: var(--sl-color-text);
+
+ &[data-placeholder] {
+ color: var(--sl-color-text-dimmed);
+ }
+ }
+ }
+}
+
+.parts {
+ display: flex;
+ flex-direction: column;
+ gap: 0.625rem;
+
+ [data-section="part"] {
+ display: flex;
+ gap: 0.625rem;
+ }
+
+ [data-section="decoration"] {
+ flex: 0 0 auto;
+ display: flex;
+ flex-direction: column;
+ gap: 0.625rem;
+ align-items: center;
+ justify-content: flex-start;
+
+ div:first-child {
+ flex: 0 0 auto;
+ width: 18px;
+ svg {
+ color: var(--sl-color-text-secondary);
+ display: block;
+ }
+ }
+
+ div:last-child {
+ width: 3px;
+ height: 100%;
+ border-radius: 1px;
+ background-color: var(--sl-color-hairline);
+ }
+ }
+
+ [data-section="content"] {
+ padding: 0 0 0.375rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ [data-part-tool-body] {
+ display: flex;
+ flex-direction: column;
+ gap: 0.375rem;
+ }
+
+ span[data-part-title] {
+ line-height: 18px;
+ font-size: 0.75rem;
+
+ &[data-size="md"] {
+ font-size: 0.875rem;
+ }
+ }
+
+ span[data-part-footer] {
+ align-self: flex-start;
+ font-size: 0.75rem;
+ color: var(--sl-color-text-dimmed);
+ }
+
+ span[data-part-model] {
+ line-height: 1.5;
+ }
+
+ [data-part-tool-args] {
+ display: inline-grid;
+ align-items: center;
+ grid-template-columns: max-content max-content minmax(0, 1fr);
+ max-width: 100%;
+ gap: 0.25rem 0.375rem;
+
+
+ & > div:nth-child(3n+1) {
+ width: 8px;
+ height: 2px;
+ border-radius: 1px;
+ background: var(--sl-color-divider);
+ }
+
+ & > div:nth-child(3n+2),
+ & > div:nth-child(3n+3) {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 0.75rem;
+ line-height: 1.5;
+ }
+
+ & > div:nth-child(3n+3) {
+ padding-left: 0.125rem;
+ color: var(--sl-color-text-dimmed);
+ }
+ }
+
+ [data-part-tool-result] {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.5rem;
+
+ button {
+ font-size: 0.75rem;
+ }
+ }
+ }
+}
+
+[data-element-message-text] {
+ 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;
+
+ pre {
+ line-height: 1.5;
+ font-size: 0.875rem;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ color: var(--sl-color-text);
+ }
+
+ &[data-size="sm"] {
+ pre {
+ font-size: 0.75rem;
+ }
+ }
+
+ &[data-color="dimmed"] {
+ pre {
+ color: var(--sl-color-text-dimmed);
+ }
+ }
+
+ button {
+ flex: 0 0 auto;
+ padding: 2px 0;
+ font-size: 0.75rem;
+ }
+
+ &[data-highlight="true"] {
+ background-color: var(--sl-color-blue-high);
+
+ pre {
+ color: var(--sl-color-text-invert);
+ }
+
+ button {
+ opacity: 0.85;
+ color: var(--sl-color-text-invert);
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
+
+ &[data-expanded="true"] {
+ pre {
+ display: block;
+ }
+ }
+ &[data-expanded="false"] {
+ pre {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3;
+ overflow: hidden;
+ }
+ }
+}
+
+.code-block {
+ pre {
+ line-height: 1.25;
+ font-size: 0.75rem;
+ }
+}
diff --git a/packages/web/src/content.config.ts b/packages/web/src/content.config.ts
new file mode 100644
index 000000000..d9ee8c9d1
--- /dev/null
+++ b/packages/web/src/content.config.ts
@@ -0,0 +1,7 @@
+import { defineCollection } from 'astro:content';
+import { docsLoader } from '@astrojs/starlight/loaders';
+import { docsSchema } from '@astrojs/starlight/schema';
+
+export const collections = {
+ docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
+};
diff --git a/packages/web/src/content/docs/docs/cli.mdx b/packages/web/src/content/docs/docs/cli.mdx
new file mode 100644
index 000000000..44a56e1fb
--- /dev/null
+++ b/packages/web/src/content/docs/docs/cli.mdx
@@ -0,0 +1,89 @@
+---
+title: CLI
+---
+
+Once installed you can run the OpenCode CLI.
+
+```bash
+opencode
+```
+
+Or pass in flags. For example, to start with debug logging:
+
+```bash
+opencode -d
+```
+
+Or start with a specific working directory.
+
+```bash
+opencode -c /path/to/project
+```
+
+## Flags
+
+The OpenCode CLI takes the following flags.
+
+| Flag | Short | Description |
+| -- | -- | -- |
+| `--help` | `-h` | Display help |
+| `--debug` | `-d` | Enable debug mode |
+| `--cwd` | `-c` | Set current working directory |
+| `--prompt` | `-p` | Run a single prompt in non-interactive mode |
+| `--output-format` | `-f` | Output format for non-interactive mode, `text` or `json` |
+| `--quiet` | `-q` | Hide spinner in non-interactive mode |
+| `--verbose` | | Display logs to stderr in non-interactive mode |
+| `--allowedTools` | | Restrict the agent to only use specified tools |
+| `--excludedTools` | | Prevent the agent from using specified tools |
+
+## Non-interactive
+
+By default, OpenCode runs in interactive mode.
+
+But you can also run OpenCode in non-interactive mode by passing a prompt directly as a command-line argument. This is useful for scripting, automation, or when you want a quick answer without launching the full TUI.
+
+For example, to run a single prompt use the `-p` flag.
+
+```bash "-p"
+opencode -p "Explain the use of context in Go"
+```
+
+If you want to run without showing the spinner, use `-q`.
+
+```bash "-q"
+opencode -p "Explain the use of context in Go" -q
+```
+
+In this mode, OpenCode will process your prompt, print the result to standard output, and then exit. All **permissions are auto-approved** for the session.
+
+#### Tool restrictions
+
+You can control which tools the AI assistant has access to in non-interactive mode.
+
+- `--allowedTools`
+
+ A comma-separated list of tools that the agent is allowed to use. Only these tools will be available.
+
+ ```bash "--allowedTools"
+ opencode -p "Explain the use of context in Go" --allowedTools=view,ls,glob
+ ```
+
+- `--excludedTools`
+
+ Comma-separated list of tools that the agent is not allowed to use. All other tools will be available.
+
+ ```bash "--excludedTools"
+ opencode -p "Explain the use of context in Go" --excludedTools=bash,edit
+ ```
+
+These flags are mutually exclusive. So you can either use `--allowedTools` or `--excludedTools`, but not both.
+
+#### Output formats
+
+In non-interactive mode, you can also set the CLI to return as JSON using `-f`.
+
+```bash "-f json"
+opencode -p "Explain the use of context in Go" -f json
+```
+
+By default, this is set to `text`, to return plain text.
diff --git a/packages/web/src/content/docs/docs/config.mdx b/packages/web/src/content/docs/docs/config.mdx
new file mode 100644
index 000000000..288f194c5
--- /dev/null
+++ b/packages/web/src/content/docs/docs/config.mdx
@@ -0,0 +1,88 @@
+---
+title: Config
+---
+
+You can configure OpenCode using the OpenCode config. It can be places in:
+
+- `$HOME/.opencode.json`
+- `$XDG_CONFIG_HOME/opencode/.opencode.json`
+
+Or in the current directory, `./.opencode.json`.
+
+## OpenCode config
+
+The config file has the following structure.
+
+```json title=".opencode.json"
+{
+ "data": {
+ "directory": ".opencode"
+ },
+ "providers": {
+ "openai": {
+ "apiKey": "your-api-key",
+ "disabled": false
+ },
+ "anthropic": {
+ "apiKey": "your-api-key",
+ "disabled": false
+ },
+ "groq": {
+ "apiKey": "your-api-key",
+ "disabled": false
+ },
+ "openrouter": {
+ "apiKey": "your-api-key",
+ "disabled": false
+ }
+ },
+ "agents": {
+ "primary": {
+ "model": "claude-3.7-sonnet",
+ "maxTokens": 5000
+ },
+ "task": {
+ "model": "claude-3.7-sonnet",
+ "maxTokens": 5000
+ },
+ "title": {
+ "model": "claude-3.7-sonnet",
+ "maxTokens": 80
+ }
+ },
+ "mcpServers": {
+ "example": {
+ "type": "stdio",
+ "command": "path/to/mcp-server",
+ "env": [],
+ "args": []
+ }
+ },
+ "lsp": {
+ "go": {
+ "disabled": false,
+ "command": "gopls"
+ }
+ },
+ "debug": false,
+ "debugLSP": false
+}
+```
+
+## Environment variables
+
+For the providers, you can also specify the keys using environment variables.
+
+| Environment Variable | Models |
+| -------------------------- | ----------- |
+| `ANTHROPIC_API_KEY` | Claude |
+| `OPENAI_API_KEY` | OpenAI |
+| `GEMINI_API_KEY` | Google Gemini |
+| `GROQ_API_KEY` | Groq |
+| `AWS_ACCESS_KEY_ID` | Amazon Bedrock |
+| `AWS_SECRET_ACCESS_KEY` | Amazon Bedrock |
+| `AWS_REGION` | Amazon Bedrock |
+| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI |
+| `AZURE_OPENAI_API_KEY` | Azure OpenAI, optional when using Entra ID |
+| `AZURE_OPENAI_API_VERSION` | Azure OpenAI |
+
diff --git a/packages/web/src/content/docs/docs/index.mdx b/packages/web/src/content/docs/docs/index.mdx
new file mode 100644
index 000000000..e6f71be19
--- /dev/null
+++ b/packages/web/src/content/docs/docs/index.mdx
@@ -0,0 +1,58 @@
+---
+title: Intro
+---
+
+OpenCode is an AI coding agent built natively for the terminal. It features:
+
+- Native TUI for a smoother, snappier experience
+- Uses LSPs to help the LLM make fewer mistakes
+- Opening multiple conversations with the same project
+- Use of any model through the AI SDK
+- Tracks and visualizes all the file changes
+- Editing longer messages with Vim
+
+## Installation
+
+```bash
+npm i -g opencode
+```
+
+If you don't have NPM installed, you can also install the OpenCode binary through the following.
+
+#### Using the install script
+
+```bash
+curl -fsSL https://opencode.ai/install | bash
+```
+
+Or install a specific version.
+
+```bash
+curl -fsSL https://opencode.ai/install | VERSION=0.1.0 bash
+```
+
+#### Using Homebrew on macOS and Linux
+
+```bash
+brew install sst/tap/opencode
+```
+
+#### Using AUR in Arch Linux
+
+With yay.
+
+```bash
+yay -S opencode-bin
+```
+
+Or with paru.
+
+```bash
+paru -S opencode-bin
+```
+
+#### Using Go
+
+```bash
+go install github.com/sst/opencode@latest
+```
diff --git a/packages/web/src/content/docs/docs/lsp-servers.mdx b/packages/web/src/content/docs/docs/lsp-servers.mdx
new file mode 100644
index 000000000..cd259dea7
--- /dev/null
+++ b/packages/web/src/content/docs/docs/lsp-servers.mdx
@@ -0,0 +1,34 @@
+---
+title: LSP servers
+---
+
+OpenCode integrates with _Language Server Protocol_, or LSP to improve how the LLM interacts with your codebase.
+
+LSP servers for different languages give the LLM:
+
+- **Diagnostics**: These include things like errors and lint warnings. So the LLM can generate code that has fewer mistakes without having to run the code.
+- **Quick actions**: The LSP can allow the LLM to better navigate the codebase through features like _go-to-definition_ and _find references_.
+
+## Auto-detection
+
+By default, OpenCode will **automatically detect** the languages used in your project and add the right LSP servers.
+
+## Manual configuration
+
+You can also manually configure LSP servers by adding them under the `lsp` section in your OpenCode config.
+
+```json title=".opencode.json"
+{
+ "lsp": {
+ "go": {
+ "disabled": false,
+ "command": "gopls"
+ },
+ "typescript": {
+ "disabled": false,
+ "command": "typescript-language-server",
+ "args": ["--stdio"]
+ }
+ }
+}
+```
diff --git a/packages/web/src/content/docs/docs/mcp-servers.mdx b/packages/web/src/content/docs/docs/mcp-servers.mdx
new file mode 100644
index 000000000..28c6d2ab2
--- /dev/null
+++ b/packages/web/src/content/docs/docs/mcp-servers.mdx
@@ -0,0 +1,51 @@
+---
+title: MCP servers
+---
+
+You can add external tools to OpenCode using the _Model Context Protocol_, or MCP. OpenCode supports both:
+
+- Local servers that use standard input/output, `stdio`
+- Remote servers that use server-sent events `sse`
+
+## Add MCP servers
+
+You can define MCP servers in your OpenCode config under the `mcpServers` section:
+
+### Local
+
+To add a local or `stdio` MCP server.
+
+```json title=".opencode.json" {4}
+{
+ "mcpServers": {
+ "local-example": {
+ "type": "stdio",
+ "command": "path/to/mcp-server",
+ "env": [],
+ "args": []
+ }
+ }
+}
+```
+
+### Remote
+
+To add a remote or `sse` MCP server.
+
+```json title=".opencode.json" {4}
+{
+ "mcpServers": {
+ "remote-example": {
+ "type": "sse",
+ "url": "https://example.com/mcp",
+ "headers": {
+ "Authorization": "Bearer token"
+ }
+ }
+ }
+}
+```
+
+## Usage
+
+Once added, MCP tools are automatically available to the LLM alongside built-in tools. They follow the same permission model; requiring user approval before execution.
diff --git a/packages/web/src/content/docs/docs/models.mdx b/packages/web/src/content/docs/docs/models.mdx
new file mode 100644
index 000000000..c40216695
--- /dev/null
+++ b/packages/web/src/content/docs/docs/models.mdx
@@ -0,0 +1,34 @@
+---
+title: Models
+---
+
+OpenCode uses the [AI SDK](https://ai-sdk.dev/) to have the support for **all the AI models**.
+
+Start by setting the [keys for the providers](/docs/config) you want to use in your OpenCode config.
+
+## Model select
+
+You can now select the model you want from the menu by hitting `Ctrl+O`.
+
+## Multiple models
+
+You can also use specific models for specific tasks. For example, you can use a smaller model to generate the title of the conversation or to run a sub task.
+
+```json title=".opencode.json"
+{
+ "agents": {
+ "primary": {
+ "model": "gpt-4",
+ "maxTokens": 5000
+ },
+ "task": {
+ "model": "gpt-3.5-turbo",
+ "maxTokens": 5000
+ },
+ "title": {
+ "model": "gpt-3.5-turbo",
+ "maxTokens": 80
+ }
+ }
+}
+```
diff --git a/packages/web/src/content/docs/docs/shortcuts.mdx b/packages/web/src/content/docs/docs/shortcuts.mdx
new file mode 100644
index 000000000..dd866e0f3
--- /dev/null
+++ b/packages/web/src/content/docs/docs/shortcuts.mdx
@@ -0,0 +1,68 @@
+---
+title: Keyboard shortcuts
+sidebar:
+ label: Shortcuts
+---
+
+Below are a list of keyboard shortcuts that OpenCode supports.
+
+## Global
+
+| Shortcut | Action |
+| -------- | ------------------------------------------------------- |
+| `Ctrl+C` | Quit application |
+| `Ctrl+?` | Toggle help dialog |
+| `?` | Toggle help dialog (when not in editing mode) |
+| `Ctrl+L` | View logs |
+| `Ctrl+A` | Switch session |
+| `Ctrl+K` | Command dialog |
+| `Ctrl+O` | Toggle model selection dialog |
+| `Esc` | Close current overlay/dialog or return to previous mode |
+
+## Chat pane
+
+| Shortcut | Action |
+| -------- | --------------------------------------- |
+| `Ctrl+N` | Create new session |
+| `Ctrl+X` | Cancel current operation/generation |
+| `i` | Focus editor (when not in writing mode) |
+| `Esc` | Exit writing mode and focus messages |
+
+## Editor view
+
+| Shortcut | Action |
+| ------------------- | ----------------------------------------- |
+| `Ctrl+S` | Send message (when editor is focused) |
+| `Enter` or `Ctrl+S` | Send message (when editor is not focused) |
+| `Ctrl+E` | Open external editor |
+| `Esc` | Blur editor and focus messages |
+
+## Session dialog
+
+| Shortcut | Action |
+| ---------- | ---------------- |
+| `↑` or `k` | Previous session |
+| `↓` or `j` | Next session |
+| `Enter` | Select session |
+| `Esc` | Close dialog |
+
+## Model dialog
+
+| Shortcut | Action |
+| ---------- | ----------------- |
+| `↑` or `k` | Move up |
+| `↓` or `j` | Move down |
+| `←` or `h` | Previous provider |
+| `→` or `l` | Next provider |
+| `Esc` | Close dialog |
+
+## Permission dialog
+
+| Shortcut | Action |
+| ----------------------- | ---------------------------- |
+| `←` or `left` | Switch options left |
+| `→` or `right` or `tab` | Switch options right |
+| `Enter` or `space` | Confirm selection |
+| `a` | Allow permission |
+| `A` | Allow permission for session |
+| `d` | Deny permission |
diff --git a/packages/web/src/content/docs/docs/themes.mdx b/packages/web/src/content/docs/docs/themes.mdx
new file mode 100644
index 000000000..e691a22e7
--- /dev/null
+++ b/packages/web/src/content/docs/docs/themes.mdx
@@ -0,0 +1,75 @@
+---
+title: Themes
+---
+
+OpenCode supports most common terminal themes and you can create your own custom theme.
+
+## Built-in themes
+
+The following predefined themes are available:
+
+- `opencode`
+- `catppuccin`
+- `dracula`
+- `flexoki`
+- `gruvbox`
+- `monokai`
+- `onedark`
+- `tokyonight`
+- `tron`
+- `custom`
+
+Where `opencode` is the default theme and `custom` let's you define your own theme.
+
+## Setting a theme
+
+You can set your theme in your OpenCode config.
+
+```json title=".opencode.json"
+{
+ "tui": {
+ "theme": "monokai"
+ }
+}
+```
+
+## Create a theme
+
+You can create your own custom theme by setting the `theme: custom` and providing color definitions through the `customTheme`.
+
+```json title=".opencode.json"
+{
+ "tui": {
+ "theme": "custom",
+ "customTheme": {
+ "primary": "#ffcc00",
+ "secondary": "#00ccff",
+ "accent": { "dark": "#aa00ff", "light": "#ddccff" },
+ "error": "#ff0000"
+ }
+ }
+}
+```
+
+#### Color keys
+
+You can define any of the following color keys in your `customTheme`.
+
+| Type | Color keys |
+| --- | --- |
+| Base colors | `primary`, `secondary`, `accent` |
+| Status colors | `error`, `warning`, `success`, `info` |
+| Text colors | `text`, `textMuted`, `textEmphasized` |
+| Background colors | `background`, `backgroundSecondary`, `backgroundDarker` |
+| Border colors | `borderNormal`, `borderFocused`, `borderDim` |
+| Diff view colors | `diffAdded`, `diffRemoved`, `diffContext`, etc. |
+
+You don't need to define all the color keys. Any undefined colors will fall back to the default `opencode` theme colors.
+
+#### Color definitions
+
+Color keys can take:
+
+1. **Hex string**: A single hex color string, like `"#aabbcc"`, that'll be used for both light and dark terminal backgrounds.
+
+2. **Light and dark colors**: An object with `dark` and `light` hex colors that'll be set based on the terminal's background.
diff --git a/packages/web/src/content/docs/index.mdx b/packages/web/src/content/docs/index.mdx
new file mode 100644
index 000000000..176520ec5
--- /dev/null
+++ b/packages/web/src/content/docs/index.mdx
@@ -0,0 +1,12 @@
+---
+title: OpenCode
+description: The AI coding agent built for the terminal.
+template: splash
+hero:
+ title: The AI coding agent built for the terminal.
+ tagline: The AI coding agent built for the terminal.
+ image:
+ dark: ../../assets/logo-dark.svg
+ light: ../../assets/logo-light.svg
+ alt: OpenCode logo
+---
diff --git a/packages/web/src/pages/s/index.astro b/packages/web/src/pages/s/index.astro
new file mode 100644
index 000000000..b678c0db9
--- /dev/null
+++ b/packages/web/src/pages/s/index.astro
@@ -0,0 +1,28 @@
+---
+import config from "virtual:starlight/user-config";
+
+import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
+import Share from "../../components/Share.tsx";
+
+---
+
+<StarlightPage
+ hasSidebar={false}
+ frontmatter={{
+ title: "Share",
+ pagefind: false,
+ template: "splash",
+ tableOfContents: false,
+ }}
+>
+ <Share api={import.meta.env.VITE_API_URL} client:only="solid" />
+</StarlightPage>
+
+<style is:global>
+body > .page > .main-frame .main-pane > main > .content-panel:first-of-type {
+ display: none;
+}
+body > .page > .main-frame .main-pane > main > .content-panel + .content-panel {
+ border-top: none !important;
+}
+</style>
diff --git a/packages/web/src/styles/custom.css b/packages/web/src/styles/custom.css
new file mode 100644
index 000000000..9c4c71f00
--- /dev/null
+++ b/packages/web/src/styles/custom.css
@@ -0,0 +1,16 @@
+:root {
+ --sl-color-bg-surface: var(--sl-color-bg-nav);
+ --sl-color-divider: var(--sl-color-gray-5);
+}
+
+@media (prefers-color-scheme: dark) {
+ .shiki,
+ .shiki span {
+ color: var(--shiki-dark) !important;
+ background-color: var(--shiki-dark-bg) !important;
+ /* Optional, if you also want font styles */
+ font-style: var(--shiki-dark-font-style) !important;
+ font-weight: var(--shiki-dark-font-weight) !important;
+ text-decoration: var(--shiki-dark-text-decoration) !important;
+ }
+}
diff --git a/packages/web/sst-env.d.ts b/packages/web/sst-env.d.ts
new file mode 100644
index 000000000..b6a7e9066
--- /dev/null
+++ b/packages/web/sst-env.d.ts
@@ -0,0 +1,9 @@
+/* This file is auto-generated by SST. Do not edit. */
+/* tslint:disable */
+/* eslint-disable */
+/* deno-fmt-ignore-file */
+
+/// <reference path="../../sst-env.d.ts" />
+
+import "sst"
+export {} \ No newline at end of file
diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json
new file mode 100644
index 000000000..973603872
--- /dev/null
+++ b/packages/web/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"],
+ "compilerOptions": {
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js"
+ }
+}