Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
48 changes: 21 additions & 27 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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/`

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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 `<svg>` 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 `<svg>` 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/`.
59 changes: 9 additions & 50 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
Expand All @@ -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`
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
8 changes: 0 additions & 8 deletions desktop/src-tauri/tauri.react.conf.json

This file was deleted.

13 changes: 10 additions & 3 deletions internal/theme/gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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="<name>"] block
// - web/src/styles/tokens.generated.css : one [data-theme="<name>"] 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

Expand All @@ -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()))
}

Expand Down Expand Up @@ -61,6 +61,13 @@ func ts() string {
}

func writeFile(path, content string) {
// Generated files are gitignored; their parent dirs may not exist on a
// clean checkout (CI). Create them so go generate never fails only because
// tokens.generated.css / themes.generated.ts were never committed.
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
fmt.Fprintf(os.Stderr, "theme/gen: mkdir %s: %v\n", filepath.Dir(path), err)
os.Exit(1)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
fmt.Fprintf(os.Stderr, "theme/gen: write %s: %v\n", path, err)
os.Exit(1)
Expand Down
Loading
Loading