From f3da73553c45f17e04b1e77cb13eb0fca714d1bd Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 30 May 2025 20:47:56 -0400 Subject: sync --- .gitignore | 49 +- .goreleaser.yml | 77 - CONTEXT.md | 24 - LICENSE | 2 +- app/.gitignore | 3 - app/bun.lock | 1401 ----- app/infra/app.ts | 44 - app/package.json | 40 - app/packages/function/package.json | 10 - app/packages/function/src/api.ts | 168 - app/packages/function/sst-env.d.ts | 25 - app/packages/function/tsconfig.json | 9 - app/packages/web/.gitignore | 21 - app/packages/web/README.md | 54 - app/packages/web/astro.config.mjs | 69 - app/packages/web/package.json | 29 - app/packages/web/public/favicon.svg | 3 - app/packages/web/public/social-share.png | Bin 25480 -> 0 bytes app/packages/web/src/assets/lander/check.svg | 2 - app/packages/web/src/assets/lander/copy.svg | 2 - app/packages/web/src/assets/logo-dark.svg | 11 - app/packages/web/src/assets/logo-light.svg | 11 - app/packages/web/src/components/CodeBlock.tsx | 47 - app/packages/web/src/components/DiffView.tsx | 73 - app/packages/web/src/components/Header.astro | 62 - app/packages/web/src/components/Hero.astro | 11 - app/packages/web/src/components/Lander.astro | 269 - app/packages/web/src/components/Share.tsx | 772 --- .../web/src/components/diffview.module.css | 80 - app/packages/web/src/components/icons/custom.tsx | 22 - app/packages/web/src/components/icons/index.tsx | 6101 -------------------- app/packages/web/src/components/share.module.css | 326 -- app/packages/web/src/content.config.ts | 7 - app/packages/web/src/content/docs/docs/cli.mdx | 89 - app/packages/web/src/content/docs/docs/config.mdx | 88 - app/packages/web/src/content/docs/docs/index.mdx | 58 - .../web/src/content/docs/docs/lsp-servers.mdx | 34 - .../web/src/content/docs/docs/mcp-servers.mdx | 51 - app/packages/web/src/content/docs/docs/models.mdx | 34 - .../web/src/content/docs/docs/shortcuts.mdx | 68 - app/packages/web/src/content/docs/docs/themes.mdx | 75 - app/packages/web/src/content/docs/index.mdx | 12 - app/packages/web/src/pages/s/index.astro | 28 - app/packages/web/src/styles/custom.css | 16 - app/packages/web/sst-env.d.ts | 9 - app/packages/web/tsconfig.json | 9 - app/sst-env.d.ts | 24 - app/sst.config.ts | 18 - app/tsconfig.json | 1 - bun.lock | 1674 ++++++ cmd/root.go | 258 - go.mod | 105 - go.sum | 338 -- infra/app.ts | 44 + internal/completions/files-folders.go | 191 - internal/config/config.go | 266 - internal/config/init.go | 60 - internal/diff/diff.go | 869 --- internal/diff/diff_test.go | 103 - internal/diff/patch.go | 740 --- internal/fileutil/fileutil.go | 163 - internal/format/format.go | 46 - internal/format/format_test.go | 90 - internal/pubsub/broker.go | 113 - internal/pubsub/broker_test.go | 144 - internal/pubsub/events.go | 24 - internal/status/status.go | 142 - internal/tui/app/app.go | 215 - internal/tui/app/bridge.go | 42 - internal/tui/app/interfaces.go | 13 - internal/tui/components/chat/chat.go | 133 - internal/tui/components/chat/editor.go | 406 -- internal/tui/components/chat/message.go | 716 --- internal/tui/components/chat/messages.go | 344 -- internal/tui/components/chat/sidebar.go | 220 - internal/tui/components/core/status.go | 366 -- internal/tui/components/dialog/arguments.go | 257 - internal/tui/components/dialog/commands.go | 180 - internal/tui/components/dialog/complete.go | 263 - internal/tui/components/dialog/custom_commands.go | 186 - .../tui/components/dialog/custom_commands_test.go | 106 - internal/tui/components/dialog/filepicker.go | 485 -- internal/tui/components/dialog/help.go | 200 - internal/tui/components/dialog/init.go | 189 - internal/tui/components/dialog/models.go | 327 -- internal/tui/components/dialog/permission.go | 502 -- internal/tui/components/dialog/quit.go | 136 - internal/tui/components/dialog/session.go | 230 - internal/tui/components/dialog/theme.go | 199 - internal/tui/components/dialog/tools.go | 178 - internal/tui/components/qr/qr.go | 58 - internal/tui/components/spinner/spinner.go | 127 - internal/tui/components/spinner/spinner_test.go | 24 - internal/tui/components/util/simple-list.go | 159 - internal/tui/image/clipboard_unix.go | 49 - internal/tui/image/clipboard_windows.go | 192 - internal/tui/image/images.go | 85 - internal/tui/layout/container.go | 230 - internal/tui/layout/layout.go | 35 - internal/tui/layout/overlay.go | 169 - internal/tui/layout/split.go | 283 - internal/tui/page/chat.go | 233 - internal/tui/page/page.go | 8 - internal/tui/state/state.go | 19 - internal/tui/styles/background.go | 123 - internal/tui/styles/icons.go | 12 - internal/tui/styles/markdown.go | 283 - internal/tui/styles/styles.go | 153 - internal/tui/theme/ayu.go | 280 - internal/tui/theme/catppuccin.go | 248 - internal/tui/theme/dracula.go | 274 - internal/tui/theme/flexoki.go | 282 - internal/tui/theme/gruvbox.go | 302 - internal/tui/theme/manager.go | 265 - internal/tui/theme/monokai.go | 273 - internal/tui/theme/onedark.go | 274 - internal/tui/theme/opencode.go | 276 - internal/tui/theme/theme.go | 290 - internal/tui/theme/theme_test.go | 89 - internal/tui/theme/tokyonight.go | 274 - internal/tui/theme/tron.go | 276 - internal/tui/tui.go | 988 ---- internal/tui/util/util.go | 18 - internal/version/version.go | 25 - js/.gitignore | 3 - js/README.md | 15 - js/bun.lock | 348 -- js/package.json | 40 - js/scrap.ts | 30 - js/src/app/app.ts | 78 - js/src/app/path.ts | 11 - js/src/bun/index.ts | 28 - js/src/bus/index.ts | 101 - js/src/config/config.ts | 51 - js/src/global/index.ts | 20 - js/src/id/id.ts | 74 - js/src/index.ts | 85 - js/src/llm/llm.ts | 172 - js/src/lsp/client.ts | 208 - js/src/lsp/index.ts | 131 - js/src/lsp/language.ts | 89 - js/src/provider/provider.ts | 35 - js/src/server/server.ts | 309 - js/src/session/message.ts | 171 - js/src/session/prompt/anthropic.txt | 95 - js/src/session/prompt/summarize.txt | 10 - js/src/session/prompt/title.txt | 7 - js/src/session/session.ts | 498 -- js/src/share/share.ts | 67 - js/src/storage/storage.ts | 55 - js/src/tool/bash.ts | 199 - js/src/tool/edit.ts | 136 - js/src/tool/fetch.ts | 137 - js/src/tool/glob.ts | 96 - js/src/tool/grep.ts | 345 -- js/src/tool/index.ts | 9 - js/src/tool/ls.ts | 96 - js/src/tool/lsp-diagnostics.ts | 53 - js/src/tool/lsp-hover.ts | 38 - js/src/tool/patch.ts | 420 -- js/src/tool/tool.ts | 61 - js/src/tool/util/file-times.ts | 20 - js/src/tool/view.ts | 152 - js/src/util/context.ts | 25 - js/src/util/event.ts | 0 js/src/util/log.ts | 64 - js/src/util/scrap.ts | 5 - js/test/tool/__snapshots__/tool.test.ts.snap | 17 - js/test/tool/tool.test.ts | 55 - js/tsconfig.json | 5 - main.go | 9 - package.json | 40 + packages/function/package.json | 10 + packages/function/src/api.ts | 167 + packages/function/sst-env.d.ts | 25 + packages/function/tsconfig.json | 9 + packages/opencode/.gitignore | 3 + packages/opencode/README.md | 15 + packages/opencode/bin/opencode.mjs | 29 + packages/opencode/package.json | 39 + packages/opencode/scrap.ts | 30 + packages/opencode/script/release.ts | 68 + packages/opencode/src/app/app.ts | 78 + packages/opencode/src/app/path.ts | 11 + packages/opencode/src/bun/index.ts | 28 + packages/opencode/src/bus/index.ts | 101 + packages/opencode/src/config/config.ts | 51 + packages/opencode/src/global/index.ts | 20 + packages/opencode/src/id/id.ts | 74 + packages/opencode/src/index.ts | 85 + packages/opencode/src/llm/llm.ts | 172 + packages/opencode/src/lsp/client.ts | 208 + packages/opencode/src/lsp/index.ts | 131 + packages/opencode/src/lsp/language.ts | 89 + packages/opencode/src/provider/provider.ts | 35 + packages/opencode/src/server/server.ts | 309 + packages/opencode/src/session/message.ts | 171 + packages/opencode/src/session/prompt/anthropic.txt | 95 + packages/opencode/src/session/prompt/summarize.txt | 10 + packages/opencode/src/session/prompt/title.txt | 7 + packages/opencode/src/session/session.ts | 498 ++ packages/opencode/src/share/share.ts | 67 + packages/opencode/src/storage/storage.ts | 55 + packages/opencode/src/tool/bash.ts | 199 + packages/opencode/src/tool/edit.ts | 136 + packages/opencode/src/tool/fetch.ts | 137 + packages/opencode/src/tool/glob.ts | 96 + packages/opencode/src/tool/grep.ts | 345 ++ packages/opencode/src/tool/index.ts | 9 + packages/opencode/src/tool/ls.ts | 96 + packages/opencode/src/tool/lsp-diagnostics.ts | 53 + packages/opencode/src/tool/lsp-hover.ts | 38 + packages/opencode/src/tool/patch.ts | 420 ++ packages/opencode/src/tool/tool.ts | 61 + packages/opencode/src/tool/util/file-times.ts | 20 + packages/opencode/src/tool/view.ts | 152 + packages/opencode/src/util/context.ts | 25 + packages/opencode/src/util/event.ts | 0 packages/opencode/src/util/log.ts | 64 + packages/opencode/src/util/scrap.ts | 5 + .../test/tool/__snapshots__/tool.test.ts.snap | 17 + packages/opencode/test/tool/tool.test.ts | 55 + packages/opencode/tsconfig.json | 5 + packages/tui/.goreleaser.yml | 77 + packages/tui/app.log | 8 + packages/tui/cmd/root.go | 258 + packages/tui/go.mod | 105 + packages/tui/go.sum | 338 ++ packages/tui/internal/completions/files-folders.go | 191 + packages/tui/internal/config/config.go | 266 + packages/tui/internal/config/init.go | 60 + packages/tui/internal/diff/diff.go | 869 +++ packages/tui/internal/diff/diff_test.go | 103 + packages/tui/internal/diff/patch.go | 740 +++ packages/tui/internal/fileutil/fileutil.go | 163 + packages/tui/internal/format/format.go | 46 + packages/tui/internal/format/format_test.go | 90 + packages/tui/internal/pubsub/broker.go | 113 + packages/tui/internal/pubsub/broker_test.go | 144 + packages/tui/internal/pubsub/events.go | 24 + packages/tui/internal/status/status.go | 142 + packages/tui/internal/tui/app/app.go | 215 + packages/tui/internal/tui/app/bridge.go | 42 + packages/tui/internal/tui/app/interfaces.go | 13 + packages/tui/internal/tui/components/chat/chat.go | 133 + .../tui/internal/tui/components/chat/editor.go | 406 ++ .../tui/internal/tui/components/chat/message.go | 716 +++ .../tui/internal/tui/components/chat/messages.go | 344 ++ .../tui/internal/tui/components/chat/sidebar.go | 220 + .../tui/internal/tui/components/core/status.go | 366 ++ .../internal/tui/components/dialog/arguments.go | 257 + .../tui/internal/tui/components/dialog/commands.go | 180 + .../tui/internal/tui/components/dialog/complete.go | 263 + .../tui/components/dialog/custom_commands.go | 186 + .../tui/components/dialog/custom_commands_test.go | 106 + .../internal/tui/components/dialog/filepicker.go | 485 ++ .../tui/internal/tui/components/dialog/help.go | 200 + .../tui/internal/tui/components/dialog/init.go | 189 + .../tui/internal/tui/components/dialog/models.go | 327 ++ .../internal/tui/components/dialog/permission.go | 502 ++ .../tui/internal/tui/components/dialog/quit.go | 136 + .../tui/internal/tui/components/dialog/session.go | 230 + .../tui/internal/tui/components/dialog/theme.go | 199 + .../tui/internal/tui/components/dialog/tools.go | 178 + packages/tui/internal/tui/components/qr/qr.go | 58 + .../tui/internal/tui/components/spinner/spinner.go | 127 + .../tui/components/spinner/spinner_test.go | 24 + .../internal/tui/components/util/simple-list.go | 159 + packages/tui/internal/tui/image/clipboard_unix.go | 49 + .../tui/internal/tui/image/clipboard_windows.go | 192 + packages/tui/internal/tui/image/images.go | 85 + packages/tui/internal/tui/layout/container.go | 230 + packages/tui/internal/tui/layout/layout.go | 35 + packages/tui/internal/tui/layout/overlay.go | 169 + packages/tui/internal/tui/layout/split.go | 283 + packages/tui/internal/tui/page/chat.go | 233 + packages/tui/internal/tui/page/page.go | 8 + packages/tui/internal/tui/state/state.go | 19 + packages/tui/internal/tui/styles/background.go | 123 + packages/tui/internal/tui/styles/icons.go | 12 + packages/tui/internal/tui/styles/markdown.go | 283 + packages/tui/internal/tui/styles/styles.go | 153 + packages/tui/internal/tui/theme/ayu.go | 280 + packages/tui/internal/tui/theme/catppuccin.go | 248 + packages/tui/internal/tui/theme/dracula.go | 274 + packages/tui/internal/tui/theme/flexoki.go | 282 + packages/tui/internal/tui/theme/gruvbox.go | 302 + packages/tui/internal/tui/theme/manager.go | 265 + packages/tui/internal/tui/theme/monokai.go | 273 + packages/tui/internal/tui/theme/onedark.go | 274 + packages/tui/internal/tui/theme/opencode.go | 276 + packages/tui/internal/tui/theme/theme.go | 290 + packages/tui/internal/tui/theme/theme_test.go | 89 + packages/tui/internal/tui/theme/tokyonight.go | 274 + packages/tui/internal/tui/theme/tron.go | 276 + packages/tui/internal/tui/tui.go | 988 ++++ packages/tui/internal/tui/util/util.go | 18 + packages/tui/internal/version/version.go | 25 + packages/tui/main.go | 9 + packages/tui/pkg/client/.gitignore | 2 + packages/tui/pkg/client/client.go | 4 + packages/tui/pkg/client/event.go | 53 + packages/tui/pkg/tui/theme/opencode.go | 276 + packages/web/.gitignore | 21 + packages/web/README.md | 54 + packages/web/astro.config.mjs | 69 + packages/web/package.json | 29 + packages/web/public/favicon.svg | 3 + packages/web/public/social-share.png | Bin 0 -> 25480 bytes packages/web/src/assets/lander/check.svg | 2 + packages/web/src/assets/lander/copy.svg | 2 + packages/web/src/assets/logo-dark.svg | 11 + packages/web/src/assets/logo-light.svg | 11 + packages/web/src/components/CodeBlock.tsx | 47 + packages/web/src/components/DiffView.tsx | 73 + packages/web/src/components/Header.astro | 62 + packages/web/src/components/Hero.astro | 11 + packages/web/src/components/Lander.astro | 269 + packages/web/src/components/Share.tsx | 772 +++ packages/web/src/components/diffview.module.css | 80 + packages/web/src/components/icons/custom.tsx | 22 + packages/web/src/components/icons/index.tsx | 6101 ++++++++++++++++++++ packages/web/src/components/share.module.css | 326 ++ packages/web/src/content.config.ts | 7 + packages/web/src/content/docs/docs/cli.mdx | 89 + packages/web/src/content/docs/docs/config.mdx | 88 + packages/web/src/content/docs/docs/index.mdx | 58 + packages/web/src/content/docs/docs/lsp-servers.mdx | 34 + packages/web/src/content/docs/docs/mcp-servers.mdx | 51 + packages/web/src/content/docs/docs/models.mdx | 34 + packages/web/src/content/docs/docs/shortcuts.mdx | 68 + packages/web/src/content/docs/docs/themes.mdx | 75 + packages/web/src/content/docs/index.mdx | 12 + packages/web/src/pages/s/index.astro | 28 + packages/web/src/styles/custom.css | 16 + packages/web/sst-env.d.ts | 9 + packages/web/tsconfig.json | 9 + pkg/client/.gitignore | 2 - pkg/client/client.go | 4 - pkg/client/event.go | 53 - pkg/client/gen/openapi.json | 898 --- pkg/client/generated-client.go | 1953 ------- sst-env.d.ts | 24 + sst.config.ts | 18 + tsconfig.json | 1 + 345 files changed, 31621 insertions(+), 34238 deletions(-) delete mode 100644 .goreleaser.yml delete mode 100644 CONTEXT.md delete mode 100644 app/.gitignore delete mode 100644 app/bun.lock delete mode 100644 app/infra/app.ts delete mode 100644 app/package.json delete mode 100644 app/packages/function/package.json delete mode 100644 app/packages/function/src/api.ts delete mode 100644 app/packages/function/sst-env.d.ts delete mode 100644 app/packages/function/tsconfig.json delete mode 100644 app/packages/web/.gitignore delete mode 100644 app/packages/web/README.md delete mode 100644 app/packages/web/astro.config.mjs delete mode 100644 app/packages/web/package.json delete mode 100644 app/packages/web/public/favicon.svg delete mode 100644 app/packages/web/public/social-share.png delete mode 100644 app/packages/web/src/assets/lander/check.svg delete mode 100644 app/packages/web/src/assets/lander/copy.svg delete mode 100644 app/packages/web/src/assets/logo-dark.svg delete mode 100644 app/packages/web/src/assets/logo-light.svg delete mode 100644 app/packages/web/src/components/CodeBlock.tsx delete mode 100644 app/packages/web/src/components/DiffView.tsx delete mode 100644 app/packages/web/src/components/Header.astro delete mode 100644 app/packages/web/src/components/Hero.astro delete mode 100644 app/packages/web/src/components/Lander.astro delete mode 100644 app/packages/web/src/components/Share.tsx delete mode 100644 app/packages/web/src/components/diffview.module.css delete mode 100644 app/packages/web/src/components/icons/custom.tsx delete mode 100644 app/packages/web/src/components/icons/index.tsx delete mode 100644 app/packages/web/src/components/share.module.css delete mode 100644 app/packages/web/src/content.config.ts delete mode 100644 app/packages/web/src/content/docs/docs/cli.mdx delete mode 100644 app/packages/web/src/content/docs/docs/config.mdx delete mode 100644 app/packages/web/src/content/docs/docs/index.mdx delete mode 100644 app/packages/web/src/content/docs/docs/lsp-servers.mdx delete mode 100644 app/packages/web/src/content/docs/docs/mcp-servers.mdx delete mode 100644 app/packages/web/src/content/docs/docs/models.mdx delete mode 100644 app/packages/web/src/content/docs/docs/shortcuts.mdx delete mode 100644 app/packages/web/src/content/docs/docs/themes.mdx delete mode 100644 app/packages/web/src/content/docs/index.mdx delete mode 100644 app/packages/web/src/pages/s/index.astro delete mode 100644 app/packages/web/src/styles/custom.css delete mode 100644 app/packages/web/sst-env.d.ts delete mode 100644 app/packages/web/tsconfig.json delete mode 100644 app/sst-env.d.ts delete mode 100644 app/sst.config.ts delete mode 100644 app/tsconfig.json create mode 100644 bun.lock delete mode 100644 cmd/root.go delete mode 100644 go.mod delete mode 100644 go.sum create mode 100644 infra/app.ts delete mode 100644 internal/completions/files-folders.go delete mode 100644 internal/config/config.go delete mode 100644 internal/config/init.go delete mode 100644 internal/diff/diff.go delete mode 100644 internal/diff/diff_test.go delete mode 100644 internal/diff/patch.go delete mode 100644 internal/fileutil/fileutil.go delete mode 100644 internal/format/format.go delete mode 100644 internal/format/format_test.go delete mode 100644 internal/pubsub/broker.go delete mode 100644 internal/pubsub/broker_test.go delete mode 100644 internal/pubsub/events.go delete mode 100644 internal/status/status.go delete mode 100644 internal/tui/app/app.go delete mode 100644 internal/tui/app/bridge.go delete mode 100644 internal/tui/app/interfaces.go delete mode 100644 internal/tui/components/chat/chat.go delete mode 100644 internal/tui/components/chat/editor.go delete mode 100644 internal/tui/components/chat/message.go delete mode 100644 internal/tui/components/chat/messages.go delete mode 100644 internal/tui/components/chat/sidebar.go delete mode 100644 internal/tui/components/core/status.go delete mode 100644 internal/tui/components/dialog/arguments.go delete mode 100644 internal/tui/components/dialog/commands.go delete mode 100644 internal/tui/components/dialog/complete.go delete mode 100644 internal/tui/components/dialog/custom_commands.go delete mode 100644 internal/tui/components/dialog/custom_commands_test.go delete mode 100644 internal/tui/components/dialog/filepicker.go delete mode 100644 internal/tui/components/dialog/help.go delete mode 100644 internal/tui/components/dialog/init.go delete mode 100644 internal/tui/components/dialog/models.go delete mode 100644 internal/tui/components/dialog/permission.go delete mode 100644 internal/tui/components/dialog/quit.go delete mode 100644 internal/tui/components/dialog/session.go delete mode 100644 internal/tui/components/dialog/theme.go delete mode 100644 internal/tui/components/dialog/tools.go delete mode 100644 internal/tui/components/qr/qr.go delete mode 100644 internal/tui/components/spinner/spinner.go delete mode 100644 internal/tui/components/spinner/spinner_test.go delete mode 100644 internal/tui/components/util/simple-list.go delete mode 100644 internal/tui/image/clipboard_unix.go delete mode 100644 internal/tui/image/clipboard_windows.go delete mode 100644 internal/tui/image/images.go delete mode 100644 internal/tui/layout/container.go delete mode 100644 internal/tui/layout/layout.go delete mode 100644 internal/tui/layout/overlay.go delete mode 100644 internal/tui/layout/split.go delete mode 100644 internal/tui/page/chat.go delete mode 100644 internal/tui/page/page.go delete mode 100644 internal/tui/state/state.go delete mode 100644 internal/tui/styles/background.go delete mode 100644 internal/tui/styles/icons.go delete mode 100644 internal/tui/styles/markdown.go delete mode 100644 internal/tui/styles/styles.go delete mode 100644 internal/tui/theme/ayu.go delete mode 100644 internal/tui/theme/catppuccin.go delete mode 100644 internal/tui/theme/dracula.go delete mode 100644 internal/tui/theme/flexoki.go delete mode 100644 internal/tui/theme/gruvbox.go delete mode 100644 internal/tui/theme/manager.go delete mode 100644 internal/tui/theme/monokai.go delete mode 100644 internal/tui/theme/onedark.go delete mode 100644 internal/tui/theme/opencode.go delete mode 100644 internal/tui/theme/theme.go delete mode 100644 internal/tui/theme/theme_test.go delete mode 100644 internal/tui/theme/tokyonight.go delete mode 100644 internal/tui/theme/tron.go delete mode 100644 internal/tui/tui.go delete mode 100644 internal/tui/util/util.go delete mode 100644 internal/version/version.go delete mode 100644 js/.gitignore delete mode 100644 js/README.md delete mode 100644 js/bun.lock delete mode 100644 js/package.json delete mode 100644 js/scrap.ts delete mode 100644 js/src/app/app.ts delete mode 100644 js/src/app/path.ts delete mode 100644 js/src/bun/index.ts delete mode 100644 js/src/bus/index.ts delete mode 100644 js/src/config/config.ts delete mode 100644 js/src/global/index.ts delete mode 100644 js/src/id/id.ts delete mode 100644 js/src/index.ts delete mode 100644 js/src/llm/llm.ts delete mode 100644 js/src/lsp/client.ts delete mode 100644 js/src/lsp/index.ts delete mode 100644 js/src/lsp/language.ts delete mode 100644 js/src/provider/provider.ts delete mode 100644 js/src/server/server.ts delete mode 100644 js/src/session/message.ts delete mode 100644 js/src/session/prompt/anthropic.txt delete mode 100644 js/src/session/prompt/summarize.txt delete mode 100644 js/src/session/prompt/title.txt delete mode 100644 js/src/session/session.ts delete mode 100644 js/src/share/share.ts delete mode 100644 js/src/storage/storage.ts delete mode 100644 js/src/tool/bash.ts delete mode 100644 js/src/tool/edit.ts delete mode 100644 js/src/tool/fetch.ts delete mode 100644 js/src/tool/glob.ts delete mode 100644 js/src/tool/grep.ts delete mode 100644 js/src/tool/index.ts delete mode 100644 js/src/tool/ls.ts delete mode 100644 js/src/tool/lsp-diagnostics.ts delete mode 100644 js/src/tool/lsp-hover.ts delete mode 100644 js/src/tool/patch.ts delete mode 100644 js/src/tool/tool.ts delete mode 100644 js/src/tool/util/file-times.ts delete mode 100644 js/src/tool/view.ts delete mode 100644 js/src/util/context.ts delete mode 100644 js/src/util/event.ts delete mode 100644 js/src/util/log.ts delete mode 100644 js/src/util/scrap.ts delete mode 100644 js/test/tool/__snapshots__/tool.test.ts.snap delete mode 100644 js/test/tool/tool.test.ts delete mode 100644 js/tsconfig.json delete mode 100644 main.go create mode 100644 package.json create mode 100644 packages/function/package.json create mode 100644 packages/function/src/api.ts create mode 100644 packages/function/sst-env.d.ts create mode 100644 packages/function/tsconfig.json create mode 100644 packages/opencode/.gitignore create mode 100644 packages/opencode/README.md create mode 100644 packages/opencode/bin/opencode.mjs create mode 100644 packages/opencode/package.json create mode 100644 packages/opencode/scrap.ts create mode 100755 packages/opencode/script/release.ts create mode 100644 packages/opencode/src/app/app.ts create mode 100644 packages/opencode/src/app/path.ts create mode 100644 packages/opencode/src/bun/index.ts create mode 100644 packages/opencode/src/bus/index.ts create mode 100644 packages/opencode/src/config/config.ts create mode 100644 packages/opencode/src/global/index.ts create mode 100644 packages/opencode/src/id/id.ts create mode 100644 packages/opencode/src/index.ts create mode 100644 packages/opencode/src/llm/llm.ts create mode 100644 packages/opencode/src/lsp/client.ts create mode 100644 packages/opencode/src/lsp/index.ts create mode 100644 packages/opencode/src/lsp/language.ts create mode 100644 packages/opencode/src/provider/provider.ts create mode 100644 packages/opencode/src/server/server.ts create mode 100644 packages/opencode/src/session/message.ts create mode 100644 packages/opencode/src/session/prompt/anthropic.txt create mode 100644 packages/opencode/src/session/prompt/summarize.txt create mode 100644 packages/opencode/src/session/prompt/title.txt create mode 100644 packages/opencode/src/session/session.ts create mode 100644 packages/opencode/src/share/share.ts create mode 100644 packages/opencode/src/storage/storage.ts create mode 100644 packages/opencode/src/tool/bash.ts create mode 100644 packages/opencode/src/tool/edit.ts create mode 100644 packages/opencode/src/tool/fetch.ts create mode 100644 packages/opencode/src/tool/glob.ts create mode 100644 packages/opencode/src/tool/grep.ts create mode 100644 packages/opencode/src/tool/index.ts create mode 100644 packages/opencode/src/tool/ls.ts create mode 100644 packages/opencode/src/tool/lsp-diagnostics.ts create mode 100644 packages/opencode/src/tool/lsp-hover.ts create mode 100644 packages/opencode/src/tool/patch.ts create mode 100644 packages/opencode/src/tool/tool.ts create mode 100644 packages/opencode/src/tool/util/file-times.ts create mode 100644 packages/opencode/src/tool/view.ts create mode 100644 packages/opencode/src/util/context.ts create mode 100644 packages/opencode/src/util/event.ts create mode 100644 packages/opencode/src/util/log.ts create mode 100644 packages/opencode/src/util/scrap.ts create mode 100644 packages/opencode/test/tool/__snapshots__/tool.test.ts.snap create mode 100644 packages/opencode/test/tool/tool.test.ts create mode 100644 packages/opencode/tsconfig.json create mode 100644 packages/tui/.goreleaser.yml create mode 100644 packages/tui/app.log create mode 100644 packages/tui/cmd/root.go create mode 100644 packages/tui/go.mod create mode 100644 packages/tui/go.sum create mode 100644 packages/tui/internal/completions/files-folders.go create mode 100644 packages/tui/internal/config/config.go create mode 100644 packages/tui/internal/config/init.go create mode 100644 packages/tui/internal/diff/diff.go create mode 100644 packages/tui/internal/diff/diff_test.go create mode 100644 packages/tui/internal/diff/patch.go create mode 100644 packages/tui/internal/fileutil/fileutil.go create mode 100644 packages/tui/internal/format/format.go create mode 100644 packages/tui/internal/format/format_test.go create mode 100644 packages/tui/internal/pubsub/broker.go create mode 100644 packages/tui/internal/pubsub/broker_test.go create mode 100644 packages/tui/internal/pubsub/events.go create mode 100644 packages/tui/internal/status/status.go create mode 100644 packages/tui/internal/tui/app/app.go create mode 100644 packages/tui/internal/tui/app/bridge.go create mode 100644 packages/tui/internal/tui/app/interfaces.go create mode 100644 packages/tui/internal/tui/components/chat/chat.go create mode 100644 packages/tui/internal/tui/components/chat/editor.go create mode 100644 packages/tui/internal/tui/components/chat/message.go create mode 100644 packages/tui/internal/tui/components/chat/messages.go create mode 100644 packages/tui/internal/tui/components/chat/sidebar.go create mode 100644 packages/tui/internal/tui/components/core/status.go create mode 100644 packages/tui/internal/tui/components/dialog/arguments.go create mode 100644 packages/tui/internal/tui/components/dialog/commands.go create mode 100644 packages/tui/internal/tui/components/dialog/complete.go create mode 100644 packages/tui/internal/tui/components/dialog/custom_commands.go create mode 100644 packages/tui/internal/tui/components/dialog/custom_commands_test.go create mode 100644 packages/tui/internal/tui/components/dialog/filepicker.go create mode 100644 packages/tui/internal/tui/components/dialog/help.go create mode 100644 packages/tui/internal/tui/components/dialog/init.go create mode 100644 packages/tui/internal/tui/components/dialog/models.go create mode 100644 packages/tui/internal/tui/components/dialog/permission.go create mode 100644 packages/tui/internal/tui/components/dialog/quit.go create mode 100644 packages/tui/internal/tui/components/dialog/session.go create mode 100644 packages/tui/internal/tui/components/dialog/theme.go create mode 100644 packages/tui/internal/tui/components/dialog/tools.go create mode 100644 packages/tui/internal/tui/components/qr/qr.go create mode 100644 packages/tui/internal/tui/components/spinner/spinner.go create mode 100644 packages/tui/internal/tui/components/spinner/spinner_test.go create mode 100644 packages/tui/internal/tui/components/util/simple-list.go create mode 100644 packages/tui/internal/tui/image/clipboard_unix.go create mode 100644 packages/tui/internal/tui/image/clipboard_windows.go create mode 100644 packages/tui/internal/tui/image/images.go create mode 100644 packages/tui/internal/tui/layout/container.go create mode 100644 packages/tui/internal/tui/layout/layout.go create mode 100644 packages/tui/internal/tui/layout/overlay.go create mode 100644 packages/tui/internal/tui/layout/split.go create mode 100644 packages/tui/internal/tui/page/chat.go create mode 100644 packages/tui/internal/tui/page/page.go create mode 100644 packages/tui/internal/tui/state/state.go create mode 100644 packages/tui/internal/tui/styles/background.go create mode 100644 packages/tui/internal/tui/styles/icons.go create mode 100644 packages/tui/internal/tui/styles/markdown.go create mode 100644 packages/tui/internal/tui/styles/styles.go create mode 100644 packages/tui/internal/tui/theme/ayu.go create mode 100644 packages/tui/internal/tui/theme/catppuccin.go create mode 100644 packages/tui/internal/tui/theme/dracula.go create mode 100644 packages/tui/internal/tui/theme/flexoki.go create mode 100644 packages/tui/internal/tui/theme/gruvbox.go create mode 100644 packages/tui/internal/tui/theme/manager.go create mode 100644 packages/tui/internal/tui/theme/monokai.go create mode 100644 packages/tui/internal/tui/theme/onedark.go create mode 100644 packages/tui/internal/tui/theme/opencode.go create mode 100644 packages/tui/internal/tui/theme/theme.go create mode 100644 packages/tui/internal/tui/theme/theme_test.go create mode 100644 packages/tui/internal/tui/theme/tokyonight.go create mode 100644 packages/tui/internal/tui/theme/tron.go create mode 100644 packages/tui/internal/tui/tui.go create mode 100644 packages/tui/internal/tui/util/util.go create mode 100644 packages/tui/internal/version/version.go create mode 100644 packages/tui/main.go create mode 100644 packages/tui/pkg/client/.gitignore create mode 100644 packages/tui/pkg/client/client.go create mode 100644 packages/tui/pkg/client/event.go create mode 100644 packages/tui/pkg/tui/theme/opencode.go create mode 100644 packages/web/.gitignore create mode 100644 packages/web/README.md create mode 100644 packages/web/astro.config.mjs create mode 100644 packages/web/package.json create mode 100644 packages/web/public/favicon.svg create mode 100644 packages/web/public/social-share.png create mode 100644 packages/web/src/assets/lander/check.svg create mode 100644 packages/web/src/assets/lander/copy.svg create mode 100644 packages/web/src/assets/logo-dark.svg create mode 100644 packages/web/src/assets/logo-light.svg create mode 100644 packages/web/src/components/CodeBlock.tsx create mode 100644 packages/web/src/components/DiffView.tsx create mode 100644 packages/web/src/components/Header.astro create mode 100644 packages/web/src/components/Hero.astro create mode 100644 packages/web/src/components/Lander.astro create mode 100644 packages/web/src/components/Share.tsx create mode 100644 packages/web/src/components/diffview.module.css create mode 100644 packages/web/src/components/icons/custom.tsx create mode 100644 packages/web/src/components/icons/index.tsx create mode 100644 packages/web/src/components/share.module.css create mode 100644 packages/web/src/content.config.ts create mode 100644 packages/web/src/content/docs/docs/cli.mdx create mode 100644 packages/web/src/content/docs/docs/config.mdx create mode 100644 packages/web/src/content/docs/docs/index.mdx create mode 100644 packages/web/src/content/docs/docs/lsp-servers.mdx create mode 100644 packages/web/src/content/docs/docs/mcp-servers.mdx create mode 100644 packages/web/src/content/docs/docs/models.mdx create mode 100644 packages/web/src/content/docs/docs/shortcuts.mdx create mode 100644 packages/web/src/content/docs/docs/themes.mdx create mode 100644 packages/web/src/content/docs/index.mdx create mode 100644 packages/web/src/pages/s/index.astro create mode 100644 packages/web/src/styles/custom.css create mode 100644 packages/web/sst-env.d.ts create mode 100644 packages/web/tsconfig.json delete mode 100644 pkg/client/.gitignore delete mode 100644 pkg/client/client.go delete mode 100644 pkg/client/event.go delete mode 100644 pkg/client/gen/openapi.json delete mode 100644 pkg/client/generated-client.go create mode 100644 sst-env.d.ts create mode 100644 sst.config.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index e51e0598a..32cb0b4dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,46 +1,3 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work - -# IDE specific files -.idea/ -.vscode/ -*.swp -*.swo - -# OS specific files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db -*.log - -# Binary output directory -/bin/ -/dist/ - -# Local environment variables -.env -.env.local - -.opencode/ -# ignore locally built binary -opencode* +node_modules +.opencode +.sst diff --git a/.goreleaser.yml b/.goreleaser.yml deleted file mode 100644 index 1545199d5..000000000 --- a/.goreleaser.yml +++ /dev/null @@ -1,77 +0,0 @@ -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://aur@aur.archlinux.org/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/CONTEXT.md b/CONTEXT.md deleted file mode 100644 index 42c02f15d..000000000 --- a/CONTEXT.md +++ /dev/null @@ -1,24 +0,0 @@ -# OpenCode Development Context - -## Build Commands -- Build: `go build` -- Run: `go run main.go` -- Test: `go test ./...` -- Test single package: `go test ./internal/package/...` -- Test single test: `go test ./internal/package -run TestName` -- Verbose test: `go test -v ./...` -- Coverage: `go test -cover ./...` -- Lint: `go vet ./...` -- Format: `go fmt ./...` -- Build snapshot: `./scripts/snapshot` - -## Code Style -- Use Go 1.24+ features -- Follow standard Go formatting (gofmt) -- Use table-driven tests with t.Parallel() when possible -- Error handling: check errors immediately, return early -- Naming: CamelCase for exported, camelCase for unexported -- Imports: standard library first, then external, then internal -- Use context.Context for cancellation and timeouts -- Prefer interfaces for dependencies to enable testing -- Use testify for assertions in tests \ No newline at end of file diff --git a/LICENSE b/LICENSE index e6208d775..28734bfd5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Kujtim Hoxha +Copyright (c) 2025 OpenCode Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/app/.gitignore b/app/.gitignore deleted file mode 100644 index d8beb623b..000000000 --- a/app/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.env -node_modules -.sst \ No newline at end of file diff --git a/app/bun.lock b/app/bun.lock deleted file mode 100644 index 91b28d227..000000000 --- a/app/bun.lock +++ /dev/null @@ -1,1401 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "opencontrol", - "devDependencies": { - "@tsconfig/node22": "22.0.0", - "@types/node": "^22.13.9", - "prettier": "^3.5.3", - "sst": "3.16.0", - "typescript": "5.8.2", - }, - }, - "packages/function": { - "name": "@opencode/function", - "version": "0.0.1", - "devDependencies": { - "@cloudflare/workers-types": "^4.20250522.0", - }, - }, - "packages/web": { - "name": "web", - "version": "0.0.1", - "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", - }, - }, - }, - "trustedDependencies": [ - "sharp", - "esbuild", - ], - "overrides": { - "zod": "3.24.2", - }, - "packages": { - "@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-canary.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-aN83hjdjDCyhkOdulwMsxmGb91owS+bCSe6FWg1TEwusNM35vv020nY//Gid/0NdIpVkZJGzAajgCWrnno2zzA=="], - - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-canary.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-canary.14", "@standard-schema/spec": "^1.0.0", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-4IJw6/wkWYLYfFYPvCs5go0L/sBRZsIRW1l/R6LniF4WjAH2+R4dMbESgBmzx+Z2+W+W6gFeK8dnQByn7vaA/w=="], - - "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], - - "@astrojs/compiler": ["@astrojs/compiler@2.12.0", "", {}, "sha512-7bCjW6tVDpUurQLeKBUN9tZ5kSv5qYrGmcn0sG0IwacL7isR2ZbyyA3AdZ4uxsuUFOS2SlgReTH7wkxO6zpqWA=="], - - "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], - - "@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="], - - "@astrojs/mdx": ["@astrojs/mdx@4.3.0", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.2", "@mdx-js/mdx": "^3.1.0", "acorn": "^8.14.1", "es-module-lexer": "^1.6.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "kleur": "^4.1.5", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.4", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-OGX2KvPeBzjSSKhkCqrUoDMyzFcjKt5nTE5SFw3RdoLf0nrhyCXBQcCyclzWy1+P+XpOamn+p+hm1EhpCRyPxw=="], - - "@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], - - "@astrojs/sitemap": ["@astrojs/sitemap@3.4.0", "", { "dependencies": { "sitemap": "^8.0.0", "stream-replace-string": "^2.0.0", "zod": "^3.24.2" } }, "sha512-C5m/xsKvRSILKM3hy47n5wKtTQtJXn8epoYuUmCCstaE9XBt20yInym3Bz2uNbEiNfv11bokoW0MqeXPIvjFIQ=="], - - "@astrojs/solid-js": ["@astrojs/solid-js@5.1.0", "", { "dependencies": { "vite": "^6.3.5", "vite-plugin-solid": "^2.11.6" }, "peerDependencies": { "solid-devtools": "^0.30.1", "solid-js": "^1.8.5" }, "optionalPeers": ["solid-devtools"] }, "sha512-VmPHOU9k7m6HHCT2Y1mNzifilUnttlowBM36frGcfj5wERJE9Ci0QtWJbzdf6AlcoIirb7xVw+ByupU011Di9w=="], - - "@astrojs/starlight": ["@astrojs/starlight@0.34.3", "", { "dependencies": { "@astrojs/markdown-remark": "^6.3.1", "@astrojs/mdx": "^4.2.3", "@astrojs/sitemap": "^3.3.0", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", "astro-expressive-code": "^0.41.1", "bcp-47": "^2.1.0", "hast-util-from-html": "^2.0.1", "hast-util-select": "^6.0.2", "hast-util-to-string": "^3.0.0", "hastscript": "^9.0.0", "i18next": "^23.11.5", "js-yaml": "^4.1.0", "klona": "^2.0.6", "mdast-util-directive": "^3.0.0", "mdast-util-to-markdown": "^2.1.0", "mdast-util-to-string": "^4.0.0", "pagefind": "^1.3.0", "rehype": "^13.0.1", "rehype-format": "^5.0.0", "remark-directive": "^3.0.0", "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "vfile": "^6.0.2" }, "peerDependencies": { "astro": "^5.5.0" } }, "sha512-MAuD3NF+E+QXJJuVKofoR6xcPTP4BJmYWeOBd03udVdubNGVnPnSWVZAi+ZtnTofES4+mJdp8BNGf+ubUxkiiA=="], - - "@astrojs/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="], - - "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - - "@babel/compat-data": ["@babel/compat-data@7.27.3", "", {}, "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw=="], - - "@babel/core": ["@babel/core@7.27.3", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.3", "@babel/parser": "^7.27.3", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.3", "@babel/types": "^7.27.3", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA=="], - - "@babel/generator": ["@babel/generator@7.27.3", "", { "dependencies": { "@babel/parser": "^7.27.3", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], - - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - - "@babel/helpers": ["@babel/helpers@7.27.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.3" } }, "sha512-h/eKy9agOya1IGuLaZ9tEUgz+uIRXcbtOhRtUyyMf8JFmn1iT13vnl/IGVWSkdOCG/pC57U4S1jnAabAavTMwg=="], - - "@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="], - - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="], - - "@babel/runtime": ["@babel/runtime@7.27.3", "", {}, "sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw=="], - - "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], - - "@babel/traverse": ["@babel/traverse@7.27.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.3", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ=="], - - "@babel/types": ["@babel/types@7.27.3", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw=="], - - "@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="], - - "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250529.0", "", {}, "sha512-l6tVFpI6MUChMD0wK+Jhikb+aCbrmIR58CVpV/BhRT4THjl+nFhTT5N5ZqX42FDXdE3hCPLjueBMpPRhPUOB2A=="], - - "@ctrl/tinycolor": ["@ctrl/tinycolor@4.1.0", "", {}, "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], - - "@expressive-code/core": ["@expressive-code/core@0.41.2", "", { "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-AJW5Tp9czbLqKMzwudL9Rv4js9afXBxkSGLmCNPq1iRgAYcx9NkTPJiSNCesjKRWoVC328AdSu6fqrD22zDgDg=="], - - "@expressive-code/plugin-frames": ["@expressive-code/plugin-frames@0.41.2", "", { "dependencies": { "@expressive-code/core": "^0.41.2" } }, "sha512-pfy0hkJI4nbaONjmksFDcuHmIuyPTFmi1JpABe4q2ajskiJtfBf+WDAL2pg595R9JNoPrrH5+aT9lbkx2noicw=="], - - "@expressive-code/plugin-shiki": ["@expressive-code/plugin-shiki@0.41.2", "", { "dependencies": { "@expressive-code/core": "^0.41.2", "shiki": "^3.2.2" } }, "sha512-xD4zwqAkDccXqye+235BH5bN038jYiSMLfUrCOmMlzxPDGWdxJDk5z4uUB/aLfivEF2tXyO2zyaarL3Oqht0fQ=="], - - "@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.2", "", { "dependencies": { "@expressive-code/core": "^0.41.2" } }, "sha512-JFWBz2qYxxJOJkkWf96LpeolbnOqJY95TvwYc0hXIHf9oSWV0h0SY268w/5N3EtQaD9KktzDE+VIVwb9jdb3nw=="], - - "@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.2.5", "", {}, "sha512-G09N3GfuT9qj3Ax2FDZvKqZttzM3v+cco2l8uXamhKyXLdmlaUDH5o88/C3vtTHj2oT7yRKsvxz9F+BXbWKMYA=="], - - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], - - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], - - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], - - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], - - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], - - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], - - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], - - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], - - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], - - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], - - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], - - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], - - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], - - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], - - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], - - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], - - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], - - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], - - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], - - "@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="], - - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], - - "@opencode/function": ["@opencode/function@workspace:packages/function"], - - "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - - "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], - - "@pagefind/darwin-arm64": ["@pagefind/darwin-arm64@1.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-365BEGl6ChOsauRjyVpBjXybflXAOvoMROw3TucAROHIcdBvXk9/2AmEvGFU0r75+vdQI4LJdJdpH4Y6Yqaj4A=="], - - "@pagefind/darwin-x64": ["@pagefind/darwin-x64@1.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-zlGHA23uuXmS8z3XxEGmbHpWDxXfPZ47QS06tGUq0HDcZjXjXHeLG+cboOy828QIV5FXsm9MjfkP5e4ZNbOkow=="], - - "@pagefind/default-ui": ["@pagefind/default-ui@1.3.0", "", {}, "sha512-CGKT9ccd3+oRK6STXGgfH+m0DbOKayX6QGlq38TfE1ZfUcPc5+ulTuzDbZUnMo+bubsEOIypm4Pl2iEyzZ1cNg=="], - - "@pagefind/linux-arm64": ["@pagefind/linux-arm64@1.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-8lsxNAiBRUk72JvetSBXs4WRpYrQrVJXjlRRnOL6UCdBN9Nlsz0t7hWstRk36+JqHpGWOKYiuHLzGYqYAqoOnQ=="], - - "@pagefind/linux-x64": ["@pagefind/linux-x64@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-hAvqdPJv7A20Ucb6FQGE6jhjqy+vZ6pf+s2tFMNtMBG+fzcdc91uTw7aP/1Vo5plD0dAOHwdxfkyw0ugal4kcQ=="], - - "@pagefind/windows-x64": ["@pagefind/windows-x64@1.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-BR1bIRWOMqkf8IoU576YDhij1Wd/Zf2kX/kCI0b2qzCKC8wcc2GQJaaRMCpzvCCrmliO4vtJ6RITp/AnoYUUmQ=="], - - "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.41.1", "", { "os": "android", "cpu": "arm" }, "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.41.1", "", { "os": "android", "cpu": "arm64" }, "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.41.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.41.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.41.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.41.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.41.1", "", { "os": "linux", "cpu": "arm" }, "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.41.1", "", { "os": "linux", "cpu": "arm" }, "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.41.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.41.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg=="], - - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.41.1", "", { "os": "linux", "cpu": "none" }, "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw=="], - - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.41.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.41.1", "", { "os": "linux", "cpu": "none" }, "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.41.1", "", { "os": "linux", "cpu": "none" }, "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.41.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.41.1", "", { "os": "linux", "cpu": "x64" }, "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.41.1", "", { "os": "linux", "cpu": "x64" }, "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.41.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.41.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.41.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw=="], - - "@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="], - - "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-1/adJbSMBOkpScCE/SB6XkjJU17ANln3Wky7lOmrnpl+zBdQ1qXUJg2GXTYVHRq+2j3hd1DesmElTXYDgtfSOQ=="], - - "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-zcZKMnNndgRa3ORja6Iemsr3DrLtkX3cAF7lTJkdMB6v9alhlBsX9uNiCpqofNrXOvpA3h6lHcLJxgCIhVOU5Q=="], - - "@shikijs/langs": ["@shikijs/langs@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2" } }, "sha512-H6azIAM+OXD98yztIfs/KH5H4PU39t+SREhmM8LaNXyUrqj2mx+zVkr8MWYqjceSjDw9I1jawm1WdFqU806rMA=="], - - "@shikijs/themes": ["@shikijs/themes@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2" } }, "sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg=="], - - "@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="], - - "@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="], - - "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], - - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], - - "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], - - "@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="], - - "@tsconfig/node22": ["@tsconfig/node22@22.0.0", "", {}, "sha512-twLQ77zevtxobBOD4ToAtVmuYrpeYUh3qh+TEp+08IWhpsrIflVHqQ1F1CiPxQGL7doCdBIOOCF+1Tm833faNg=="], - - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], - - "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], - - "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], - - "@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="], - - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], - - "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], - - "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], - - "@types/fontkit": ["@types/fontkit@2.0.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew=="], - - "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], - - "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], - - "@types/luxon": ["@types/luxon@3.6.2", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="], - - "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], - - "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], - - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - - "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], - - "@types/node": ["@types/node@22.15.24", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng=="], - - "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], - - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "ai": ["ai@5.0.0-canary.24", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-canary.14", "@ai-sdk/provider-utils": "3.0.0-canary.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-vqaMmM6XFwjz9mNjox9ehjkWFwXbSchhor5MiqgKZ1qRyoTvoYzAt6oCZwg5kN5jXNQ3rZVuyE8N3BbPbwma2Q=="], - - "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], - - "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - - "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - - "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - - "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - - "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], - - "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], - - "astro": ["astro@5.8.1", "", { "dependencies": { "@astrojs/compiler": "^2.11.0", "@astrojs/internal-helpers": "0.6.1", "@astrojs/markdown-remark": "6.3.2", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^2.4.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.4", "acorn": "^8.14.1", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.2.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.0", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.6.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.1.1", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.17", "magicast": "^0.3.5", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", "package-manager-detector": "^1.1.0", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.1", "shiki": "^3.2.1", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.12", "tsconfck": "^3.1.5", "ultrahtml": "^1.6.0", "unifont": "~0.5.0", "unist-util-visit": "^5.0.0", "unstorage": "^1.15.0", "vfile": "^6.0.3", "vite": "^6.3.4", "vitefu": "^1.0.6", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.1", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.5", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "bin": { "astro": "astro.js" } }, "sha512-lkBg1smMRFW+FQ6i92SgEN53o4+ItRjlRt6Ck+rEjmTcb57Bid7faTNKUQNYuNnxiesTWw3NJDyVPQPbfKDyfw=="], - - "astro-expressive-code": ["astro-expressive-code@0.41.2", "", { "dependencies": { "rehype-expressive-code": "^0.41.2" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0" } }, "sha512-HN0jWTnhr7mIV/2e6uu4PPRNNo/k4UEgTLZqbp3MrHU+caCARveG2yZxaZVBmxyiVdYqW5Pd3u3n2zjnshixbw=="], - - "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - - "aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="], - - "aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="], - - "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - - "b4a": ["b4a@1.6.7", "", {}, "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg=="], - - "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.39.8", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2", "validate-html-nesting": "^1.2.1" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/MVOIIjonylDXnrWmG23ZX82m9mtKATsVHB7zYlPfDR9Vdd/NBE48if+wv27bSkBtyO7EPMUlcUc4J63QwuACQ=="], - - "babel-preset-solid": ["babel-preset-solid@1.9.6", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.39.8" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-HXTK9f93QxoH8dYn1M2mJdOlWgMsR88Lg/ul6QCZGkNTktjTE5HAf93YxQumHoCudLEtZrU1cFCMFOVho6GqFg=="], - - "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - - "bare-events": ["bare-events@2.5.4", "", {}, "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA=="], - - "bare-fs": ["bare-fs@4.1.5", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA=="], - - "bare-os": ["bare-os@3.6.1", "", {}, "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g=="], - - "bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="], - - "bare-stream": ["bare-stream@2.6.5", "", { "dependencies": { "streamx": "^2.21.0" }, "peerDependencies": { "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-buffer", "bare-events"] }, "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA=="], - - "base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="], - - "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], - - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - - "blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="], - - "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], - - "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - - "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], - - "brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="], - - "browserslist": ["browserslist@4.25.0", "", { "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA=="], - - "buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], - - "caniuse-lite": ["caniuse-lite@1.0.30001720", "", {}, "sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g=="], - - "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - - "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - - "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], - - "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], - - "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], - - "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], - - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - - "ci-info": ["ci-info@4.2.0", "", {}, "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg=="], - - "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], - - "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], - - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - - "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], - - "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - - "common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], - - "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - - "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], - - "cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], - - "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], - - "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], - - "css-selector-parser": ["css-selector-parser@3.1.2", "", {}, "sha512-WfUcL99xWDs7b3eZPoRszWVfbNo8ErCF15PTvVROjkShGlAfjIkG6hlfj/sl6/rfo5Q9x9ryJ3VqVnAZDA+gcw=="], - - "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="], - - "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "decode-named-character-reference": ["decode-named-character-reference@1.1.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w=="], - - "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], - - "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], - - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], - - "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - - "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], - - "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], - - "deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="], - - "devalue": ["devalue@5.1.1", "", {}, "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="], - - "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - - "dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="], - - "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], - - "direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="], - - "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], - - "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "electron-to-chromium": ["electron-to-chromium@1.5.161", "", {}, "sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA=="], - - "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], - - "entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], - - "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], - - "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - - "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], - - "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], - - "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], - - "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], - - "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], - - "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - - "events": ["events@1.1.1", "", {}, "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw=="], - - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - - "eventsource-parser": ["eventsource-parser@3.0.2", "", {}, "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA=="], - - "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], - - "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], - - "express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="], - - "expressive-code": ["expressive-code@0.41.2", "", { "dependencies": { "@expressive-code/core": "^0.41.2", "@expressive-code/plugin-frames": "^0.41.2", "@expressive-code/plugin-shiki": "^0.41.2", "@expressive-code/plugin-text-markers": "^0.41.2" } }, "sha512-aLZiZaqorRtNExtGpUjK9zFH9aTpWeoTXMyLo4b4IcuXfPqtLPPxhRm/QlPb8QqIcMMXnSiGRHSFpQfX0m7HJw=="], - - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], - - "fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="], - - "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], - - "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], - - "fontace": ["fontace@0.3.0", "", { "dependencies": { "@types/fontkit": "^2.0.8", "fontkit": "^2.0.4" } }, "sha512-czoqATrcnxgWb/nAkfyIrRp6Q8biYj7nGnL6zfhTcX+JKKpWHFBnb8uNMw/kZr7u++3Y3wYSYoZgHkCcsuBpBg=="], - - "fontkit": ["fontkit@2.0.4", "", { "dependencies": { "@swc/helpers": "^0.5.12", "brotli": "^1.3.2", "clone": "^2.1.2", "dfa": "^1.2.0", "fast-deep-equal": "^3.1.3", "restructure": "^3.0.0", "tiny-inflate": "^1.0.3", "unicode-properties": "^1.4.0", "unicode-trie": "^2.0.0" } }, "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g=="], - - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - - "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], - - "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - - "globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "h3": ["h3@1.15.3", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.4", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.0", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ=="], - - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "hast-util-embedded": ["hast-util-embedded@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA=="], - - "hast-util-format": ["hast-util-format@1.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-whitespace": "^3.0.0", "html-whitespace-sensitive-tag-names": "^3.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA=="], - - "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], - - "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], - - "hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="], - - "hast-util-heading-rank": ["hast-util-heading-rank@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA=="], - - "hast-util-is-body-ok-link": ["hast-util-is-body-ok-link@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ=="], - - "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], - - "hast-util-minify-whitespace": ["hast-util-minify-whitespace@1.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-is-element": "^3.0.0", "hast-util-whitespace": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw=="], - - "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], - - "hast-util-phrasing": ["hast-util-phrasing@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-has-property": "^3.0.0", "hast-util-is-body-ok-link": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ=="], - - "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], - - "hast-util-select": ["hast-util-select@6.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "bcp-47-match": "^2.0.0", "comma-separated-tokens": "^2.0.0", "css-selector-parser": "^3.0.0", "devlop": "^1.0.0", "direction": "^2.0.0", "hast-util-has-property": "^3.0.0", "hast-util-to-string": "^3.0.0", "hast-util-whitespace": "^3.0.0", "nth-check": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw=="], - - "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], - - "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], - - "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], - - "hast-util-to-parse5": ["hast-util-to-parse5@8.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw=="], - - "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], - - "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], - - "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], - - "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], - - "hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="], - - "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], - - "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], - - "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], - - "html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="], - - "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], - - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], - - "i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="], - - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "ieee754": ["ieee754@1.1.13", "", {}, "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="], - - "import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - - "inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], - - "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], - - "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], - - "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="], - - "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], - - "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - - "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], - - "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="], - - "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], - - "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], - - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], - - "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], - - "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], - - "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], - - "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - - "jmespath": ["jmespath@0.16.0", "", {}, "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw=="], - - "jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - - "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], - - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], - - "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], - - "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - - "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - - "luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="], - - "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], - - "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], - - "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], - - "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="], - - "mdast-util-directive": ["mdast-util-directive@3.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q=="], - - "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], - - "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], - - "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], - - "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], - - "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], - - "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], - - "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], - - "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], - - "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], - - "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], - - "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], - - "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], - - "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], - - "mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="], - - "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], - - "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], - - "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], - - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], - - "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], - - "micromark-extension-directive": ["micromark-extension-directive@3.0.2", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA=="], - - "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], - - "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], - - "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], - - "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], - - "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], - - "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], - - "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], - - "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], - - "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], - - "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], - - "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], - - "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], - - "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], - - "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], - - "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], - - "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], - - "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], - - "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], - - "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], - - "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], - - "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], - - "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], - - "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], - - "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], - - "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], - - "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], - - "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], - - "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], - - "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], - - "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], - - "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], - - "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], - - "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], - - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], - - "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - - "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], - - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], - - "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], - - "node-abi": ["node-abi@3.75.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg=="], - - "node-addon-api": ["node-addon-api@6.1.0", "", {}, "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="], - - "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - - "node-fetch-native": ["node-fetch-native@1.6.6", "", {}, "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ=="], - - "node-mock-http": ["node-mock-http@1.0.0", "", {}, "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ=="], - - "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], - - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - - "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "ofetch": ["ofetch@1.4.1", "", { "dependencies": { "destr": "^2.0.3", "node-fetch-native": "^1.6.4", "ufo": "^1.5.4" } }, "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw=="], - - "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], - - "oidc-token-hash": ["oidc-token-hash@5.1.0", "", {}, "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], - - "oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="], - - "opencontrol": ["opencontrol@0.0.6", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.6.1", "@tsconfig/bun": "1.0.7", "hono": "4.7.4", "zod": "3.24.2", "zod-to-json-schema": "3.24.3" }, "bin": { "opencontrol": "bin/index.mjs" } }, "sha512-QeCrpOK5D15QV8kjnGVeD/BHFLwcVr+sn4T6KKmP0WAMs2pww56e4h+eOGHb5iPOufUQXbdbBKi6WV2kk7tefQ=="], - - "openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="], - - "p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="], - - "p-queue": ["p-queue@8.1.0", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw=="], - - "p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], - - "package-manager-detector": ["package-manager-detector@1.3.0", "", {}, "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="], - - "pagefind": ["pagefind@1.3.0", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.3.0", "@pagefind/darwin-x64": "1.3.0", "@pagefind/linux-arm64": "1.3.0", "@pagefind/linux-x64": "1.3.0", "@pagefind/windows-x64": "1.3.0" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-8KPLGT5g9s+olKMRTU9LFekLizkVIu9tes90O1/aigJ0T5LmyPqTzGJrETnSw3meSYg58YH7JTzhTTW/3z6VAw=="], - - "pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], - - "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], - - "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], - - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], - - "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - - "postcss": ["postcss@8.5.4", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w=="], - - "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], - - "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], - - "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], - - "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], - - "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], - - "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], - - "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], - - "punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="], - - "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], - - "querystring": ["querystring@0.2.0", "", {}, "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g=="], - - "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], - - "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - - "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], - - "recma-jsx": ["recma-jsx@1.0.0", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" } }, "sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q=="], - - "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], - - "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], - - "regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="], - - "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], - - "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], - - "rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="], - - "rehype-autolink-headings": ["rehype-autolink-headings@7.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-is-element": "^3.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw=="], - - "rehype-expressive-code": ["rehype-expressive-code@0.41.2", "", { "dependencies": { "expressive-code": "^0.41.2" } }, "sha512-vHYfWO9WxAw6kHHctddOt+P4266BtyT1mrOIuxJD+1ELuvuJAa5uBIhYt0OVMyOhlvf57hzWOXJkHnMhpaHyxw=="], - - "rehype-format": ["rehype-format@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-format": "^1.0.0" } }, "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ=="], - - "rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="], - - "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], - - "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], - - "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="], - - "remark-directive": ["remark-directive@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^3.0.0", "unified": "^11.0.0" } }, "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A=="], - - "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], - - "remark-mdx": ["remark-mdx@3.1.0", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA=="], - - "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], - - "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], - - "remark-smartypants": ["remark-smartypants@3.0.2", "", { "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA=="], - - "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], - - "restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="], - - "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], - - "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="], - - "retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="], - - "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="], - - "rollup": ["rollup@4.41.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="], - - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="], - - "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], - - "seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], - - "seroval-plugins": ["seroval-plugins@1.3.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ=="], - - "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], - - "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "sharp": ["sharp@0.32.6", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.2", "node-addon-api": "^6.1.0", "prebuild-install": "^7.1.1", "semver": "^7.5.4", "simple-get": "^4.0.1", "tar-fs": "^3.0.4", "tunnel-agent": "^0.6.0" } }, "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w=="], - - "shiki": ["shiki@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/engine-javascript": "3.4.2", "@shikijs/engine-oniguruma": "3.4.2", "@shikijs/langs": "3.4.2", "@shikijs/themes": "3.4.2", "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-wuxzZzQG8kvZndD7nustrNFIKYJ1jJoWIPaBpVe2+KHSvtzMi4SBjOxrigs8qeqce/l3U0cwiC+VAkLKSunHQQ=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], - - "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], - - "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], - - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - - "sitemap": ["sitemap@8.0.0", "", { "dependencies": { "@types/node": "^17.0.5", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.2.4" }, "bin": { "sitemap": "dist/cli.js" } }, "sha512-+AbdxhM9kJsHtruUF39bwS/B0Fytw6Fr1o4ZAIAEqA6cke2xcoO2GleBw9Zw7nRzILVEgz7zBM5GiTJjie1G9A=="], - - "smol-toml": ["smol-toml@1.3.4", "", {}, "sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA=="], - - "solid-js": ["solid-js@1.9.7", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw=="], - - "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], - - "source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - - "sst": ["sst@3.16.0", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.16.0", "sst-darwin-x64": "3.16.0", "sst-linux-arm64": "3.16.0", "sst-linux-x64": "3.16.0", "sst-linux-x86": "3.16.0", "sst-win32-arm64": "3.16.0", "sst-win32-x64": "3.16.0", "sst-win32-x86": "3.16.0" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-XX4ktkpyvB2lQCG8/jgH/RWAtdNahE/MjlK6OctM4Nagk1rOrMxxzARit2ZFCisX+ZBvcN0ymgJimnfZqWjM7Q=="], - - "sst-darwin-arm64": ["sst-darwin-arm64@3.16.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NJDGjZ0bl+hWJoT1P2KLyA65OXHkItVg3BNkRsas6qaa+c1U6GU8wLFnfdqkDRnJIYhOIsnZtIquNp7lMIr3ww=="], - - "sst-darwin-x64": ["sst-darwin-x64@3.16.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-IZiIUZgTGKjW+aR6/qgqNMzdFS2789PBQjsMquXaTZc7ebRutbCozQ4CC78SuwPXDUZ0/Bz+hyGeXHNOB52lVQ=="], - - "sst-linux-arm64": ["sst-linux-arm64@3.16.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-pvbxU+hrbvtIt347ilbXzVQEl2kXXBIYArLMzblPNg4KblCRvJvH595P86hrr8ZojlOkEb1rC8NSAwiZyBad4g=="], - - "sst-linux-x64": ["sst-linux-x64@3.16.0", "", { "os": "linux", "cpu": "x64" }, "sha512-p7qtmdiG1imWSZ7gUbVecee6TIuILTvIe6dgpvkgznYvuTFiMCZaY2SNMFuNRiBoN5zZI0y5osDDXY2uzzo7rA=="], - - "sst-linux-x86": ["sst-linux-x86@3.16.0", "", { "os": "linux", "cpu": "none" }, "sha512-bw+enzOnZ+MjGd7/h4swGfHpIoyLbIdguelGV7vZLIeuV4t4rZ/hAqfR25GEMjjonVv3Py5hsasMzR2s9GsBLA=="], - - "sst-win32-arm64": ["sst-win32-arm64@3.16.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-bjjYn1gq1p1tBFKvuB3K5ZMuofK/WWmrtKpD+OJOf8PE5fxzRJ8yvOFVIm1QhD4hxDQAPdNonE/P/cXDtKXQig=="], - - "sst-win32-x64": ["sst-win32-x64@3.16.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WFHaUyaLHepKPOMFr7Kf8iECIg1prn3L7c5r+IKgIiv+KznOeslwG6f9mYwxcOYZrZNiXVeDJmDp9xZtB5auSQ=="], - - "sst-win32-x86": ["sst-win32-x86@3.16.0", "", { "os": "win32", "cpu": "none" }, "sha512-Twbdy9sS6zSKsdwpWVEYpFEjVcRAn0Ufdbz+pVdMK3JXniSB7MLC6/KQuxUwvUk2a88+jnPuPVXAYC51SgzDyg=="], - - "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], - - "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], - - "streamx": ["streamx@2.22.0", "", { "dependencies": { "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw=="], - - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - - "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - - "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - - "style-to-js": ["style-to-js@1.1.16", "", { "dependencies": { "style-to-object": "1.0.8" } }, "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw=="], - - "style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="], - - "tar-fs": ["tar-fs@3.0.9", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA=="], - - "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], - - "text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="], - - "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], - - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "toolbeam-docs-theme": ["toolbeam-docs-theme@0.2.4", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-W5mdbcgRpTBDFyEdcU81USs3MFZoXMInpSznc/AFZCwqz8atk4iBNDIlhvihpGHY54Nf5crKmZwJjxVojkHFvA=="], - - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - - "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], - - "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - - "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], - - "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], - - "ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="], - - "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "unicode-properties": ["unicode-properties@1.4.1", "", { "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" } }, "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg=="], - - "unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="], - - "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], - - "unifont": ["unifont@0.5.0", "", { "dependencies": { "css-tree": "^3.0.0", "ohash": "^2.0.0" } }, "sha512-4DueXMP5Hy4n607sh+vJ+rajoLu778aU3GzqeTCqsD/EaUcvqZT9wPC8kgK6Vjh22ZskrxyRCR71FwNOaYn6jA=="], - - "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], - - "unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="], - - "unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="], - - "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], - - "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], - - "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], - - "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], - - "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], - - "unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="], - - "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "unstorage": ["unstorage@1.16.0", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.2", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.6", "ofetch": "^1.4.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-WQ37/H5A7LcRPWfYOrDa1Ys02xAbpPJq6q5GkO88FBXVSQzHd7+BjEwfRqyaSWCv9MbsJy058GWjjPjcJ16GGA=="], - - "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], - - "url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="], - - "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "uuid": ["uuid@8.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw=="], - - "validate-html-nesting": ["validate-html-nesting@1.2.2", "", {}, "sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], - - "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], - - "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], - - "vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], - - "vite-plugin-solid": ["vite-plugin-solid@2.11.6", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-Sl5CTqJTGyEeOsmdH6BOgalIZlwH3t4/y0RQuFLMGnvWMBvxb4+lq7x3BSiAw6etf0QexfNJW7HSOO/Qf7pigg=="], - - "vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="], - - "web": ["web@workspace:packages/web"], - - "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], - - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - - "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], - - "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], - - "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], - - "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], - - "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], - - "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], - - "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="], - - "yocto-spinner": ["yocto-spinner@0.2.3", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ=="], - - "yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="], - - "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], - - "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], - - "zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="], - - "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - - "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - - "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "astro/diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="], - - "astro/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], - - "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], - - "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - - "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], - - "opencontrol/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], - - "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], - - "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - - "prebuild-install/tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="], - - "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - - "sitemap/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="], - - "sitemap/sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="], - - "unstorage/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - - "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - } -} diff --git a/app/infra/app.ts b/app/infra/app.ts deleted file mode 100644 index 73b80994d..000000000 --- a/app/infra/app.ts +++ /dev/null @@ -1,44 +0,0 @@ -export const domain = (() => { - if ($app.stage === "production") return "opencode.ai" - if ($app.stage === "dev") return "dev.opencode.ai" - return `${$app.stage}.dev.opencode.ai` -})() - -const bucket = new sst.cloudflare.Bucket("Bucket") - -export const api = new sst.cloudflare.Worker("Api", { - domain: `api.${domain}`, - handler: "packages/function/src/api.ts", - url: true, - link: [bucket], - transform: { - worker: (args) => { - args.logpush = true - args.bindings = $resolve(args.bindings).apply((bindings) => [ - ...bindings, - { - name: "SYNC_SERVER", - type: "durable_object_namespace", - className: "SyncServer", - }, - ]) - args.migrations = { - oldTag: "v1", - newTag: "v1", - //newSqliteClasses: ["SyncServer"], - } - }, - }, -}) - -new sst.cloudflare.StaticSite("Web", { - path: "packages/web", - domain, - environment: { - VITE_API_URL: api.url, - }, - build: { - command: "bun run build", - output: "dist", - }, -}) diff --git a/app/package.json b/app/package.json deleted file mode 100644 index 1e4954367..000000000 --- a/app/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/package.json", - "name": "opencontrol", - "private": true, - "type": "module", - "packageManager": "bun@1.2.14", - "scripts": { - "dev": "sst dev" - }, - "workspaces": [ - "packages/*" - ], - "devDependencies": { - "@tsconfig/node22": "22.0.0", - "@types/node": "^22.13.9", - "prettier": "^3.5.3", - "sst": "3.16.0", - "typescript": "5.8.2" - }, - "engines": { - "bun": ">=1.0.0", - "node": ">=18.0.0" - }, - "repository": { - "type": "git", - "url": "https://github.com/sst/opencode" - }, - "license": "MIT", - "prettier": { - "semi": false - }, - "overrides": { - "zod": "3.24.2" - }, - "trustedDependencies": [ - "esbuild", - "protobufjs", - "sharp" - ] -} diff --git a/app/packages/function/package.json b/app/packages/function/package.json deleted file mode 100644 index 46c83e840..000000000 --- a/app/packages/function/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "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/app/packages/function/src/api.ts b/app/packages/function/src/api.ts deleted file mode 100644 index 8b6c52dd8..000000000 --- a/app/packages/function/src/api.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { DurableObject } from "cloudflare:workers" -import { randomUUID } from "node:crypto" -import { Resource } from "sst" - -type Env = { - SYNC_SERVER: DurableObjectNamespace - Bucket: R2Bucket -} - -export class SyncServer extends DurableObject { - 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("secret") - } - - private async getSessionID() { - return this.ctx.storage.get("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() - 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() - 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/app/packages/function/sst-env.d.ts b/app/packages/function/sst-env.d.ts deleted file mode 100644 index 41727ee9d..000000000 --- a/app/packages/function/sst-env.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* 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/app/packages/function/tsconfig.json b/app/packages/function/tsconfig.json deleted file mode 100644 index 0faf16aab..000000000 --- a/app/packages/function/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "@tsconfig/node22/tsconfig.json", - "compilerOptions": { - "module": "ESNext", - "moduleResolution": "bundler", - "types": ["@cloudflare/workers-types", "node"] - } -} diff --git a/app/packages/web/.gitignore b/app/packages/web/.gitignore deleted file mode 100644 index 6240da8b1..000000000 --- a/app/packages/web/.gitignore +++ /dev/null @@ -1,21 +0,0 @@ -# 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/app/packages/web/README.md b/app/packages/web/README.md deleted file mode 100644 index f9f6d31c6..000000000 --- a/app/packages/web/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# 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/app/packages/web/astro.config.mjs b/app/packages/web/astro.config.mjs deleted file mode 100644 index bfe18acd8..000000000 --- a/app/packages/web/astro.config.mjs +++ /dev/null @@ -1,69 +0,0 @@ -// @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/app/packages/web/package.json b/app/packages/web/package.json deleted file mode 100644 index aa0b1dc01..000000000 --- a/app/packages/web/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "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/app/packages/web/public/favicon.svg b/app/packages/web/public/favicon.svg deleted file mode 100644 index 8b011db3f..000000000 --- a/app/packages/web/public/favicon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/packages/web/public/social-share.png b/app/packages/web/public/social-share.png deleted file mode 100644 index 23905086a..000000000 Binary files a/app/packages/web/public/social-share.png and /dev/null differ diff --git a/app/packages/web/src/assets/lander/check.svg b/app/packages/web/src/assets/lander/check.svg deleted file mode 100644 index 22de6f2a8..000000000 --- a/app/packages/web/src/assets/lander/check.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/packages/web/src/assets/lander/copy.svg b/app/packages/web/src/assets/lander/copy.svg deleted file mode 100644 index f1baac30a..000000000 --- a/app/packages/web/src/assets/lander/copy.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/packages/web/src/assets/logo-dark.svg b/app/packages/web/src/assets/logo-dark.svg deleted file mode 100644 index 8fd212081..000000000 --- a/app/packages/web/src/assets/logo-dark.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/app/packages/web/src/assets/logo-light.svg b/app/packages/web/src/assets/logo-light.svg deleted file mode 100644 index 0a9007e1a..000000000 --- a/app/packages/web/src/assets/logo-light.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/app/packages/web/src/components/CodeBlock.tsx b/app/packages/web/src/components/CodeBlock.tsx deleted file mode 100644 index 17559ece1..000000000 --- a/app/packages/web/src/components/CodeBlock.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { - type JSX, - onCleanup, - splitProps, - createEffect, - createResource, -} from "solid-js" -import { codeToHtml } from "shiki" -import { transformerNotationDiff } from '@shikijs/transformers' - -interface CodeBlockProps extends JSX.HTMLAttributes { - 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 ( -
- ) -} - -export default CodeBlock diff --git a/app/packages/web/src/components/DiffView.tsx b/app/packages/web/src/components/DiffView.tsx deleted file mode 100644 index 44feef140..000000000 --- a/app/packages/web/src/components/DiffView.tsx +++ /dev/null @@ -1,73 +0,0 @@ -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 = (props) => { - const [rows, setRows] = createSignal([]) - - 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 ( -
-
- {rows().map((r) => ( - - ))} -
- -
- {rows().map((r) => ( - - ))} -
-
- ) -} - -export default DiffView diff --git a/app/packages/web/src/components/Header.astro b/app/packages/web/src/components/Header.astro deleted file mode 100644 index a45899ff8..000000000 --- a/app/packages/web/src/components/Header.astro +++ /dev/null @@ -1,62 +0,0 @@ ---- -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") - ?
-
- -
-
- -
-
- : -} - - - - diff --git a/app/packages/web/src/components/Hero.astro b/app/packages/web/src/components/Hero.astro deleted file mode 100644 index f80f85266..000000000 --- a/app/packages/web/src/components/Hero.astro +++ /dev/null @@ -1,11 +0,0 @@ ---- -import Default from '@astrojs/starlight/components/Hero.astro'; -import Lander from './Lander.astro'; - -const { slug } = Astro.locals.starlightRoute.entry; ---- - -{ slug === "" - ? - : -} diff --git a/app/packages/web/src/components/Lander.astro b/app/packages/web/src/components/Lander.astro deleted file mode 100644 index d27358f8f..000000000 --- a/app/packages/web/src/components/Lander.astro +++ /dev/null @@ -1,269 +0,0 @@ ---- -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; - } -} ---- -
-
- -

The AI coding agent built for the terminal.

-
- -
- -
- -
- -
- -
-
    -
  • Native TUI: A native terminal UI for a smoother, snappier experience.
  • -
  • LSP enabled: Loads the right LSPs for your codebase. Helps the LLM make fewer mistakes.
  • -
  • Multi-session: Start multiple conversations in a project to have agents working in parallel.
  • -
  • Use any model: Supports all the models from OpenAI, Anthropic, Google, OpenRouter, and more.
  • -
  • Change tracking: View the file changes from the current conversation in the sidebar.
  • -
  • Edit with Vim: Use Vim as an external editor to compose longer messages.
  • -
-
- - -
- - - - - - diff --git a/app/packages/web/src/components/Share.tsx b/app/packages/web/src/components/Share.tsx deleted file mode 100644 index ac75a3cf7..000000000 --- a/app/packages/web/src/components/Share.tsx +++ /dev/null @@ -1,772 +0,0 @@ -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 - 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 ( - - }> - - - - - - - - - - - ) -} - -interface ResultsButtonProps extends JSX.HTMLAttributes { - results: boolean -} -function ResultsButton(props: ResultsButtonProps) { - const [local, rest] = splitProps(props, ["results"]) - return ( - - ) -} - -interface TextPartProps extends JSX.HTMLAttributes { - 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 ( -
-
 (preEl = el)}>{local.text}
- {overflowed() && - - } -
- ) -} - -function PartFooter(props: { time: number }) { - return ( - - {DateTime.fromMillis(props.time).toLocaleString(DateTime.TIME_WITH_SECONDS)} - - ) -} - -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 - }>({ - 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 ( -
-
-
-

{store.info?.title}

-

- - {getStatusText(connectionStatus())} -

-
-
-
    -
  • - Cost - {metrics().cost !== undefined ? - ${metrics().cost.toFixed(2)} - : - - } -
  • -
  • - Input Tokens - {metrics().tokens.input ? - {metrics().tokens.input} - : - - } -
  • -
  • - Output Tokens - {metrics().tokens.output ? - {metrics().tokens.output} - : - - } -
  • -
  • - Reasoning Tokens - {metrics().tokens.reasoning ? - {metrics().tokens.reasoning} - : - - } -
  • -
-
    - {models().length > 0 ? - - {([provider, model]) => ( -
  • -
    - -
    - {model} -
  • - )} -
    - : -
  • - Models - -
  • - } -
-
- {messages().length > 0 && messages()[0].metadata?.time.created ? - - {DateTime.fromMillis( - messages()[0].metadata?.time.created || 0 - ).toLocaleString(DateTime.DATE_MED)} - - : - Started at — - } -
-
-
- -
- 0} - fallback={

Waiting for messages...

} - > -
- - {(msg, msgIndex) => ( - - {(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 ( - - { /* User text */} - - {part => -
-
-
- -
-
-
-
- - -
-
- } -
- { /* AI text */} - - {part => -
-
-
-
-
-
- - -
-
- } -
- { /* AI model */} - - {assistant => -
-
-
- -
-
-
-
-
- - {assistant().providerID} - - - {assistant().modelID} - -
-
-
- } -
- { /* System text */} - - {part => -
-
-
- -
-
-
-
-
- - System - - -
- -
-
- } -
- { /* Edit tool */} - - {part => { - const args = part().toolInvocation.args - const filePath = args.filePath - return ( -
-
-
- -
-
-
-
-
- - Edit {filePath} - -
- -
-
- -
-
- ) - }} -
- { /* Tool call */} - - {part => -
-
-
- -
-
-
-
-
- - {part().toolInvocation.toolName} - -
- - {([name, value]) => - <> -
-
{name}
-
{value}
- - } -
-
- - -
- showResults(e => !e)} - /> - - - -
-
- - - -
-
- -
-
- } -
- { /* Fallback */} - -
-
-
- - }> - - - - - - - - - - -
-
-
-
-
- - {part.type} - - -
- -
-
-
-
- ) - }} -
- )} -
-
-
-
- -
-
- 0} - fallback={

Waiting for messages...

} - > -
    - - {(msg) => ( -
  • -
    - Key: {msg.id} -
    -
    {JSON.stringify(msg, null, 2)}
    -
  • - )} -
    -
-
-
-
-
- ) -} diff --git a/app/packages/web/src/components/diffview.module.css b/app/packages/web/src/components/diffview.module.css deleted file mode 100644 index 1a0e6c523..000000000 --- a/app/packages/web/src/components/diffview.module.css +++ /dev/null @@ -1,80 +0,0 @@ -.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/app/packages/web/src/components/icons/custom.tsx b/app/packages/web/src/components/icons/custom.tsx deleted file mode 100644 index f016b83cf..000000000 --- a/app/packages/web/src/components/icons/custom.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { type JSX } from "solid-js" - -// https://icones.js.org/collection/ri?s=openai&icon=ri:openai-fill -export function IconOpenAI(props: JSX.SvgSVGAttributes) { - return ( - - ) -} - -// https://icones.js.org/collection/ri?s=anthropic&icon=ri:anthropic-fill -export function IconAnthropic(props: JSX.SvgSVGAttributes) { - return ( - - ) -} - -// https://icones.js.org/collection/ri?s=gemini&icon=ri:gemini-fill -export function IconGemini(props: JSX.SvgSVGAttributes) { - return ( - - ) -} diff --git a/app/packages/web/src/components/icons/index.tsx b/app/packages/web/src/components/icons/index.tsx deleted file mode 100644 index 9603925d5..000000000 --- a/app/packages/web/src/components/icons/index.tsx +++ /dev/null @@ -1,6101 +0,0 @@ -import { type JSX } from "solid-js" -// heroicons - -export function IconAcademicCap(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconAdjustmentsHorizontal( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconAdjustmentsVertical( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArchiveBoxArrowDown( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArchiveBoxXMark( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArchiveBox(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowDownCircle( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowDownLeft(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowDownOnSquareStack( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowDownOnSquare( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowDownRight(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowDownTray(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowDown(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowLeftCircle( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowLeftOnRectangle( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowLeft(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowLongDown(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowLongLeft(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowLongRight(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowLongUp(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowPathRoundedSquare( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowPath(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowRightCircle( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowRightOnRectangle( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowRight(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowSmallDown(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowSmallLeft(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowSmallRight( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowSmallUp(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowTopRightOnSquare( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowTrendingDown( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowTrendingUp( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowUpCircle(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowUpLeft(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowUpOnSquareStack( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowUpOnSquare( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowUpRight(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowUpTray(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowUp(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowUturnDown(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowUturnLeft(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowUturnRight( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowUturnUp(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconArrowsPointingIn( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowsPointingOut( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowsRightLeft( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconArrowsUpDown(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconAtSymbol(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBackspace(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBackward(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} -export function IconBanknotes(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBars2(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBars3BottomLeft( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconBars3BottomRight( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconBars3CenterLeft( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconBars3(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBars4(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBarsArrowDown(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBarsArrowUp(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBattery0(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBattery100(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBattery50(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBeaker(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBellAlert(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBellSlash(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBellSnooze(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBell(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBoltSlash(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBolt(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} - -export function IconBoltSolid(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBookOpen(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBookmarkSlash(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBookmarkSquare(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBookmark(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBriefcase(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} -export function IconBugAnt(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBuildingLibrary( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconBuildingOffice2( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconBuildingOffice(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconBuildingStorefront( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconCake(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCalculator(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCalendarDays(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCalendar(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCamera(props: JSX.SvgSVGAttributes) { - return ( - - - - - - ) -} -export function IconChartBarSquare(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconChartBar(props: JSX.SvgSVGAttributes) { - return ( - - - - - - ) -} -export function IconChartPie(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} -export function IconChatBubbleBottomCenterText( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconChatBubbleBottomCenter( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconChatBubbleLeftEllipsis( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconChatBubbleLeftRight( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconChatBubbleLeft(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconChatBubbleOvalLeftEllipsis( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconChatBubbleOvalLeft( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconCheckBadge(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCheckCircle(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCheck(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconChevronDoubleDown( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconChevronDoubleLeft( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconChevronDoubleRight( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconChevronDoubleUp( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconChevronDown(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconChevronLeft(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconChevronRight(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconChevronUpDown(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconChevronUp(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCircleStack(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconClipboardDocumentCheck( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconClipboardDocumentList( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconClipboardDocument( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconClipboard(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconClock(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCloudArrowDown(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCloudArrowUp(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCloud(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCodeBracketSquare( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconCodeBracket(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCog6Tooth(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} -export function IconCog8Tooth(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} -export function IconCog(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCommandLine(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconComputerDesktop( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconCpuChip(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCreditCard(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCubeTransparent( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconCube(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCurrencyBangladeshi( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconCurrencyDollar(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCurrencyEuro(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCurrencyPound(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCurrencyRupee(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCurrencyYen(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconCursorArrowRays( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconCursorArrowRipple( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconDevicePhoneMobile( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconDeviceTablet(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconDocumentArrowDown( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconDocumentArrowUp( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconDocumentChartBar( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconDocumentCheck(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconDocumentDuplicate( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconDocumentMagnifyingGlass( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconDocumentMinus(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconDocumentPlus(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconDocumentText(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconDocument(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconEllipsisHorizontalCircle( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconEllipsisHorizontal( - props: JSX.SvgSVGAttributes -) { - return ( - - - - - - ) -} -export function IconEllipsisVertical( - props: JSX.SvgSVGAttributes -) { - return ( - - - - - - ) -} -export function IconEnvelopeOpen(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconEnvelope(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconEnvelopeSolid(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} -export function IconExclamationCircle( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconExclamationTriangle( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconEyeDropper(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconEyeSlash(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconEye(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} -export function IconFaceFrown(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconFaceSmile(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconFilm(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconFingerPrint(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconFire(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} -export function IconFlag(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconFolderArrowDown( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconFolderMinus(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconFolderOpen(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconFolderPlus(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconFolder(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconForward(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} -export function IconFunnel(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconGif(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconGiftTop(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconGift(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconGlobeAlt(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconGlobeAmericas(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconGlobeAsiaAustralia( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconGlobeEuropeAfrica( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconHandRaised(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconHandThumbDown(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconHandThumbUp(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconHashtag(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconHeart(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconHomeModern(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconHome(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconIdentification(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconInboxArrowDown(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconInboxStack(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconInbox(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconInformationCircle( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconKey(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconLanguage(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconLifebuoy(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconLightBulb(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconLink(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconListBullet(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconLockClosed(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconLockOpen(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconMagnifyingGlassCircle( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconMagnifyingGlassMinus( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconMagnifyingGlassPlus( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconMagnifyingGlass( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconMapPin(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} -export function IconMap(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconMegaphone(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconMicrophone(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconMinusCircle(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconMinusSmall(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconMinus(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconMoon(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconMusicalNote(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconNewspaper(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconNoSymbol(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPaintBrush(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPaperAirplane(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPaperClip(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPauseCircle(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPause(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPencilSquare(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPencil(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPhoneArrowDownLeft( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconPhoneArrowUpRight( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconPhoneXMark(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPhone(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPhoto(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPlayCircle(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} -export function IconPlayPause(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPlay(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPlusCircle(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPlusSmall(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPlus(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPower(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPresentationChartBar( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconPresentationChartLine( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconPrinter(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconPuzzlePiece(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconQrCode(props: JSX.SvgSVGAttributes) { - return ( - - - - - - - - - - - - - - ) -} -export function IconQuestionMarkCircle( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconQueueList(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconRadio(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconReceiptPercent(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconReceiptRefund(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconRectangleGroup(props: JSX.SvgSVGAttributes) { - return ( - - - - - - ) -} -export function IconRectangleStack(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconRocketLaunch(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconRss(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconScale(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconScissors(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconServerStack(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconServer(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconShare(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconShieldCheck(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconShieldExclamation( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconShoppingBag(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconShoppingCart(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconSignalSlash(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconSignal(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconSparkles(props: JSX.SvgSVGAttributes) { - return ( - - - - - - ) -} -export function IconSpeakerWave(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconSpeakerXMark(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconSquare2Stack(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconSquare3Stack3d(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconSquares2x2(props: JSX.SvgSVGAttributes) { - return ( - - - - - - - ) -} -export function IconSquaresPlus(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconStar(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconStopCircle(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} -export function IconStop(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconSun(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconSwatch(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconTableCells(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconTag(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} -export function IconTicket(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconTrash(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconTrophy(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconTruck(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconTv(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconUserCircle(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconUserGroup(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconUserMinus(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconUserPlus(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconUser(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} -export function IconUsers(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconVariable(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconVideoCameraSlash( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconVideoCamera(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconViewColumns(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconViewfinderCircle( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconWallet(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconWifi(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconWindow(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconWrenchScrewdriver( - props: JSX.SvgSVGAttributes -) { - return ( - - - - ) -} -export function IconWrench(props: JSX.SvgSVGAttributes) { - return ( - - - - - ) -} -export function IconXCircle(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconXMark(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -// index -export function IconCommand(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconLetter(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconMultiSelect(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} -export function IconSettings(props: JSX.SvgSVGAttributes) { - return ( - - - - - - - - - - - - ) -} -export function IconSingleSelect(props: JSX.SvgSVGAttributes) { - return ( - - - - ) -} diff --git a/app/packages/web/src/components/share.module.css b/app/packages/web/src/components/share.module.css deleted file mode 100644 index 5d1dab1bf..000000000 --- a/app/packages/web/src/components/share.module.css +++ /dev/null @@ -1,326 +0,0 @@ -.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/app/packages/web/src/content.config.ts b/app/packages/web/src/content.config.ts deleted file mode 100644 index d9ee8c9d1..000000000 --- a/app/packages/web/src/content.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -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/app/packages/web/src/content/docs/docs/cli.mdx b/app/packages/web/src/content/docs/docs/cli.mdx deleted file mode 100644 index 44a56e1fb..000000000 --- a/app/packages/web/src/content/docs/docs/cli.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -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/app/packages/web/src/content/docs/docs/config.mdx b/app/packages/web/src/content/docs/docs/config.mdx deleted file mode 100644 index 288f194c5..000000000 --- a/app/packages/web/src/content/docs/docs/config.mdx +++ /dev/null @@ -1,88 +0,0 @@ ---- -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/app/packages/web/src/content/docs/docs/index.mdx b/app/packages/web/src/content/docs/docs/index.mdx deleted file mode 100644 index e6f71be19..000000000 --- a/app/packages/web/src/content/docs/docs/index.mdx +++ /dev/null @@ -1,58 +0,0 @@ ---- -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/app/packages/web/src/content/docs/docs/lsp-servers.mdx b/app/packages/web/src/content/docs/docs/lsp-servers.mdx deleted file mode 100644 index cd259dea7..000000000 --- a/app/packages/web/src/content/docs/docs/lsp-servers.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -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/app/packages/web/src/content/docs/docs/mcp-servers.mdx b/app/packages/web/src/content/docs/docs/mcp-servers.mdx deleted file mode 100644 index 28c6d2ab2..000000000 --- a/app/packages/web/src/content/docs/docs/mcp-servers.mdx +++ /dev/null @@ -1,51 +0,0 @@ ---- -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/app/packages/web/src/content/docs/docs/models.mdx b/app/packages/web/src/content/docs/docs/models.mdx deleted file mode 100644 index c40216695..000000000 --- a/app/packages/web/src/content/docs/docs/models.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -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/app/packages/web/src/content/docs/docs/shortcuts.mdx b/app/packages/web/src/content/docs/docs/shortcuts.mdx deleted file mode 100644 index dd866e0f3..000000000 --- a/app/packages/web/src/content/docs/docs/shortcuts.mdx +++ /dev/null @@ -1,68 +0,0 @@ ---- -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/app/packages/web/src/content/docs/docs/themes.mdx b/app/packages/web/src/content/docs/docs/themes.mdx deleted file mode 100644 index e691a22e7..000000000 --- a/app/packages/web/src/content/docs/docs/themes.mdx +++ /dev/null @@ -1,75 +0,0 @@ ---- -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/app/packages/web/src/content/docs/index.mdx b/app/packages/web/src/content/docs/index.mdx deleted file mode 100644 index 176520ec5..000000000 --- a/app/packages/web/src/content/docs/index.mdx +++ /dev/null @@ -1,12 +0,0 @@ ---- -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/app/packages/web/src/pages/s/index.astro b/app/packages/web/src/pages/s/index.astro deleted file mode 100644 index b678c0db9..000000000 --- a/app/packages/web/src/pages/s/index.astro +++ /dev/null @@ -1,28 +0,0 @@ ---- -import config from "virtual:starlight/user-config"; - -import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; -import Share from "../../components/Share.tsx"; - ---- - - - - - - diff --git a/app/packages/web/src/styles/custom.css b/app/packages/web/src/styles/custom.css deleted file mode 100644 index 9c4c71f00..000000000 --- a/app/packages/web/src/styles/custom.css +++ /dev/null @@ -1,16 +0,0 @@ -: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/app/packages/web/sst-env.d.ts b/app/packages/web/sst-env.d.ts deleted file mode 100644 index b6a7e9066..000000000 --- a/app/packages/web/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/app/packages/web/tsconfig.json b/app/packages/web/tsconfig.json deleted file mode 100644 index 973603872..000000000 --- a/app/packages/web/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "astro/tsconfigs/strict", - "include": [".astro/types.d.ts", "**/*"], - "exclude": ["dist"], - "compilerOptions": { - "jsx": "preserve", - "jsxImportSource": "solid-js" - } -} diff --git a/app/sst-env.d.ts b/app/sst-env.d.ts deleted file mode 100644 index 7ca38b723..000000000 --- a/app/sst-env.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - "Api": { - "type": "sst.cloudflare.Worker" - "url": string - } - "Bucket": { - "type": "sst.cloudflare.Bucket" - } - "Web": { - "type": "sst.cloudflare.StaticSite" - "url": string - } - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/app/sst.config.ts b/app/sst.config.ts deleted file mode 100644 index 4c36fea58..000000000 --- a/app/sst.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -/// - -export default $config({ - app(input) { - return { - name: "opencode", - removal: input?.stage === "production" ? "retain" : "remove", - protect: ["production"].includes(input?.stage), - home: "cloudflare", - } - }, - async run() { - const { api } = await import("./infra/app.js") - return { - api: api.url, - } - }, -}) diff --git a/app/tsconfig.json b/app/tsconfig.json deleted file mode 100644 index 0967ef424..000000000 --- a/app/tsconfig.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/bun.lock b/bun.lock new file mode 100644 index 000000000..d560f5ffb --- /dev/null +++ b/bun.lock @@ -0,0 +1,1674 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "opencontrol", + "devDependencies": { + "@tsconfig/node22": "22.0.0", + "@types/node": "^22.13.9", + "prettier": "^3.5.3", + "sst": "3.16.0", + "typescript": "5.8.2", + }, + }, + "packages/function": { + "name": "@opencode/function", + "version": "0.0.1", + "devDependencies": { + "@cloudflare/workers-types": "^4.20250522.0", + }, + }, + "packages/opencode": { + "name": "opencode", + "version": "0.0.0", + "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", + }, + "devDependencies": { + "@tsconfig/bun": "^1.0.7", + "@types/bun": "latest", + "@types/jsdom": "^21.1.7", + "@types/turndown": "^5.0.5", + }, + }, + "packages/web": { + "name": "@opencode/web", + "version": "0.0.1", + "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", + }, + }, + }, + "trustedDependencies": [ + "sharp", + "esbuild", + ], + "overrides": { + "zod": "3.24.2", + }, + "packages": { + "@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.7", "@ai-sdk/provider-utils": "3.0.0-alpha.7" }, "peerDependencies": { "zod": "^3.24.0" } }, "sha512-gz1V165eiJnQIexfLyKm11vimrmQ3zdcJhPpjeLFmDU9wrvZwLuklfZ0WgfYSb+EjiP1cKypwt6JSGvWkfKIAQ=="], + + "@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-canary.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-aN83hjdjDCyhkOdulwMsxmGb91owS+bCSe6FWg1TEwusNM35vv020nY//Gid/0NdIpVkZJGzAajgCWrnno2zzA=="], + + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-canary.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-canary.14", "@standard-schema/spec": "^1.0.0", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-4IJw6/wkWYLYfFYPvCs5go0L/sBRZsIRW1l/R6LniF4WjAH2+R4dMbESgBmzx+Z2+W+W6gFeK8dnQByn7vaA/w=="], + + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.9.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="], + + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], + + "@astrojs/compiler": ["@astrojs/compiler@2.12.0", "", {}, "sha512-7bCjW6tVDpUurQLeKBUN9tZ5kSv5qYrGmcn0sG0IwacL7isR2ZbyyA3AdZ4uxsuUFOS2SlgReTH7wkxO6zpqWA=="], + + "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], + + "@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="], + + "@astrojs/mdx": ["@astrojs/mdx@4.3.0", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.2", "@mdx-js/mdx": "^3.1.0", "acorn": "^8.14.1", "es-module-lexer": "^1.6.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "kleur": "^4.1.5", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.4", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-OGX2KvPeBzjSSKhkCqrUoDMyzFcjKt5nTE5SFw3RdoLf0nrhyCXBQcCyclzWy1+P+XpOamn+p+hm1EhpCRyPxw=="], + + "@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], + + "@astrojs/sitemap": ["@astrojs/sitemap@3.4.0", "", { "dependencies": { "sitemap": "^8.0.0", "stream-replace-string": "^2.0.0", "zod": "^3.24.2" } }, "sha512-C5m/xsKvRSILKM3hy47n5wKtTQtJXn8epoYuUmCCstaE9XBt20yInym3Bz2uNbEiNfv11bokoW0MqeXPIvjFIQ=="], + + "@astrojs/solid-js": ["@astrojs/solid-js@5.1.0", "", { "dependencies": { "vite": "^6.3.5", "vite-plugin-solid": "^2.11.6" }, "peerDependencies": { "solid-devtools": "^0.30.1", "solid-js": "^1.8.5" }, "optionalPeers": ["solid-devtools"] }, "sha512-VmPHOU9k7m6HHCT2Y1mNzifilUnttlowBM36frGcfj5wERJE9Ci0QtWJbzdf6AlcoIirb7xVw+ByupU011Di9w=="], + + "@astrojs/starlight": ["@astrojs/starlight@0.34.3", "", { "dependencies": { "@astrojs/markdown-remark": "^6.3.1", "@astrojs/mdx": "^4.2.3", "@astrojs/sitemap": "^3.3.0", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", "astro-expressive-code": "^0.41.1", "bcp-47": "^2.1.0", "hast-util-from-html": "^2.0.1", "hast-util-select": "^6.0.2", "hast-util-to-string": "^3.0.0", "hastscript": "^9.0.0", "i18next": "^23.11.5", "js-yaml": "^4.1.0", "klona": "^2.0.6", "mdast-util-directive": "^3.0.0", "mdast-util-to-markdown": "^2.1.0", "mdast-util-to-string": "^4.0.0", "pagefind": "^1.3.0", "rehype": "^13.0.1", "rehype-format": "^5.0.0", "remark-directive": "^3.0.0", "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "vfile": "^6.0.2" }, "peerDependencies": { "astro": "^5.5.0" } }, "sha512-MAuD3NF+E+QXJJuVKofoR6xcPTP4BJmYWeOBd03udVdubNGVnPnSWVZAi+ZtnTofES4+mJdp8BNGf+ubUxkiiA=="], + + "@astrojs/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="], + + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/compat-data": ["@babel/compat-data@7.27.3", "", {}, "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw=="], + + "@babel/core": ["@babel/core@7.27.3", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.3", "@babel/parser": "^7.27.3", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.3", "@babel/types": "^7.27.3", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA=="], + + "@babel/generator": ["@babel/generator@7.27.3", "", { "dependencies": { "@babel/parser": "^7.27.3", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.27.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.3" } }, "sha512-h/eKy9agOya1IGuLaZ9tEUgz+uIRXcbtOhRtUyyMf8JFmn1iT13vnl/IGVWSkdOCG/pC57U4S1jnAabAavTMwg=="], + + "@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="], + + "@babel/runtime": ["@babel/runtime@7.27.3", "", {}, "sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw=="], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/traverse": ["@babel/traverse@7.27.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.3", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ=="], + + "@babel/types": ["@babel/types@7.27.3", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw=="], + + "@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="], + + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250529.0", "", {}, "sha512-l6tVFpI6MUChMD0wK+Jhikb+aCbrmIR58CVpV/BhRT4THjl+nFhTT5N5ZqX42FDXdE3hCPLjueBMpPRhPUOB2A=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@5.0.2", "", {}, "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA=="], + + "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@3.0.10", "", { "dependencies": { "@csstools/color-helpers": "^5.0.2", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], + + "@ctrl/tinycolor": ["@ctrl/tinycolor@4.1.0", "", {}, "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], + + "@expressive-code/core": ["@expressive-code/core@0.41.2", "", { "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-AJW5Tp9czbLqKMzwudL9Rv4js9afXBxkSGLmCNPq1iRgAYcx9NkTPJiSNCesjKRWoVC328AdSu6fqrD22zDgDg=="], + + "@expressive-code/plugin-frames": ["@expressive-code/plugin-frames@0.41.2", "", { "dependencies": { "@expressive-code/core": "^0.41.2" } }, "sha512-pfy0hkJI4nbaONjmksFDcuHmIuyPTFmi1JpABe4q2ajskiJtfBf+WDAL2pg595R9JNoPrrH5+aT9lbkx2noicw=="], + + "@expressive-code/plugin-shiki": ["@expressive-code/plugin-shiki@0.41.2", "", { "dependencies": { "@expressive-code/core": "^0.41.2", "shiki": "^3.2.2" } }, "sha512-xD4zwqAkDccXqye+235BH5bN038jYiSMLfUrCOmMlzxPDGWdxJDk5z4uUB/aLfivEF2tXyO2zyaarL3Oqht0fQ=="], + + "@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.2", "", { "dependencies": { "@expressive-code/core": "^0.41.2" } }, "sha512-JFWBz2qYxxJOJkkWf96LpeolbnOqJY95TvwYc0hXIHf9oSWV0h0SY268w/5N3EtQaD9KktzDE+VIVwb9jdb3nw=="], + + "@flystorage/dynamic-import": ["@flystorage/dynamic-import@1.0.0", "", {}, "sha512-CIbIUrBdaPFyKnkVBaqzksvzNtsMSXITR/G/6zlil3MBnPFq2LX+X4Mv5p2XOmv/3OulFs/ff2SNb+5dc2Twtg=="], + + "@flystorage/file-storage": ["@flystorage/file-storage@1.1.0", "", {}, "sha512-25Gd5EsXDmhHrK5orpRuVqebQms1Cm9m5ACMZ0sVDX+Sbl1V0G88CbcWt7mEoWRYLvQ1U072htqg6Sav76ZlVA=="], + + "@flystorage/local-fs": ["@flystorage/local-fs@1.1.0", "", { "dependencies": { "@flystorage/dynamic-import": "^1.0.0", "@flystorage/file-storage": "^1.1.0", "file-type": "^20.5.0", "mime-types": "^3.0.1" } }, "sha512-dbErRhqmCv2UF0zPdeH7iVWuVeTWAJHuJD/mXDe2V370/SL7XIvdE3ditBHWC+1SzBKXJ0lkykOenwlum+oqIA=="], + + "@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.2.5", "", {}, "sha512-G09N3GfuT9qj3Ax2FDZvKqZttzM3v+cco2l8uXamhKyXLdmlaUDH5o88/C3vtTHj2oT7yRKsvxz9F+BXbWKMYA=="], + + "@hapi/bourne": ["@hapi/bourne@2.1.0", "", {}, "sha512-i1BpaNDVLJdRBEKeJWkVO6tYX6DMFBuwMhSuWqLsY4ufeTKGVuV5rBsUhxPayXqnnWHgXUAmWK16H/ykO5Wj4Q=="], + + "@hono/zod-validator": ["@hono/zod-validator@0.5.0", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-ds5bW6DCgAnNHP33E3ieSbaZFd5dkV52ZjyaXtGoR06APFrCtzAsKZxTHwOrJNBdXsi0e5wNwo5L4nVEVnJUdg=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + + "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], + + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="], + + "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], + + "@opencode/function": ["@opencode/function@workspace:packages/function"], + + "@opencode/web": ["@opencode/web@workspace:packages/web"], + + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], + + "@pagefind/darwin-arm64": ["@pagefind/darwin-arm64@1.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-365BEGl6ChOsauRjyVpBjXybflXAOvoMROw3TucAROHIcdBvXk9/2AmEvGFU0r75+vdQI4LJdJdpH4Y6Yqaj4A=="], + + "@pagefind/darwin-x64": ["@pagefind/darwin-x64@1.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-zlGHA23uuXmS8z3XxEGmbHpWDxXfPZ47QS06tGUq0HDcZjXjXHeLG+cboOy828QIV5FXsm9MjfkP5e4ZNbOkow=="], + + "@pagefind/default-ui": ["@pagefind/default-ui@1.3.0", "", {}, "sha512-CGKT9ccd3+oRK6STXGgfH+m0DbOKayX6QGlq38TfE1ZfUcPc5+ulTuzDbZUnMo+bubsEOIypm4Pl2iEyzZ1cNg=="], + + "@pagefind/linux-arm64": ["@pagefind/linux-arm64@1.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-8lsxNAiBRUk72JvetSBXs4WRpYrQrVJXjlRRnOL6UCdBN9Nlsz0t7hWstRk36+JqHpGWOKYiuHLzGYqYAqoOnQ=="], + + "@pagefind/linux-x64": ["@pagefind/linux-x64@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-hAvqdPJv7A20Ucb6FQGE6jhjqy+vZ6pf+s2tFMNtMBG+fzcdc91uTw7aP/1Vo5plD0dAOHwdxfkyw0ugal4kcQ=="], + + "@pagefind/windows-x64": ["@pagefind/windows-x64@1.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-BR1bIRWOMqkf8IoU576YDhij1Wd/Zf2kX/kCI0b2qzCKC8wcc2GQJaaRMCpzvCCrmliO4vtJ6RITp/AnoYUUmQ=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.41.1", "", { "os": "android", "cpu": "arm" }, "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.41.1", "", { "os": "android", "cpu": "arm64" }, "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.41.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.41.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.41.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.41.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.41.1", "", { "os": "linux", "cpu": "arm" }, "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.41.1", "", { "os": "linux", "cpu": "arm" }, "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.41.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.41.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg=="], + + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.41.1", "", { "os": "linux", "cpu": "none" }, "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw=="], + + "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.41.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.41.1", "", { "os": "linux", "cpu": "none" }, "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.41.1", "", { "os": "linux", "cpu": "none" }, "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.41.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.41.1", "", { "os": "linux", "cpu": "x64" }, "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.41.1", "", { "os": "linux", "cpu": "x64" }, "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.41.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.41.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.41.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw=="], + + "@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-1/adJbSMBOkpScCE/SB6XkjJU17ANln3Wky7lOmrnpl+zBdQ1qXUJg2GXTYVHRq+2j3hd1DesmElTXYDgtfSOQ=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-zcZKMnNndgRa3ORja6Iemsr3DrLtkX3cAF7lTJkdMB6v9alhlBsX9uNiCpqofNrXOvpA3h6lHcLJxgCIhVOU5Q=="], + + "@shikijs/langs": ["@shikijs/langs@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2" } }, "sha512-H6azIAM+OXD98yztIfs/KH5H4PU39t+SREhmM8LaNXyUrqj2mx+zVkr8MWYqjceSjDw9I1jawm1WdFqU806rMA=="], + + "@shikijs/themes": ["@shikijs/themes@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2" } }, "sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg=="], + + "@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="], + + "@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + + "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], + + "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="], + + "@tsconfig/node22": ["@tsconfig/node22@22.0.0", "", {}, "sha512-twLQ77zevtxobBOD4ToAtVmuYrpeYUh3qh+TEp+08IWhpsrIflVHqQ1F1CiPxQGL7doCdBIOOCF+1Tm833faNg=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="], + + "@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="], + + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + + "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], + + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + + "@types/fontkit": ["@types/fontkit@2.0.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew=="], + + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + + "@types/jsdom": ["@types/jsdom@21.1.7", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/luxon": ["@types/luxon@3.6.2", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], + + "@types/node": ["@types/node@22.15.24", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng=="], + + "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], + + "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], + + "@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="], + + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], + + "ai": ["ai@5.0.0-canary.24", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-canary.14", "@ai-sdk/provider-utils": "3.0.0-canary.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-vqaMmM6XFwjz9mNjox9ehjkWFwXbSchhor5MiqgKZ1qRyoTvoYzAt6oCZwg5kN5jXNQ3rZVuyE8N3BbPbwma2Q=="], + + "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], + + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "args": ["args@5.0.3", "", { "dependencies": { "camelcase": "5.0.0", "chalk": "2.4.2", "leven": "2.1.0", "mri": "1.1.4" } }, "sha512-h6k/zfFgusnv3i5TU08KQkVKuCPBtL/PWQbWkHUxvJrZ2nAyeaUupneemcrgn1xmqxPQsPIzwkUhOpoqPDRZuA=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], + + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + + "astro": ["astro@5.8.1", "", { "dependencies": { "@astrojs/compiler": "^2.11.0", "@astrojs/internal-helpers": "0.6.1", "@astrojs/markdown-remark": "6.3.2", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^2.4.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.4", "acorn": "^8.14.1", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.2.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.0", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.6.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.1.1", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.17", "magicast": "^0.3.5", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", "package-manager-detector": "^1.1.0", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.1", "shiki": "^3.2.1", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.12", "tsconfck": "^3.1.5", "ultrahtml": "^1.6.0", "unifont": "~0.5.0", "unist-util-visit": "^5.0.0", "unstorage": "^1.15.0", "vfile": "^6.0.3", "vite": "^6.3.4", "vitefu": "^1.0.6", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.1", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.5", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "bin": { "astro": "astro.js" } }, "sha512-lkBg1smMRFW+FQ6i92SgEN53o4+ItRjlRt6Ck+rEjmTcb57Bid7faTNKUQNYuNnxiesTWw3NJDyVPQPbfKDyfw=="], + + "astro-expressive-code": ["astro-expressive-code@0.41.2", "", { "dependencies": { "rehype-expressive-code": "^0.41.2" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0" } }, "sha512-HN0jWTnhr7mIV/2e6uu4PPRNNo/k4UEgTLZqbp3MrHU+caCARveG2yZxaZVBmxyiVdYqW5Pd3u3n2zjnshixbw=="], + + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="], + + "aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "b4a": ["b4a@1.6.7", "", {}, "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg=="], + + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.39.8", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2", "validate-html-nesting": "^1.2.1" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/MVOIIjonylDXnrWmG23ZX82m9mtKATsVHB7zYlPfDR9Vdd/NBE48if+wv27bSkBtyO7EPMUlcUc4J63QwuACQ=="], + + "babel-preset-solid": ["babel-preset-solid@1.9.6", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.39.8" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-HXTK9f93QxoH8dYn1M2mJdOlWgMsR88Lg/ul6QCZGkNTktjTE5HAf93YxQumHoCudLEtZrU1cFCMFOVho6GqFg=="], + + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "bare-events": ["bare-events@2.5.4", "", {}, "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA=="], + + "bare-fs": ["bare-fs@4.1.5", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA=="], + + "bare-os": ["bare-os@3.6.1", "", {}, "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g=="], + + "bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="], + + "bare-stream": ["bare-stream@2.6.5", "", { "dependencies": { "streamx": "^2.21.0" }, "peerDependencies": { "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-buffer", "bare-events"] }, "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA=="], + + "base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="], + + "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="], + + "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], + + "brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="], + + "browserslist": ["browserslist@4.25.0", "", { "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA=="], + + "buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="], + + "bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001720", "", {}, "sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g=="], + + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "ci-info": ["ci-info@4.2.0", "", {}, "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg=="], + + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], + + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], + + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], + + "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + + "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], + + "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], + + "css-selector-parser": ["css-selector-parser@3.1.2", "", {}, "sha512-WfUcL99xWDs7b3eZPoRszWVfbNo8ErCF15PTvVROjkShGlAfjIkG6hlfj/sl6/rfo5Q9x9ryJ3VqVnAZDA+gcw=="], + + "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "cssstyle": ["cssstyle@4.3.1", "", { "dependencies": { "@asamuzakjp/css-color": "^3.1.2", "rrweb-cssom": "^0.8.0" } }, "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], + + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.1.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w=="], + + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + + "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + + "deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="], + + "devalue": ["devalue@5.1.1", "", {}, "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="], + + "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + + "direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.161", "", {}, "sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA=="], + + "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], + + "entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="], + + "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], + + "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + + "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], + + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], + + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + + "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], + + "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], + + "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "events": ["events@1.1.1", "", {}, "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.2", "", {}, "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA=="], + + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + + "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + + "express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="], + + "expressive-code": ["expressive-code@0.41.2", "", { "dependencies": { "@expressive-code/core": "^0.41.2", "@expressive-code/plugin-frames": "^0.41.2", "@expressive-code/plugin-shiki": "^0.41.2", "@expressive-code/plugin-text-markers": "^0.41.2" } }, "sha512-aLZiZaqorRtNExtGpUjK9zFH9aTpWeoTXMyLo4b4IcuXfPqtLPPxhRm/QlPb8QqIcMMXnSiGRHSFpQfX0m7HJw=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], + + "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], + + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + + "fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="], + + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + + "file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="], + + "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], + + "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], + + "fontace": ["fontace@0.3.0", "", { "dependencies": { "@types/fontkit": "^2.0.8", "fontkit": "^2.0.4" } }, "sha512-czoqATrcnxgWb/nAkfyIrRp6Q8biYj7nGnL6zfhTcX+JKKpWHFBnb8uNMw/kZr7u++3Y3wYSYoZgHkCcsuBpBg=="], + + "fontkit": ["fontkit@2.0.4", "", { "dependencies": { "@swc/helpers": "^0.5.12", "brotli": "^1.3.2", "clone": "^2.1.2", "dfa": "^1.2.0", "fast-deep-equal": "^3.1.3", "restructure": "^3.0.0", "tiny-inflate": "^1.0.3", "unicode-properties": "^1.4.0", "unicode-trie": "^2.0.0" } }, "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], + + "globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "h3": ["h3@1.15.3", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.4", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.0", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hast-util-embedded": ["hast-util-embedded@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA=="], + + "hast-util-format": ["hast-util-format@1.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-whitespace": "^3.0.0", "html-whitespace-sensitive-tag-names": "^3.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA=="], + + "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], + + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], + + "hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="], + + "hast-util-heading-rank": ["hast-util-heading-rank@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA=="], + + "hast-util-is-body-ok-link": ["hast-util-is-body-ok-link@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ=="], + + "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], + + "hast-util-minify-whitespace": ["hast-util-minify-whitespace@1.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-is-element": "^3.0.0", "hast-util-whitespace": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw=="], + + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + + "hast-util-phrasing": ["hast-util-phrasing@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-has-property": "^3.0.0", "hast-util-is-body-ok-link": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ=="], + + "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], + + "hast-util-select": ["hast-util-select@6.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "bcp-47-match": "^2.0.0", "comma-separated-tokens": "^2.0.0", "css-selector-parser": "^3.0.0", "devlop": "^1.0.0", "direction": "^2.0.0", "hast-util-has-property": "^3.0.0", "hast-util-to-string": "^3.0.0", "hast-util-whitespace": "^3.0.0", "nth-check": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw=="], + + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], + + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw=="], + + "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], + + "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + + "hono": ["hono@4.7.10", "", {}, "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ=="], + + "hono-openapi": ["hono-openapi@0.4.8", "", { "dependencies": { "json-schema-walker": "^2.0.0" }, "peerDependencies": { "@hono/arktype-validator": "^2.0.0", "@hono/effect-validator": "^1.2.0", "@hono/typebox-validator": "^0.2.0 || ^0.3.0", "@hono/valibot-validator": "^0.5.1", "@hono/zod-validator": "^0.4.1", "@sinclair/typebox": "^0.34.9", "@valibot/to-json-schema": "^1.0.0-beta.3", "arktype": "^2.0.0", "effect": "^3.11.3", "hono": "^4.6.13", "openapi-types": "^12.1.3", "valibot": "^1.0.0-beta.9", "zod": "^3.23.8", "zod-openapi": "^4.0.0" }, "optionalPeers": ["@hono/arktype-validator", "@hono/effect-validator", "@hono/typebox-validator", "@hono/valibot-validator", "@hono/zod-validator", "@sinclair/typebox", "@valibot/to-json-schema", "arktype", "effect", "hono", "valibot", "zod", "zod-openapi"] }, "sha512-LYr5xdtD49M7hEAduV1PftOMzuT8ZNvkyWfh1DThkLsIr4RkvDb12UxgIiFbwrJB6FLtFXLoOZL9x4IeDk2+VA=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], + + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], + + "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="], + + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "ieee754": ["ieee754@1.1.13", "", {}, "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="], + + "import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], + + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="], + + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="], + + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], + + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "jmespath": ["jmespath@0.16.0", "", {}, "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw=="], + + "jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], + + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-rpc-2.0": ["json-rpc-2.0@1.7.0", "", {}, "sha512-asnLgC1qD5ytP+fvBP8uL0rvj+l8P6iYICbzZ8dVxCpESffVjzA7KkYkbKCIbavs7cllwH1ZUaNtJwphdeRqpg=="], + + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + + "json-schema-walker": ["json-schema-walker@2.0.0", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.1.0", "clone": "^2.1.2" } }, "sha512-nXN2cMky0Iw7Af28w061hmxaPDaML5/bQD9nwm1lOoIKEGjHcRGxqWe4MfrkYThYAPjSUhmsp4bJNoLAyVn9Xw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], + + "leven": ["leven@2.1.0", "", {}, "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA=="], + + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="], + + "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + + "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], + + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="], + + "mdast-util-directive": ["mdast-util-directive@3.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q=="], + + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-directive": ["micromark-extension-directive@3.0.2", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], + + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], + + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], + + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], + + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "mri": ["mri@1.1.4", "", {}, "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], + + "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], + + "node-abi": ["node-abi@3.75.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg=="], + + "node-addon-api": ["node-addon-api@6.1.0", "", {}, "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-fetch-native": ["node-fetch-native@1.6.6", "", {}, "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ=="], + + "node-mock-http": ["node-mock-http@1.0.0", "", {}, "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ=="], + + "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "nwsapi": ["nwsapi@2.2.20", "", {}, "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "ofetch": ["ofetch@1.4.1", "", { "dependencies": { "destr": "^2.0.3", "node-fetch-native": "^1.6.4", "ufo": "^1.5.4" } }, "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw=="], + + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + + "oidc-token-hash": ["oidc-token-hash@5.1.0", "", {}, "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA=="], + + "on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "opencode": ["opencode@workspace:packages/opencode"], + + "opencontrol": ["opencontrol@0.0.6", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.6.1", "@tsconfig/bun": "1.0.7", "hono": "4.7.4", "zod": "3.24.2", "zod-to-json-schema": "3.24.3" }, "bin": { "opencontrol": "bin/index.mjs" } }, "sha512-QeCrpOK5D15QV8kjnGVeD/BHFLwcVr+sn4T6KKmP0WAMs2pww56e4h+eOGHb5iPOufUQXbdbBKi6WV2kk7tefQ=="], + + "openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="], + + "p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="], + + "p-queue": ["p-queue@8.1.0", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw=="], + + "p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], + + "package-manager-detector": ["package-manager-detector@1.3.0", "", {}, "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="], + + "pagefind": ["pagefind@1.3.0", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.3.0", "@pagefind/darwin-x64": "1.3.0", "@pagefind/linux-arm64": "1.3.0", "@pagefind/linux-x64": "1.3.0", "@pagefind/windows-x64": "1.3.0" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-8KPLGT5g9s+olKMRTU9LFekLizkVIu9tes90O1/aigJ0T5LmyPqTzGJrETnSw3meSYg58YH7JTzhTTW/3z6VAw=="], + + "pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + + "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + + "peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + + "pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], + + "pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="], + + "pino-pretty": ["pino-pretty@5.1.3", "", { "dependencies": { "@hapi/bourne": "^2.0.0", "args": "^5.0.1", "chalk": "^4.0.0", "dateformat": "^4.5.1", "fast-safe-stringify": "^2.0.7", "jmespath": "^0.15.0", "joycon": "^3.0.0", "pump": "^3.0.0", "readable-stream": "^3.6.0", "rfdc": "^1.3.0", "split2": "^3.1.1", "strip-json-comments": "^3.1.1" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-Zj+0TVdYKkAAIx9EUCL5e4TttwgsaFvJh2ceIMQeFCY8ak9tseEZQGSgpvyjEj1/iIVGIh5tdhkGEQWSMILKHA=="], + + "pino-std-serializers": ["pino-std-serializers@4.0.0", "", {}, "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q=="], + + "pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss": ["postcss@8.5.4", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + + "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], + + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], + + "process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], + + "punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="], + + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + + "querystring": ["querystring@0.2.0", "", {}, "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g=="], + + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + + "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "real-require": ["real-require@0.1.0", "", {}, "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg=="], + + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], + + "recma-jsx": ["recma-jsx@1.0.0", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" } }, "sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q=="], + + "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], + + "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + + "regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + + "rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="], + + "rehype-autolink-headings": ["rehype-autolink-headings@7.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-is-element": "^3.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw=="], + + "rehype-expressive-code": ["rehype-expressive-code@0.41.2", "", { "dependencies": { "expressive-code": "^0.41.2" } }, "sha512-vHYfWO9WxAw6kHHctddOt+P4266BtyT1mrOIuxJD+1ELuvuJAa5uBIhYt0OVMyOhlvf57hzWOXJkHnMhpaHyxw=="], + + "rehype-format": ["rehype-format@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-format": "^1.0.0" } }, "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ=="], + + "rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="], + + "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], + + "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + + "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="], + + "remark-directive": ["remark-directive@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^3.0.0", "unified": "^11.0.0" } }, "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A=="], + + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + + "remark-mdx": ["remark-mdx@3.1.0", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "remark-smartypants": ["remark-smartypants@3.0.2", "", { "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + + "remeda": ["remeda@2.22.4", "", { "dependencies": { "type-fest": "^4.40.1" } }, "sha512-BDQhIgs3baVyxOUb6IRm+HnT7NqbIPOq3FiBFPEqmzkJqN3AS3RwbGH+DVi6MKnZcIcKO7mSN9gsbwBor4YVVw=="], + + "restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="], + + "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], + + "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="], + + "retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="], + + "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "rollup": ["rollup@4.41.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], + + "seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], + + "seroval-plugins": ["seroval-plugins@1.3.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ=="], + + "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "sharp": ["sharp@0.32.6", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.2", "node-addon-api": "^6.1.0", "prebuild-install": "^7.1.1", "semver": "^7.5.4", "simple-get": "^4.0.1", "tar-fs": "^3.0.4", "tunnel-agent": "^0.6.0" } }, "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w=="], + + "shiki": ["shiki@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/engine-javascript": "3.4.2", "@shikijs/engine-oniguruma": "3.4.2", "@shikijs/langs": "3.4.2", "@shikijs/themes": "3.4.2", "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-wuxzZzQG8kvZndD7nustrNFIKYJ1jJoWIPaBpVe2+KHSvtzMi4SBjOxrigs8qeqce/l3U0cwiC+VAkLKSunHQQ=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "sitemap": ["sitemap@8.0.0", "", { "dependencies": { "@types/node": "^17.0.5", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.2.4" }, "bin": { "sitemap": "dist/cli.js" } }, "sha512-+AbdxhM9kJsHtruUF39bwS/B0Fytw6Fr1o4ZAIAEqA6cke2xcoO2GleBw9Zw7nRzILVEgz7zBM5GiTJjie1G9A=="], + + "smol-toml": ["smol-toml@1.3.4", "", {}, "sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA=="], + + "solid-js": ["solid-js@1.9.7", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw=="], + + "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], + + "sonic-boom": ["sonic-boom@2.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg=="], + + "source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="], + + "sst": ["sst@3.16.0", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.16.0", "sst-darwin-x64": "3.16.0", "sst-linux-arm64": "3.16.0", "sst-linux-x64": "3.16.0", "sst-linux-x86": "3.16.0", "sst-win32-arm64": "3.16.0", "sst-win32-x64": "3.16.0", "sst-win32-x86": "3.16.0" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-XX4ktkpyvB2lQCG8/jgH/RWAtdNahE/MjlK6OctM4Nagk1rOrMxxzARit2ZFCisX+ZBvcN0ymgJimnfZqWjM7Q=="], + + "sst-darwin-arm64": ["sst-darwin-arm64@3.16.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NJDGjZ0bl+hWJoT1P2KLyA65OXHkItVg3BNkRsas6qaa+c1U6GU8wLFnfdqkDRnJIYhOIsnZtIquNp7lMIr3ww=="], + + "sst-darwin-x64": ["sst-darwin-x64@3.16.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-IZiIUZgTGKjW+aR6/qgqNMzdFS2789PBQjsMquXaTZc7ebRutbCozQ4CC78SuwPXDUZ0/Bz+hyGeXHNOB52lVQ=="], + + "sst-linux-arm64": ["sst-linux-arm64@3.16.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-pvbxU+hrbvtIt347ilbXzVQEl2kXXBIYArLMzblPNg4KblCRvJvH595P86hrr8ZojlOkEb1rC8NSAwiZyBad4g=="], + + "sst-linux-x64": ["sst-linux-x64@3.16.0", "", { "os": "linux", "cpu": "x64" }, "sha512-p7qtmdiG1imWSZ7gUbVecee6TIuILTvIe6dgpvkgznYvuTFiMCZaY2SNMFuNRiBoN5zZI0y5osDDXY2uzzo7rA=="], + + "sst-linux-x86": ["sst-linux-x86@3.16.0", "", { "os": "linux", "cpu": "none" }, "sha512-bw+enzOnZ+MjGd7/h4swGfHpIoyLbIdguelGV7vZLIeuV4t4rZ/hAqfR25GEMjjonVv3Py5hsasMzR2s9GsBLA=="], + + "sst-win32-arm64": ["sst-win32-arm64@3.16.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-bjjYn1gq1p1tBFKvuB3K5ZMuofK/WWmrtKpD+OJOf8PE5fxzRJ8yvOFVIm1QhD4hxDQAPdNonE/P/cXDtKXQig=="], + + "sst-win32-x64": ["sst-win32-x64@3.16.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WFHaUyaLHepKPOMFr7Kf8iECIg1prn3L7c5r+IKgIiv+KznOeslwG6f9mYwxcOYZrZNiXVeDJmDp9xZtB5auSQ=="], + + "sst-win32-x86": ["sst-win32-x86@3.16.0", "", { "os": "win32", "cpu": "none" }, "sha512-Twbdy9sS6zSKsdwpWVEYpFEjVcRAn0Ufdbz+pVdMK3JXniSB7MLC6/KQuxUwvUk2a88+jnPuPVXAYC51SgzDyg=="], + + "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], + + "stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="], + + "streamx": ["streamx@2.22.0", "", { "dependencies": { "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "strtok3": ["strtok3@10.2.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="], + + "style-to-js": ["style-to-js@1.1.16", "", { "dependencies": { "style-to-object": "1.0.8" } }, "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw=="], + + "style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tar-fs": ["tar-fs@3.0.9", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA=="], + + "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], + + "text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="], + + "thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], + + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + + "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], + + "tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="], + + "toolbeam-docs-theme": ["toolbeam-docs-theme@0.2.4", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-W5mdbcgRpTBDFyEdcU81USs3MFZoXMInpSznc/AFZCwqz8atk4iBNDIlhvihpGHY54Nf5crKmZwJjxVojkHFvA=="], + + "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], + + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + + "ts-lsp-client": ["ts-lsp-client@1.0.3", "", { "dependencies": { "json-rpc-2.0": "^1.7.0", "pino": "^7.0.5", "pino-pretty": "^5.1.3", "tslib": "~2.6.2" } }, "sha512-0ItrsqvNUM9KNFGbeT1N8jSi9gvasGOvxJUXjGf4P2TX0w250AUWLeRStaSrQbYcFDshDtE5d4BshUmYwodDgw=="], + + "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], + + "tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], + + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + + "turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="], + + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + + "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], + + "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], + + "ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="], + + "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unicode-properties": ["unicode-properties@1.4.1", "", { "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" } }, "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg=="], + + "unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="], + + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unifont": ["unifont@0.5.0", "", { "dependencies": { "css-tree": "^3.0.0", "ohash": "^2.0.0" } }, "sha512-4DueXMP5Hy4n607sh+vJ+rajoLu778aU3GzqeTCqsD/EaUcvqZT9wPC8kgK6Vjh22ZskrxyRCR71FwNOaYn6jA=="], + + "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], + + "unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="], + + "unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], + + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + + "unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "unstorage": ["unstorage@1.16.0", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.2", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.6", "ofetch": "^1.4.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-WQ37/H5A7LcRPWfYOrDa1Ys02xAbpPJq6q5GkO88FBXVSQzHd7+BjEwfRqyaSWCv9MbsJy058GWjjPjcJ16GGA=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + + "url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="], + + "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@8.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw=="], + + "validate-html-nesting": ["validate-html-nesting@1.2.2", "", {}, "sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], + + "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], + + "vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], + + "vite-plugin-solid": ["vite-plugin-solid@2.11.6", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-Sl5CTqJTGyEeOsmdH6BOgalIZlwH3t4/y0RQuFLMGnvWMBvxb4+lq7x3BSiAw6etf0QexfNJW7HSOO/Qf7pigg=="], + + "vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="], + + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="], + + "vscode-languageclient": ["vscode-languageclient@8.1.0", "", { "dependencies": { "minimatch": "^5.1.0", "semver": "^7.3.7", "vscode-languageserver-protocol": "3.17.3" } }, "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing=="], + + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.3", "", { "dependencies": { "vscode-jsonrpc": "8.1.0", "vscode-languageserver-types": "3.17.3" } }, "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA=="], + + "vscode-languageserver-types": ["vscode-languageserver-types@3.17.3", "", {}, "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + + "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], + + "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], + + "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], + + "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], + + "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="], + + "yocto-spinner": ["yocto-spinner@0.2.3", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ=="], + + "yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="], + + "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], + + "zod-openapi": ["zod-openapi@4.2.4", "", { "peerDependencies": { "zod": "^3.21.4" } }, "sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], + + "zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-alpha.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-lhdrARU3SSmt5p/GNNK7VhazvZpKSCIOjpHUfX7f5jIhVGi/vvlxP1rD6Go57nn1MtuGKNqL04AebSRFDQsQbw=="], + + "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.7", "@standard-schema/spec": "^1.0.0", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-AYkT3jskmo7Lwzijo/yHKD1jC+UZizsROO8ULTg9aJZUwR4ABZzAxh4NxDIEy4TWRfBGufp+/9ICHAn6pkU71w=="], + + "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "args/camelcase": ["camelcase@5.0.0", "", {}, "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA=="], + + "args/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "astro/diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="], + + "astro/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + + "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], + + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "opencode/ai": ["ai@5.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-alpha.7", "@ai-sdk/provider": "2.0.0-alpha.7", "@ai-sdk/provider-utils": "3.0.0-alpha.7", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-ShCk3frIMdVtK9knvWKiFS7N6Vwnf8mLMv670+T//W9oqfoetSVPBhTF6Dy+oDM/bjVSsBf1BuYImLDvHICOIQ=="], + + "opencontrol/hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="], + + "opencontrol/zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], + + "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], + + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "pino-abstract-transport/split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "pino-pretty/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "pino-pretty/jmespath": ["jmespath@0.15.0", "", {}, "sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w=="], + + "prebuild-install/tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="], + + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + + "sitemap/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="], + + "sitemap/sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="], + + "token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "unstorage/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.1.0", "", {}, "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw=="], + + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "args/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "args/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "opencode/ai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-alpha.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-lhdrARU3SSmt5p/GNNK7VhazvZpKSCIOjpHUfX7f5jIhVGi/vvlxP1rD6Go57nn1MtuGKNqL04AebSRFDQsQbw=="], + + "opencode/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.7", "@standard-schema/spec": "^1.0.0", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-AYkT3jskmo7Lwzijo/yHKD1jC+UZizsROO8ULTg9aJZUwR4ABZzAxh4NxDIEy4TWRfBGufp+/9ICHAn6pkU71w=="], + + "pino-pretty/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "args/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "args/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "args/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + } +} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index 85258d591..000000000 --- a/cmd/root.go +++ /dev/null @@ -1,258 +0,0 @@ -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/go.mod b/go.mod deleted file mode 100644 index ebd00828b..000000000 --- a/go.mod +++ /dev/null @@ -1,105 +0,0 @@ -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/go.sum b/go.sum deleted file mode 100644 index c11a945ac..000000000 --- a/go.sum +++ /dev/null @@ -1,338 +0,0 @@ -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/infra/app.ts b/infra/app.ts new file mode 100644 index 000000000..73b80994d --- /dev/null +++ b/infra/app.ts @@ -0,0 +1,44 @@ +export const domain = (() => { + if ($app.stage === "production") return "opencode.ai" + if ($app.stage === "dev") return "dev.opencode.ai" + return `${$app.stage}.dev.opencode.ai` +})() + +const bucket = new sst.cloudflare.Bucket("Bucket") + +export const api = new sst.cloudflare.Worker("Api", { + domain: `api.${domain}`, + handler: "packages/function/src/api.ts", + url: true, + link: [bucket], + transform: { + worker: (args) => { + args.logpush = true + args.bindings = $resolve(args.bindings).apply((bindings) => [ + ...bindings, + { + name: "SYNC_SERVER", + type: "durable_object_namespace", + className: "SyncServer", + }, + ]) + args.migrations = { + oldTag: "v1", + newTag: "v1", + //newSqliteClasses: ["SyncServer"], + } + }, + }, +}) + +new sst.cloudflare.StaticSite("Web", { + path: "packages/web", + domain, + environment: { + VITE_API_URL: api.url, + }, + build: { + command: "bun run build", + output: "dist", + }, +}) diff --git a/internal/completions/files-folders.go b/internal/completions/files-folders.go deleted file mode 100644 index a405b34cd..000000000 --- a/internal/completions/files-folders.go +++ /dev/null @@ -1,191 +0,0 @@ -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/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index 2197f5aa4..000000000 --- a/internal/config/config.go +++ /dev/null @@ -1,266 +0,0 @@ -// 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/internal/config/init.go b/internal/config/init.go deleted file mode 100644 index 5f8860f52..000000000 --- a/internal/config/init.go +++ /dev/null @@ -1,60 +0,0 @@ -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/internal/diff/diff.go b/internal/diff/diff.go deleted file mode 100644 index 350db664a..000000000 --- a/internal/diff/diff.go +++ /dev/null @@ -1,869 +0,0 @@ -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(` - -`, - 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/internal/diff/diff_test.go b/internal/diff/diff_test.go deleted file mode 100644 index 4c014e45c..000000000 --- a/internal/diff/diff_test.go +++ /dev/null @@ -1,103 +0,0 @@ -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/internal/diff/patch.go b/internal/diff/patch.go deleted file mode 100644 index 49242f7ef..000000000 --- a/internal/diff/patch.go +++ /dev/null @@ -1,740 +0,0 @@ -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/internal/fileutil/fileutil.go b/internal/fileutil/fileutil.go deleted file mode 100644 index b48152f7a..000000000 --- a/internal/fileutil/fileutil.go +++ /dev/null @@ -1,163 +0,0 @@ -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/internal/format/format.go b/internal/format/format.go deleted file mode 100644 index 321f5c102..000000000 --- a/internal/format/format.go +++ /dev/null @@ -1,46 +0,0 @@ -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/internal/format/format_test.go b/internal/format/format_test.go deleted file mode 100644 index 04054a7c4..000000000 --- a/internal/format/format_test.go +++ /dev/null @@ -1,90 +0,0 @@ -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/internal/pubsub/broker.go b/internal/pubsub/broker.go deleted file mode 100644 index 05a4476c8..000000000 --- a/internal/pubsub/broker.go +++ /dev/null @@ -1,113 +0,0 @@ -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/internal/pubsub/broker_test.go b/internal/pubsub/broker_test.go deleted file mode 100644 index b4caa98f3..000000000 --- a/internal/pubsub/broker_test.go +++ /dev/null @@ -1,144 +0,0 @@ -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/internal/pubsub/events.go b/internal/pubsub/events.go deleted file mode 100644 index e3910f9f5..000000000 --- a/internal/pubsub/events.go +++ /dev/null @@ -1,24 +0,0 @@ -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/internal/status/status.go b/internal/status/status.go deleted file mode 100644 index 3648a64ae..000000000 --- a/internal/status/status.go +++ /dev/null @@ -1,142 +0,0 @@ -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/internal/tui/app/app.go b/internal/tui/app/app.go deleted file mode 100644 index 8320d815b..000000000 --- a/internal/tui/app/app.go +++ /dev/null @@ -1,215 +0,0 @@ -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/internal/tui/app/bridge.go b/internal/tui/app/bridge.go deleted file mode 100644 index cd149f6b3..000000000 --- a/internal/tui/app/bridge.go +++ /dev/null @@ -1,42 +0,0 @@ -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/internal/tui/app/interfaces.go b/internal/tui/app/interfaces.go deleted file mode 100644 index a396ef586..000000000 --- a/internal/tui/app/interfaces.go +++ /dev/null @@ -1,13 +0,0 @@ -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/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go deleted file mode 100644 index 2fabea43d..000000000 --- a/internal/tui/components/chat/chat.go +++ /dev/null @@ -1,133 +0,0 @@ -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/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go deleted file mode 100644 index 6586f2020..000000000 --- a/internal/tui/components/chat/editor.go +++ /dev/null @@ -1,406 +0,0 @@ -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/internal/tui/components/chat/message.go b/internal/tui/components/chat/message.go deleted file mode 100644 index feed7ec59..000000000 --- a/internal/tui/components/chat/message.go +++ /dev/null @@ -1,716 +0,0 @@ -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), ¶ms) - // // prompt := strings.ReplaceAll(params.Prompt, "\n", " ") - // // return renderParams(paramWidth, prompt) - // case "bash": - // var params tools.BashParams - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // command := strings.ReplaceAll(params.Command, "\n", " ") - // return renderParams(paramWidth, command) - // case "edit": - // var params tools.EditParams - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // filePath := removeWorkingDirPrefix(params.FilePath) - // return renderParams(paramWidth, filePath) - // case "fetch": - // var params tools.FetchParams - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // 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), ¶ms) - // 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), ¶ms) - // 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), ¶ms) - // path := params.Path - // if path == "" { - // path = "." - // } - // return renderParams(paramWidth, path) - // case tools.ViewToolName: - // var params tools.ViewParams - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // 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), ¶ms) - // filePath := removeWorkingDirPrefix(params.FilePath) - // return renderParams(paramWidth, filePath) - // case tools.BatchToolName: - // var params tools.BatchParams - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // 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), ¶ms) - // 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), ¶ms) - // 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/internal/tui/components/chat/messages.go b/internal/tui/components/chat/messages.go deleted file mode 100644 index ae6f2a687..000000000 --- a/internal/tui/components/chat/messages.go +++ /dev/null @@ -1,344 +0,0 @@ -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/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go deleted file mode 100644 index d6895a164..000000000 --- a/internal/tui/components/chat/sidebar.go +++ /dev/null @@ -1,220 +0,0 @@ -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/internal/tui/components/core/status.go b/internal/tui/components/core/status.go deleted file mode 100644 index 18a0ad6b1..000000000 --- a/internal/tui/components/core/status.go +++ /dev/null @@ -1,366 +0,0 @@ -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/internal/tui/components/dialog/arguments.go b/internal/tui/components/dialog/arguments.go deleted file mode 100644 index fed79bce3..000000000 --- a/internal/tui/components/dialog/arguments.go +++ /dev/null @@ -1,257 +0,0 @@ -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/internal/tui/components/dialog/commands.go b/internal/tui/components/dialog/commands.go deleted file mode 100644 index b989154c6..000000000 --- a/internal/tui/components/dialog/commands.go +++ /dev/null @@ -1,180 +0,0 @@ -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/internal/tui/components/dialog/complete.go b/internal/tui/components/dialog/complete.go deleted file mode 100644 index 57193d00c..000000000 --- a/internal/tui/components/dialog/complete.go +++ /dev/null @@ -1,263 +0,0 @@ -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/internal/tui/components/dialog/custom_commands.go b/internal/tui/components/dialog/custom_commands.go deleted file mode 100644 index be6746feb..000000000 --- a/internal/tui/components/dialog/custom_commands.go +++ /dev/null @@ -1,186 +0,0 @@ -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/internal/tui/components/dialog/custom_commands_test.go b/internal/tui/components/dialog/custom_commands_test.go deleted file mode 100644 index 3468ac3b0..000000000 --- a/internal/tui/components/dialog/custom_commands_test.go +++ /dev/null @@ -1,106 +0,0 @@ -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/internal/tui/components/dialog/filepicker.go b/internal/tui/components/dialog/filepicker.go deleted file mode 100644 index 088e205f2..000000000 --- a/internal/tui/components/dialog/filepicker.go +++ /dev/null @@ -1,485 +0,0 @@ -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/internal/tui/components/dialog/help.go b/internal/tui/components/dialog/help.go deleted file mode 100644 index 1f7f53e11..000000000 --- a/internal/tui/components/dialog/help.go +++ /dev/null @@ -1,200 +0,0 @@ -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/internal/tui/components/dialog/init.go b/internal/tui/components/dialog/init.go deleted file mode 100644 index 2ef8546f6..000000000 --- a/internal/tui/components/dialog/init.go +++ /dev/null @@ -1,189 +0,0 @@ -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/internal/tui/components/dialog/models.go b/internal/tui/components/dialog/models.go deleted file mode 100644 index 2dd1e2fea..000000000 --- a/internal/tui/components/dialog/models.go +++ /dev/null @@ -1,327 +0,0 @@ -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/internal/tui/components/dialog/permission.go b/internal/tui/components/dialog/permission.go deleted file mode 100644 index 0e5afdeab..000000000 --- a/internal/tui/components/dialog/permission.go +++ /dev/null @@ -1,502 +0,0 @@ -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/internal/tui/components/dialog/quit.go b/internal/tui/components/dialog/quit.go deleted file mode 100644 index 3fd2ea920..000000000 --- a/internal/tui/components/dialog/quit.go +++ /dev/null @@ -1,136 +0,0 @@ -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/internal/tui/components/dialog/session.go b/internal/tui/components/dialog/session.go deleted file mode 100644 index 99aa41515..000000000 --- a/internal/tui/components/dialog/session.go +++ /dev/null @@ -1,230 +0,0 @@ -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/internal/tui/components/dialog/theme.go b/internal/tui/components/dialog/theme.go deleted file mode 100644 index 54856e8a9..000000000 --- a/internal/tui/components/dialog/theme.go +++ /dev/null @@ -1,199 +0,0 @@ -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/internal/tui/components/dialog/tools.go b/internal/tui/components/dialog/tools.go deleted file mode 100644 index 76e6ff227..000000000 --- a/internal/tui/components/dialog/tools.go +++ /dev/null @@ -1,178 +0,0 @@ -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/internal/tui/components/qr/qr.go b/internal/tui/components/qr/qr.go deleted file mode 100644 index 42a60bb5e..000000000 --- a/internal/tui/components/qr/qr.go +++ /dev/null @@ -1,58 +0,0 @@ -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/internal/tui/components/spinner/spinner.go b/internal/tui/components/spinner/spinner.go deleted file mode 100644 index 5e1af8771..000000000 --- a/internal/tui/components/spinner/spinner.go +++ /dev/null @@ -1,127 +0,0 @@ -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/internal/tui/components/spinner/spinner_test.go b/internal/tui/components/spinner/spinner_test.go deleted file mode 100644 index 065726e91..000000000 --- a/internal/tui/components/spinner/spinner_test.go +++ /dev/null @@ -1,24 +0,0 @@ -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/internal/tui/components/util/simple-list.go b/internal/tui/components/util/simple-list.go deleted file mode 100644 index 7b8c4b1cb..000000000 --- a/internal/tui/components/util/simple-list.go +++ /dev/null @@ -1,159 +0,0 @@ -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/internal/tui/image/clipboard_unix.go b/internal/tui/image/clipboard_unix.go deleted file mode 100644 index 3cb590207..000000000 --- a/internal/tui/image/clipboard_unix.go +++ /dev/null @@ -1,49 +0,0 @@ -//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/internal/tui/image/clipboard_windows.go b/internal/tui/image/clipboard_windows.go deleted file mode 100644 index 6431ce3d4..000000000 --- a/internal/tui/image/clipboard_windows.go +++ /dev/null @@ -1,192 +0,0 @@ -//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/internal/tui/image/images.go b/internal/tui/image/images.go deleted file mode 100644 index f476b201c..000000000 --- a/internal/tui/image/images.go +++ /dev/null @@ -1,85 +0,0 @@ -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/internal/tui/layout/container.go b/internal/tui/layout/container.go deleted file mode 100644 index b5bdca20a..000000000 --- a/internal/tui/layout/container.go +++ /dev/null @@ -1,230 +0,0 @@ -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/internal/tui/layout/layout.go b/internal/tui/layout/layout.go deleted file mode 100644 index 495a3fbc5..000000000 --- a/internal/tui/layout/layout.go +++ /dev/null @@ -1,35 +0,0 @@ -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/internal/tui/layout/overlay.go b/internal/tui/layout/overlay.go deleted file mode 100644 index 64836463d..000000000 --- a/internal/tui/layout/overlay.go +++ /dev/null @@ -1,169 +0,0 @@ -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/internal/tui/layout/split.go b/internal/tui/layout/split.go deleted file mode 100644 index 81e159517..000000000 --- a/internal/tui/layout/split.go +++ /dev/null @@ -1,283 +0,0 @@ -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/internal/tui/page/chat.go b/internal/tui/page/chat.go deleted file mode 100644 index bf30193f5..000000000 --- a/internal/tui/page/chat.go +++ /dev/null @@ -1,233 +0,0 @@ -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/internal/tui/page/page.go b/internal/tui/page/page.go deleted file mode 100644 index 482df5fd7..000000000 --- a/internal/tui/page/page.go +++ /dev/null @@ -1,8 +0,0 @@ -package page - -type PageID string - -// PageChangeMsg is used to change the current page -type PageChangeMsg struct { - ID PageID -} diff --git a/internal/tui/state/state.go b/internal/tui/state/state.go deleted file mode 100644 index 6b117518e..000000000 --- a/internal/tui/state/state.go +++ /dev/null @@ -1,19 +0,0 @@ -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/internal/tui/styles/background.go b/internal/tui/styles/background.go deleted file mode 100644 index 2fbb34efb..000000000 --- a/internal/tui/styles/background.go +++ /dev/null @@ -1,123 +0,0 @@ -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/internal/tui/styles/icons.go b/internal/tui/styles/icons.go deleted file mode 100644 index 8ff5fe8bf..000000000 --- a/internal/tui/styles/icons.go +++ /dev/null @@ -1,12 +0,0 @@ -package styles - -const ( - OpenCodeIcon string = "◍" - - ErrorIcon string = "ⓔ" - WarningIcon string = "ⓦ" - InfoIcon string = "ⓘ" - HintIcon string = "ⓗ" - SpinnerIcon string = "⟳" - DocumentIcon string = "🖼" -) diff --git a/internal/tui/styles/markdown.go b/internal/tui/styles/markdown.go deleted file mode 100644 index 77fb51bae..000000000 --- a/internal/tui/styles/markdown.go +++ /dev/null @@ -1,283 +0,0 @@ -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/internal/tui/styles/styles.go b/internal/tui/styles/styles.go deleted file mode 100644 index 91661a1dd..000000000 --- a/internal/tui/styles/styles.go +++ /dev/null @@ -1,153 +0,0 @@ -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/internal/tui/theme/ayu.go b/internal/tui/theme/ayu.go deleted file mode 100644 index b3dfa870a..000000000 --- a/internal/tui/theme/ayu.go +++ /dev/null @@ -1,280 +0,0 @@ -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/internal/tui/theme/catppuccin.go b/internal/tui/theme/catppuccin.go deleted file mode 100644 index c3c32501e..000000000 --- a/internal/tui/theme/catppuccin.go +++ /dev/null @@ -1,248 +0,0 @@ -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/internal/tui/theme/dracula.go b/internal/tui/theme/dracula.go deleted file mode 100644 index 29a1457d4..000000000 --- a/internal/tui/theme/dracula.go +++ /dev/null @@ -1,274 +0,0 @@ -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/internal/tui/theme/flexoki.go b/internal/tui/theme/flexoki.go deleted file mode 100644 index 5da5683c5..000000000 --- a/internal/tui/theme/flexoki.go +++ /dev/null @@ -1,282 +0,0 @@ -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/internal/tui/theme/gruvbox.go b/internal/tui/theme/gruvbox.go deleted file mode 100644 index 51719faaa..000000000 --- a/internal/tui/theme/gruvbox.go +++ /dev/null @@ -1,302 +0,0 @@ -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/internal/tui/theme/manager.go b/internal/tui/theme/manager.go deleted file mode 100644 index 5a5c791fb..000000000 --- a/internal/tui/theme/manager.go +++ /dev/null @@ -1,265 +0,0 @@ -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/internal/tui/theme/monokai.go b/internal/tui/theme/monokai.go deleted file mode 100644 index 7511d3333..000000000 --- a/internal/tui/theme/monokai.go +++ /dev/null @@ -1,273 +0,0 @@ -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/internal/tui/theme/onedark.go b/internal/tui/theme/onedark.go deleted file mode 100644 index a2c1447ca..000000000 --- a/internal/tui/theme/onedark.go +++ /dev/null @@ -1,274 +0,0 @@ -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/internal/tui/theme/opencode.go b/internal/tui/theme/opencode.go deleted file mode 100644 index 7ee6f15e5..000000000 --- a/internal/tui/theme/opencode.go +++ /dev/null @@ -1,276 +0,0 @@ -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/internal/tui/theme/theme.go b/internal/tui/theme/theme.go deleted file mode 100644 index c97b95478..000000000 --- a/internal/tui/theme/theme.go +++ /dev/null @@ -1,290 +0,0 @@ -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/internal/tui/theme/theme_test.go b/internal/tui/theme/theme_test.go deleted file mode 100644 index 790ee3aa8..000000000 --- a/internal/tui/theme/theme_test.go +++ /dev/null @@ -1,89 +0,0 @@ -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/internal/tui/theme/tokyonight.go b/internal/tui/theme/tokyonight.go deleted file mode 100644 index a6499a25d..000000000 --- a/internal/tui/theme/tokyonight.go +++ /dev/null @@ -1,274 +0,0 @@ -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/internal/tui/theme/tron.go b/internal/tui/theme/tron.go deleted file mode 100644 index c4997a6d1..000000000 --- a/internal/tui/theme/tron.go +++ /dev/null @@ -1,276 +0,0 @@ -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/internal/tui/tui.go b/internal/tui/tui.go deleted file mode 100644 index 236d5e453..000000000 --- a/internal/tui/tui.go +++ /dev/null @@ -1,988 +0,0 @@ -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/internal/tui/util/util.go b/internal/tui/util/util.go deleted file mode 100644 index 207382d1e..000000000 --- a/internal/tui/util/util.go +++ /dev/null @@ -1,18 +0,0 @@ -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/internal/version/version.go b/internal/version/version.go deleted file mode 100644 index 69fd5282b..000000000 --- a/internal/version/version.go +++ /dev/null @@ -1,25 +0,0 @@ -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/js/.gitignore b/js/.gitignore deleted file mode 100644 index 5f9a3e7af..000000000 --- a/js/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -dist -gen diff --git a/js/README.md b/js/README.md deleted file mode 100644 index 75890119c..000000000 --- a/js/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# 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/js/bun.lock b/js/bun.lock deleted file mode 100644 index 5319f5bc4..000000000 --- a/js/bun.lock +++ /dev/null @@ -1,348 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "js", - "dependencies": { - "@ai-sdk/anthropic": "^2.0.0-alpha.4", - "@flystorage/file-storage": "^1.1.0", - "@flystorage/local-fs": "^1.1.0", - "@hono/zod-validator": "^0.5.0", - "ai": "^5.0.0-alpha.4", - "cac": "^6.7.14", - "clipanion": "^4.0.0-rc.4", - "decimal.js": "^10.5.0", - "diff": "^8.0.2", - "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", - }, - "devDependencies": { - "@tsconfig/bun": "^1.0.7", - "@types/bun": "latest", - "@types/jsdom": "^21.1.7", - "@types/turndown": "^5.0.5", - }, - "peerDependencies": { - "typescript": "5", - }, - }, - }, - "packages": { - "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0-alpha.4", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.4", "@ai-sdk/provider-utils": "3.0.0-alpha.4" }, "peerDependencies": { "zod": "^3.24.0" } }, "sha512-uknR4CfBVjkxhlLqOz0ThCtsH5GzRT6IBVzV5oI/n+CgOSwWNQXfsyBmmO8RopPCEHq+HEb5xSbG/l/FmkxafA=="], - - "@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-alpha.4", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-g6IVjm0iGasiv4fAv6TtPIDTfeMTqFRQt3J6Jz8skm/Tb2r48qKEtcKNrDSpspDIFrQJ4fuC1Xw/aAPrVCs7tQ=="], - - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-alpha.4", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.4", "@standard-schema/spec": "^1.0.0", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-edEnOh8i676TLX8ibWD69gHTaU6btJ9uCN+7/8vXWlMHZJo2ST4mgjRP5f6xC4rH8XXVMYK0rVPwbnuyR/PGzA=="], - - "@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.9.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="], - - "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], - - "@csstools/color-helpers": ["@csstools/color-helpers@5.0.2", "", {}, "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA=="], - - "@csstools/css-calc": ["@csstools/css-calc@2.1.3", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3" } }, "sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw=="], - - "@csstools/css-color-parser": ["@csstools/css-color-parser@3.0.9", "", { "dependencies": { "@csstools/color-helpers": "^5.0.2", "@csstools/css-calc": "^2.1.3" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3" } }, "sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw=="], - - "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.4", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.3" } }, "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A=="], - - "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.3", "", {}, "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw=="], - - "@flystorage/dynamic-import": ["@flystorage/dynamic-import@1.0.0", "", {}, "sha512-CIbIUrBdaPFyKnkVBaqzksvzNtsMSXITR/G/6zlil3MBnPFq2LX+X4Mv5p2XOmv/3OulFs/ff2SNb+5dc2Twtg=="], - - "@flystorage/file-storage": ["@flystorage/file-storage@1.1.0", "", {}, "sha512-25Gd5EsXDmhHrK5orpRuVqebQms1Cm9m5ACMZ0sVDX+Sbl1V0G88CbcWt7mEoWRYLvQ1U072htqg6Sav76ZlVA=="], - - "@flystorage/local-fs": ["@flystorage/local-fs@1.1.0", "", { "dependencies": { "@flystorage/dynamic-import": "^1.0.0", "@flystorage/file-storage": "^1.1.0", "file-type": "^20.5.0", "mime-types": "^3.0.1" } }, "sha512-dbErRhqmCv2UF0zPdeH7iVWuVeTWAJHuJD/mXDe2V370/SL7XIvdE3ditBHWC+1SzBKXJ0lkykOenwlum+oqIA=="], - - "@hapi/bourne": ["@hapi/bourne@2.1.0", "", {}, "sha512-i1BpaNDVLJdRBEKeJWkVO6tYX6DMFBuwMhSuWqLsY4ufeTKGVuV5rBsUhxPayXqnnWHgXUAmWK16H/ykO5Wj4Q=="], - - "@hono/zod-validator": ["@hono/zod-validator@0.5.0", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-ds5bW6DCgAnNHP33E3ieSbaZFd5dkV52ZjyaXtGoR06APFrCtzAsKZxTHwOrJNBdXsi0e5wNwo5L4nVEVnJUdg=="], - - "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], - - "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], - - "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], - - "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="], - - "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - - "@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="], - - "@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="], - - "@types/jsdom": ["@types/jsdom@21.1.7", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA=="], - - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - - "@types/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="], - - "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], - - "@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="], - - "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], - - "ai": ["ai@5.0.0-alpha.4", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.4", "@ai-sdk/provider-utils": "3.0.0-alpha.4", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-u2JGxWcmL19knVOhIMMNFp1k2/8Z4VpsT6BBg6YcpL5Vh1rl7y657sFTLQe3U+pao4b5oxwzd8rKR0ETvVRjnw=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "args": ["args@5.0.3", "", { "dependencies": { "camelcase": "5.0.0", "chalk": "2.4.2", "leven": "2.1.0", "mri": "1.1.4" } }, "sha512-h6k/zfFgusnv3i5TU08KQkVKuCPBtL/PWQbWkHUxvJrZ2nAyeaUupneemcrgn1xmqxPQsPIzwkUhOpoqPDRZuA=="], - - "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], - - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - - "bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="], - - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - - "camelcase": ["camelcase@5.0.0", "", {}, "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA=="], - - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "clipanion": ["clipanion@4.0.0-rc.4", "", { "dependencies": { "typanion": "^3.8.0" } }, "sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q=="], - - "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "cssstyle": ["cssstyle@4.3.1", "", { "dependencies": { "@asamuzakjp/css-color": "^3.1.2", "rrweb-cssom": "^0.8.0" } }, "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q=="], - - "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], - - "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], - - "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], - - "duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="], - - "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], - - "entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="], - - "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], - - "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - - "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], - - "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], - - "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], - - "file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "hono": ["hono@4.7.10", "", {}, "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ=="], - - "hono-openapi": ["hono-openapi@0.4.8", "", { "dependencies": { "json-schema-walker": "^2.0.0" }, "peerDependencies": { "@hono/arktype-validator": "^2.0.0", "@hono/effect-validator": "^1.2.0", "@hono/typebox-validator": "^0.2.0 || ^0.3.0", "@hono/valibot-validator": "^0.5.1", "@hono/zod-validator": "^0.4.1", "@sinclair/typebox": "^0.34.9", "@valibot/to-json-schema": "^1.0.0-beta.3", "arktype": "^2.0.0", "effect": "^3.11.3", "hono": "^4.6.13", "openapi-types": "^12.1.3", "valibot": "^1.0.0-beta.9", "zod": "^3.23.8", "zod-openapi": "^4.0.0" }, "optionalPeers": ["@hono/arktype-validator", "@hono/effect-validator", "@hono/typebox-validator", "@hono/valibot-validator", "@hono/zod-validator", "@sinclair/typebox", "@valibot/to-json-schema", "arktype", "effect", "hono", "valibot", "zod", "zod-openapi"] }, "sha512-LYr5xdtD49M7hEAduV1PftOMzuT8ZNvkyWfh1DThkLsIr4RkvDb12UxgIiFbwrJB6FLtFXLoOZL9x4IeDk2+VA=="], - - "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], - - "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], - - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], - - "jmespath": ["jmespath@0.15.0", "", {}, "sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w=="], - - "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], - - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - - "jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="], - - "json-rpc-2.0": ["json-rpc-2.0@1.7.0", "", {}, "sha512-asnLgC1qD5ytP+fvBP8uL0rvj+l8P6iYICbzZ8dVxCpESffVjzA7KkYkbKCIbavs7cllwH1ZUaNtJwphdeRqpg=="], - - "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], - - "json-schema-walker": ["json-schema-walker@2.0.0", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.1.0", "clone": "^2.1.2" } }, "sha512-nXN2cMky0Iw7Af28w061hmxaPDaML5/bQD9nwm1lOoIKEGjHcRGxqWe4MfrkYThYAPjSUhmsp4bJNoLAyVn9Xw=="], - - "leven": ["leven@2.1.0", "", {}, "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA=="], - - "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], - - "minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], - - "mri": ["mri@1.1.4", "", {}, "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nwsapi": ["nwsapi@2.2.20", "", {}, "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA=="], - - "on-exit-leak-free": ["on-exit-leak-free@0.2.0", "", {}, "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - - "peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="], - - "pino": ["pino@7.11.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="], - - "pino-abstract-transport": ["pino-abstract-transport@0.5.0", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="], - - "pino-pretty": ["pino-pretty@5.1.3", "", { "dependencies": { "@hapi/bourne": "^2.0.0", "args": "^5.0.1", "chalk": "^4.0.0", "dateformat": "^4.5.1", "fast-safe-stringify": "^2.0.7", "jmespath": "^0.15.0", "joycon": "^3.0.0", "pump": "^3.0.0", "readable-stream": "^3.6.0", "rfdc": "^1.3.0", "split2": "^3.1.1", "strip-json-comments": "^3.1.1" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-Zj+0TVdYKkAAIx9EUCL5e4TttwgsaFvJh2ceIMQeFCY8ak9tseEZQGSgpvyjEj1/iIVGIh5tdhkGEQWSMILKHA=="], - - "pino-std-serializers": ["pino-std-serializers@4.0.0", "", {}, "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q=="], - - "process-warning": ["process-warning@1.0.0", "", {}, "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="], - - "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], - - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], - - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "real-require": ["real-require@0.1.0", "", {}, "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg=="], - - "remeda": ["remeda@2.22.3", "", { "dependencies": { "type-fest": "^4.40.1" } }, "sha512-Ka6965m9Zu9OLsysWxVf3jdJKmp6+PKzDv7HWHinEevf0JOJ9y02YpjiC/sKxRpCqGhVyvm1U+0YIj+E6DMgKw=="], - - "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - - "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], - - "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "sonic-boom": ["sonic-boom@2.8.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg=="], - - "split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="], - - "stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="], - - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - - "strtok3": ["strtok3@10.2.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], - - "thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="], - - "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], - - "tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="], - - "token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="], - - "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], - - "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], - - "ts-lsp-client": ["ts-lsp-client@1.0.3", "", { "dependencies": { "json-rpc-2.0": "^1.7.0", "pino": "^7.0.5", "pino-pretty": "^5.1.3", "tslib": "~2.6.2" } }, "sha512-0ItrsqvNUM9KNFGbeT1N8jSi9gvasGOvxJUXjGf4P2TX0w250AUWLeRStaSrQbYcFDshDtE5d4BshUmYwodDgw=="], - - "tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="], - - "turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="], - - "typanion": ["typanion@3.14.0", "", {}, "sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug=="], - - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - - "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="], - - "vscode-languageclient": ["vscode-languageclient@8.1.0", "", { "dependencies": { "minimatch": "^5.1.0", "semver": "^7.3.7", "vscode-languageserver-protocol": "3.17.3" } }, "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing=="], - - "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.3", "", { "dependencies": { "vscode-jsonrpc": "8.1.0", "vscode-languageserver-types": "3.17.3" } }, "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA=="], - - "vscode-languageserver-types": ["vscode-languageserver-types@3.17.3", "", {}, "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA=="], - - "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], - - "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], - - "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], - - "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], - - "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="], - - "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], - - "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], - - "zod": ["zod@3.25.3", "", {}, "sha512-VGZqnyYNrl8JpEJRZaFPqeVNIuqgXNu4cXZ5cOb6zEUO1OxKbRnWB4UdDIXMmiERWncs0yDQukssHov8JUxykQ=="], - - "zod-openapi": ["zod-openapi@4.2.4", "", { "peerDependencies": { "zod": "^3.21.4" } }, "sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g=="], - - "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], - - "args/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - - "pino-abstract-transport/split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - - "vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.1.0", "", {}, "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw=="], - - "args/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], - - "args/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - - "args/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "args/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - - "args/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - } -} diff --git a/js/package.json b/js/package.json deleted file mode 100644 index d386dac89..000000000 --- a/js/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/package.json", - "name": "opencode", - "type": "module", - "private": true, - "scripts": { - "build": "bun build src/index.ts --compile --outfile ./dist/opencode" - }, - "devDependencies": { - "@tsconfig/bun": "^1.0.7", - "@types/bun": "latest", - "@types/jsdom": "^21.1.7", - "@types/turndown": "^5.0.5" - }, - "peerDependencies": { - "typescript": "5" - }, - "dependencies": { - "@ai-sdk/anthropic": "^2.0.0-alpha.4", - "@flystorage/file-storage": "^1.1.0", - "@flystorage/local-fs": "^1.1.0", - "@hono/zod-validator": "^0.5.0", - "ai": "^5.0.0-alpha.4", - "cac": "^6.7.14", - "clipanion": "^4.0.0-rc.4", - "decimal.js": "^10.5.0", - "diff": "^8.0.2", - "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/js/scrap.ts b/js/scrap.ts deleted file mode 100644 index 35ff3fbfb..000000000 --- a/js/scrap.ts +++ /dev/null @@ -1,30 +0,0 @@ -// 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 { - return ['dummy', 'mega', 'function']; -} \ No newline at end of file diff --git a/js/src/app/app.ts b/js/src/app/app.ts deleted file mode 100644 index 0c6260bc7..000000000 --- a/js/src/app/app.ts +++ /dev/null @@ -1,78 +0,0 @@ -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>; - - const ctx = Context.create("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; - } - >(); - - const result = { - get services() { - return services; - }, - get root() { - return input.directory; - }, - }; - - return result; - } - - export function state( - key: any, - init: (app: Info) => State, - shutdown?: (state: Awaited) => Promise, - ) { - 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 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/js/src/app/path.ts b/js/src/app/path.ts deleted file mode 100644 index 972d18c41..000000000 --- a/js/src/app/path.ts +++ /dev/null @@ -1,11 +0,0 @@ -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/js/src/bun/index.ts b/js/src/bun/index.ts deleted file mode 100644 index 35e8cbbf9..000000000 --- a/js/src/bun/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -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, - ) { - 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/js/src/bus/index.ts b/js/src/bus/index.ts deleted file mode 100644 index 342d82b6d..000000000 --- a/js/src/bus/index.ts +++ /dev/null @@ -1,101 +0,0 @@ -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(); - - return { - subscriptions, - }; - }); - - export type EventDefinition = ReturnType; - - const registry = new Map(); - - export function event( - 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( - def: Definition, - properties: z.output, - ) { - 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( - def: Definition, - callback: (event: { - type: Definition["type"]; - properties: z.infer; - }) => 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/js/src/config/config.ts b/js/src/config/config.ts deleted file mode 100644 index 8c374b3f3..000000000 --- a/js/src/config/config.ts +++ /dev/null @@ -1,51 +0,0 @@ -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; - - 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/js/src/global/index.ts b/js/src/global/index.ts deleted file mode 100644 index 1e097f38c..000000000 --- a/js/src/global/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/js/src/id/id.ts b/js/src/id/id.ts deleted file mode 100644 index 62c6a12bf..000000000 --- a/js/src/id/id.ts +++ /dev/null @@ -1,74 +0,0 @@ -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/js/src/index.ts b/js/src/index.ts deleted file mode 100644 index c86e60955..000000000 --- a/js/src/index.ts +++ /dev/null @@ -1,85 +0,0 @@ -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 ", "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 = Object.keys(providers[providerID].info.models!)[0]; - 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/js/src/llm/llm.ts b/js/src/llm/llm.ts deleted file mode 100644 index a7d31fb99..000000000 --- a/js/src/llm/llm.ts +++ /dev/null @@ -1,172 +0,0 @@ -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 = { - 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/js/src/lsp/client.ts b/js/src/lsp/client.ts deleted file mode 100644 index 82caa82a2..000000000 --- a/js/src/lsp/client.ts +++ /dev/null @@ -1,208 +0,0 @@ -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>; - - 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(); - 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(); - - 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(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((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/js/src/lsp/index.ts b/js/src/lsp/index.ts deleted file mode 100644 index e3344a934..000000000 --- a/js/src/lsp/index.ts +++ /dev/null @@ -1,131 +0,0 @@ -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(); - - 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 = {}; - 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( - input: (client: LSPClient.Info) => Promise, - ): Promise { - 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; - }[] = [ - { - 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/js/src/lsp/language.ts b/js/src/lsp/language.ts deleted file mode 100644 index e28d7a79c..000000000 --- a/js/src/lsp/language.ts +++ /dev/null @@ -1,89 +0,0 @@ -export const LANGUAGE_EXTENSIONS: Record = { - ".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/js/src/provider/provider.ts b/js/src/provider/provider.ts deleted file mode 100644 index d4719ffbb..000000000 --- a/js/src/provider/provider.ts +++ /dev/null @@ -1,35 +0,0 @@ -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; - - 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; -} diff --git a/js/src/server/server.ts b/js/src/server/server.ts deleted file mode 100644 index 28591cbd2..000000000 --- a/js/src/server/server.ts +++ /dev/null @@ -1,309 +0,0 @@ -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; - - 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((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/js/src/session/message.ts b/js/src/session/message.ts deleted file mode 100644 index 11ec5c6b3..000000000 --- a/js/src/session/message.ts +++ /dev/null @@ -1,171 +0,0 @@ -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>(), - }) - .openapi({ - ref: "Message.ToolInvocation.ToolCall", - }); - export type ToolCall = z.infer; - - export const ToolPartialCall = z - .object({ - state: z.literal("partial-call"), - step: z.number().optional(), - toolCallId: z.string(), - toolName: z.string(), - args: z.custom>(), - }) - .openapi({ - ref: "Message.ToolInvocation.ToolPartialCall", - }); - export type ToolPartialCall = z.infer; - - export const ToolResult = z - .object({ - state: z.literal("result"), - step: z.number().optional(), - toolCallId: z.string(), - toolName: z.string(), - args: z.custom>(), - result: z.string(), - }) - .openapi({ - ref: "Message.ToolInvocation.ToolResult", - }); - export type ToolResult = z.infer; - - export const ToolInvocation = z - .discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]) - .openapi({ - ref: "Message.ToolInvocation", - }); - export type ToolInvocation = z.infer; - - export const TextPart = z - .object({ - type: z.literal("text"), - text: z.string(), - }) - .openapi({ - ref: "Message.Part.Text", - }); - export type TextPart = z.infer; - - 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; - - export const ToolInvocationPart = z - .object({ - type: z.literal("tool-invocation"), - toolInvocation: ToolInvocation, - }) - .openapi({ - ref: "Message.Part.ToolInvocation", - }); - export type ToolInvocationPart = z.infer; - - 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; - - 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; - - export const StepStartPart = z - .object({ - type: z.literal("step-start"), - }) - .openapi({ - ref: "Message.Part.StepStart", - }); - export type StepStartPart = z.infer; - - export const Part = z - .discriminatedUnion("type", [ - TextPart, - ReasoningPart, - ToolInvocationPart, - SourceUrlPart, - FilePart, - StepStartPart, - ]) - .openapi({ - ref: "Message.Part", - }); - export type Part = z.infer; - - 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; - - export const Event = { - Updated: Bus.event( - "message.updated", - z.object({ - info: Info, - }), - ), - }; -} diff --git a/js/src/session/prompt/anthropic.txt b/js/src/session/prompt/anthropic.txt deleted file mode 100644 index c8ff697e8..000000000 --- a/js/src/session/prompt/anthropic.txt +++ /dev/null @@ -1,95 +0,0 @@ -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 .", "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: - -user: 2 + 2 -assistant: 4 - - - -user: what is 2+2? -assistant: 4 - - - -user: is 11 a prime number? -assistant: yes - - - -user: what command should I run to list files in the current directory? -assistant: ls - - - -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 - - - -user: How many golf balls fit inside a jetta? -assistant: 150000 - - - -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 - - - -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] - - -# 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/js/src/session/prompt/summarize.txt b/js/src/session/prompt/summarize.txt deleted file mode 100644 index 4751c8d71..000000000 --- a/js/src/session/prompt/summarize.txt +++ /dev/null @@ -1,10 +0,0 @@ -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/js/src/session/prompt/title.txt b/js/src/session/prompt/title.txt deleted file mode 100644 index a57f9082b..000000000 --- a/js/src/session/prompt/title.txt +++ /dev/null @@ -1,7 +0,0 @@ -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/js/src/session/session.ts b/js/src/session/session.ts deleted file mode 100644 index 6eb5f0373..000000000 --- a/js/src/session/session.ts +++ /dev/null @@ -1,498 +0,0 @@ -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; - - export const Event = { - Updated: Bus.event( - "session.updated", - z.object({ - info: Info, - }), - ), - }; - - const state = App.state("session", () => { - const sessions = new Map(); - const messages = new Map(); - - 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("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(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(); - 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/js/src/share/share.ts b/js/src/share/share.ts deleted file mode 100644 index 8d4af8795..000000000 --- a/js/src/share/share.ts +++ /dev/null @@ -1,67 +0,0 @@ -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 = Promise.resolve(); - const pending = new Map(); - - 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/js/src/storage/storage.ts b/js/src/storage/storage.ts deleted file mode 100644 index 983b7e0f9..000000000 --- a/js/src/storage/storage.ts +++ /dev/null @@ -1,55 +0,0 @@ -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(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(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/js/src/tool/bash.ts b/js/src/tool/bash.ts deleted file mode 100644 index 5ad92e3c8..000000000 --- a/js/src/tool/bash.ts +++ /dev/null @@ -1,199 +0,0 @@ -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. - -pytest /foo/bar/tests - - -cd /foo/bar && pytest tests - - -# 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 tags: - - -- 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 - - -4. Create the commit with a message ending with: -🤖 Generated with opencode -Co-Authored-By: opencode - -- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example: - -git commit -m "$(cat <<'EOF' - Commit message here. - - 🤖 Generated with opencode - Co-Authored-By: opencode - EOF - )" - - -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 tags: - - -- 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 - - -6. Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting. - -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 -)" - - -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/js/src/tool/edit.ts b/js/src/tool/edit.ts deleted file mode 100644 index 9ba99a3be..000000000 --- a/js/src/tool/edit.ts +++ /dev/null @@ -1,136 +0,0 @@ -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\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n\n`; - continue; - } - output += `\n\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n\n`; - } - - return { - metadata: { - diagnostics, - }, - output, - }; - }, -}); diff --git a/js/src/tool/fetch.ts b/js/src/tool/fetch.ts deleted file mode 100644 index 573e0eec7..000000000 --- a/js/src/tool/fetch.ts +++ /dev/null @@ -1,137 +0,0 @@ -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/js/src/tool/glob.ts b/js/src/tool/glob.ts deleted file mode 100644 index da47ee4ca..000000000 --- a/js/src/tool/glob.ts +++ /dev/null @@ -1,96 +0,0 @@ -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/js/src/tool/grep.ts b/js/src/tool/grep.ts deleted file mode 100644 index 8b2375f63..000000000 --- a/js/src/tool/grep.ts +++ /dev/null @@ -1,345 +0,0 @@ -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 { - 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 { - 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/js/src/tool/index.ts b/js/src/tool/index.ts deleted file mode 100644 index 3930c87c4..000000000 --- a/js/src/tool/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -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/js/src/tool/ls.ts b/js/src/tool/ls.ts deleted file mode 100644 index 46efb307a..000000000 --- a/js/src/tool/ls.ts +++ /dev/null @@ -1,96 +0,0 @@ -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(); - const filesByDir = new Map(); - - 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/js/src/tool/lsp-diagnostics.ts b/js/src/tool/lsp-diagnostics.ts deleted file mode 100644 index 736efc034..000000000 --- a/js/src/tool/lsp-diagnostics.ts +++ /dev/null @@ -1,53 +0,0 @@ -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/js/src/tool/lsp-hover.ts b/js/src/tool/lsp-hover.ts deleted file mode 100644 index c7a132645..000000000 --- a/js/src/tool/lsp-hover.ts +++ /dev/null @@ -1,38 +0,0 @@ -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/js/src/tool/patch.ts b/js/src/tool/patch.ts deleted file mode 100644 index 9f9192fda..000000000 --- a/js/src/tool/patch.ts +++ /dev/null @@ -1,420 +0,0 @@ -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; -} - -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, -): [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, -): Commit { - const changes: Record = {}; - - 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, - deleteFile: (path: string) => Promise, -): Promise { - 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 = {}; - 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/js/src/tool/tool.ts b/js/src/tool/tool.ts deleted file mode 100644 index 0cd65e007..000000000 --- a/js/src/tool/tool.ts +++ /dev/null @@ -1,61 +0,0 @@ -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 = Record, - > { - properties: Properties; - time: { - start: number; - end: number; - }; - } - export function define< - Params, - Output extends { metadata?: any; output: any }, - Name extends string, - >( - input: AITool & { - 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 = { - ...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/js/src/tool/util/file-times.ts b/js/src/tool/util/file-times.ts deleted file mode 100644 index 8d36d007d..000000000 --- a/js/src/tool/util/file-times.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { App } from "../../app/app"; - -export namespace FileTimes { - export const state = App.state("tool.filetimes", () => ({ - read: new Map(), - write: new Map(), - })); - - 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/js/src/tool/view.ts b/js/src/tool/view.ts deleted file mode 100644 index ee11881d3..000000000 --- a/js/src/tool/view.ts +++ /dev/null @@ -1,152 +0,0 @@ -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 = "\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"; - - // 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/js/src/util/context.ts b/js/src/util/context.ts deleted file mode 100644 index bcaf7ee3c..000000000 --- a/js/src/util/context.ts +++ /dev/null @@ -1,25 +0,0 @@ -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(name: string) { - const storage = new AsyncLocalStorage(); - return { - use() { - const result = storage.getStore(); - if (!result) { - throw new NotFound(name); - } - return result; - }, - provide(value: T, fn: () => R) { - return storage.run(value, fn); - }, - }; - } -} diff --git a/js/src/util/event.ts b/js/src/util/event.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/js/src/util/log.ts b/js/src/util/log.ts deleted file mode 100644 index 34707b136..000000000 --- a/js/src/util/log.ts +++ /dev/null @@ -1,64 +0,0 @@ -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) { - tags = tags || {}; - - function build(message: any, extra?: Record) { - 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) { - write.out(build(message, extra)); - }, - error(message?: any, extra?: Record) { - 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/js/src/util/scrap.ts b/js/src/util/scrap.ts deleted file mode 100644 index 16005acdc..000000000 --- a/js/src/util/scrap.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const foo: string = "42"; - -export function dummyFunction(): void { - console.log("This is a dummy function"); -} diff --git a/js/test/tool/__snapshots__/tool.test.ts.snap b/js/test/tool/__snapshots__/tool.test.ts.snap deleted file mode 100644 index 12669e387..000000000 --- a/js/test/tool/__snapshots__/tool.test.ts.snap +++ /dev/null @@ -1,17 +0,0 @@ -// 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/js/test/tool/tool.test.ts b/js/test/tool/tool.test.ts deleted file mode 100644 index 4b6d2efd3..000000000 --- a/js/test/tool/tool.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -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/js/tsconfig.json b/js/tsconfig.json deleted file mode 100644 index 65fa6c7f3..000000000 --- a/js/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "@tsconfig/bun/tsconfig.json", - "compilerOptions": {} -} diff --git a/main.go b/main.go deleted file mode 100644 index d81e6f8f9..000000000 --- a/main.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - "github.com/sst/opencode/cmd" -) - -func main() { - cmd.Execute() -} diff --git a/package.json b/package.json new file mode 100644 index 000000000..329c5ba56 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "opencode", + "private": true, + "type": "module", + "packageManager": "bun@1.2.14", + "scripts": { + "dev": "sst dev" + }, + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "@tsconfig/node22": "22.0.0", + "@types/node": "^22.13.9", + "prettier": "^3.5.3", + "sst": "3.16.0", + "typescript": "5.8.2" + }, + "engines": { + "bun": ">=1.0.0", + "node": ">=18.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/sst/opencode" + }, + "license": "MIT", + "prettier": { + "semi": false + }, + "overrides": { + "zod": "3.24.2" + }, + "trustedDependencies": [ + "esbuild", + "protobufjs", + "sharp" + ] +} 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 + Bucket: R2Bucket +} + +export class SyncServer extends DurableObject { + 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("secret") + } + + private async getSessionID() { + return this.ctx.storage.get("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() + 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() + 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 { + 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 = {} +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>; + + const ctx = Context.create("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; + } + >(); + + const result = { + get services() { + return services; + }, + get root() { + return input.directory; + }, + }; + + return result; + } + + export function state( + key: any, + init: (app: Info) => State, + shutdown?: (state: Awaited) => Promise, + ) { + 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 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, + ) { + 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(); + + return { + subscriptions, + }; + }); + + export type EventDefinition = ReturnType; + + const registry = new Map(); + + export function event( + 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( + def: Definition, + properties: z.output, + ) { + 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( + def: Definition, + callback: (event: { + type: Definition["type"]; + properties: z.infer; + }) => 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; + + 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 ", "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 = { + 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>; + + 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(); + 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(); + + 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(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((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(); + + 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 = {}; + 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( + input: (client: LSPClient.Info) => Promise, + ): Promise { + 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; + }[] = [ + { + 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 = { + ".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; + + 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; +} 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; + + 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((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>(), + }) + .openapi({ + ref: "Message.ToolInvocation.ToolCall", + }); + export type ToolCall = z.infer; + + export const ToolPartialCall = z + .object({ + state: z.literal("partial-call"), + step: z.number().optional(), + toolCallId: z.string(), + toolName: z.string(), + args: z.custom>(), + }) + .openapi({ + ref: "Message.ToolInvocation.ToolPartialCall", + }); + export type ToolPartialCall = z.infer; + + export const ToolResult = z + .object({ + state: z.literal("result"), + step: z.number().optional(), + toolCallId: z.string(), + toolName: z.string(), + args: z.custom>(), + result: z.string(), + }) + .openapi({ + ref: "Message.ToolInvocation.ToolResult", + }); + export type ToolResult = z.infer; + + export const ToolInvocation = z + .discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult]) + .openapi({ + ref: "Message.ToolInvocation", + }); + export type ToolInvocation = z.infer; + + export const TextPart = z + .object({ + type: z.literal("text"), + text: z.string(), + }) + .openapi({ + ref: "Message.Part.Text", + }); + export type TextPart = z.infer; + + 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; + + export const ToolInvocationPart = z + .object({ + type: z.literal("tool-invocation"), + toolInvocation: ToolInvocation, + }) + .openapi({ + ref: "Message.Part.ToolInvocation", + }); + export type ToolInvocationPart = z.infer; + + 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; + + 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; + + export const StepStartPart = z + .object({ + type: z.literal("step-start"), + }) + .openapi({ + ref: "Message.Part.StepStart", + }); + export type StepStartPart = z.infer; + + export const Part = z + .discriminatedUnion("type", [ + TextPart, + ReasoningPart, + ToolInvocationPart, + SourceUrlPart, + FilePart, + StepStartPart, + ]) + .openapi({ + ref: "Message.Part", + }); + export type Part = z.infer; + + 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; + + 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 .", "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: + +user: 2 + 2 +assistant: 4 + + + +user: what is 2+2? +assistant: 4 + + + +user: is 11 a prime number? +assistant: yes + + + +user: what command should I run to list files in the current directory? +assistant: ls + + + +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 + + + +user: How many golf balls fit inside a jetta? +assistant: 150000 + + + +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 + + + +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] + + +# 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; + + export const Event = { + Updated: Bus.event( + "session.updated", + z.object({ + info: Info, + }), + ), + }; + + const state = App.state("session", () => { + const sessions = new Map(); + const messages = new Map(); + + 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("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(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(); + 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 = Promise.resolve(); + const pending = new Map(); + + 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(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(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. + +pytest /foo/bar/tests + + +cd /foo/bar && pytest tests + + +# 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 tags: + + +- 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 + + +4. Create the commit with a message ending with: +🤖 Generated with opencode +Co-Authored-By: opencode + +- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example: + +git commit -m "$(cat <<'EOF' + Commit message here. + + 🤖 Generated with opencode + Co-Authored-By: opencode + EOF + )" + + +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 tags: + + +- 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 + + +6. Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting. + +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 +)" + + +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\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n\n`; + continue; + } + output += `\n\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n\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 { + 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 { + 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(); + const filesByDir = new Map(); + + 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; +} + +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, +): [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, +): Commit { + const changes: Record = {}; + + 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, + deleteFile: (path: string) => Promise, +): Promise { + 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 = {}; + 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 = Record, + > { + properties: Properties; + time: { + start: number; + end: number; + }; + } + export function define< + Params, + Output extends { metadata?: any; output: any }, + Name extends string, + >( + input: AITool & { + 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 = { + ...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(), + write: new Map(), + })); + + 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 = "\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"; + + // 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(name: string) { + const storage = new AsyncLocalStorage(); + return { + use() { + const result = storage.getStore(); + if (!result) { + throw new NotFound(name); + } + return result; + }, + provide(value: T, fn: () => R) { + return storage.run(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 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) { + tags = tags || {}; + + function build(message: any, extra?: Record) { + 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) { + write.out(build(message, extra)); + }, + error(message?: any, extra?: Record) { + 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://aur@aur.archlinux.org/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:}} 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(` + +`, + 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), ¶ms) + // // prompt := strings.ReplaceAll(params.Prompt, "\n", " ") + // // return renderParams(paramWidth, prompt) + // case "bash": + // var params tools.BashParams + // json.Unmarshal([]byte(toolCall.Input), ¶ms) + // command := strings.ReplaceAll(params.Command, "\n", " ") + // return renderParams(paramWidth, command) + // case "edit": + // var params tools.EditParams + // json.Unmarshal([]byte(toolCall.Input), ¶ms) + // filePath := removeWorkingDirPrefix(params.FilePath) + // return renderParams(paramWidth, filePath) + // case "fetch": + // var params tools.FetchParams + // json.Unmarshal([]byte(toolCall.Input), ¶ms) + // 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), ¶ms) + // 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), ¶ms) + // 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), ¶ms) + // path := params.Path + // if path == "" { + // path = "." + // } + // return renderParams(paramWidth, path) + // case tools.ViewToolName: + // var params tools.ViewParams + // json.Unmarshal([]byte(toolCall.Input), ¶ms) + // 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), ¶ms) + // filePath := removeWorkingDirPrefix(params.FilePath) + // return renderParams(paramWidth, filePath) + // case tools.BatchToolName: + // var params tools.BatchParams + // json.Unmarshal([]byte(toolCall.Input), ¶ms) + // 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), ¶ms) + // 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), ¶ms) + // 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 @@ + + + diff --git a/packages/web/public/social-share.png b/packages/web/public/social-share.png new file mode 100644 index 000000000..23905086a Binary files /dev/null and b/packages/web/public/social-share.png 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 @@ + + 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 @@ + + 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 @@ + + + + + + + + + + + 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 @@ + + + + + + + + + + + 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 { + 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 ( +
+ ) +} + +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 = (props) => { + const [rows, setRows] = createSignal([]) + + 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 ( +
+
+ {rows().map((r) => ( + + ))} +
+ +
+ {rows().map((r) => ( + + ))} +
+
+ ) +} + +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") + ?
+
+ +
+
+ +
+
+ : +} + + + + 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 === "" + ? + : +} 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; + } +} +--- +
+
+ +

The AI coding agent built for the terminal.

+
+ +
+ +
+ +
+ +
+ +
+
    +
  • Native TUI: A native terminal UI for a smoother, snappier experience.
  • +
  • LSP enabled: Loads the right LSPs for your codebase. Helps the LLM make fewer mistakes.
  • +
  • Multi-session: Start multiple conversations in a project to have agents working in parallel.
  • +
  • Use any model: Supports all the models from OpenAI, Anthropic, Google, OpenRouter, and more.
  • +
  • Change tracking: View the file changes from the current conversation in the sidebar.
  • +
  • Edit with Vim: Use Vim as an external editor to compose longer messages.
  • +
+
+ + +
+ + + + + + 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 + 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 ( + + }> + + + + + + + + + + + ) +} + +interface ResultsButtonProps extends JSX.HTMLAttributes { + results: boolean +} +function ResultsButton(props: ResultsButtonProps) { + const [local, rest] = splitProps(props, ["results"]) + return ( + + ) +} + +interface TextPartProps extends JSX.HTMLAttributes { + 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 ( +
+
 (preEl = el)}>{local.text}
+ {overflowed() && + + } +
+ ) +} + +function PartFooter(props: { time: number }) { + return ( + + {DateTime.fromMillis(props.time).toLocaleString(DateTime.TIME_WITH_SECONDS)} + + ) +} + +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 + }>({ + 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 ( +
+
+
+

{store.info?.title}

+

+ + {getStatusText(connectionStatus())} +

+
+
+
    +
  • + Cost + {metrics().cost !== undefined ? + ${metrics().cost.toFixed(2)} + : + + } +
  • +
  • + Input Tokens + {metrics().tokens.input ? + {metrics().tokens.input} + : + + } +
  • +
  • + Output Tokens + {metrics().tokens.output ? + {metrics().tokens.output} + : + + } +
  • +
  • + Reasoning Tokens + {metrics().tokens.reasoning ? + {metrics().tokens.reasoning} + : + + } +
  • +
+
    + {models().length > 0 ? + + {([provider, model]) => ( +
  • +
    + +
    + {model} +
  • + )} +
    + : +
  • + Models + +
  • + } +
+
+ {messages().length > 0 && messages()[0].metadata?.time.created ? + + {DateTime.fromMillis( + messages()[0].metadata?.time.created || 0 + ).toLocaleString(DateTime.DATE_MED)} + + : + Started at — + } +
+
+
+ +
+ 0} + fallback={

Waiting for messages...

} + > +
+ + {(msg, msgIndex) => ( + + {(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 ( + + { /* User text */} + + {part => +
+
+
+ +
+
+
+
+ + +
+
+ } +
+ { /* AI text */} + + {part => +
+
+
+
+
+
+ + +
+
+ } +
+ { /* AI model */} + + {assistant => +
+
+
+ +
+
+
+
+
+ + {assistant().providerID} + + + {assistant().modelID} + +
+
+
+ } +
+ { /* System text */} + + {part => +
+
+
+ +
+
+
+
+
+ + System + + +
+ +
+
+ } +
+ { /* Edit tool */} + + {part => { + const args = part().toolInvocation.args + const filePath = args.filePath + return ( +
+
+
+ +
+
+
+
+
+ + Edit {filePath} + +
+ +
+
+ +
+
+ ) + }} +
+ { /* Tool call */} + + {part => +
+
+
+ +
+
+
+
+
+ + {part().toolInvocation.toolName} + +
+ + {([name, value]) => + <> +
+
{name}
+
{value}
+ + } +
+
+ + +
+ showResults(e => !e)} + /> + + + +
+
+ + + +
+
+ +
+
+ } +
+ { /* Fallback */} + +
+
+
+ + }> + + + + + + + + + + +
+
+
+
+
+ + {part.type} + + +
+ +
+
+
+
+ ) + }} +
+ )} +
+
+
+
+ +
+
+ 0} + fallback={

Waiting for messages...

} + > +
    + + {(msg) => ( +
  • +
    + Key: {msg.id} +
    +
    {JSON.stringify(msg, null, 2)}
    +
  • + )} +
    +
+
+
+
+
+ ) +} 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) { + return ( + + ) +} + +// https://icones.js.org/collection/ri?s=anthropic&icon=ri:anthropic-fill +export function IconAnthropic(props: JSX.SvgSVGAttributes) { + return ( + + ) +} + +// https://icones.js.org/collection/ri?s=gemini&icon=ri:gemini-fill +export function IconGemini(props: JSX.SvgSVGAttributes) { + return ( + + ) +} 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) { + return ( + + + + ) +} +export function IconAdjustmentsHorizontal( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconAdjustmentsVertical( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArchiveBoxArrowDown( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArchiveBoxXMark( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArchiveBox(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowDownCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowDownLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowDownOnSquareStack( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowDownOnSquare( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowDownRight(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowDownTray(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowLeftCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowLeftOnRectangle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowLongDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowLongLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowLongRight(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowLongUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowPathRoundedSquare( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowPath(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowRightCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowRightOnRectangle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowRight(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowSmallDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowSmallLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowSmallRight( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowSmallUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowTopRightOnSquare( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowTrendingDown( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowTrendingUp( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowUpCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowUpLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowUpOnSquareStack( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowUpOnSquare( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowUpRight(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowUpTray(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowUturnDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowUturnLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowUturnRight( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowUturnUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconArrowsPointingIn( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowsPointingOut( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowsRightLeft( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconArrowsUpDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconAtSymbol(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBackspace(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBackward(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconBanknotes(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBars2(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBars3BottomLeft( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconBars3BottomRight( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconBars3CenterLeft( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconBars3(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBars4(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBarsArrowDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBarsArrowUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBattery0(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBattery100(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBattery50(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBeaker(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBellAlert(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBellSlash(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBellSnooze(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBell(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBoltSlash(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBolt(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconBoltSolid(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBookOpen(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBookmarkSlash(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBookmarkSquare(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBookmark(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBriefcase(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconBugAnt(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBuildingLibrary( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconBuildingOffice2( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconBuildingOffice(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconBuildingStorefront( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconCake(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCalculator(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCalendarDays(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCalendar(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCamera(props: JSX.SvgSVGAttributes) { + return ( + + + + + + ) +} +export function IconChartBarSquare(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconChartBar(props: JSX.SvgSVGAttributes) { + return ( + + + + + + ) +} +export function IconChartPie(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconChatBubbleBottomCenterText( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChatBubbleBottomCenter( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChatBubbleLeftEllipsis( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChatBubbleLeftRight( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChatBubbleLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconChatBubbleOvalLeftEllipsis( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChatBubbleOvalLeft( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconCheckBadge(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCheckCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCheck(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconChevronDoubleDown( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChevronDoubleLeft( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChevronDoubleRight( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChevronDoubleUp( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconChevronDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconChevronLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconChevronRight(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconChevronUpDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconChevronUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCircleStack(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconClipboardDocumentCheck( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconClipboardDocumentList( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconClipboardDocument( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconClipboard(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconClock(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCloudArrowDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCloudArrowUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCloud(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCodeBracketSquare( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconCodeBracket(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCog6Tooth(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconCog8Tooth(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconCog(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCommandLine(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconComputerDesktop( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconCpuChip(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCreditCard(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCubeTransparent( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconCube(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCurrencyBangladeshi( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconCurrencyDollar(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCurrencyEuro(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCurrencyPound(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCurrencyRupee(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCurrencyYen(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconCursorArrowRays( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconCursorArrowRipple( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconDevicePhoneMobile( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconDeviceTablet(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconDocumentArrowDown( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconDocumentArrowUp( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconDocumentChartBar( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconDocumentCheck(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconDocumentDuplicate( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconDocumentMagnifyingGlass( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconDocumentMinus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconDocumentPlus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconDocumentText(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconDocument(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconEllipsisHorizontalCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconEllipsisHorizontal( + props: JSX.SvgSVGAttributes +) { + return ( + + + + + + ) +} +export function IconEllipsisVertical( + props: JSX.SvgSVGAttributes +) { + return ( + + + + + + ) +} +export function IconEnvelopeOpen(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconEnvelope(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconEnvelopeSolid(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconExclamationCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconExclamationTriangle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconEyeDropper(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconEyeSlash(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconEye(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconFaceFrown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFaceSmile(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFilm(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFingerPrint(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFire(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconFlag(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFolderArrowDown( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconFolderMinus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFolderOpen(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFolderPlus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconFolder(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconForward(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconFunnel(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconGif(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconGiftTop(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconGift(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconGlobeAlt(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconGlobeAmericas(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconGlobeAsiaAustralia( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconGlobeEuropeAfrica( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconHandRaised(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconHandThumbDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconHandThumbUp(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconHashtag(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconHeart(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconHomeModern(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconHome(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconIdentification(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconInboxArrowDown(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconInboxStack(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconInbox(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconInformationCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconKey(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconLanguage(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconLifebuoy(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconLightBulb(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconLink(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconListBullet(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconLockClosed(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconLockOpen(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMagnifyingGlassCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconMagnifyingGlassMinus( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconMagnifyingGlassPlus( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconMagnifyingGlass( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconMapPin(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconMap(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMegaphone(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMicrophone(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMinusCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMinusSmall(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMinus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMoon(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMusicalNote(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconNewspaper(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconNoSymbol(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPaintBrush(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPaperAirplane(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPaperClip(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPauseCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPause(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPencilSquare(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPencil(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPhoneArrowDownLeft( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconPhoneArrowUpRight( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconPhoneXMark(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPhone(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPhoto(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPlayCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconPlayPause(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPlay(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPlusCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPlusSmall(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPlus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPower(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPresentationChartBar( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconPresentationChartLine( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconPrinter(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconPuzzlePiece(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconQrCode(props: JSX.SvgSVGAttributes) { + return ( + + + + + + + + + + + + + + ) +} +export function IconQuestionMarkCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconQueueList(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconRadio(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconReceiptPercent(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconReceiptRefund(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconRectangleGroup(props: JSX.SvgSVGAttributes) { + return ( + + + + + + ) +} +export function IconRectangleStack(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconRocketLaunch(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconRss(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconScale(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconScissors(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconServerStack(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconServer(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconShare(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconShieldCheck(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconShieldExclamation( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconShoppingBag(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconShoppingCart(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSignalSlash(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSignal(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSparkles(props: JSX.SvgSVGAttributes) { + return ( + + + + + + ) +} +export function IconSpeakerWave(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSpeakerXMark(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSquare2Stack(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSquare3Stack3d(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSquares2x2(props: JSX.SvgSVGAttributes) { + return ( + + + + + + + ) +} +export function IconSquaresPlus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconStar(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconStopCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconStop(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSun(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSwatch(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconTableCells(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconTag(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconTicket(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconTrash(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconTrophy(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconTruck(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconTv(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconUserCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconUserGroup(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconUserMinus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconUserPlus(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconUser(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconUsers(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconVariable(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconVideoCameraSlash( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconVideoCamera(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconViewColumns(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconViewfinderCircle( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconWallet(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconWifi(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconWindow(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconWrenchScrewdriver( + props: JSX.SvgSVGAttributes +) { + return ( + + + + ) +} +export function IconWrench(props: JSX.SvgSVGAttributes) { + return ( + + + + + ) +} +export function IconXCircle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconXMark(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +// index +export function IconCommand(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconLetter(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconMultiSelect(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} +export function IconSettings(props: JSX.SvgSVGAttributes) { + return ( + + + + + + + + + + + + ) +} +export function IconSingleSelect(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} 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"; + +--- + + + + + + 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 */ + +/// + +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" + } +} diff --git a/pkg/client/.gitignore b/pkg/client/.gitignore deleted file mode 100644 index c56971e89..000000000 --- a/pkg/client/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -gen -generated-*.go diff --git a/pkg/client/client.go b/pkg/client/client.go deleted file mode 100644 index bf2d5038e..000000000 --- a/pkg/client/client.go +++ /dev/null @@ -1,4 +0,0 @@ -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/pkg/client/event.go b/pkg/client/event.go deleted file mode 100644 index f5aab2d26..000000000 --- a/pkg/client/event.go +++ /dev/null @@ -1,53 +0,0 @@ -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/pkg/client/gen/openapi.json b/pkg/client/gen/openapi.json deleted file mode 100644 index c5f1c64b3..000000000 --- a/pkg/client/gen/openapi.json +++ /dev/null @@ -1,898 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "opencode", - "description": "opencode api", - "version": "1.0.0" - }, - "paths": { - "/event": { - "get": { - "responses": { - "200": { - "description": "Event stream", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Event" - } - } - } - } - }, - "operationId": "getEvent", - "parameters": [], - "description": "Get events" - } - }, - "/session_create": { - "post": { - "responses": { - "200": { - "description": "Successfully created session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/session.info" - } - } - } - } - }, - "operationId": "postSession_create", - "parameters": [], - "description": "Create a new session" - } - }, - "/session_share": { - "post": { - "responses": { - "200": { - "description": "Successfully shared session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/session.info" - } - } - } - } - }, - "operationId": "postSession_share", - "parameters": [], - "description": "Share the session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - } - }, - "required": [ - "sessionID" - ] - } - } - } - } - } - }, - "/session_messages": { - "post": { - "responses": { - "200": { - "description": "Successfully created session", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Message.Info" - } - } - } - } - } - }, - "operationId": "postSession_messages", - "parameters": [], - "description": "Get messages for a session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - } - }, - "required": [ - "sessionID" - ] - } - } - } - } - } - }, - "/session_list": { - "post": { - "responses": { - "200": { - "description": "List of sessions", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/session.info" - } - } - } - } - } - }, - "operationId": "postSession_list", - "parameters": [], - "description": "List all sessions" - } - }, - "/session_abort": { - "post": { - "responses": { - "200": { - "description": "Aborted session", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - } - }, - "operationId": "postSession_abort", - "parameters": [], - "description": "Abort a session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - } - }, - "required": [ - "sessionID" - ] - } - } - } - } - } - }, - "/session_summarize": { - "post": { - "responses": { - "200": { - "description": "Summarize the session", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - } - }, - "operationId": "postSession_summarize", - "parameters": [], - "description": "Summarize the session", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": [ - "sessionID", - "providerID", - "modelID" - ] - } - } - } - } - } - }, - "/session_chat": { - "post": { - "responses": { - "200": { - "description": "Chat with a model", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Message.Info" - } - } - } - } - }, - "operationId": "postSession_chat", - "parameters": [], - "description": "Chat with a model", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Message.Part" - } - } - }, - "required": [ - "sessionID", - "providerID", - "modelID", - "parts" - ] - } - } - } - } - } - }, - "/provider_list": { - "post": { - "responses": { - "200": { - "description": "List of providers", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Provider.Info" - } - } - } - } - } - }, - "operationId": "postProvider_list", - "parameters": [], - "description": "List all providers" - } - } - }, - "components": { - "schemas": { - "Event": { - "oneOf": [ - { - "$ref": "#/components/schemas/Event.storage.write" - }, - { - "$ref": "#/components/schemas/Event.lsp.client.diagnostics" - }, - { - "$ref": "#/components/schemas/Event.message.updated" - }, - { - "$ref": "#/components/schemas/Event.session.updated" - } - ], - "discriminator": { - "propertyName": "type", - "mapping": { - "storage.write": "#/components/schemas/Event.storage.write", - "lsp.client.diagnostics": "#/components/schemas/Event.lsp.client.diagnostics", - "message.updated": "#/components/schemas/Event.message.updated", - "session.updated": "#/components/schemas/Event.session.updated" - } - } - }, - "Event.storage.write": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "storage.write" - }, - "properties": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "content": {} - }, - "required": [ - "key" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.lsp.client.diagnostics": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "lsp.client.diagnostics" - }, - "properties": { - "type": "object", - "properties": { - "serverID": { - "type": "string" - }, - "path": { - "type": "string" - } - }, - "required": [ - "serverID", - "path" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.message.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "message.updated" - }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Message.Info" - } - }, - "required": [ - "info" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Message.Info": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "role": { - "type": "string", - "enum": [ - "system", - "user", - "assistant" - ] - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Message.Part" - } - }, - "metadata": { - "type": "object", - "properties": { - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "completed": { - "type": "number" - } - }, - "required": [ - "created" - ] - }, - "sessionID": { - "type": "string" - }, - "tool": { - "type": "object", - "additionalProperties": {} - }, - "assistant": { - "type": "object", - "properties": { - "modelID": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "cost": { - "type": "number" - }, - "summary": { - "type": "boolean" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - } - }, - "required": [ - "input", - "output", - "reasoning" - ] - } - }, - "required": [ - "modelID", - "providerID", - "cost", - "tokens" - ] - } - }, - "required": [ - "time", - "sessionID", - "tool" - ] - } - }, - "required": [ - "id", - "role", - "parts", - "metadata" - ] - }, - "Message.Part": { - "oneOf": [ - { - "$ref": "#/components/schemas/Message.Part.Text" - }, - { - "$ref": "#/components/schemas/Message.Part.Reasoning" - }, - { - "$ref": "#/components/schemas/Message.Part.ToolInvocation" - }, - { - "$ref": "#/components/schemas/Message.Part.SourceUrl" - }, - { - "$ref": "#/components/schemas/Message.Part.File" - }, - { - "$ref": "#/components/schemas/Message.Part.StepStart" - } - ], - "discriminator": { - "propertyName": "type", - "mapping": { - "text": "#/components/schemas/Message.Part.Text", - "reasoning": "#/components/schemas/Message.Part.Reasoning", - "tool-invocation": "#/components/schemas/Message.Part.ToolInvocation", - "source-url": "#/components/schemas/Message.Part.SourceUrl", - "file": "#/components/schemas/Message.Part.File", - "step-start": "#/components/schemas/Message.Part.StepStart" - } - } - }, - "Message.Part.Text": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "text" - }, - "text": { - "type": "string" - } - }, - "required": [ - "type", - "text" - ] - }, - "Message.Part.Reasoning": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "reasoning" - }, - "text": { - "type": "string" - }, - "providerMetadata": { - "type": "object", - "additionalProperties": {} - } - }, - "required": [ - "type", - "text" - ] - }, - "Message.Part.ToolInvocation": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "tool-invocation" - }, - "toolInvocation": { - "$ref": "#/components/schemas/Message.ToolInvocation" - } - }, - "required": [ - "type", - "toolInvocation" - ] - }, - "Message.ToolInvocation": { - "oneOf": [ - { - "$ref": "#/components/schemas/Message.ToolInvocation.ToolCall" - }, - { - "$ref": "#/components/schemas/Message.ToolInvocation.ToolPartialCall" - }, - { - "$ref": "#/components/schemas/Message.ToolInvocation.ToolResult" - } - ], - "discriminator": { - "propertyName": "state", - "mapping": { - "call": "#/components/schemas/Message.ToolInvocation.ToolCall", - "partial-call": "#/components/schemas/Message.ToolInvocation.ToolPartialCall", - "result": "#/components/schemas/Message.ToolInvocation.ToolResult" - } - } - }, - "Message.ToolInvocation.ToolCall": { - "type": "object", - "properties": { - "state": { - "type": "string", - "const": "call" - }, - "step": { - "type": "number" - }, - "toolCallId": { - "type": "string" - }, - "toolName": { - "type": "string" - }, - "args": {} - }, - "required": [ - "state", - "toolCallId", - "toolName" - ] - }, - "Message.ToolInvocation.ToolPartialCall": { - "type": "object", - "properties": { - "state": { - "type": "string", - "const": "partial-call" - }, - "step": { - "type": "number" - }, - "toolCallId": { - "type": "string" - }, - "toolName": { - "type": "string" - }, - "args": {} - }, - "required": [ - "state", - "toolCallId", - "toolName" - ] - }, - "Message.ToolInvocation.ToolResult": { - "type": "object", - "properties": { - "state": { - "type": "string", - "const": "result" - }, - "step": { - "type": "number" - }, - "toolCallId": { - "type": "string" - }, - "toolName": { - "type": "string" - }, - "args": {}, - "result": { - "type": "string" - } - }, - "required": [ - "state", - "toolCallId", - "toolName", - "result" - ] - }, - "Message.Part.SourceUrl": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "source-url" - }, - "sourceId": { - "type": "string" - }, - "url": { - "type": "string" - }, - "title": { - "type": "string" - }, - "providerMetadata": { - "type": "object", - "additionalProperties": {} - } - }, - "required": [ - "type", - "sourceId", - "url" - ] - }, - "Message.Part.File": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "file" - }, - "mediaType": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": [ - "type", - "mediaType", - "url" - ] - }, - "Message.Part.StepStart": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "step-start" - } - }, - "required": [ - "type" - ] - }, - "Event.session.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "session.updated" - }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/session.info" - } - }, - "required": [ - "info" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "session.info": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^ses" - }, - "share": { - "type": "object", - "properties": { - "secret": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": [ - "secret", - "url" - ] - }, - "title": { - "type": "string" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "updated": { - "type": "number" - } - }, - "required": [ - "created", - "updated" - ] - } - }, - "required": [ - "id", - "title", - "time" - ] - }, - "Provider.Info": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "options": { - "type": "object", - "additionalProperties": {} - }, - "models": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Provider.Model" - } - } - }, - "required": [ - "id", - "name", - "models" - ] - }, - "Provider.Model": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "cost": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "inputCached": { - "type": "number" - }, - "output": { - "type": "number" - }, - "outputCached": { - "type": "number" - } - }, - "required": [ - "input", - "inputCached", - "output", - "outputCached" - ] - }, - "contextWindow": { - "type": "number" - }, - "maxOutputTokens": { - "type": "number" - }, - "attachment": { - "type": "boolean" - }, - "reasoning": { - "type": "boolean" - } - }, - "required": [ - "id", - "cost", - "contextWindow", - "attachment" - ] - } - } - } -} \ No newline at end of file diff --git a/pkg/client/generated-client.go b/pkg/client/generated-client.go deleted file mode 100644 index 925468a61..000000000 --- a/pkg/client/generated-client.go +++ /dev/null @@ -1,1953 +0,0 @@ -// Package client provides primitives to interact with the openapi HTTP API. -// -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. -package client - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strings" - - "github.com/oapi-codegen/runtime" -) - -// Defines values for MessageInfoRole. -const ( - Assistant MessageInfoRole = "assistant" - System MessageInfoRole = "system" - User MessageInfoRole = "user" -) - -// Event defines model for Event. -type Event struct { - union json.RawMessage -} - -// EventLspClientDiagnostics defines model for Event.lsp.client.diagnostics. -type EventLspClientDiagnostics struct { - Properties struct { - Path string `json:"path"` - ServerID string `json:"serverID"` - } `json:"properties"` - Type string `json:"type"` -} - -// EventMessageUpdated defines model for Event.message.updated. -type EventMessageUpdated struct { - Properties struct { - Info MessageInfo `json:"info"` - } `json:"properties"` - Type string `json:"type"` -} - -// EventSessionUpdated defines model for Event.session.updated. -type EventSessionUpdated struct { - Properties struct { - Info SessionInfo `json:"info"` - } `json:"properties"` - Type string `json:"type"` -} - -// EventStorageWrite defines model for Event.storage.write. -type EventStorageWrite struct { - Properties struct { - Content *interface{} `json:"content,omitempty"` - Key string `json:"key"` - } `json:"properties"` - Type string `json:"type"` -} - -// MessageInfo defines model for Message.Info. -type MessageInfo struct { - Id string `json:"id"` - Metadata struct { - Assistant *struct { - Cost float32 `json:"cost"` - ModelID string `json:"modelID"` - ProviderID string `json:"providerID"` - Summary *bool `json:"summary,omitempty"` - Tokens struct { - Input float32 `json:"input"` - Output float32 `json:"output"` - Reasoning float32 `json:"reasoning"` - } `json:"tokens"` - } `json:"assistant,omitempty"` - SessionID string `json:"sessionID"` - Time struct { - Completed *float32 `json:"completed,omitempty"` - Created float32 `json:"created"` - } `json:"time"` - Tool map[string]interface{} `json:"tool"` - } `json:"metadata"` - Parts []MessagePart `json:"parts"` - Role MessageInfoRole `json:"role"` -} - -// MessageInfoRole defines model for MessageInfo.Role. -type MessageInfoRole string - -// MessagePart defines model for Message.Part. -type MessagePart struct { - union json.RawMessage -} - -// MessagePartFile defines model for Message.Part.File. -type MessagePartFile struct { - Filename *string `json:"filename,omitempty"` - MediaType string `json:"mediaType"` - Type string `json:"type"` - Url string `json:"url"` -} - -// MessagePartReasoning defines model for Message.Part.Reasoning. -type MessagePartReasoning struct { - ProviderMetadata *map[string]interface{} `json:"providerMetadata,omitempty"` - Text string `json:"text"` - Type string `json:"type"` -} - -// MessagePartSourceUrl defines model for Message.Part.SourceUrl. -type MessagePartSourceUrl struct { - ProviderMetadata *map[string]interface{} `json:"providerMetadata,omitempty"` - SourceId string `json:"sourceId"` - Title *string `json:"title,omitempty"` - Type string `json:"type"` - Url string `json:"url"` -} - -// MessagePartStepStart defines model for Message.Part.StepStart. -type MessagePartStepStart struct { - Type string `json:"type"` -} - -// MessagePartText defines model for Message.Part.Text. -type MessagePartText struct { - Text string `json:"text"` - Type string `json:"type"` -} - -// MessagePartToolInvocation defines model for Message.Part.ToolInvocation. -type MessagePartToolInvocation struct { - ToolInvocation MessageToolInvocation `json:"toolInvocation"` - Type string `json:"type"` -} - -// MessageToolInvocation defines model for Message.ToolInvocation. -type MessageToolInvocation struct { - union json.RawMessage -} - -// MessageToolInvocationToolCall defines model for Message.ToolInvocation.ToolCall. -type MessageToolInvocationToolCall struct { - Args *interface{} `json:"args,omitempty"` - State string `json:"state"` - Step *float32 `json:"step,omitempty"` - ToolCallId string `json:"toolCallId"` - ToolName string `json:"toolName"` -} - -// MessageToolInvocationToolPartialCall defines model for Message.ToolInvocation.ToolPartialCall. -type MessageToolInvocationToolPartialCall struct { - Args *interface{} `json:"args,omitempty"` - State string `json:"state"` - Step *float32 `json:"step,omitempty"` - ToolCallId string `json:"toolCallId"` - ToolName string `json:"toolName"` -} - -// MessageToolInvocationToolResult defines model for Message.ToolInvocation.ToolResult. -type MessageToolInvocationToolResult struct { - Args *interface{} `json:"args,omitempty"` - Result string `json:"result"` - State string `json:"state"` - Step *float32 `json:"step,omitempty"` - ToolCallId string `json:"toolCallId"` - ToolName string `json:"toolName"` -} - -// ProviderInfo defines model for Provider.Info. -type ProviderInfo struct { - Id string `json:"id"` - Models []ProviderModel `json:"models"` - Name string `json:"name"` - Options *map[string]interface{} `json:"options,omitempty"` -} - -// ProviderModel defines model for Provider.Model. -type ProviderModel struct { - Attachment bool `json:"attachment"` - ContextWindow float32 `json:"contextWindow"` - Cost struct { - Input float32 `json:"input"` - InputCached float32 `json:"inputCached"` - Output float32 `json:"output"` - OutputCached float32 `json:"outputCached"` - } `json:"cost"` - Id string `json:"id"` - MaxOutputTokens *float32 `json:"maxOutputTokens,omitempty"` - Name *string `json:"name,omitempty"` - Reasoning *bool `json:"reasoning,omitempty"` -} - -// SessionInfo defines model for session.info. -type SessionInfo struct { - Id string `json:"id"` - Share *struct { - Secret string `json:"secret"` - Url string `json:"url"` - } `json:"share,omitempty"` - Time struct { - Created float32 `json:"created"` - Updated float32 `json:"updated"` - } `json:"time"` - Title string `json:"title"` -} - -// PostSessionAbortJSONBody defines parameters for PostSessionAbort. -type PostSessionAbortJSONBody struct { - SessionID string `json:"sessionID"` -} - -// PostSessionChatJSONBody defines parameters for PostSessionChat. -type PostSessionChatJSONBody struct { - ModelID string `json:"modelID"` - Parts []MessagePart `json:"parts"` - ProviderID string `json:"providerID"` - SessionID string `json:"sessionID"` -} - -// PostSessionMessagesJSONBody defines parameters for PostSessionMessages. -type PostSessionMessagesJSONBody struct { - SessionID string `json:"sessionID"` -} - -// PostSessionShareJSONBody defines parameters for PostSessionShare. -type PostSessionShareJSONBody struct { - SessionID string `json:"sessionID"` -} - -// PostSessionSummarizeJSONBody defines parameters for PostSessionSummarize. -type PostSessionSummarizeJSONBody struct { - ModelID string `json:"modelID"` - ProviderID string `json:"providerID"` - SessionID string `json:"sessionID"` -} - -// PostSessionAbortJSONRequestBody defines body for PostSessionAbort for application/json ContentType. -type PostSessionAbortJSONRequestBody PostSessionAbortJSONBody - -// PostSessionChatJSONRequestBody defines body for PostSessionChat for application/json ContentType. -type PostSessionChatJSONRequestBody PostSessionChatJSONBody - -// PostSessionMessagesJSONRequestBody defines body for PostSessionMessages for application/json ContentType. -type PostSessionMessagesJSONRequestBody PostSessionMessagesJSONBody - -// PostSessionShareJSONRequestBody defines body for PostSessionShare for application/json ContentType. -type PostSessionShareJSONRequestBody PostSessionShareJSONBody - -// PostSessionSummarizeJSONRequestBody defines body for PostSessionSummarize for application/json ContentType. -type PostSessionSummarizeJSONRequestBody PostSessionSummarizeJSONBody - -// AsEventStorageWrite returns the union data inside the Event as a EventStorageWrite -func (t Event) AsEventStorageWrite() (EventStorageWrite, error) { - var body EventStorageWrite - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromEventStorageWrite overwrites any union data inside the Event as the provided EventStorageWrite -func (t *Event) FromEventStorageWrite(v EventStorageWrite) error { - v.Type = "storage.write" - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeEventStorageWrite performs a merge with any union data inside the Event, using the provided EventStorageWrite -func (t *Event) MergeEventStorageWrite(v EventStorageWrite) error { - v.Type = "storage.write" - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsEventLspClientDiagnostics returns the union data inside the Event as a EventLspClientDiagnostics -func (t Event) AsEventLspClientDiagnostics() (EventLspClientDiagnostics, error) { - var body EventLspClientDiagnostics - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromEventLspClientDiagnostics overwrites any union data inside the Event as the provided EventLspClientDiagnostics -func (t *Event) FromEventLspClientDiagnostics(v EventLspClientDiagnostics) error { - v.Type = "lsp.client.diagnostics" - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeEventLspClientDiagnostics performs a merge with any union data inside the Event, using the provided EventLspClientDiagnostics -func (t *Event) MergeEventLspClientDiagnostics(v EventLspClientDiagnostics) error { - v.Type = "lsp.client.diagnostics" - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsEventMessageUpdated returns the union data inside the Event as a EventMessageUpdated -func (t Event) AsEventMessageUpdated() (EventMessageUpdated, error) { - var body EventMessageUpdated - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromEventMessageUpdated overwrites any union data inside the Event as the provided EventMessageUpdated -func (t *Event) FromEventMessageUpdated(v EventMessageUpdated) error { - v.Type = "message.updated" - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeEventMessageUpdated performs a merge with any union data inside the Event, using the provided EventMessageUpdated -func (t *Event) MergeEventMessageUpdated(v EventMessageUpdated) error { - v.Type = "message.updated" - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsEventSessionUpdated returns the union data inside the Event as a EventSessionUpdated -func (t Event) AsEventSessionUpdated() (EventSessionUpdated, error) { - var body EventSessionUpdated - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromEventSessionUpdated overwrites any union data inside the Event as the provided EventSessionUpdated -func (t *Event) FromEventSessionUpdated(v EventSessionUpdated) error { - v.Type = "session.updated" - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeEventSessionUpdated performs a merge with any union data inside the Event, using the provided EventSessionUpdated -func (t *Event) MergeEventSessionUpdated(v EventSessionUpdated) error { - v.Type = "session.updated" - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t Event) Discriminator() (string, error) { - var discriminator struct { - Discriminator string `json:"type"` - } - err := json.Unmarshal(t.union, &discriminator) - return discriminator.Discriminator, err -} - -func (t Event) ValueByDiscriminator() (interface{}, error) { - discriminator, err := t.Discriminator() - if err != nil { - return nil, err - } - switch discriminator { - case "lsp.client.diagnostics": - return t.AsEventLspClientDiagnostics() - case "message.updated": - return t.AsEventMessageUpdated() - case "session.updated": - return t.AsEventSessionUpdated() - case "storage.write": - return t.AsEventStorageWrite() - default: - return nil, errors.New("unknown discriminator value: " + discriminator) - } -} - -func (t Event) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *Event) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsMessagePartText returns the union data inside the MessagePart as a MessagePartText -func (t MessagePart) AsMessagePartText() (MessagePartText, error) { - var body MessagePartText - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromMessagePartText overwrites any union data inside the MessagePart as the provided MessagePartText -func (t *MessagePart) FromMessagePartText(v MessagePartText) error { - v.Type = "text" - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeMessagePartText performs a merge with any union data inside the MessagePart, using the provided MessagePartText -func (t *MessagePart) MergeMessagePartText(v MessagePartText) error { - v.Type = "text" - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsMessagePartReasoning returns the union data inside the MessagePart as a MessagePartReasoning -func (t MessagePart) AsMessagePartReasoning() (MessagePartReasoning, error) { - var body MessagePartReasoning - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromMessagePartReasoning overwrites any union data inside the MessagePart as the provided MessagePartReasoning -func (t *MessagePart) FromMessagePartReasoning(v MessagePartReasoning) error { - v.Type = "reasoning" - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeMessagePartReasoning performs a merge with any union data inside the MessagePart, using the provided MessagePartReasoning -func (t *MessagePart) MergeMessagePartReasoning(v MessagePartReasoning) error { - v.Type = "reasoning" - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsMessagePartToolInvocation returns the union data inside the MessagePart as a MessagePartToolInvocation -func (t MessagePart) AsMessagePartToolInvocation() (MessagePartToolInvocation, error) { - var body MessagePartToolInvocation - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromMessagePartToolInvocation overwrites any union data inside the MessagePart as the provided MessagePartToolInvocation -func (t *MessagePart) FromMessagePartToolInvocation(v MessagePartToolInvocation) error { - v.Type = "tool-invocation" - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeMessagePartToolInvocation performs a merge with any union data inside the MessagePart, using the provided MessagePartToolInvocation -func (t *MessagePart) MergeMessagePartToolInvocation(v MessagePartToolInvocation) error { - v.Type = "tool-invocation" - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsMessagePartSourceUrl returns the union data inside the MessagePart as a MessagePartSourceUrl -func (t MessagePart) AsMessagePartSourceUrl() (MessagePartSourceUrl, error) { - var body MessagePartSourceUrl - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromMessagePartSourceUrl overwrites any union data inside the MessagePart as the provided MessagePartSourceUrl -func (t *MessagePart) FromMessagePartSourceUrl(v MessagePartSourceUrl) error { - v.Type = "source-url" - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeMessagePartSourceUrl performs a merge with any union data inside the MessagePart, using the provided MessagePartSourceUrl -func (t *MessagePart) MergeMessagePartSourceUrl(v MessagePartSourceUrl) error { - v.Type = "source-url" - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsMessagePartFile returns the union data inside the MessagePart as a MessagePartFile -func (t MessagePart) AsMessagePartFile() (MessagePartFile, error) { - var body MessagePartFile - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromMessagePartFile overwrites any union data inside the MessagePart as the provided MessagePartFile -func (t *MessagePart) FromMessagePartFile(v MessagePartFile) error { - v.Type = "file" - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeMessagePartFile performs a merge with any union data inside the MessagePart, using the provided MessagePartFile -func (t *MessagePart) MergeMessagePartFile(v MessagePartFile) error { - v.Type = "file" - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsMessagePartStepStart returns the union data inside the MessagePart as a MessagePartStepStart -func (t MessagePart) AsMessagePartStepStart() (MessagePartStepStart, error) { - var body MessagePartStepStart - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromMessagePartStepStart overwrites any union data inside the MessagePart as the provided MessagePartStepStart -func (t *MessagePart) FromMessagePartStepStart(v MessagePartStepStart) error { - v.Type = "step-start" - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeMessagePartStepStart performs a merge with any union data inside the MessagePart, using the provided MessagePartStepStart -func (t *MessagePart) MergeMessagePartStepStart(v MessagePartStepStart) error { - v.Type = "step-start" - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t MessagePart) Discriminator() (string, error) { - var discriminator struct { - Discriminator string `json:"type"` - } - err := json.Unmarshal(t.union, &discriminator) - return discriminator.Discriminator, err -} - -func (t MessagePart) ValueByDiscriminator() (interface{}, error) { - discriminator, err := t.Discriminator() - if err != nil { - return nil, err - } - switch discriminator { - case "file": - return t.AsMessagePartFile() - case "reasoning": - return t.AsMessagePartReasoning() - case "source-url": - return t.AsMessagePartSourceUrl() - case "step-start": - return t.AsMessagePartStepStart() - case "text": - return t.AsMessagePartText() - case "tool-invocation": - return t.AsMessagePartToolInvocation() - default: - return nil, errors.New("unknown discriminator value: " + discriminator) - } -} - -func (t MessagePart) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *MessagePart) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// AsMessageToolInvocationToolCall returns the union data inside the MessageToolInvocation as a MessageToolInvocationToolCall -func (t MessageToolInvocation) AsMessageToolInvocationToolCall() (MessageToolInvocationToolCall, error) { - var body MessageToolInvocationToolCall - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromMessageToolInvocationToolCall overwrites any union data inside the MessageToolInvocation as the provided MessageToolInvocationToolCall -func (t *MessageToolInvocation) FromMessageToolInvocationToolCall(v MessageToolInvocationToolCall) error { - v.State = "call" - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeMessageToolInvocationToolCall performs a merge with any union data inside the MessageToolInvocation, using the provided MessageToolInvocationToolCall -func (t *MessageToolInvocation) MergeMessageToolInvocationToolCall(v MessageToolInvocationToolCall) error { - v.State = "call" - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsMessageToolInvocationToolPartialCall returns the union data inside the MessageToolInvocation as a MessageToolInvocationToolPartialCall -func (t MessageToolInvocation) AsMessageToolInvocationToolPartialCall() (MessageToolInvocationToolPartialCall, error) { - var body MessageToolInvocationToolPartialCall - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromMessageToolInvocationToolPartialCall overwrites any union data inside the MessageToolInvocation as the provided MessageToolInvocationToolPartialCall -func (t *MessageToolInvocation) FromMessageToolInvocationToolPartialCall(v MessageToolInvocationToolPartialCall) error { - v.State = "partial-call" - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeMessageToolInvocationToolPartialCall performs a merge with any union data inside the MessageToolInvocation, using the provided MessageToolInvocationToolPartialCall -func (t *MessageToolInvocation) MergeMessageToolInvocationToolPartialCall(v MessageToolInvocationToolPartialCall) error { - v.State = "partial-call" - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsMessageToolInvocationToolResult returns the union data inside the MessageToolInvocation as a MessageToolInvocationToolResult -func (t MessageToolInvocation) AsMessageToolInvocationToolResult() (MessageToolInvocationToolResult, error) { - var body MessageToolInvocationToolResult - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromMessageToolInvocationToolResult overwrites any union data inside the MessageToolInvocation as the provided MessageToolInvocationToolResult -func (t *MessageToolInvocation) FromMessageToolInvocationToolResult(v MessageToolInvocationToolResult) error { - v.State = "result" - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeMessageToolInvocationToolResult performs a merge with any union data inside the MessageToolInvocation, using the provided MessageToolInvocationToolResult -func (t *MessageToolInvocation) MergeMessageToolInvocationToolResult(v MessageToolInvocationToolResult) error { - v.State = "result" - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t MessageToolInvocation) Discriminator() (string, error) { - var discriminator struct { - Discriminator string `json:"state"` - } - err := json.Unmarshal(t.union, &discriminator) - return discriminator.Discriminator, err -} - -func (t MessageToolInvocation) ValueByDiscriminator() (interface{}, error) { - discriminator, err := t.Discriminator() - if err != nil { - return nil, err - } - switch discriminator { - case "call": - return t.AsMessageToolInvocationToolCall() - case "partial-call": - return t.AsMessageToolInvocationToolPartialCall() - case "result": - return t.AsMessageToolInvocationToolResult() - default: - return nil, errors.New("unknown discriminator value: " + discriminator) - } -} - -func (t MessageToolInvocation) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *MessageToolInvocation) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - -// RequestEditorFn is the function signature for the RequestEditor callback function -type RequestEditorFn func(ctx context.Context, req *http.Request) error - -// Doer performs HTTP requests. -// -// The standard http.Client implements this interface. -type HttpRequestDoer interface { - Do(req *http.Request) (*http.Response, error) -} - -// Client which conforms to the OpenAPI3 specification for this service. -type Client struct { - // The endpoint of the server conforming to this interface, with scheme, - // https://api.deepmap.com for example. This can contain a path relative - // to the server, such as https://api.deepmap.com/dev-test, and all the - // paths in the swagger spec will be appended to the server. - Server string - - // Doer for performing requests, typically a *http.Client with any - // customized settings, such as certificate chains. - Client HttpRequestDoer - - // A list of callbacks for modifying requests which are generated before sending over - // the network. - RequestEditors []RequestEditorFn -} - -// ClientOption allows setting custom parameters during construction -type ClientOption func(*Client) error - -// Creates a new Client, with reasonable defaults -func NewClient(server string, opts ...ClientOption) (*Client, error) { - // create a client with sane default values - client := Client{ - Server: server, - } - // mutate client and add all optional params - for _, o := range opts { - if err := o(&client); err != nil { - return nil, err - } - } - // ensure the server URL always has a trailing slash - if !strings.HasSuffix(client.Server, "/") { - client.Server += "/" - } - // create httpClient, if not already present - if client.Client == nil { - client.Client = &http.Client{} - } - return &client, nil -} - -// WithHTTPClient allows overriding the default Doer, which is -// automatically created using http.Client. This is useful for tests. -func WithHTTPClient(doer HttpRequestDoer) ClientOption { - return func(c *Client) error { - c.Client = doer - return nil - } -} - -// WithRequestEditorFn allows setting up a callback function, which will be -// called right before sending the request. This can be used to mutate the request. -func WithRequestEditorFn(fn RequestEditorFn) ClientOption { - return func(c *Client) error { - c.RequestEditors = append(c.RequestEditors, fn) - return nil - } -} - -// The interface specification for the client above. -type ClientInterface interface { - // GetEvent request - GetEvent(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) - - // PostProviderList request - PostProviderList(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) - - // PostSessionAbortWithBody request with any body - PostSessionAbortWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - PostSessionAbort(ctx context.Context, body PostSessionAbortJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // PostSessionChatWithBody request with any body - PostSessionChatWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - PostSessionChat(ctx context.Context, body PostSessionChatJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // PostSessionCreate request - PostSessionCreate(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) - - // PostSessionList request - PostSessionList(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) - - // PostSessionMessagesWithBody request with any body - PostSessionMessagesWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - PostSessionMessages(ctx context.Context, body PostSessionMessagesJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // PostSessionShareWithBody request with any body - PostSessionShareWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - PostSessionShare(ctx context.Context, body PostSessionShareJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - - // PostSessionSummarizeWithBody request with any body - PostSessionSummarizeWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - PostSessionSummarize(ctx context.Context, body PostSessionSummarizeJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) -} - -func (c *Client) GetEvent(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetEventRequest(c.Server) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) PostProviderList(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewPostProviderListRequest(c.Server) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) PostSessionAbortWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewPostSessionAbortRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) PostSessionAbort(ctx context.Context, body PostSessionAbortJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewPostSessionAbortRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) PostSessionChatWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewPostSessionChatRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) PostSessionChat(ctx context.Context, body PostSessionChatJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewPostSessionChatRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) PostSessionCreate(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewPostSessionCreateRequest(c.Server) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) PostSessionList(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewPostSessionListRequest(c.Server) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) PostSessionMessagesWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewPostSessionMessagesRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) PostSessionMessages(ctx context.Context, body PostSessionMessagesJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewPostSessionMessagesRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) PostSessionShareWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewPostSessionShareRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) PostSessionShare(ctx context.Context, body PostSessionShareJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewPostSessionShareRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) PostSessionSummarizeWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewPostSessionSummarizeRequestWithBody(c.Server, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) PostSessionSummarize(ctx context.Context, body PostSessionSummarizeJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewPostSessionSummarizeRequest(c.Server, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -// NewGetEventRequest generates requests for GetEvent -func NewGetEventRequest(server string) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/event") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewPostProviderListRequest generates requests for PostProviderList -func NewPostProviderListRequest(server string) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/provider_list") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewPostSessionAbortRequest calls the generic PostSessionAbort builder with application/json body -func NewPostSessionAbortRequest(server string, body PostSessionAbortJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewPostSessionAbortRequestWithBody(server, "application/json", bodyReader) -} - -// NewPostSessionAbortRequestWithBody generates requests for PostSessionAbort with any type of body -func NewPostSessionAbortRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/session_abort") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewPostSessionChatRequest calls the generic PostSessionChat builder with application/json body -func NewPostSessionChatRequest(server string, body PostSessionChatJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewPostSessionChatRequestWithBody(server, "application/json", bodyReader) -} - -// NewPostSessionChatRequestWithBody generates requests for PostSessionChat with any type of body -func NewPostSessionChatRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/session_chat") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewPostSessionCreateRequest generates requests for PostSessionCreate -func NewPostSessionCreateRequest(server string) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/session_create") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewPostSessionListRequest generates requests for PostSessionList -func NewPostSessionListRequest(server string) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/session_list") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), nil) - if err != nil { - return nil, err - } - - return req, nil -} - -// NewPostSessionMessagesRequest calls the generic PostSessionMessages builder with application/json body -func NewPostSessionMessagesRequest(server string, body PostSessionMessagesJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewPostSessionMessagesRequestWithBody(server, "application/json", bodyReader) -} - -// NewPostSessionMessagesRequestWithBody generates requests for PostSessionMessages with any type of body -func NewPostSessionMessagesRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/session_messages") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewPostSessionShareRequest calls the generic PostSessionShare builder with application/json body -func NewPostSessionShareRequest(server string, body PostSessionShareJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewPostSessionShareRequestWithBody(server, "application/json", bodyReader) -} - -// NewPostSessionShareRequestWithBody generates requests for PostSessionShare with any type of body -func NewPostSessionShareRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/session_share") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewPostSessionSummarizeRequest calls the generic PostSessionSummarize builder with application/json body -func NewPostSessionSummarizeRequest(server string, body PostSessionSummarizeJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewPostSessionSummarizeRequestWithBody(server, "application/json", bodyReader) -} - -// NewPostSessionSummarizeRequestWithBody generates requests for PostSessionSummarize with any type of body -func NewPostSessionSummarizeRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/session_summarize") - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { - for _, r := range c.RequestEditors { - if err := r(ctx, req); err != nil { - return err - } - } - for _, r := range additionalEditors { - if err := r(ctx, req); err != nil { - return err - } - } - return nil -} - -// ClientWithResponses builds on ClientInterface to offer response payloads -type ClientWithResponses struct { - ClientInterface -} - -// NewClientWithResponses creates a new ClientWithResponses, which wraps -// Client with return type handling -func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { - client, err := NewClient(server, opts...) - if err != nil { - return nil, err - } - return &ClientWithResponses{client}, nil -} - -// WithBaseURL overrides the baseURL. -func WithBaseURL(baseURL string) ClientOption { - return func(c *Client) error { - newBaseURL, err := url.Parse(baseURL) - if err != nil { - return err - } - c.Server = newBaseURL.String() - return nil - } -} - -// ClientWithResponsesInterface is the interface specification for the client with responses above. -type ClientWithResponsesInterface interface { - // GetEventWithResponse request - GetEventWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetEventResponse, error) - - // PostProviderListWithResponse request - PostProviderListWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostProviderListResponse, error) - - // PostSessionAbortWithBodyWithResponse request with any body - PostSessionAbortWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionAbortResponse, error) - - PostSessionAbortWithResponse(ctx context.Context, body PostSessionAbortJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionAbortResponse, error) - - // PostSessionChatWithBodyWithResponse request with any body - PostSessionChatWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionChatResponse, error) - - PostSessionChatWithResponse(ctx context.Context, body PostSessionChatJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionChatResponse, error) - - // PostSessionCreateWithResponse request - PostSessionCreateWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostSessionCreateResponse, error) - - // PostSessionListWithResponse request - PostSessionListWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostSessionListResponse, error) - - // PostSessionMessagesWithBodyWithResponse request with any body - PostSessionMessagesWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionMessagesResponse, error) - - PostSessionMessagesWithResponse(ctx context.Context, body PostSessionMessagesJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionMessagesResponse, error) - - // PostSessionShareWithBodyWithResponse request with any body - PostSessionShareWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionShareResponse, error) - - PostSessionShareWithResponse(ctx context.Context, body PostSessionShareJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionShareResponse, error) - - // PostSessionSummarizeWithBodyWithResponse request with any body - PostSessionSummarizeWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionSummarizeResponse, error) - - PostSessionSummarizeWithResponse(ctx context.Context, body PostSessionSummarizeJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionSummarizeResponse, error) -} - -type GetEventResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *Event -} - -// Status returns HTTPResponse.Status -func (r GetEventResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r GetEventResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type PostProviderListResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *[]ProviderInfo -} - -// Status returns HTTPResponse.Status -func (r PostProviderListResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r PostProviderListResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type PostSessionAbortResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *bool -} - -// Status returns HTTPResponse.Status -func (r PostSessionAbortResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r PostSessionAbortResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type PostSessionChatResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *MessageInfo -} - -// Status returns HTTPResponse.Status -func (r PostSessionChatResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r PostSessionChatResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type PostSessionCreateResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *SessionInfo -} - -// Status returns HTTPResponse.Status -func (r PostSessionCreateResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r PostSessionCreateResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type PostSessionListResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *[]SessionInfo -} - -// Status returns HTTPResponse.Status -func (r PostSessionListResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r PostSessionListResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type PostSessionMessagesResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *[]MessageInfo -} - -// Status returns HTTPResponse.Status -func (r PostSessionMessagesResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r PostSessionMessagesResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type PostSessionShareResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *SessionInfo -} - -// Status returns HTTPResponse.Status -func (r PostSessionShareResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r PostSessionShareResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -type PostSessionSummarizeResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *bool -} - -// Status returns HTTPResponse.Status -func (r PostSessionSummarizeResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r PostSessionSummarizeResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - -// GetEventWithResponse request returning *GetEventResponse -func (c *ClientWithResponses) GetEventWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetEventResponse, error) { - rsp, err := c.GetEvent(ctx, reqEditors...) - if err != nil { - return nil, err - } - return ParseGetEventResponse(rsp) -} - -// PostProviderListWithResponse request returning *PostProviderListResponse -func (c *ClientWithResponses) PostProviderListWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostProviderListResponse, error) { - rsp, err := c.PostProviderList(ctx, reqEditors...) - if err != nil { - return nil, err - } - return ParsePostProviderListResponse(rsp) -} - -// PostSessionAbortWithBodyWithResponse request with arbitrary body returning *PostSessionAbortResponse -func (c *ClientWithResponses) PostSessionAbortWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionAbortResponse, error) { - rsp, err := c.PostSessionAbortWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParsePostSessionAbortResponse(rsp) -} - -func (c *ClientWithResponses) PostSessionAbortWithResponse(ctx context.Context, body PostSessionAbortJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionAbortResponse, error) { - rsp, err := c.PostSessionAbort(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParsePostSessionAbortResponse(rsp) -} - -// PostSessionChatWithBodyWithResponse request with arbitrary body returning *PostSessionChatResponse -func (c *ClientWithResponses) PostSessionChatWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionChatResponse, error) { - rsp, err := c.PostSessionChatWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParsePostSessionChatResponse(rsp) -} - -func (c *ClientWithResponses) PostSessionChatWithResponse(ctx context.Context, body PostSessionChatJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionChatResponse, error) { - rsp, err := c.PostSessionChat(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParsePostSessionChatResponse(rsp) -} - -// PostSessionCreateWithResponse request returning *PostSessionCreateResponse -func (c *ClientWithResponses) PostSessionCreateWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostSessionCreateResponse, error) { - rsp, err := c.PostSessionCreate(ctx, reqEditors...) - if err != nil { - return nil, err - } - return ParsePostSessionCreateResponse(rsp) -} - -// PostSessionListWithResponse request returning *PostSessionListResponse -func (c *ClientWithResponses) PostSessionListWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostSessionListResponse, error) { - rsp, err := c.PostSessionList(ctx, reqEditors...) - if err != nil { - return nil, err - } - return ParsePostSessionListResponse(rsp) -} - -// PostSessionMessagesWithBodyWithResponse request with arbitrary body returning *PostSessionMessagesResponse -func (c *ClientWithResponses) PostSessionMessagesWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionMessagesResponse, error) { - rsp, err := c.PostSessionMessagesWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParsePostSessionMessagesResponse(rsp) -} - -func (c *ClientWithResponses) PostSessionMessagesWithResponse(ctx context.Context, body PostSessionMessagesJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionMessagesResponse, error) { - rsp, err := c.PostSessionMessages(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParsePostSessionMessagesResponse(rsp) -} - -// PostSessionShareWithBodyWithResponse request with arbitrary body returning *PostSessionShareResponse -func (c *ClientWithResponses) PostSessionShareWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionShareResponse, error) { - rsp, err := c.PostSessionShareWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParsePostSessionShareResponse(rsp) -} - -func (c *ClientWithResponses) PostSessionShareWithResponse(ctx context.Context, body PostSessionShareJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionShareResponse, error) { - rsp, err := c.PostSessionShare(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParsePostSessionShareResponse(rsp) -} - -// PostSessionSummarizeWithBodyWithResponse request with arbitrary body returning *PostSessionSummarizeResponse -func (c *ClientWithResponses) PostSessionSummarizeWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionSummarizeResponse, error) { - rsp, err := c.PostSessionSummarizeWithBody(ctx, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParsePostSessionSummarizeResponse(rsp) -} - -func (c *ClientWithResponses) PostSessionSummarizeWithResponse(ctx context.Context, body PostSessionSummarizeJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionSummarizeResponse, error) { - rsp, err := c.PostSessionSummarize(ctx, body, reqEditors...) - if err != nil { - return nil, err - } - return ParsePostSessionSummarizeResponse(rsp) -} - -// ParseGetEventResponse parses an HTTP response from a GetEventWithResponse call -func ParseGetEventResponse(rsp *http.Response) (*GetEventResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &GetEventResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest Event - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParsePostProviderListResponse parses an HTTP response from a PostProviderListWithResponse call -func ParsePostProviderListResponse(rsp *http.Response) (*PostProviderListResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &PostProviderListResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []ProviderInfo - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParsePostSessionAbortResponse parses an HTTP response from a PostSessionAbortWithResponse call -func ParsePostSessionAbortResponse(rsp *http.Response) (*PostSessionAbortResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &PostSessionAbortResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest bool - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParsePostSessionChatResponse parses an HTTP response from a PostSessionChatWithResponse call -func ParsePostSessionChatResponse(rsp *http.Response) (*PostSessionChatResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &PostSessionChatResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest MessageInfo - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParsePostSessionCreateResponse parses an HTTP response from a PostSessionCreateWithResponse call -func ParsePostSessionCreateResponse(rsp *http.Response) (*PostSessionCreateResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &PostSessionCreateResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest SessionInfo - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParsePostSessionListResponse parses an HTTP response from a PostSessionListWithResponse call -func ParsePostSessionListResponse(rsp *http.Response) (*PostSessionListResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &PostSessionListResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []SessionInfo - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParsePostSessionMessagesResponse parses an HTTP response from a PostSessionMessagesWithResponse call -func ParsePostSessionMessagesResponse(rsp *http.Response) (*PostSessionMessagesResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &PostSessionMessagesResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []MessageInfo - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParsePostSessionShareResponse parses an HTTP response from a PostSessionShareWithResponse call -func ParsePostSessionShareResponse(rsp *http.Response) (*PostSessionShareResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &PostSessionShareResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest SessionInfo - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - -// ParsePostSessionSummarizeResponse parses an HTTP response from a PostSessionSummarizeWithResponse call -func ParsePostSessionSummarizeResponse(rsp *http.Response) (*PostSessionSummarizeResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &PostSessionSummarizeResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest bool - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} diff --git a/sst-env.d.ts b/sst-env.d.ts new file mode 100644 index 000000000..7ca38b723 --- /dev/null +++ b/sst-env.d.ts @@ -0,0 +1,24 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ + +declare module "sst" { + export interface Resource { + "Api": { + "type": "sst.cloudflare.Worker" + "url": string + } + "Bucket": { + "type": "sst.cloudflare.Bucket" + } + "Web": { + "type": "sst.cloudflare.StaticSite" + "url": string + } + } +} +/// + +import "sst" +export {} \ No newline at end of file diff --git a/sst.config.ts b/sst.config.ts new file mode 100644 index 000000000..4c36fea58 --- /dev/null +++ b/sst.config.ts @@ -0,0 +1,18 @@ +/// + +export default $config({ + app(input) { + return { + name: "opencode", + removal: input?.stage === "production" ? "retain" : "remove", + protect: ["production"].includes(input?.stage), + home: "cloudflare", + } + }, + async run() { + const { api } = await import("./infra/app.js") + return { + api: api.url, + } + }, +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1 @@ +{} -- cgit v1.2.3