From 42d86991c4e1e727491b5a181bd96721e21af4a0 Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 9 Jul 2026 23:08:18 +0800 Subject: [PATCH 1/2] feat(web): replace Vue product UI with React as the sole frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the Vue app and promote the React migration to web/, so make build-web, Go embed, Tauri, and CI/release all ship one UI stack. - Delete Vue sources; rename web-react → web (React 18 + jcode-ui) - Point Vite, Makefile, Tauri, and theme codegen at web/ → internal/web/dist - Update CI web job for monorepo packages + React typecheck/build - Drop dual-path targets (FRONTEND=, build-web-react, tauri.react.conf) - Parity work for workspace picker, remote wizard, messages/tools, i18n --- .github/workflows/ci.yml | 23 +- .gitignore | 5 +- AGENTS.md | 48 +- Makefile | 59 +- desktop/src-tauri/tauri.conf.json | 2 +- desktop/src-tauri/tauri.react.conf.json | 8 - internal/theme/gen/main.go | 6 +- .../src/primitives/ToolCallView.tsx | 115 +- packages/jcode-ui/src/components/Message.tsx | 360 +- packages/jcode-ui/src/components/Thread.tsx | 62 +- .../jcode-ui/src/components/ToolCallCard.tsx | 241 +- .../src/components/ToolRegistryContext.tsx | 10 +- packages/jcode-ui/src/lib/markdown.ts | 12 +- packages/jcode-ui/src/styles/components.css | 223 +- .../src/toolRenderers/browserShot.tsx | 22 +- packages/jcode-ui/src/toolRenderers/diff.tsx | 142 +- .../jcode-ui/src/toolRenderers/fileViewer.tsx | 122 +- .../jcode-ui/src/toolRenderers/generic.tsx | 75 +- packages/jcode-ui/src/toolRenderers/index.ts | 7 +- .../jcode-ui/src/toolRenderers/search.tsx | 132 +- packages/jcode-ui/src/toolRenderers/skill.tsx | 43 +- packages/jcode-ui/src/toolRenderers/team.tsx | 308 +- .../jcode-ui/src/toolRenderers/terminal.tsx | 53 +- packages/jcode-ui/src/toolRenderers/todo.tsx | 175 +- pnpm-lock.yaml | 2 +- pnpm-workspace.yaml | 9 +- site/docs/chat-ui/components.md | 2 +- site/docs/themes.md | 2 +- web-react/index.html | 102 - web-react/package.json | 46 - web-react/src/components/GoalBanner.tsx | 25 - .../src/components/RemoteConnectWizard.tsx | 421 -- web-react/src/components/WorkspacePicker.tsx | 300 -- web-react/src/i18n/index.ts | 402 -- web-react/src/i18n/locales/en.ts | 844 ---- web-react/src/i18n/locales/ja.ts | 765 ---- web-react/src/i18n/locales/ko.ts | 765 ---- web-react/src/i18n/locales/zh-Hans.ts | 825 ---- web-react/src/i18n/locales/zh-Hant.ts | 766 ---- web-react/src/styles.css | 194 - web-react/src/styles/tokens.generated.css | 234 -- web-react/tsconfig.app.json | 28 - web-react/tsconfig.json | 7 - web-react/tsconfig.node.json | 16 - web-react/vite.config.ts | 35 - web/.editorconfig | 8 - web/.gitattributes | 1 - web/.gitignore | 39 - web/.oxfmtrc.json | 5 - web/.oxlintrc.json | 10 - web/.vscode/extensions.json | 8 - web/README.md | 48 - web/env.d.ts | 1 - web/eslint.config.ts | 26 - web/index.html | 75 +- web/package.json | 75 +- web/pnpm-lock.yaml | 3633 ----------------- web/pnpm-workspace.yaml | 3 - {web-react => web}/src/App.tsx | 85 +- web/src/App.vue | 975 ----- {web-react => web}/src/app/hooks.ts | 0 {web-react => web}/src/app/runtime.ts | 0 {web-react => web}/src/app/store.ts | 0 {web-react => web}/src/app/wsBridge.ts | 1 + web/src/components/ApprovalBanner.vue | 376 -- web/src/components/AskUserCard.vue | 305 -- .../src/components/AuthGate.tsx | 0 web/src/components/AutomationEditorDialog.vue | 311 -- web/src/components/AutomationRunView.vue | 400 -- .../src/components/AutomationsView.tsx | 0 web/src/components/AutomationsView.vue | 505 --- .../src/components/BranchPicker.tsx | 0 web/src/components/BranchPicker.vue | 533 --- .../src/components/ChannelsView.tsx | 110 +- web/src/components/ChannelsView.vue | 400 -- .../src/components/ChatInput.tsx | 34 +- web/src/components/ChatInput.vue | 2210 ---------- web/src/components/ChatMessage.vue | 206 - .../src/components/ChatView.tsx | 24 +- .../src/components/CommandPalette.tsx | 11 +- web/src/components/CommandPalette.vue | 305 -- web/src/components/ContextCapacityPopup.vue | 164 - web/src/components/DiffViewer.vue | 222 - web/src/components/FileTreePanel.vue | 319 -- web/src/components/GoalBanner.tsx | 88 + web/src/components/GoalBanner.vue | 92 - web/src/components/MenuSelect.vue | 313 -- web/src/components/ModelEditDialog.vue | 384 -- web/src/components/PageSurface.vue | 82 - .../src/components/ProjectHeader.tsx | 0 web/src/components/ProjectPickerPanel.vue | 537 --- web/src/components/ProjectSwitcher.vue | 327 -- web/src/components/ProviderEditDialog.vue | 598 --- .../src/components/ProviderIcon.tsx | 0 web/src/components/ProviderIcon.vue | 57 - web/src/components/RemoteConnectWizard.tsx | 935 +++++ web/src/components/RemoteConnectWizard.vue | 1124 ----- .../src/components/RightPanel.tsx | 0 web/src/components/RightPanel.vue | 241 -- .../src/components/SettingsDialog.tsx | 414 +- web/src/components/SettingsDialog.vue | 2732 ------------- .../src/components/SetupView.tsx | 0 web/src/components/SetupView.vue | 617 --- {web-react => web}/src/components/Sidebar.tsx | 399 +- web/src/components/Sidebar.vue | 1087 ----- web/src/components/SidebarFilterMenu.tsx | 307 ++ web/src/components/SidebarFilterMenu.vue | 366 -- web/src/components/TaskList.vue | 101 - web/src/components/TerminalInstance.vue | 252 -- .../src/components/TerminalPanel.tsx | 5 +- web/src/components/TerminalPanel.vue | 194 - .../src/components/ThemeToggle.tsx | 6 +- web/src/components/TokenGate.vue | 169 - web/src/components/ToolCallCard.vue | 637 --- {web-react => web}/src/components/TopBar.tsx | 0 web/src/components/TopBar.vue | 269 -- web/src/components/UsageStatsPanel.vue | 405 -- web/src/components/WorkspacePicker.tsx | 538 +++ web/src/components/WorkspacePicker.vue | 563 --- web/src/composables/api.ts | 384 -- web/src/composables/apiBase.ts | 99 - web/src/composables/authToken.ts | 58 - web/src/composables/markdown.ts | 31 - web/src/composables/notifications.ts | 52 - web/src/composables/toolInfo.ts | 72 - web/src/composables/useBranch.ts | 116 - web/src/composables/useDesktop.ts | 104 - web/src/composables/useFolderBrowser.ts | 68 - web/src/composables/useTheme.ts | 114 - web/src/composables/ws.ts | 173 - web/src/i18n/index.ts | 127 +- web/src/i18n/locales/en.ts | 85 +- web/src/i18n/locales/ja.ts | 112 +- web/src/i18n/locales/ko.ts | 120 +- web/src/i18n/locales/zh-Hans.ts | 81 +- web/src/i18n/locales/zh-Hant.ts | 102 +- {web-react => web}/src/lib/api.ts | 0 {web-react => web}/src/lib/apiBase.ts | 0 {web-react => web}/src/lib/authToken.ts | 0 {web-react => web}/src/lib/automation.ts | 0 {web-react => web}/src/lib/providerIcons.ts | 0 web/src/lib/remote.ts | 44 + {web-react => web}/src/lib/toolInfo.ts | 0 {web-react => web}/src/lib/types.ts | 0 {web-react => web}/src/lib/useDesktop.ts | 12 + {web-react => web}/src/lib/useTheme.ts | 36 +- {web-react => web}/src/lib/ws.ts | 5 + web/src/main.ts | 29 - {web-react => web}/src/main.tsx | 0 web/src/stores/automation.ts | 99 - web/src/stores/chat.ts | 1209 ------ web/src/stores/project.ts | 355 -- web/src/stores/usage.ts | 44 - web/src/style.css | 228 -- web/src/styles.css | 360 ++ web/src/styles/animations.css | 205 - web/src/styles/tokens.css | 263 -- web/src/types/api.ts | 685 ---- web/src/types/automation.ts | 68 - web/src/utils/providerIcons.ts | 61 - {web-react => web}/src/vite-env.d.ts | 0 web/tsconfig.app.json | 36 +- web/tsconfig.json | 8 +- web/tsconfig.node.json | 33 +- web/vite.config.ts | 20 +- 165 files changed, 5069 insertions(+), 33939 deletions(-) delete mode 100644 desktop/src-tauri/tauri.react.conf.json delete mode 100644 web-react/index.html delete mode 100644 web-react/package.json delete mode 100644 web-react/src/components/GoalBanner.tsx delete mode 100644 web-react/src/components/RemoteConnectWizard.tsx delete mode 100644 web-react/src/components/WorkspacePicker.tsx delete mode 100644 web-react/src/i18n/index.ts delete mode 100644 web-react/src/i18n/locales/en.ts delete mode 100644 web-react/src/i18n/locales/ja.ts delete mode 100644 web-react/src/i18n/locales/ko.ts delete mode 100644 web-react/src/i18n/locales/zh-Hans.ts delete mode 100644 web-react/src/i18n/locales/zh-Hant.ts delete mode 100644 web-react/src/styles.css delete mode 100644 web-react/src/styles/tokens.generated.css delete mode 100644 web-react/tsconfig.app.json delete mode 100644 web-react/tsconfig.json delete mode 100644 web-react/tsconfig.node.json delete mode 100644 web-react/vite.config.ts delete mode 100644 web/.editorconfig delete mode 100644 web/.gitattributes delete mode 100644 web/.gitignore delete mode 100644 web/.oxfmtrc.json delete mode 100644 web/.oxlintrc.json delete mode 100644 web/.vscode/extensions.json delete mode 100644 web/README.md delete mode 100644 web/env.d.ts delete mode 100644 web/eslint.config.ts delete mode 100644 web/pnpm-lock.yaml delete mode 100644 web/pnpm-workspace.yaml rename {web-react => web}/src/App.tsx (85%) delete mode 100644 web/src/App.vue rename {web-react => web}/src/app/hooks.ts (100%) rename {web-react => web}/src/app/runtime.ts (100%) rename {web-react => web}/src/app/store.ts (100%) rename {web-react => web}/src/app/wsBridge.ts (97%) delete mode 100644 web/src/components/ApprovalBanner.vue delete mode 100644 web/src/components/AskUserCard.vue rename {web-react => web}/src/components/AuthGate.tsx (100%) delete mode 100644 web/src/components/AutomationEditorDialog.vue delete mode 100644 web/src/components/AutomationRunView.vue rename {web-react => web}/src/components/AutomationsView.tsx (100%) delete mode 100644 web/src/components/AutomationsView.vue rename {web-react => web}/src/components/BranchPicker.tsx (100%) delete mode 100644 web/src/components/BranchPicker.vue rename {web-react => web}/src/components/ChannelsView.tsx (88%) delete mode 100644 web/src/components/ChannelsView.vue rename {web-react => web}/src/components/ChatInput.tsx (97%) delete mode 100644 web/src/components/ChatInput.vue delete mode 100644 web/src/components/ChatMessage.vue rename {web-react => web}/src/components/ChatView.tsx (78%) rename {web-react => web}/src/components/CommandPalette.tsx (96%) delete mode 100644 web/src/components/CommandPalette.vue delete mode 100644 web/src/components/ContextCapacityPopup.vue delete mode 100644 web/src/components/DiffViewer.vue delete mode 100644 web/src/components/FileTreePanel.vue create mode 100644 web/src/components/GoalBanner.tsx delete mode 100644 web/src/components/GoalBanner.vue delete mode 100644 web/src/components/MenuSelect.vue delete mode 100644 web/src/components/ModelEditDialog.vue delete mode 100644 web/src/components/PageSurface.vue rename {web-react => web}/src/components/ProjectHeader.tsx (100%) delete mode 100644 web/src/components/ProjectPickerPanel.vue delete mode 100644 web/src/components/ProjectSwitcher.vue delete mode 100644 web/src/components/ProviderEditDialog.vue rename {web-react => web}/src/components/ProviderIcon.tsx (100%) delete mode 100644 web/src/components/ProviderIcon.vue create mode 100644 web/src/components/RemoteConnectWizard.tsx delete mode 100644 web/src/components/RemoteConnectWizard.vue rename {web-react => web}/src/components/RightPanel.tsx (100%) delete mode 100644 web/src/components/RightPanel.vue rename {web-react => web}/src/components/SettingsDialog.tsx (87%) delete mode 100644 web/src/components/SettingsDialog.vue rename {web-react => web}/src/components/SetupView.tsx (100%) delete mode 100644 web/src/components/SetupView.vue rename {web-react => web}/src/components/Sidebar.tsx (72%) delete mode 100644 web/src/components/Sidebar.vue create mode 100644 web/src/components/SidebarFilterMenu.tsx delete mode 100644 web/src/components/SidebarFilterMenu.vue delete mode 100644 web/src/components/TaskList.vue delete mode 100644 web/src/components/TerminalInstance.vue rename {web-react => web}/src/components/TerminalPanel.tsx (97%) delete mode 100644 web/src/components/TerminalPanel.vue rename {web-react => web}/src/components/ThemeToggle.tsx (94%) delete mode 100644 web/src/components/TokenGate.vue delete mode 100644 web/src/components/ToolCallCard.vue rename {web-react => web}/src/components/TopBar.tsx (100%) delete mode 100644 web/src/components/TopBar.vue delete mode 100644 web/src/components/UsageStatsPanel.vue create mode 100644 web/src/components/WorkspacePicker.tsx delete mode 100644 web/src/components/WorkspacePicker.vue delete mode 100644 web/src/composables/api.ts delete mode 100644 web/src/composables/apiBase.ts delete mode 100644 web/src/composables/authToken.ts delete mode 100644 web/src/composables/markdown.ts delete mode 100644 web/src/composables/notifications.ts delete mode 100644 web/src/composables/toolInfo.ts delete mode 100644 web/src/composables/useBranch.ts delete mode 100644 web/src/composables/useDesktop.ts delete mode 100644 web/src/composables/useFolderBrowser.ts delete mode 100644 web/src/composables/useTheme.ts delete mode 100644 web/src/composables/ws.ts rename {web-react => web}/src/lib/api.ts (100%) rename {web-react => web}/src/lib/apiBase.ts (100%) rename {web-react => web}/src/lib/authToken.ts (100%) rename {web-react => web}/src/lib/automation.ts (100%) rename {web-react => web}/src/lib/providerIcons.ts (100%) create mode 100644 web/src/lib/remote.ts rename {web-react => web}/src/lib/toolInfo.ts (100%) rename {web-react => web}/src/lib/types.ts (100%) rename {web-react => web}/src/lib/useDesktop.ts (80%) rename {web-react => web}/src/lib/useTheme.ts (80%) rename {web-react => web}/src/lib/ws.ts (95%) delete mode 100644 web/src/main.ts rename {web-react => web}/src/main.tsx (100%) delete mode 100644 web/src/stores/automation.ts delete mode 100644 web/src/stores/chat.ts delete mode 100644 web/src/stores/project.ts delete mode 100644 web/src/stores/usage.ts delete mode 100644 web/src/style.css create mode 100644 web/src/styles.css delete mode 100644 web/src/styles/animations.css delete mode 100644 web/src/styles/tokens.css delete mode 100644 web/src/types/api.ts delete mode 100644 web/src/types/automation.ts delete mode 100644 web/src/utils/providerIcons.ts rename {web-react => web}/src/vite-env.d.ts (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b765ff1..79a586d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,8 +76,8 @@ jobs: - uses: actions/checkout@v5 # Go is needed only to run the theme generator, which writes - # web/src/composables/themes.generated.ts + styles/tokens.generated.css - # (gitignored) — without them vue-tsc can't resolve ./themes.generated. + # web/src/lib/themes.generated.ts + styles/tokens.generated.css + # (gitignored) — without them tsc can't resolve the theme registry. - uses: actions/setup-go@v6 with: go-version-file: 'go.mod' @@ -92,18 +92,19 @@ jobs: - name: Generate theme assets run: go generate ./internal/theme/... - - name: Install deps - working-directory: web - run: pnpm install --frozen-lockfile + - name: Install monorepo deps + run: pnpm install --frozen-lockfile || pnpm install - - name: Type-check - working-directory: web - run: pnpm type-check + - name: Build packages + run: | + cd packages/jcode-ui-core && npx tsc -p tsconfig.build.json + cd ../jcode-ui && npx tsc -p tsconfig.build.json + npx tailwindcss -i src/styles/entry.css -o dist/styles.css --minify - - name: Lint + - name: Type-check web app working-directory: web - run: npx oxlint . + run: npx tsc --noEmit -p tsconfig.app.json - - name: Build + - name: Build web app working-directory: web run: npx vite build diff --git a/.gitignore b/.gitignore index bf3bd38b..e1b6e9c4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,17 +3,16 @@ /jcode-new /weixin_poc internal/web/dist -internal/web/dist-react internal/model/registry_generated.go web/src/styles/tokens.generated.css -web/src/composables/themes.generated.ts +web/src/lib/themes.generated.ts # Node / pnpm (root monorepo + per-app workspaces) node_modules/ **/node_modules/ *.tsbuildinfo packages/*/dist/ -web-react/dist/ +web/dist/ # Lockfile — committed at repo root for the monorepo # (pnpm-lock.yaml IS committed; do NOT ignore it) diff --git a/AGENTS.md b/AGENTS.md index dddb03c4..4666bd6d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md — JCode Project Development Guide -Go coding agent — [Eino](https://github.com/cloudwego/eino) + BubbleTea v2 TUI + Vue 3 web UI + Tauri 2 desktop shell. +Go coding agent — [Eino](https://github.com/cloudwego/eino) + BubbleTea v2 TUI + React web UI + Tauri 2 desktop shell. - **Module:** `github.com/cnjack/jcode` | **Entry:** `cmd/jcode/` | **Config dir:** `~/.jcode/` @@ -12,7 +12,7 @@ Go coding agent — [Eino](https://github.com/cloudwego/eino) + BubbleTea v2 TUI make build # generate → build-web → go build make install # generate → build-web → go install make run # go run ./cmd/jcode/ -make lint # golangci-lint + eslint/oxlint (lint-go / lint-web) +make lint # golangci-lint + React typecheck (lint-go / lint-web) make doctor # system check make desktop-dev # Tauri desktop app in dev mode (rebuilds the Go sidecar first) make desktop-build # distributable desktop bundle (.app/.dmg/.msi) @@ -52,9 +52,8 @@ internal/ feature/ # Compile-time feature flags via build tags (e.g. desktop, jcode_headless, ble) telemetry/ # Optional Langfuse tracing tui/ # BubbleTea v2 TUI components - web/ # HTTP server (REST + WS + PTY) + embedded Vue dist -web/ # Vue 3 + Vite + TypeScript frontend source (the CURRENT product UI) -web-react/ # React 18 + Vite + RTK product app (migration in progress; parallel to web/) + web/ # HTTP server (REST + WS + PTY) + embedded React dist +web/ # React 18 + Vite + RTK product UI (embedded in the binary / Tauri) packages/ # pnpm workspace: the reusable jcode-ui component library jcode-ui/ # published styled React chat components (→ npm: jcode-ui) jcode-ui-core/ # framework-agnostic core: types, ChatRuntime, headless primitives @@ -66,18 +65,15 @@ script/ # Build-time code generation + install.sh agent-eval/ # Agent evaluation harness + showcase generation ``` -### Frontend migration (Vue → React) — in progress +### Frontend (React) -The product UI is migrating from Vue 3 (`web/`) to React 18 (`web-react/`), -built on a new reusable component library (`packages/jcode-ui` + `jcode-ui-core`). -**During the migration both coexist:** +The product UI is React 18 (`web/`) built on `packages/jcode-ui` + `jcode-ui-core`. -- `make build-web` (default) builds the **Vue** app → `internal/web/dist/` (production). -- `make build-web-react` builds the **React** app + packages → `internal/web/dist-react/` (parallel validation). -- `make lint-react` typechecks the React app + both packages. -- The Go `embed.FS` and Tauri `frontendDist` still point at the Vue `dist/`. The switch-over happens once `web-react` reaches feature parity. +- `make build-web` builds packages + the React app → `internal/web/dist/` (production embed). +- `make lint-web` typechecks the React app + both packages. +- Go `//go:embed dist/*` and Tauri `frontendDist` both point at `internal/web/dist/`. -The component library is the migration's organizing principle — see `packages/jcode-ui/README.md` and `site/docs/chat-ui/`. It's published to npm as `jcode-ui` (styled) + `jcode-ui-core` (headless). The runtime abstraction (`ChatRuntime` + `createExternalStoreRuntime`) is the seam that lets the components render from any Redux-shaped store. +See `packages/jcode-ui/README.md` and `site/docs/chat-ui/`. Published to npm as `jcode-ui` (styled) + `jcode-ui-core` (headless). The runtime abstraction (`ChatRuntime` + `createExternalStoreRuntime`) is the seam that lets the components render from any Redux-shaped store. ### Key Design Decisions @@ -192,21 +188,19 @@ The component library is the migration's organizing principle — see `packages/ --- -## Frontend (web/) — Vue (production) +## Frontend (web/) — React (production) -> **Note:** the product UI is migrating to React (`web-react/` + `packages/jcode-ui`). The Vue app remains the production build during the migration. New reusable UI work goes in `packages/jcode-ui` (React); see the migration section above and `packages/jcode-ui/README.md`. - -- **Stack:** Vue 3 + TypeScript + Vite -- **Build:** `cd web && pnpm install && npx vite build` (or `make build-web`) -- **Output:** builds to `internal/web/dist/`, embedded in Go binary via `//go:embed` -- **Lint:** `cd web && pnpm lint` (eslint + oxlint) +- **Stack:** React 18 + TypeScript + Vite + Redux Toolkit + `jcode-ui` / `jcode-ui-core` +- **Build:** `make build-web` (packages + `cd web && npx vite build`) +- **Output:** builds to `internal/web/dist/`, embedded in the Go binary via `//go:embed` +- **Lint:** `make lint-web` (tsc for web + packages) - Changes to the frontend require rebuilding via `make build-web` for the Go binary to pick them up -- **Don't confuse `web/` with `site/`:** `web/` (Vue) is the product UI embedded in the binary and reused by the desktop app; `site/` (React) is the public website + docs at www.j-code.net and is deployed separately (`cd site && pnpm build`). +- **Don't confuse `web/` with `site/`:** `web/` is the product UI embedded in the binary and reused by the desktop app; `site/` is the public website + docs at www.j-code.net and is deployed separately (`cd site && pnpm build`). ### Icons & Styling -- **Icons:** use `@heroicons/vue/24/outline` exclusively. Import each icon by name from its subpath (`import { XMarkIcon } from '@heroicons/vue/24/outline'`) for per-file tree-shaking. Do **not** hand-write inline `` icons or `v-html` SVG path strings. -- **Icon sizing:** use Tailwind `w-N h-N` classes (e.g. `class="w-3.5 h-3.5"`), never a `:size` prop. -- **Colors:** every color must come from a CSS custom property defined in `src/styles/tokens.css`. Never hardcode hex/rgb/`#fff`/`white` in `.vue` or `.css`. Text on the primary/destructive fills uses `--color-on-primary` / `--color-on-destructive`; code blocks use `--code-bg` / `--code-border`; syntax highlighting uses `--hljs-*`. -- **Terminal (xterm) colors:** live in tokens (`--term-*` and the 16-color ANSI palette) and are read at runtime via `getComputedStyle` in `TerminalInstance.vue` — see `termTheme()`. Do not define terminal colors inline. -- **Adding a new color:** add the token to `tokens.css` (both `:root` light and `.dark`) first, then reference it by `var(...)`. To theme it per generated theme, edit `internal/theme/palette.go` and regenerate — never edit `tokens.generated.css` by hand. +- **Icons:** use `@heroicons/react/24/outline` exclusively. Import each icon by name. Do **not** hand-write inline `` icons. +- **Icon sizing:** use Tailwind `h-N w-N` classes (e.g. `className="h-3.5 w-3.5"`). +- **Colors:** every color must come from a CSS custom property (jcode-ui tokens / `tokens.generated.css`). Never hardcode hex/rgb/`#fff`/`white` in components. +- **Themes:** edit `internal/theme/palette.go` and run `make generate` — never edit `tokens.generated.css` or `themes.generated.ts` by hand. +- **Reusable chat UI:** prefer components from `packages/jcode-ui` over one-off markup in `web/`. diff --git a/Makefile b/Makefile index caa852d5..a9b26c3b 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ LDFLAGS := -s -w \ export GOFLAGS := -buildvcs=false -.PHONY: build build-binary run doctor version install clean build-web build-web-react fmt lint lint-go lint-web lint-react generate setup-hooks desktop-icons desktop-sidecar desktop-dev desktop-build desktop-react-dev desktop-react-build desktop-clean +.PHONY: build build-binary run doctor version install clean build-web fmt lint lint-go lint-web generate setup-hooks desktop-icons desktop-sidecar desktop-dev desktop-build desktop-clean build-ble fmt: @echo "Formatting Go..." @@ -26,54 +26,29 @@ lint-go: golangci-lint run lint-web: - @echo "Linting frontend (Vue)..." - cd web && (pnpm install --frozen-lockfile 2>/dev/null || pnpm install) - cd web && pnpm lint - -lint-react: @echo "Type-checking React frontend + packages..." - cd packages/jcode-ui-core && pnpm install --frozen-lockfile 2>/dev/null || pnpm install + -pnpm install --frozen-lockfile 2>/dev/null || true cd packages/jcode-ui-core && npx tsc --noEmit -p tsconfig.json cd packages/jcode-ui && npx tsc --noEmit -p tsconfig.json - cd web-react && npx tsc --noEmit -p tsconfig.app.json + cd web && npx tsc --noEmit -p tsconfig.app.json generate: @echo "Generating code..." go generate ./internal/model/... go generate ./internal/theme/... -# The frontend build. FRONTEND selects which app to build: -# web (default) — the current Vue app (production) -# web-react — the new React app (migration in progress; produces dist-react/) -# During the migration both coexist. The switch-over to React-as-default happens -# once web-react reaches feature parity and the Go embed points at dist-react. -FRONTEND ?= web - -build-web: generate - @echo "Building frontend ($(FRONTEND))..." -ifeq ($(FRONTEND),web-react) - $(MAKE) build-web-react -else - cd web && (pnpm install --frozen-lockfile 2>/dev/null || pnpm install) - cd web && npx vite build -endif - -# Build the React frontend + the two component-library packages it depends on. -# Output goes to ../internal/web/dist-react (kept separate from the Vue dist -# until the embed path is switched). The packages are workspace-linked, so this -# also builds jcode-ui-core (a dependency of jcode-ui). +# Build the React product UI (web/) + jcode-ui packages into internal/web/dist +# for Go //go:embed and the Tauri shell. # # NOTE: pnpm's ERR_PNPM_IGNORED_BUILDS (esbuild/@parcel/watcher native scripts) -# returns a non-zero exit even when deps are fully installed. We tolerate that -# exit code from the install step — the build steps below don't depend on those -# build scripts having run. -build-web-react: generate +# can return non-zero even when deps are fully installed. Tolerate install exit. +build-web: generate @echo "Building React frontend + packages..." -pnpm install --frozen-lockfile 2>/dev/null || true cd packages/jcode-ui-core && npx tsc -p tsconfig.build.json cd packages/jcode-ui && npx tsc -p tsconfig.build.json cd packages/jcode-ui && npx tailwindcss -i src/styles/entry.css -o dist/styles.css --minify - cd web-react && npx vite build + cd web && npx vite build # The main binary never links CoreBluetooth (whose eager init triggers the macOS # Bluetooth permission prompt at startup). BLE runs in a separate `jcode-ble` @@ -112,6 +87,7 @@ version: clean: rm -f $(BIN) rm -rf internal/web/dist + rm -rf packages/jcode-ui/dist packages/jcode-ui-core/dist setup-hooks: @git config core.hooksPath .githooks @@ -155,22 +131,5 @@ desktop-dev: desktop-sidecar desktop-build: desktop-sidecar cd $(DESKTOP_DIR) && (pnpm install 2>/dev/null || npm install) && pnpm tauri build -# ─── React desktop variants ──────────────────────────────────────────────── -# Same as desktop-dev/desktop-build but load the React frontend (web-react/) -# instead of the Vue app (web/). Uses tauri.react.conf.json to override the -# build block (frontendDist / beforeDevCommand / beforeBuildCommand) — every -# other Tauri setting (window, tray, sidecar, capabilities) is inherited. -# Requires `pnpm install` to have run once at the repo root (the React -# workspace lives there, not under desktop/). -desktop-react-dev: desktop-sidecar - @echo "Launching desktop (React frontend)…" - cd $(DESKTOP_DIR) && (pnpm install 2>/dev/null || npm install) && \ - pnpm tauri dev --config src-tauri/tauri.react.conf.json - -desktop-react-build: desktop-sidecar - @echo "Bundling desktop (React frontend)…" - cd $(DESKTOP_DIR) && (pnpm install 2>/dev/null || npm install) && \ - pnpm tauri build --config src-tauri/tauri.react.conf.json - desktop-clean: rm -rf $(SIDECAR_DIR) $(DESKTOP_DIR)/src-tauri/target diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index cb802754..a11223de 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -5,7 +5,7 @@ "identifier": "com.cnjack.jcode", "build": { "frontendDist": "../../internal/web/dist", - "beforeDevCommand": "pnpm --dir ../web dev", + "beforeDevCommand": "pnpm --dir ../web dev --host", "devUrl": "http://localhost:5173", "beforeBuildCommand": "make -C .. build-web" }, diff --git a/desktop/src-tauri/tauri.react.conf.json b/desktop/src-tauri/tauri.react.conf.json deleted file mode 100644 index 1d46c63b..00000000 --- a/desktop/src-tauri/tauri.react.conf.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "build": { - "frontendDist": "../../internal/web/dist-react", - "beforeDevCommand": "pnpm --dir ../web-react dev", - "devUrl": "http://localhost:5173", - "beforeBuildCommand": "make -C .. build-web-react" - } -} diff --git a/internal/theme/gen/main.go b/internal/theme/gen/main.go index debca903..685f934a 100644 --- a/internal/theme/gen/main.go +++ b/internal/theme/gen/main.go @@ -4,9 +4,9 @@ // // Two files are generated (both build artifacts, gitignored — never edit by // hand): -// - web/src/styles/tokens.generated.css : one html[data-theme=""] block +// - web/src/styles/tokens.generated.css : one [data-theme=""] block // of CSS custom properties per theme. -// - web/src/composables/themes.generated.ts : the theme registry (id, label, +// - web/src/lib/themes.generated.ts : the theme registry (id, label, // appearance) the picker UI iterates, so the list can't drift either. package main @@ -22,7 +22,7 @@ import ( func main() { writeFile(repoPath("web", "src", "styles", "tokens.generated.css"), css()) - writeFile(repoPath("web", "src", "composables", "themes.generated.ts"), ts()) + writeFile(repoPath("web", "src", "lib", "themes.generated.ts"), ts()) fmt.Printf("theme/gen: wrote %d themes (css + ts)\n", len(theme.All())) } diff --git a/packages/jcode-ui-core/src/primitives/ToolCallView.tsx b/packages/jcode-ui-core/src/primitives/ToolCallView.tsx index 35789e94..da6ee7e1 100644 --- a/packages/jcode-ui-core/src/primitives/ToolCallView.tsx +++ b/packages/jcode-ui-core/src/primitives/ToolCallView.tsx @@ -1,17 +1,16 @@ /** * ToolCallView — the headless expand/collapse shell for a tool invocation. * - * Owns: expand/collapse state, status glyph dispatch, and renderer lookup via a - * ToolRendererRegistry (provided through context by the host). Does NOT own the - * per-tool rendering logic — that lives in registered renderers. The styled - * `jcode-ui` `ToolCallCard` wraps this with the jcode visual language. + * Owns: expand/collapse state, renderer lookup via ToolRendererRegistry, and + * subagent recursion. Does NOT own per-tool body chrome — the styled + * `jcode-ui` `ToolCallCard` supplies header styling + CSS for `.toolcall-body`. * - * Subagent recursion: when `tool.name === 'subagent'`, children are rendered as - * nested ToolCallView instances (capped at a max-depth). ask_user tools are - * routed to the AskUserBlock renderer. + * Subagent: only `tool.name === 'subagent'` (NOT team_spawn — that has its own + * renderer). Children recurse as nested ToolCallView instances. ask_user tools + * route to the host's renderAskUser slot. */ -import { useContext, useMemo, useState } from 'react' +import { createContext, useContext, useMemo, useState } from 'react' import type { ReactNode } from 'react' import type { ToolCall } from '../types/index.js' import type { ToolRendererRegistry, ToolRendererProps } from '../adapters/index.js' @@ -25,7 +24,6 @@ export interface ToolCallContextValue { renderAskUser?: (tool: ToolCall) => ReactNode } -import { createContext } from 'react' const ToolCallCtx = createContext(null) export function ToolCallProvider({ value, children }: { value: ToolCallContextValue; children: ReactNode }) { @@ -44,14 +42,17 @@ export interface ToolCallViewProps { maxDepth?: number /** Default expanded state. Default false (subagents default true). */ defaultExpanded?: boolean - /** Render-prop for the collapsed header. Falls back to a default row. */ + /** Render-prop for the header. Falls back to a default row. */ renderHeader?: (tool: ToolCall, expanded: boolean, toggle: () => void) => ReactNode + /** + * Optional subagent body (output/error). Styled layer supplies markdown. + * Receives only output-related fields — never args. + */ + renderSubagentOutput?: (tool: ToolCall) => ReactNode /** className passthrough. */ className?: string } -const SUBAGENT_NAMES = new Set(['subagent', 'team_spawn']) - export function ToolCallView({ tool, depth = 0, @@ -59,9 +60,11 @@ export function ToolCallView({ defaultExpanded, className, renderHeader, + renderSubagentOutput, }: ToolCallViewProps): ReactNode { const ctx = useToolCallContext() - const isSubagent = SUBAGENT_NAMES.has(tool.name) + // Only the recursive subagent tool — team_spawn has its own renderer (Vue parity). + const isSubagent = tool.name === 'subagent' const isAskUser = tool.name === 'ask_user' && (!!tool.askUserId || tool.status === 'running') const [expanded, setExpanded] = useState(defaultExpanded ?? isSubagent) @@ -72,7 +75,7 @@ export function ToolCallView({ return <>{ctx.renderAskUser(tool)} } - // Look up a renderer for the body. + // Look up a renderer for the body (not used for subagent shells — no args dump). const Renderer = ctx?.registry.get(tool.name) ?? null const header = @@ -80,23 +83,63 @@ export function ToolCallView({ ) - const body = Renderer ? : null + const body = !isSubagent && Renderer ? : null + const children = isSubagent && tool.children && tool.children.length > 0 && depth < maxDepth - ? tool.children.map((c) => ( -
- {ctx?.renderChild ? ctx.renderChild(c, depth + 1) : } -
- )) + ? tool.children.map((c) => + ctx?.renderChild ? ( +
{ctx.renderChild(c, depth + 1)}
+ ) : ( + + ), + ) : null + // Prefer displayOutput (clean) over raw output; never surface args for subagents. + const subagentText = tool.displayOutput || tool.output || '' + return ( -
+
{header} - {expanded && ( -
+ + {/* Subagent: children + output only (no args). Output rendered by styled slot. */} + {expanded && isSubagent && ( +
+ {children && children.length > 0 ? ( +
{children}
+ ) : tool.status === 'running' && !subagentText ? ( +
Starting…
+ ) : null} + {renderSubagentOutput + ? renderSubagentOutput(tool) + : subagentText + ?
{truncate(subagentText, 2000)}
+ : null} + {tool.error ?
{tool.error}
: null} +
+ )} + + {/* Regular tool: single content box under the title (top edge = divider). */} + {expanded && !isSubagent && ( +
{body} - {children}
)}
@@ -117,7 +160,12 @@ function toRendererProps(tool: ToolCall): ToolRendererProps { } } -/** Minimal default header: status glyph + title + subtitle + chevron. */ +function truncate(text: string, max: number): string { + const chars = [...text] + return chars.length > max ? chars.slice(0, max).join('') + `… (${chars.length} chars)` : text +} + +/** Minimal default header (headless fallback). */ function DefaultToolHeader({ tool, expanded, @@ -127,12 +175,23 @@ function DefaultToolHeader({ expanded: boolean onToggle: () => void }): ReactNode { - const glyph = tool.status === 'running' ? '◈' : tool.status === 'error' ? '✗' : '✓' const title = tool.displayInfo?.title ?? tool.name const subtitle = tool.displayInfo?.subtitle ?? '' return ( - - {canEdit && ( - + {message.reasoning && ( +
+ +
+ )} + + {/* Body or inline edit — flat prose, no card bg/border. */} + {editing ? ( +
+