diff --git a/.changeset/config.json b/.changeset/config.json index a6e4a9d8..bf7c8d18 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -2,7 +2,21 @@ "$schema": "https://unpkg.com/@changesets/config@3.1.3/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, - "fixed": [], + "fixed": [ + [ + "@app/web", + "@app/server", + "@app/landing", + "@app/cli", + "@app/feedback-proxy", + "@app/electrobun", + "@app/core", + "@app/shared", + "@app/pro", + "@app/analytics", + "@app/logger" + ] + ], "linked": [], "access": "restricted", "baseBranch": "main", diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml index 4066c56f..109672c7 100644 --- a/.github/workflows/desktop.yml +++ b/.github/workflows/desktop.yml @@ -82,6 +82,7 @@ jobs: ELECTROBUN_APPLEAPIISSUER: ${{ secrets.APPLE_API_ISSUER }} ELECTROBUN_APPLEAPIKEY: ${{ secrets.APPLE_API_KEY_ID }} ELECTROBUN_APPLEAPIKEYPATH: /tmp/api_key.p8 + VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }} - name: Cleanup API key if: matrix.platform == 'darwin' run: rm -f /tmp/api_key.p8 diff --git a/.gitignore b/.gitignore index 957e814b..44947548 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ next-env.d.ts .env .env.local .env*.local +.dev.vars coverage/ .nyc_output/ .turbo/ @@ -47,6 +48,9 @@ tmp/ # ? > git config --global core.excludesfile ~/.gitignore-global # ? > git config --global core.excludesfile +# cloudflare +.wrangler/ + # project specific *.db* .osgrep diff --git a/AGENTS.md b/AGENTS.md index 3cb29d24..3b04a86c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -200,6 +200,8 @@ bun test bun run typecheck ``` +> **Backpressure:** Run `bun run validate` (from repo root or any package dir) to check typecheck, lint, format, and tests. Use as gate before marking implementation tasks complete. + ### Portless (Named Dev URLs) Dev servers use [portless](https://github.com/vercel-labs/portless) for stable `.localhost` URLs instead of port numbers. First run auto-starts the HTTPS proxy on port 443 and generates a local CA (run `npx portless trust` if you see certificate warnings). @@ -244,6 +246,20 @@ These defaults are wired into `use-settings.ts` via `NEXT_PUBLIC_*` variables so Specs are written in Gauge Markdown and run via agent-browser. See [docs/testing/e2e-specs.md](docs/testing/e2e-specs.md) for conventions, built-in steps, and how to add new ones. +--- + +## Release Process + +Uses **@changesets/cli** with unified versioning (all 11 workspace packages in `fixed` array). Changesets only manages workspace packages — root and Electrobun version files need manual sync. + +**Workflow:** +1. `bunx changeset` — describe change and bump type +2. `bunx changeset version` — bumps all workspace packages +3. Manually update: root `package.json`, root `CHANGELOG.md` +4. Commit and tag: `git commit -m "chore: release vX.Y.Z" && git tag vX.Y.Z && git push --tags` + +Desktop builds via GitHub Actions on `v*` tags. + ## Package-Specific Guides Dive deeper into the area you're working on: diff --git a/CHANGELOG.md b/CHANGELOG.md index dd7dec0c..fe925c27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,47 +5,147 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.3.0] - 2025-04-26 +## [0.5.0] - 2026-05-06 ### Added -- **Mastra AI Pipeline** — Full workflow-based design pipeline with Mastra, including workflow steps, stream consumer hook, and integration tests -- **Hono Server** — New standalone Hono server with CORS, route handlers, and API routes for the design pipeline -- **Vite Migration** — Web app migrated from Next.js to Vite with TanStack Router for SPA routing -- **Theme System** — Swappable Tailwind v4 theme infrastructure with shadcn/ui utilities -- **Internationalization** — New `lib/i18n` module with English translations and `lib/export` with `.design` format support -- **IndexedDB Hook** — Custom IndexedDB hook for client-side data persistence -- **Brand Rebrand** — Complete rebrand from Gosto to Calca with new logo assets, brand voice ADR, and updated UI copy +- **Onboarding** — Restored welcome-modal with original flow; replaced SettingsModal/WelcomeModal with SettingsDialog; migrated onboarding-banner to shadcn design system +- **Feedback** — Updated feedback UI to show discussion comment URL; switched feedback-proxy from GitHub Issues to Discussion comments +- **Desktop Context Menus** — Implemented frame clipboard, context menu, and keyboard shortcuts; added Duplicate, disabled states, and shortcut labels; added Delete menu item and accelerator shortcuts +- **NavigationMenu** — New shared NavigationMenu UI component replacing dropdown-menu patterns in context toolbar, remix button, and export menu +- **Custom Remix** — Added custom remix flow via prompt bar +- **Landing Page** — Built landing page with Astro, React, and Tailwind v4; added static assets, sections, and GitHub Pages deployment +- **Analytics** — Enabled PostHog initialization on app startup +- **Code Signing** — Added Apple code signing and notarization steps to desktop CI workflow; enabled signing in Electrobun config +- **Feedback Proxy CF Workers** — Added Cloudflare Workers deployment workflow; wired CF Workers entry point; extracted modules and scaffolding ### Changed -- **Pipeline Architecture** — Slimmed `use-generation-pipeline` to UI-only; Mastra stream consumer handles server communication -- **Brand Rename** — Renamed Gosto→Calca across all infrastructure files and feature code +- **shadcn Migration** — Migrated remaining raw HTML primitives, buttons, popovers, and dropdowns to shadcn components across the web app +- **Desktop Build** — Simplified build scripts with shared logger and extracted helpers; added build script with env loading and artifact prep +- **Mode Sidebar** — Improved mode sidebar and system prompt button layout +- **Desktop Context Menu** — Suppressed default WebKit context menu; fixed platform detection and wired web-to-native RPC ### Fixed -- Removed old API routes and wired remix/revision through the Mastra workflow -- Resolved AI SDK provider-utils version mismatch -- Added missing workspace dependencies and package exports for the Mastra pipeline -- Fixed request body passing as `inputData` -- Fixed index route availability with `createFileRoute` +- Fixed desktop `Updater.getLocalInfo` usage per electrobun lint +- Made feedback email required with disposable email validation +- Added react types back to web app +- Extracted action button to separate module +- Pinned `@tailwindcss/browser` to 4.2.4 for CI reliability +- Resolved workflow failures for feedback-proxy, desktop build, and landing page +- Only notarize in CI to avoid local build failures +- Cleaned up imports in feedback-modal +- Prevented text overflow in critique mode button +- Improved React type safety in error-boundary with PropsWithChildren pattern +- Used direct file imports for lazy route components +- Used default exports for lazy comment loading +- Migrated remaining raw buttons to shadcn Button, fixed textarea border +- Updated design card for desktop layouts +- Added Tailwind CSS browser build support for desktop ### Tests -- Added integration tests for Mastra design pipeline workflow -- Added rebrand verification tests +- Added unit tests for frame clipboard copy/paste ### Docs -- Updated pipeline route example to workflow pattern +- Consolidated architecture decisions into clean ADRs and PRD +- Cleaned up e2e testing best practices heading +- Fixed markdown formatting in PRD v2 +- Removed roadmap directory +- Renumbered MADR files and added pro-package-architecture decision +- Updated project READMEs to match current state ### Chore -- Simplified `dev-web` script on root package.json -- Added Mastra + type-fest dependencies and created schemas + MADR 0012 -- Updated lockfile for Calca rebrand dependencies -- Updated public icons and pro package readme -- Removed old `otto-icon.jpg` branding assets +- Updated gitignore +- Installed shadcn popover, progress, and textarea components +- Updated lockfile after shadcn component installs +- Added react-icons and mailchecker dependencies +- Extracted action button to separate module +- Updated desktop bun.lock + +## [0.4.1] - 2026-05-03 + +### Fixed + +- Replaced `zx` with Bun.spawn to fix Windows path corruption +- Searched for Windows binary extensions (`.cmd`, `.exe`, `.ps1`) +- Used POSIX paths in zx and `fs/promises` for file ops +- Resolved electrobun binary and Windows path issues +- Updated provider tests to match pass-through behavior +- Used vitest via `bun run test` and excluded dist tests from CI + +### Changed + +- Replaced `zx` with Bun shell for build scripts + +### Chore + +- Bumped desktop version to 0.4.0 + +## [0.4.0] - 2026-05-01 + +### Added + +- **Desktop App** — Scaffolded Electrobun project with config, main process stub, build scripts, and native context menu; added application menu with Help > Report Bug +- **Auto-Updater** — Implemented auto-updater flow using Electrobun Updater API +- **Analytics** — Added PostHog client, events, types, and tests; instrumented web app with PostHog event tracking; added analytics opt-out toggle to General settings +- **Feedback** — Added feedback UI with bug icon, modal, and form; scaffolded feedback proxy with GitHub Issues creation via Octokit +- **Settings** — Added shadcn/ui components, Zod schemas, and new UI shells for settings and onboarding; added Reset to Factory Settings utility; added language section and improved provider card layout; added analyticsEnabled field; added required model and optional fallbackModel to settings types +- **Desktop CI/CD** — Added GitHub Actions CI/CD pipeline for desktop builds; added desktop tasks to Turborepo pipeline +- **Toolbar** — Added compass to zoom bar; redistributed toolbar into three FSD features +- **Error Boundary** — Added reusable ErrorBoundary component +- **AI SDK Telemetry** — Added AI SDK telemetry adapter and pipeline step transition logging + +### Changed + +- **Toolbar Rewrite** — Rewrote mode-sidebar with tooltips, lucide icons, and compact sizing; rewrote export-menu and remix-button to use dropdown-menu with hover behavior; unified canvas-hud styling +- **Desktop Scripts** — Replaced shell scripts with cross-platform TypeScript; consolidated scripts into single build command and cross-platform dev coordinator; broke down monolithic entry point into focused modules +- **Server** — Exported Hono app without starting server for embeddability; migrated console.* to @app/logger; removed custom logging wrapper +- **Frontend** — Replaced fetch with Hono RPC client; added shared dropdown-menu and tooltip components; replaced custom SVGs with lucide icons +- **Monorepo** — Simplified workspace imports to use package.json exports/imports; consolidated desktop to `platforms/desktop/` and removed stale scaffolds + +### Fixed + +- Probed user's configured models instead of hardcoded list; replaced MODEL_FALLBACK_CHAIN with buildModelFallbackChain; removed DEFAULT_MODEL from settings and pipeline +- Required model configuration in onboarding flow; removed Claude-only preset models from model picker +- Prevented welcome re-showing on reload and fixed tour re-render +- Migrated SettingsDialog to jotai atoms for state consistency +- Resolved CORS error by adding dev server proxy +- Bridged VITE_AI_* env vars from root .env to import.meta.env +- Fixed hardcoded colors and suppressed CDN warning; added semantic tokens and updated component colors; improved color visibility and mode differentiation +- Used custom jotai provider to sync desktop app bar functionality +- Removed idle timeout limit on electrobun +- Fixed HMR for server, dev scripts, and API proxy loop +- Improved server binding, dev detection, and added verification tooling +- Ensured loggers are initialized in all apps +- Implemented working cache with LRU eviction and fallback backoff +- Resolved hydration race causing onboarding to re-show on reload +- Resolved all TypeScript errors in @app/web +- Standardized lint/format/typecheck scripts across all workspaces + +### Performance + +- Lazy-loaded non-critical features and deferred devtools +- Added Vite code splitting with manualChunks +- Switched to `views://` protocol and added splash screen in desktop + +### Docs + +- Added auto-update, distribution, and storage migration plans for desktop +- Added AGENTS.md for desktop + +### Chore + +- Removed last mentions to 'gosto' in place of 'calca' +- Updated oxfmt config to sort imports +- Applied oxfmt and oxlint auto-fixes across codebase +- Disabled stylistic oxlint rules and fixed override syntax +- Ran formatter across the whole project +- Ensured oxlint and oxfmt are always executed with 'validate' script +- Updated bun.lock ## [0.1.0] - 2025-04-05 diff --git a/apps/Resources/version.json b/apps/Resources/version.json deleted file mode 100644 index 39a8c864..00000000 --- a/apps/Resources/version.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "version": "0.4.0" -} \ No newline at end of file diff --git a/apps/cli/package.json b/apps/cli/package.json index 0bbb0ef2..82229c97 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "@app/cli", - "version": "0.3.0", + "version": "0.5.0", "private": true, "license": "AGPL-3.0", "type": "module", @@ -11,6 +11,6 @@ "test": "vitest run --passWithNoTests", "lint": "oxlint --fix .", "format": "oxfmt --write .", - "validate": "echo 'validate: not yet implemented'" + "validate": "bun ../../scripts/validate.ts --cwd $INIT_CWD" } } diff --git a/apps/feedback-proxy/README.md b/apps/feedback-proxy/README.md new file mode 100644 index 00000000..30cbc241 --- /dev/null +++ b/apps/feedback-proxy/README.md @@ -0,0 +1,50 @@ +# Feedback Proxy + +A Cloudflare Worker that accepts feedback submissions and posts comments to a GitHub Discussion. + +## Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/health` | Health check | +| `POST` | `/feedback` | Submit feedback → posts comment to Discussion #5 | + +## Features + +- **Rate limiting** — IP-based throttling via Cloudflare KV +- **Input validation** — Sanitizes and validates feedback payloads +- **CORS** — Restricts requests to `ALLOWED_ORIGIN` + +## Local Development + +```bash +# 1. Copy example secrets and fill in values +cp .dev.vars.example .dev.vars + +# 2. Start local dev server +bun run dev:cf +``` + +## Deployment + +```bash +# Create KV namespaces (first time only) +bunx wrangler kv namespace create RATE_LIMIT_KV --update-config +bunx wrangler kv namespace create RATE_LIMIT_KV --preview --update-config + +# Set secrets +bunx wrangler secret put GITHUB_TOKEN +bunx wrangler secret put GITHUB_REPO +bunx wrangler secret put ALLOWED_ORIGIN + +# Deploy +bun run deploy +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `GITHUB_TOKEN` | Personal access token with Discussions read/write permission (fine-grained PAT) | +| `GITHUB_REPO` | Target repository in `owner/repo` format | +| `ALLOWED_ORIGIN` | CORS origin (e.g., `https://calca.localhost`) | diff --git a/apps/feedback-proxy/package.json b/apps/feedback-proxy/package.json index 3f2766c2..ec0a2e4b 100644 --- a/apps/feedback-proxy/package.json +++ b/apps/feedback-proxy/package.json @@ -1,6 +1,6 @@ { "name": "@app/feedback-proxy", - "version": "0.1.0", + "version": "0.5.0", "private": true, "description": "Feedback proxy server — validates input, rate-limits, and creates GitHub Issues", "license": "AGPL-3.0", @@ -11,7 +11,9 @@ "build": "bun build src/index.ts", "deploy": "wrangler deploy", "deploy:dry": "wrangler deploy --dry-run", - "typecheck": "bunx tsc --noEmit --project tsconfig.json" + "typecheck": "bunx tsc --noEmit --project tsconfig.json", + "test": "vitest run", + "validate": "bun ../../scripts/validate.ts --cwd $INIT_CWD" }, "dependencies": { "hono": "^4.0.0", diff --git a/apps/feedback-proxy/src/__tests__/feedback.test.ts b/apps/feedback-proxy/src/__tests__/feedback.test.ts index 0393e803..6d776500 100644 --- a/apps/feedback-proxy/src/__tests__/feedback.test.ts +++ b/apps/feedback-proxy/src/__tests__/feedback.test.ts @@ -2,67 +2,102 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import app from "../app.js"; -const mockOctokitInstance = { - rest: { - issues: { - create: vi.fn(), - }, - }, -}; +const { mockPostDiscussionComment, mockCreateRateLimiter } = vi.hoisted(() => { + const requestCounts = new Map(); + + const mockCreateRateLimiter = vi.fn(() => { + return { + check: vi.fn(async (ip: string) => { + const key = `rate:${ip}`; + const count = requestCounts.get(key) ?? 0; + requestCounts.set(key, count + 1); + + if (count >= 5) { + return { allowed: false, retryAfterSeconds: 3600 }; + } + return { allowed: true }; + }), + }; + }); + + const mockPostDiscussionComment = vi.fn(); + return { mockPostDiscussionComment, mockCreateRateLimiter }; +}); -vi.mock("octokit", () => ({ - Octokit: vi.fn(function () { - return mockOctokitInstance; - }), +vi.mock("../github.js", () => ({ + postDiscussionComment: mockPostDiscussionComment, })); +vi.mock("../rate-limiter.js", () => ({ + createRateLimiter: mockCreateRateLimiter, +})); + +const testEnv = { + GITHUB_TOKEN: "test-token", + GITHUB_REPO: "owner/repo", + ALLOWED_ORIGIN: "*", + RATE_LIMIT_KV: {} as unknown as KVNamespace, +}; + describe("POST /feedback", () => { beforeEach(() => { vi.clearAllMocks(); + mockPostDiscussionComment.mockReset(); + mockPostDiscussionComment.mockResolvedValue(undefined); process.env.GITHUB_TOKEN = "test-token"; process.env.GITHUB_REPO = "owner/repo"; }); - it("creates a GitHub Issue on valid request", async () => { - mockOctokitInstance.rest.issues.create.mockResolvedValue({ - data: { number: 42, html_url: "https://github.com/owner/repo/issues/42" }, - } as any); - - const res = await app.request("/feedback", { - method: "POST", - body: JSON.stringify({ - type: "bug", - title: "Test bug", - description: "This is a test bug description", - }), - headers: { "content-type": "application/json" }, + it("posts a discussion comment on valid request", async () => { + mockPostDiscussionComment.mockResolvedValue({ + ok: true, + commentUrl: "https://github.com/owner/repo/discussions/5#discussioncomment-123", }); + const res = await app.request( + "/feedback", + { + method: "POST", + body: JSON.stringify({ + type: "bug", + title: "Test bug", + description: "This is a test bug description", + }), + headers: { "content-type": "application/json" }, + }, + testEnv, + ); + expect(res.status).toBe(201); const json = (await res.json()) as Record; expect(json).toEqual({ - issueUrl: "https://github.com/owner/repo/issues/42", - issueNumber: 42, + commentUrl: "https://github.com/owner/repo/discussions/5#discussioncomment-123", }); - expect(mockOctokitInstance.rest.issues.create).toHaveBeenCalledWith({ - owner: "owner", - repo: "repo", - title: "[BUG] Test bug", - body: expect.stringContaining("Bug Report"), - labels: ["user-feedback", "bug"], + expect(mockPostDiscussionComment).toHaveBeenCalledWith({ + token: "test-token", + repo: "owner/repo", + data: expect.objectContaining({ + type: "bug", + title: "Test bug", + description: "This is a test bug description", + }), }); }); it("returns 400 for invalid type", async () => { - const res = await app.request("/feedback", { - method: "POST", - body: JSON.stringify({ - type: "invalid-type", - title: "Test", - description: "desc", - }), - headers: { "content-type": "application/json" }, - }); + const res = await app.request( + "/feedback", + { + method: "POST", + body: JSON.stringify({ + type: "invalid-type", + title: "Test", + description: "desc", + }), + headers: { "content-type": "application/json" }, + }, + testEnv, + ); expect(res.status).toBe(400); const json = (await res.json()) as Record; @@ -70,40 +105,47 @@ describe("POST /feedback", () => { }); it("returns 429 when rate limited", async () => { - mockOctokitInstance.rest.issues.create.mockResolvedValue({ - data: { number: 1, html_url: "https://github.com/owner/repo/issues/1" }, - } as any); - - await Promise.all( - [1, 2, 3, 4, 5].map( - async (i) => - await app.request("/feedback", { - method: "POST", - body: JSON.stringify({ - type: "feedback", - title: `Feedback ${i}`, - description: `Description ${i}`, - }), - headers: { - "content-type": "application/json", - "x-forwarded-for": "192.168.1.1", - }, + mockPostDiscussionComment.mockResolvedValue({ + ok: true, + commentUrl: "https://github.com/owner/repo/discussions/5#discussioncomment-1", + }); + + for (let i = 0; i < 5; i++) { + const res = await app.request( + "/feedback", + { + method: "POST", + body: JSON.stringify({ + type: "feedback", + title: `Feedback ${i}`, + description: `Description ${i}`, }), - ), - ); + headers: { + "content-type": "application/json", + "x-forwarded-for": "192.168.1.1", + }, + }, + testEnv, + ); + expect(res.status).toBe(201); + } - const res = await app.request("/feedback", { - method: "POST", - body: JSON.stringify({ - type: "feedback", - title: "Rate limited", - description: "Should be rate limited", - }), - headers: { - "content-type": "application/json", - "x-forwarded-for": "192.168.1.1", + const res = await app.request( + "/feedback", + { + method: "POST", + body: JSON.stringify({ + type: "feedback", + title: "Rate limited", + description: "Should be rate limited", + }), + headers: { + "content-type": "application/json", + "x-forwarded-for": "192.168.1.1", + }, }, - }); + testEnv, + ); expect(res.status).toBe(429); const json = (await res.json()) as Record; @@ -111,77 +153,103 @@ describe("POST /feedback", () => { }); it("returns 500 when GitHub API call fails", async () => { - mockOctokitInstance.rest.issues.create.mockRejectedValue(new Error("API Error")); - - const res = await app.request("/feedback", { - method: "POST", - body: JSON.stringify({ - type: "feature", - title: "Feature request", - description: "I want this feature", - }), - headers: { "content-type": "application/json" }, + mockPostDiscussionComment.mockResolvedValue({ + ok: false, + error: "Failed to post discussion comment. Please try again.", }); + const res = await app.request( + "/feedback", + { + method: "POST", + body: JSON.stringify({ + type: "feature", + title: "Feature request", + description: "I want this feature", + }), + headers: { "content-type": "application/json" }, + }, + testEnv, + ); + expect(res.status).toBe(500); const json = (await res.json()) as Record; - expect(json.error).toContain("Failed to create GitHub issue"); + expect(json.error).toContain("Failed to post discussion comment"); }); it("returns 400 for missing required fields", async () => { - const res = await app.request("/feedback", { - method: "POST", - body: JSON.stringify({ - type: "bug", - }), - headers: { "content-type": "application/json" }, - }); + const res = await app.request( + "/feedback", + { + method: "POST", + body: JSON.stringify({ + type: "bug", + }), + headers: { "content-type": "application/json" }, + }, + testEnv, + ); expect(res.status).toBe(400); const json = (await res.json()) as Record; expect(json.error).toBeTruthy(); }); - it("includes email and metadata in issue body", async () => { - mockOctokitInstance.rest.issues.create.mockResolvedValue({ - data: { number: 99, html_url: "https://github.com/owner/repo/issues/99" }, - } as any); - - const res = await app.request("/feedback", { - method: "POST", - body: JSON.stringify({ - type: "feedback", - title: "Feedback with extras", - description: "Description with email and metadata", - email: "test@example.com", - metadata: { browser: "Chrome", version: "1.0.0" }, - }), - headers: { "content-type": "application/json" }, + it("includes email and metadata in comment body", async () => { + mockPostDiscussionComment.mockResolvedValue({ + ok: true, + commentUrl: "https://github.com/owner/repo/discussions/5#discussioncomment-99", }); + const res = await app.request( + "/feedback", + { + method: "POST", + body: JSON.stringify({ + type: "feedback", + title: "Feedback with extras", + description: "Description with email and metadata", + email: "test@example.com", + metadata: { browser: "Chrome", version: "1.0.0" }, + }), + headers: { "content-type": "application/json" }, + }, + testEnv, + ); + expect(res.status).toBe(201); - expect(mockOctokitInstance.rest.issues.create).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.stringContaining("test@example.com"), + expect(mockPostDiscussionComment).toHaveBeenCalledWith({ + token: "test-token", + repo: "owner/repo", + data: expect.objectContaining({ + email: "test@example.com", + metadata: { browser: "Chrome", version: "1.0.0" }, }), - ); + }); }); it("returns 500 when GITHUB_TOKEN is missing", async () => { - delete process.env.GITHUB_TOKEN; + mockPostDiscussionComment.mockResolvedValue({ + ok: false, + error: "Server configuration error", + }); - const res = await app.request("/feedback", { - method: "POST", - body: JSON.stringify({ - type: "bug", - title: "Test", - description: "Test desc", - }), - headers: { - "content-type": "application/json", - "x-forwarded-for": "10.0.0.1", + const res = await app.request( + "/feedback", + { + method: "POST", + body: JSON.stringify({ + type: "bug", + title: "Test", + description: "Test desc", + }), + headers: { + "content-type": "application/json", + "x-forwarded-for": "10.0.0.1", + }, }, - }); + { ...testEnv, GITHUB_TOKEN: "" }, + ); expect(res.status).toBe(500); const json = (await res.json()) as Record; @@ -191,7 +259,7 @@ describe("POST /feedback", () => { describe("GET /health", () => { it("returns health status", async () => { - const res = await app.request("/health"); + const res = await app.request("/health", undefined, testEnv); expect(res.status).toBe(200); const json = (await res.json()) as Record; diff --git a/apps/feedback-proxy/src/app.ts b/apps/feedback-proxy/src/app.ts index 68ea1c17..1339a626 100644 --- a/apps/feedback-proxy/src/app.ts +++ b/apps/feedback-proxy/src/app.ts @@ -1,6 +1,6 @@ import { Hono, Env as BaseHonoEnv } from "hono"; -import { createIssue } from "./github.js"; +import { postDiscussionComment } from "./github.js"; import { createRateLimiter } from "./rate-limiter.js"; import type { Env, FeedbackResponse } from "./types.js"; import { validateFeedback } from "./validate.js"; @@ -54,7 +54,7 @@ const app = new Hono() const { data } = validation; // Create GitHub issue - const result = await createIssue({ + const result = await postDiscussionComment({ token: c.env.GITHUB_TOKEN, repo: c.env.GITHUB_REPO, data, @@ -65,8 +65,7 @@ const app = new Hono() } const feedbackResponse: FeedbackResponse = { - issueUrl: result.issueUrl, - issueNumber: result.issueNumber, + commentUrl: result.commentUrl, }; return c.json(feedbackResponse, 201); }); diff --git a/apps/feedback-proxy/src/github.ts b/apps/feedback-proxy/src/github.ts index 2154eb1e..b003b9d2 100644 --- a/apps/feedback-proxy/src/github.ts +++ b/apps/feedback-proxy/src/github.ts @@ -2,20 +2,40 @@ import { Octokit } from "octokit"; import { FeedbackRequest } from "./types.js"; +const DISCUSSION_NODE_ID = "D_kwDORsE6rc4AmJhK"; + +const ADD_DISCUSSION_COMMENT_MUTATION = ` + mutation AddComment($discussionId: ID!, $body: String!) { + addDiscussionComment(input: { discussionId: $discussionId, body: $body }) { + comment { + url + } + } + } +`; + +interface AddDiscussionCommentResponse { + addDiscussionComment: { + comment: { + url: string; + }; + }; +} + /** - * Creates a GitHub issue from feedback data using the provided token and repo. + * Posts a comment to GitHub Discussion #5 from feedback data using the provided token. * - * @param params - The parameters for creating the issue + * @param params - The parameters for posting the comment * @param params.token - GitHub personal access token - * @param params.repo - GitHub repository in "owner/repo" format + * @param params.repo - GitHub repository in "owner/repo" format (unused but kept for error handling) * @param params.data - The validated feedback request data - * @returns Either a successful result with issue URL and number, or an error + * @returns Either a successful result with comment URL, or an error */ -export async function createIssue(params: { +export async function postDiscussionComment(params: { token: string; repo: string; data: FeedbackRequest; -}): Promise<{ ok: true; issueUrl: string; issueNumber: number } | { ok: false; error: string }> { +}): Promise<{ ok: true; commentUrl: string } | { ok: false; error: string }> { const { token, repo, data } = params; const [owner, repoName] = repo.split("/"); @@ -44,22 +64,15 @@ export async function createIssue(params: { ].join("\n"); try { - const response = await octokit.rest.issues.create({ - owner, - repo: repoName, - title: `[${data.type.toUpperCase()}] ${data.title}`, + const result = await octokit.graphql(ADD_DISCUSSION_COMMENT_MUTATION, { + discussionId: DISCUSSION_NODE_ID, body: bodyMarkdown, - labels: ["user-feedback", data.type], }); - const responseData = response.data; - return { - ok: true, - issueUrl: responseData.html_url, - issueNumber: responseData.number, - }; + const commentUrl = result.addDiscussionComment.comment.url; + return { ok: true, commentUrl }; } catch (err) { console.error("[feedback-proxy] GitHub API error:", err); - return { ok: false, error: "Failed to create GitHub issue. Please try again." }; + return { ok: false, error: "Failed to post discussion comment. Please try again." }; } -} +} \ No newline at end of file diff --git a/apps/feedback-proxy/src/index.ts b/apps/feedback-proxy/src/index.ts index 0f191a6d..2a1a258f 100644 --- a/apps/feedback-proxy/src/index.ts +++ b/apps/feedback-proxy/src/index.ts @@ -2,7 +2,7 @@ import { Hono } from "hono"; import { createRateLimiter } from "./rate-limiter.js"; import { validateFeedback } from "./validate.js"; -import { createIssue } from "./github.js"; +import { postDiscussionComment } from "./github.js"; import type { Env, FeedbackResponse } from "./types.js"; type Bindings = Env; @@ -57,7 +57,7 @@ app.post("/feedback", async (c) => { const { data } = validation; // Create GitHub issue - const result = await createIssue({ + const result = await postDiscussionComment({ token: c.env.GITHUB_TOKEN, repo: c.env.GITHUB_REPO, data, @@ -68,8 +68,7 @@ app.post("/feedback", async (c) => { } const feedbackResponse: FeedbackResponse = { - issueUrl: result.issueUrl, - issueNumber: result.issueNumber, + commentUrl: result.commentUrl, }; return c.json(feedbackResponse, 201); }); diff --git a/apps/feedback-proxy/src/types.ts b/apps/feedback-proxy/src/types.ts index f60c07e0..11a8644a 100644 --- a/apps/feedback-proxy/src/types.ts +++ b/apps/feedback-proxy/src/types.ts @@ -9,8 +9,7 @@ export interface FeedbackRequest { } export interface FeedbackResponse { - issueUrl: string; - issueNumber: number; + commentUrl: string; } export interface Env { diff --git a/apps/feedback-proxy/vitest.config.ts b/apps/feedback-proxy/vitest.config.ts new file mode 100644 index 00000000..3f824fb9 --- /dev/null +++ b/apps/feedback-proxy/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + }, +}); diff --git a/apps/landing/package.json b/apps/landing/package.json index 7ad745fe..de989d89 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -1,6 +1,6 @@ { "name": "@app/landing", - "version": "0.4.1", + "version": "0.5.0", "private": true, "license": "AGPL-3.0", "type": "module", @@ -11,7 +11,8 @@ "preview": "astro preview", "test": "vitest run --passWithNoTests", "lint": "oxlint --fix .", - "format": "oxfmt --write ." + "format": "oxfmt --write .", + "validate": "bun ../../scripts/validate.ts --cwd $INIT_CWD" }, "dependencies": { "@astrojs/check": "^0.9.9", diff --git a/apps/server/package.json b/apps/server/package.json index 78e48052..e6dff9bf 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "@app/server", - "version": "0.3.1", + "version": "0.5.0", "private": true, "license": "AGPL-3.0", "type": "module", @@ -16,7 +16,7 @@ "test": "vitest run --passWithNoTests", "lint": "oxlint --fix .", "format": "oxfmt --write .", - "validate": "echo 'validate: not yet implemented'" + "validate": "bun ../../scripts/validate.ts --cwd $INIT_CWD" }, "dependencies": { "@app/core": "workspace:*", diff --git a/apps/web/package.json b/apps/web/package.json index 123fa051..8e73cd22 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@app/web", - "version": "0.3.1", + "version": "0.5.0", "private": true, "license": "AGPL-3.0", "type": "module", @@ -15,7 +15,8 @@ "test": "vitest", "lint": "oxlint --fix --react-plugin .", "format": "oxfmt --write .", - "clean": "rm -rf dist" + "clean": "rm -rf dist", + "validate": "bun ../../scripts/validate.ts --cwd $INIT_CWD" }, "dependencies": { "@ai-sdk/anthropic": "^3.0.68", @@ -30,8 +31,8 @@ "@radix-ui/react-use-controllable-state": "^1.2.2", "@tanstack/react-query": "^5.99.0", "@tanstack/react-query-devtools": "^5.99.0", - "@tour-kit/core": "^0.5.0", - "@tour-kit/react": "^0.5.0", + "@tour-kit/core": "^0.11.0", + "@tour-kit/react": "^0.11.0", "ai": "^6.0.156", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/apps/web/src/features/canvas-hud/ui/compass.tsx b/apps/web/src/features/canvas-hud/ui/compass.tsx index 086e67b6..09ee8e2c 100644 --- a/apps/web/src/features/canvas-hud/ui/compass.tsx +++ b/apps/web/src/features/canvas-hud/ui/compass.tsx @@ -2,7 +2,7 @@ import { useViewportSize } from "@mantine/hooks"; import { Locate, LocateFixed, Navigation } from "lucide-react"; import { useState } from "react"; -import ToolButton from "#/widgets/toolbar/ui/tool-button"; +import { Button } from "#/shared/components/ui/button"; export interface CompassProps { offset: { x: number; y: number }; @@ -24,7 +24,13 @@ const Compass = ({ offset, onResetView }: CompassProps) => { return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}> - +
); }; diff --git a/apps/web/src/features/canvas-hud/ui/onboarding-banner.tsx b/apps/web/src/features/canvas-hud/ui/onboarding-banner.tsx new file mode 100644 index 00000000..eaf57a3c --- /dev/null +++ b/apps/web/src/features/canvas-hud/ui/onboarding-banner.tsx @@ -0,0 +1,31 @@ +import { m } from "#/lib/i18n"; +import { cn } from "#/lib/utils"; +import { Button } from "#/shared/components/ui/button"; + +interface OnboardingBannerProps { + onClick?: () => void; +} + +const OnboardingBanner = ({ onClick }: OnboardingBannerProps) => { + return ( +
+ +
+ ); +}; + +export default OnboardingBanner; diff --git a/apps/web/src/features/canvas-hud/ui/zoom.tsx b/apps/web/src/features/canvas-hud/ui/zoom.tsx index 6dcc1c58..c3cd04ed 100644 --- a/apps/web/src/features/canvas-hud/ui/zoom.tsx +++ b/apps/web/src/features/canvas-hud/ui/zoom.tsx @@ -1,6 +1,6 @@ import { Minus, Plus } from "lucide-react"; -import ToolButton from "#/widgets/toolbar/ui/tool-button"; +import { Button } from "#/shared/components/ui/button"; export interface ZoomControlsProps { scale: number; @@ -10,17 +10,29 @@ export interface ZoomControlsProps { const ZoomControls = ({ onZoomIn, onZoomOut, scale }: ZoomControlsProps) => ( <> - + {Math.round(scale * 100)}% - + ); diff --git a/apps/web/src/features/canvas/ui/pipeline-status.tsx b/apps/web/src/features/canvas/ui/pipeline-status.tsx index 2a2b12b0..c8473d98 100644 --- a/apps/web/src/features/canvas/ui/pipeline-status.tsx +++ b/apps/web/src/features/canvas/ui/pipeline-status.tsx @@ -1,3 +1,4 @@ +import { Progress } from "#/shared/components/ui/progress"; import { type PipelineStatus, STAGE_CONFIG } from "#/shared/types"; interface PipelineStatusBarProps { @@ -28,16 +29,16 @@ export function PipelineStatusOverlay({ return (
-
- {!isQueued && ( -
- )} -
+ {!isQueued && ( + + )}
{config.icon} {config.label} diff --git a/apps/web/src/features/comments/ui/comment-input.tsx b/apps/web/src/features/comments/ui/comment-input.tsx index 41d63bc5..4444cb07 100644 --- a/apps/web/src/features/comments/ui/comment-input.tsx +++ b/apps/web/src/features/comments/ui/comment-input.tsx @@ -1,5 +1,7 @@ import { useRef, useState } from "react"; +import { Button } from "#/shared/components/ui/button"; +import { Textarea } from "#/shared/components/ui/textarea"; import { useMountEffect } from "#/shared/utils/use-mount-effect"; interface CommentInputProps { @@ -8,7 +10,7 @@ interface CommentInputProps { onCancel: () => void; } -export function CommentInput({ position, onSubmit, onCancel }: CommentInputProps) { +const CommentInput = ({ position, onSubmit, onCancel }: CommentInputProps) => { const [text, setText] = useState(""); const inputRef = useRef(null); const containerRef = useRef(null); @@ -42,7 +44,7 @@ export function CommentInput({ position, onSubmit, onCancel }: CommentInputProps
Revision comment
-