From 8ee5b569046b25086de10fc32f2711b077a2e831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Thu, 26 Feb 2026 23:08:15 +0000 Subject: [PATCH] feat: release notes (#298) --- .agents/skills/commit/SKILL.md | 54 ++ .claude/skills | 1 + .github/workflows/pr-title.yml | 37 ++ .github/workflows/release.yml | 15 + .pre-commit-config.yaml | 10 + AGENTS.md | 23 +- CHANGELOG.md | 547 ++++++++++++++++++ .../internal/user/controller/controller.go | 30 +- apps/backend/internal/user/dto/dto.go | 98 ++-- apps/backend/internal/user/models/models.go | 36 +- apps/backend/internal/user/service/service.go | 70 ++- .../internal/user/service/service_test.go | 63 ++ apps/backend/internal/user/store/sqlite.go | 66 ++- apps/commitlint.config.mjs | 21 + apps/package.json | 2 + apps/pnpm-lock.yaml | 369 +++++++++++- apps/web/app/page.tsx | 48 +- apps/web/app/s/[sessionId]/page.tsx | 50 +- apps/web/app/settings/changelog/page.tsx | 21 + .../web/app/settings/general/editors/page.tsx | 16 +- apps/web/app/settings/general/page.tsx | 20 +- .../kanban/kanban-header-mobile.tsx | 31 +- apps/web/components/kanban/kanban-header.tsx | 61 +- .../release-notes/release-notes-button.tsx | 27 + .../release-notes/release-notes-dialog.tsx | 87 +++ .../components/settings/changelog-list.tsx | 134 +++++ .../settings/changelog-notification-card.tsx | 86 +++ .../settings/changelog-settings.tsx | 36 ++ .../settings/editors-settings-state.tsx | 2 + .../settings/settings-app-sidebar.tsx | 11 + apps/web/generated/.gitignore | 2 + .../web/hooks/domains/settings/use-editors.ts | 24 +- apps/web/hooks/use-release-notes.ts | 102 ++++ apps/web/hooks/use-user-display-settings.ts | 55 +- apps/web/lib/api/domains/settings-api.ts | 2 + apps/web/lib/changelog.ts | 11 + apps/web/lib/release-notes.ts | 19 + apps/web/lib/ssr/user-settings.ts | 62 ++ .../state/slices/settings/settings-slice.ts | 2 + apps/web/lib/state/slices/settings/types.ts | 2 + apps/web/lib/types/backend.ts | 2 + apps/web/lib/types/http.ts | 2 + apps/web/lib/ws/handlers/users.ts | 3 + apps/web/package.json | 2 + apps/web/scripts/generate-changelog.mjs | 70 +++ apps/web/scripts/generate-release-notes.mjs | 89 +++ cliff.toml | 45 ++ scripts/release/release.sh | 34 +- 48 files changed, 2235 insertions(+), 365 deletions(-) create mode 100644 .agents/skills/commit/SKILL.md create mode 120000 .claude/skills create mode 100644 .github/workflows/pr-title.yml create mode 100644 CHANGELOG.md create mode 100644 apps/commitlint.config.mjs create mode 100644 apps/web/app/settings/changelog/page.tsx create mode 100644 apps/web/components/release-notes/release-notes-button.tsx create mode 100644 apps/web/components/release-notes/release-notes-dialog.tsx create mode 100644 apps/web/components/settings/changelog-list.tsx create mode 100644 apps/web/components/settings/changelog-notification-card.tsx create mode 100644 apps/web/components/settings/changelog-settings.tsx create mode 100644 apps/web/generated/.gitignore create mode 100644 apps/web/hooks/use-release-notes.ts create mode 100644 apps/web/lib/changelog.ts create mode 100644 apps/web/lib/release-notes.ts create mode 100644 apps/web/lib/ssr/user-settings.ts create mode 100644 apps/web/scripts/generate-changelog.mjs create mode 100644 apps/web/scripts/generate-release-notes.mjs create mode 100644 cliff.toml diff --git a/.agents/skills/commit/SKILL.md b/.agents/skills/commit/SKILL.md new file mode 100644 index 0000000000..50801750bd --- /dev/null +++ b/.agents/skills/commit/SKILL.md @@ -0,0 +1,54 @@ +--- +name: commit +description: Create git commits following Conventional Commits format. Use when committing changes, creating commits, or when the user asks to commit. Ensures commit messages conform to the project's changelog generation conventions. +--- + +# Commit + +Create a git commit following this project's Conventional Commits convention. These messages are used by git-cliff (`cliff.toml`) to auto-generate changelogs and release notes. PRs are squash-merged, so the PR title becomes the commit on `main` — CI validates it via `pr-title.yml`. + +## Format + +``` +type: lowercase description +``` + +## Allowed Types + +| Type | Use for | In changelog? | +|------|---------|---------------| +| `feat` | New features | Yes (Features) | +| `fix` | Bug fixes | Yes (Bug Fixes) | +| `perf` | Performance improvements | Yes (Performance) | +| `refactor` | Code refactoring | Yes (Refactoring) | +| `docs` | Documentation changes | Yes (Documentation) | +| `chore` | Maintenance, deps, configs | No | +| `ci` | CI/CD changes | No | +| `test` | Test-only changes | No | + +## Rules + +- Subject **must** start with a lowercase letter +- Scope is optional: `feat(ui): add dialog` is valid +- Include PR/issue number when relevant: `feat: add release notes (#295)` +- Breaking changes: add `!` after type: `feat!: remove legacy API` +- Keep the first line under 72 characters + +## Examples + +``` +feat: add release notes dialog +fix: flaky test in orchestrator (#292) +refactor: extract session handler into separate module +chore: update dependencies +ci: add PR title linting workflow +``` + +## Steps + +1. Run `git status` and `git diff` to understand all changes +2. Review recent commits with `git log --oneline -10` to match project style +3. Stage relevant files (prefer specific files over `git add -A`) +4. Write a commit message following the format above +5. If changes span multiple concerns, consider separate commits +6. After committing, run `make fmt` then `make lint` to verify diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000000..2b7a412b8f --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000000..5133687b5e --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,37 @@ +name: pr-title + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + branches: [main] + +permissions: + pull-requests: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Validate PR title follows Conventional Commits + uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + # Allowed conventional commit types (must match cliff.toml commit_parsers) + types: | + feat + fix + perf + refactor + docs + chore + ci + test + release + # Require lowercase subject + subjectPattern: ^[a-z].*$ + subjectPatternError: | + The PR title subject must start with a lowercase letter. + Example: "feat: add release notes dialog" + # Do not require a scope + requireScope: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09f074295d..60f7e847d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,8 @@ jobs: - name: Install dependencies run: pnpm -C apps install --frozen-lockfile - name: Build web (standalone) + env: + KANDEV_VERSION: ${{ github.ref_name }} run: pnpm -C apps --filter @kandev/web build - name: Package web bundle run: bash scripts/release/package-web.sh @@ -116,6 +118,18 @@ jobs: needs: build-bundles runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate release notes + uses: orhun/git-cliff-action@v4 + with: + config: cliff.toml + args: --latest --strip header + env: + OUTPUT: RELEASE_NOTES.md + - name: Download bundle artifacts uses: actions/download-artifact@v4 with: @@ -144,6 +158,7 @@ jobs: - name: Publish release assets uses: softprops/action-gh-release@v2 with: + body_path: RELEASE_NOTES.md files: | dist/release-assets/kandev-*.tar.gz dist/release-assets/kandev-*.tar.gz.sha256 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bff409c4be..713d84f78f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,14 @@ repos: + # Commit message validation (Conventional Commits via commitlint) + - repo: local + hooks: + - id: commitlint + name: Validate commit message (Conventional Commits) + entry: bash -c 'cd apps && pnpm exec commitlint --edit "$1"' -- + language: system + stages: [commit-msg] + always_run: true + # Backend (Go) hooks - repo: local hooks: diff --git a/AGENTS.md b/AGENTS.md index b90760d300..5594ab8b39 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,13 +78,21 @@ apps/backend/ │ ├── editors/ # Editor integration │ ├── events/ # Event bus for internal pub/sub │ ├── gateway/ # WebSocket gateway +│ ├── github/ # GitHub API integration (PRs, reviews, webhooks) │ ├── integration/ # External integrations │ ├── lsp/ # LSP server │ ├── mcp/ # MCP protocol support │ ├── notifications/ # Notification system │ ├── persistence/ # Persistence layer │ ├── prompts/ # Prompt management +│ ├── repoclone/ # Repository cloning for remote executors +│ ├── scriptengine/ # Script placeholder resolution and interpolation +│ ├── secrets/ # Secret management +│ ├── sprites/ # Sprites AI integration │ ├── sysprompt/ # System prompt injection +│ ├── task/ +│ │ └── ... +│ ├── tools/ # Tool integrations │ ├── user/ # User management │ ├── workflow/ # Workflow engine │ │ ├── engine/ # Typed state-machine engine (trigger evaluation, action callbacks, transition store) @@ -173,7 +181,7 @@ Client (WS) Orchestrator Lifecycle Manager Runtime age **Worktrees:** `internal/worktree/Manager` provides workspace isolation. Each session can have its own worktree (branch) to prevent conflicts between concurrent agents. -**Executor default scripts:** Default prepare scripts are owned by executor lifecycle code (`internal/agent/lifecycle/default_scripts.go`), while `internal/scriptengine/` handles placeholder resolution and interpolation. +**Executor default scripts:** Default prepare scripts are in `internal/agent/lifecycle/default_scripts.go`; `internal/scriptengine/` handles placeholder resolution. --- @@ -256,15 +264,16 @@ lib/state/ │ ├── session-runtime/ # shell, processes, git, context │ ├── workspace/ # workspaces, repos, branches │ ├── settings/ # executors, agents, editors, prompts -│ ├── diff-comments/ # code review diff comments +│ ├── comments/ # code review diff comments +│ ├── github/ # GitHub PRs, reviews │ └── ui/ # preview, connection, active state ├── hydration/ # SSR merge strategies hooks/domains/{kanban,session,workspace,settings}/ # Domain-organized hooks lib/api/domains/ # API clients ├── kanban-api, session-api, workspace-api, settings-api, process-api -├── plan-api, queue-api, workflow-api, stats-api -├── user-shell-api, debug-api +├── plan-api, queue-api, workflow-api, stats-api, github-api +├── user-shell-api, debug-api, secrets-api, sprites-api, vscode-api ``` **Key State Paths:** @@ -284,6 +293,10 @@ lib/api/domains/ # API clients ## Best Practices +### Commit Conventions (enforced by CI) + +Commits to `main` **must** follow [Conventional Commits](https://www.conventionalcommits.org/) (`type: description`). PRs are squash-merged — the PR title becomes the commit, validated by CI. Changelog is auto-generated from these via git-cliff (`cliff.toml`). See `.agents/skills/commit/SKILL.md` for allowed types and examples. + ### Code Quality (enforced by linters) Static analysis runs in CI and pre-commit. New code **must** stay within these limits: @@ -331,4 +344,4 @@ This file is read by AI coding agents (Claude Code via `CLAUDE.md` symlink, Code --- -**Last Updated**: 2026-02-22 +**Last Updated**: 2026-02-26 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..c1d48dc325 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,547 @@ +# Changelog + +All notable changes to Kandev. + +## 0.7 - 2026-02-26 + +### Bug Fixes + +- flaky test ([#292](https://github.com/kdlbs/kandev/pull/292)) + +### Features + +- improve vscode cleanup ([#294](https://github.com/kdlbs/kandev/pull/294)) +- mermaid support ([#293](https://github.com/kdlbs/kandev/pull/293)) + +## 0.6 - 2026-02-26 + +### Bug Fixes + +- duplicated start message ([#289](https://github.com/kdlbs/kandev/pull/289)) + +### Features + +- improve workflow auto start ([#291](https://github.com/kdlbs/kandev/pull/291)) +- improve layout manager ([#290](https://github.com/kdlbs/kandev/pull/290)) + +## 0.5 - 2026-02-26 + +### Features + +- pr layout after start ([#288](https://github.com/kdlbs/kandev/pull/288)) +- improve PR review watcher + PR info panel ([#281](https://github.com/kdlbs/kandev/pull/281)) +- open plan panel if agent writes to it ([#287](https://github.com/kdlbs/kandev/pull/287)) +- clean up on remote session failure ([#286](https://github.com/kdlbs/kandev/pull/286)) + +## 0.4 - 2026-02-25 + +### Bug Fixes + +- vscode not being killed ([#284](https://github.com/kdlbs/kandev/pull/284)) +- agents stuck on starting after restart ([#283](https://github.com/kdlbs/kandev/pull/283)) + +### Features + +- claude code auth setup for remote executors ([#285](https://github.com/kdlbs/kandev/pull/285)) +- reduce sql queries amount ([#282](https://github.com/kdlbs/kandev/pull/282)) + +## 0.3 - 2026-02-25 + +### Features + +- improve cli startup wait + +## 0.2 - 2026-02-25 + +### Features + +- use github.com for releases instead of api to avoid rate limiting +- add login verification to release script + +## 0.1 - 2026-02-25 + +### Bug Fixes + +- vscode ([#279](https://github.com/kdlbs/kandev/pull/279)) +- layout switching messing panels ([#278](https://github.com/kdlbs/kandev/pull/278)) +- worktree folder removal ([#277](https://github.com/kdlbs/kandev/pull/277)) +- make dev use local db ([#276](https://github.com/kdlbs/kandev/pull/276)) + +### Features + +- improve release script +- add guard in workflow engine +- chat improvements ([#280](https://github.com/kdlbs/kandev/pull/280)) +- default github runners ([#275](https://github.com/kdlbs/kandev/pull/275)) + +## 0.0.12 - 2026-02-24 + +### Bug Fixes + +- make github release atomic + +### Features + +- improve release script + +## 0.0.11 - 2026-02-24 + +### Features + +- upgrade cli version +- add release version to cli +- improve docker logging +- remove unnecessary files + +## 0.0.10 - 2026-02-24 + +### Bug Fixes + +- enforce sidebar max-width via dockview group constraints ([#274](https://github.com/kdlbs/kandev/pull/274)) +- resolve all web app ESLint linter warnings ([#246](https://github.com/kdlbs/kandev/pull/246)) +- resolve all backend golangci-lint violations ([#245](https://github.com/kdlbs/kandev/pull/245)) + +### Documentation + +- add github integration (pr watcher) ([#262](https://github.com/kdlbs/kandev/pull/262)) +- update and review main documentation ([#259](https://github.com/kdlbs/kandev/pull/259)) + +### Features + +- upgrade cli +- add session-state sections to task switcher sidebar ([#273](https://github.com/kdlbs/kandev/pull/273)) +- several UX improvements ([#272](https://github.com/kdlbs/kandev/pull/272)) +- improve workflows and agent resume ([#269](https://github.com/kdlbs/kandev/pull/269)) +- improve claude code normalized messages + review ux ([#271](https://github.com/kdlbs/kandev/pull/271)) +- pr watcher user or team review ([#270](https://github.com/kdlbs/kandev/pull/270)) +- improve remote executor sprites.dev ([#267](https://github.com/kdlbs/kandev/pull/267)) +- remove executor healthcheck ([#263](https://github.com/kdlbs/kandev/pull/263)) +- refactor big repository ([#261](https://github.com/kdlbs/kandev/pull/261)) +- improve sql queries ([#260](https://github.com/kdlbs/kandev/pull/260)) +- remote executors + secrets ([#257](https://github.com/kdlbs/kandev/pull/257)) +- opencode acp +- opencode improve sse +- improve amp +- e2e tests +- improve claude code +- vscode integration ([#256](https://github.com/kdlbs/kandev/pull/256)) +- improve acp + tracing ([#258](https://github.com/kdlbs/kandev/pull/258)) +- improve ux ([#254](https://github.com/kdlbs/kandev/pull/254)) +- improved dockview layouts ([#253](https://github.com/kdlbs/kandev/pull/253)) +- improve backend handlers ([#252](https://github.com/kdlbs/kandev/pull/252)) +- tui agents db ([#251](https://github.com/kdlbs/kandev/pull/251)) +- add SQLite single-writer/multi-reader connection pool ([#250](https://github.com/kdlbs/kandev/pull/250)) +- improve plan comments ([#249](https://github.com/kdlbs/kandev/pull/249)) +- command panel search files ([#248](https://github.com/kdlbs/kandev/pull/248)) +- improve comment system ([#247](https://github.com/kdlbs/kandev/pull/247)) +- import export workflows ([#244](https://github.com/kdlbs/kandev/pull/244)) +- update readme & agent TUI reliability ([#239](https://github.com/kdlbs/kandev/pull/239)) +- add search functionality ([#243](https://github.com/kdlbs/kandev/pull/243)) +- add more file editor keybindings ([#242](https://github.com/kdlbs/kandev/pull/242)) +- improve monaco comments ([#241](https://github.com/kdlbs/kandev/pull/241)) +- improve db abstraction ([#238](https://github.com/kdlbs/kandev/pull/238)) +- add support for adding files ([#240](https://github.com/kdlbs/kandev/pull/240)) +- improved passthrough and workflow ([#237](https://github.com/kdlbs/kandev/pull/237)) +- ci complexity linters ([#236](https://github.com/kdlbs/kandev/pull/236)) +- homelab-runner ([#233](https://github.com/kdlbs/kandev/pull/233)) +- command panel ([#235](https://github.com/kdlbs/kandev/pull/235)) +- improve changes panel ([#234](https://github.com/kdlbs/kandev/pull/234)) +- archive tasks ([#232](https://github.com/kdlbs/kandev/pull/232)) +- improve agent sort ([#231](https://github.com/kdlbs/kandev/pull/231)) +- improve onboarding dialog ([#230](https://github.com/kdlbs/kandev/pull/230)) +- improve stepper ([#229](https://github.com/kdlbs/kandev/pull/229)) +- improve workflows ([#228](https://github.com/kdlbs/kandev/pull/228)) + +### Performance + +- optimize settings page load ([#268](https://github.com/kdlbs/kandev/pull/268)) + +### Style + +- fix format issues ([#255](https://github.com/kdlbs/kandev/pull/255)) + +## 0.0.9 - 2026-02-16 + +### Features + +- reduce bundle size + +## 0.0.8 - 2026-02-16 + +### Bug Fixes + +- release bundle + +## 0.0.7 - 2026-02-16 + +### Bug Fixes + +- bundle web assets + +### Features + +- improved stats ([#227](https://github.com/kdlbs/kandev/pull/227)) + +## 0.0.6 - 2026-02-16 + +### Bug Fixes + +- bundle all web assets + +## 0.0.5 - 2026-02-16 + +## 0.0.4 - 2026-02-16 + +### Bug Fixes + +- bundle + +### Features + +- use tar for bundles +- use tar for bundles +- improve editors ([#226](https://github.com/kdlbs/kandev/pull/226)) + +## 0.0.3 - 2026-02-15 + +### Bug Fixes + +- release build + +## 0.0.2 - 2026-02-15 + +### Bug Fixes + +- sha lowercase comparison +- github release download when github token is present + +### Features + +- improve windows support +- fix cli org + +## 0.0.1 - 2026-02-15 + +### Bug Fixes + +- resolve session stuck issues and workflow transition bugs ([#206](https://github.com/kdlbs/kandev/pull/206)) +- copilot mcp ([#217](https://github.com/kdlbs/kandev/pull/217)) +- complete tool calls on turn end ([#216](https://github.com/kdlbs/kandev/pull/216)) +- start ws disconnected +- make start without public folder +- 2 cumulative diff +- cumulative diff poll +- pass MCP configuration to Claude Code via --mcp-config flag ([#205](https://github.com/kdlbs/kandev/pull/205)) +- prevent user shell terminals from prematurely completing agent tasks ([#199](https://github.com/kdlbs/kandev/pull/199)) +- refetch git status when switching back to a previously viewed task ([#198](https://github.com/kdlbs/kandev/pull/198)) +- resume failed task sessions instead of returning errors ([#197](https://github.com/kdlbs/kandev/pull/197)) +- cancel button always disabled when agent is running ([#195](https://github.com/kdlbs/kandev/pull/195)) +- diff bg lines and codex last message ([#186](https://github.com/kdlbs/kandev/pull/186)) +- prevent duplicate agent messages in database ([#181](https://github.com/kdlbs/kandev/pull/181)) +- prevent duplicate message submission while agent is working ([#180](https://github.com/kdlbs/kandev/pull/180)) +- resolve notification ordering race condition ([#179](https://github.com/kdlbs/kandev/pull/179)) +- use detached context for ask_user_question MCP tool ([#178](https://github.com/kdlbs/kandev/pull/178)) +- rollback from review step works on simple boards ([#177](https://github.com/kdlbs/kandev/pull/177)) +- wire permission handler to Copilot SDK ([#161](https://github.com/kdlbs/kandev/pull/161)) +- workaround shiki Go grammar catastrophic backtracking ([#174](https://github.com/kdlbs/kandev/pull/174)) +- db path + open workspace folder ([#169](https://github.com/kdlbs/kandev/pull/169)) +- agent profile creation ([#170](https://github.com/kdlbs/kandev/pull/170)) +- show approve button wrongly + improve plan ux ([#158](https://github.com/kdlbs/kandev/pull/158)) +- git status tracking - detect staging changes and persist in snapshots ([#149](https://github.com/kdlbs/kandev/pull/149)) +- hide 'Approval Required' badge when agent is working ([#148](https://github.com/kdlbs/kandev/pull/148)) +- disable Cmd+Enter keyboard shortcut when chat input is disabled ([#146](https://github.com/kdlbs/kandev/pull/146)) +- prevent workflow step regression on follow-up prompts ([#143](https://github.com/kdlbs/kandev/pull/143)) +- Update session/task states when ask_user_question tool is used ([#142](https://github.com/kdlbs/kandev/pull/142)) +- Fix cache keying bug and repository locks memory leak ([#138](https://github.com/kdlbs/kandev/pull/138)) +- permission request ID mismatch, SSE duplicates, and subprocess cleanup ([#137](https://github.com/kdlbs/kandev/pull/137)) +- improve ask_user_question MCP tool with clear options format ([#125](https://github.com/kdlbs/kandev/pull/125)) +- session recovery after backend restart ([#126](https://github.com/kdlbs/kandev/pull/126)) +- use correct sandbox_mode to enable file editing ([#129](https://github.com/kdlbs/kandev/pull/129)) +- Multiple bug fixes for agent lifecycle and task creation ([#122](https://github.com/kdlbs/kandev/pull/122)) +- WebSocket race condition in session hooks and chat input performance ([#119](https://github.com/kdlbs/kandev/pull/119)) +- prevent approval button showing while agent is working ([#117](https://github.com/kdlbs/kandev/pull/117)) +- fix alignment in board creation ([#112](https://github.com/kdlbs/kandev/pull/112)) +- refetch messages and git status when switching between tasks ([#110](https://github.com/kdlbs/kandev/pull/110)) +- prevent WebSocket timeout from canceling long-running agent operations ([#109](https://github.com/kdlbs/kandev/pull/109)) +- synchronous event bus dispatch with regression tests ([#106](https://github.com/kdlbs/kandev/pull/106)) +- git status not showing after page refresh ([#101](https://github.com/kdlbs/kandev/pull/101)) +- increase prompt timeout to 60min, add error feedback, rename acp to agent API ([#97](https://github.com/kdlbs/kandev/pull/97)) +- strip origin/ prefix from base branch for rebase/merge operations ([#94](https://github.com/kdlbs/kandev/pull/94)) +- filter upstream commits, fix ahead/behind, and remove duplicate types ([#91](https://github.com/kdlbs/kandev/pull/91)) +- model selector ([#90](https://github.com/kdlbs/kandev/pull/90)) +- load most recent messages and fix lazy loading pagination ([#82](https://github.com/kdlbs/kandev/pull/82)) +- use AGENTCTL_PORT env var for backend ControlClient ([#63](https://github.com/kdlbs/kandev/pull/63)) +- bind KANDEV_AGENT_STANDALONE_PORT env var to config ([#61](https://github.com/kdlbs/kandev/pull/61)) +- refactor shell streaming to use event bus pattern ([#50](https://github.com/kdlbs/kandev/pull/50)) +- make Codex Prompt() synchronous to fix premature task state transition ([#35](https://github.com/kdlbs/kandev/pull/35)) +- session state after restart +- improve session terminal and git status handling ([#34](https://github.com/kdlbs/kandev/pull/34)) +- make dev ctrl+c cleanup +- remove extra state transitions during startup resume +- skip tool call update when no active session +- resolve deadlock in adapter by not holding mutex during RPC calls +- typescript errors +- address code review issues ([#16](https://github.com/kdlbs/kandev/pull/16)) +- docker cleanup +- macos launcher +- standalone mode workspace path and worktree lookup ([#10](https://github.com/kdlbs/kandev/pull/10)) +- go vet ci +- reconnect to agent streams after backend restart +- show worktree path in UI after agent starts +- only show active worktrees in task API response +- configure git safe.directory in agent containers +- mount entire .git directory for worktree support in containers +- mount git worktree metadata directory into container +- wire worktree manager to lifecycle manager for agent isolation +- recover agent state from Docker on backend restart +- filter out internal ACP messages from WebSocket broadcast +- fix real-time comments not appearing on first agent start +- go tests +- task page +- workspace migration +- Structure session_info event correctly for protocol.Message parsing +- Publish session_info event so session ID is stored in task metadata +- build +- publish session notifications to event bus for WebSocket streaming +- web linter issues + +### Documentation + +- update WEBSOCKET_API.md with complete API reference +- Update AGENTS.md for WebSocket-only architecture +- Add comprehensive WebSocket API reference + +### Features + +- improve windows support ([#225](https://github.com/kdlbs/kandev/pull/225)) +- auggie dynamic model list ([#223](https://github.com/kdlbs/kandev/pull/223)) +- better context files ([#222](https://github.com/kdlbs/kandev/pull/222)) +- agents.json refactor ([#221](https://github.com/kdlbs/kandev/pull/221)) +- improve task creation ([#220](https://github.com/kdlbs/kandev/pull/220)) +- migrate agent operations from HTTP to WebSocket ([#218](https://github.com/kdlbs/kandev/pull/218)) +- dockview new ui ([#219](https://github.com/kdlbs/kandev/pull/219)) +- improve git pull ([#215](https://github.com/kdlbs/kandev/pull/215)) +- remove blocking http call when creating the agent ([#214](https://github.com/kdlbs/kandev/pull/214)) +- add agent boot message ([#213](https://github.com/kdlbs/kandev/pull/213)) +- preventing process kill on port use +- increase agent boot timeout from 30s to 60s +- improve plan mode ([#212](https://github.com/kdlbs/kandev/pull/212)) +- add ui debug on make start-debug +- favicon +- add make start-debug +- local executor and worktree + new task dialog ([#211](https://github.com/kdlbs/kandev/pull/211)) +- review ux ([#210](https://github.com/kdlbs/kandev/pull/210)) +- improve file tree ([#209](https://github.com/kdlbs/kandev/pull/209)) +- mock agent ([#208](https://github.com/kdlbs/kandev/pull/208)) +- queue messages ([#201](https://github.com/kdlbs/kandev/pull/201)) +- file icons ([#207](https://github.com/kdlbs/kandev/pull/207)) +- added message actions ([#203](https://github.com/kdlbs/kandev/pull/203)) +- fix random port ([#202](https://github.com/kdlbs/kandev/pull/202)) +- improve messages ux ([#193](https://github.com/kdlbs/kandev/pull/193)) +- add Claude Opus 4.6 model support ([#194](https://github.com/kdlbs/kandev/pull/194)) +- improve web fetch message ([#192](https://github.com/kdlbs/kandev/pull/192)) +- Implement image paste functionality for Claude Code ([#188](https://github.com/kdlbs/kandev/pull/188)) +- improve session terminals ([#185](https://github.com/kdlbs/kandev/pull/185)) +- restore session when a worktree folder is deleted ([#184](https://github.com/kdlbs/kandev/pull/184)) +- remove thinking selection ([#182](https://github.com/kdlbs/kandev/pull/182)) +- improved chat input keybinding ([#173](https://github.com/kdlbs/kandev/pull/173)) +- improve diff colors ([#171](https://github.com/kdlbs/kandev/pull/171)) +- Add force push option to git push menu ([#167](https://github.com/kdlbs/kandev/pull/167)) +- improve workspace file tree loading +- improve make start +- draft pr support ([#165](https://github.com/kdlbs/kandev/pull/165)) +- improved file tree ([#164](https://github.com/kdlbs/kandev/pull/164)) +- session mobile design ([#163](https://github.com/kdlbs/kandev/pull/163)) +- custom commands ([#162](https://github.com/kdlbs/kandev/pull/162)) +- add support for mcpToolCall item type ([#159](https://github.com/kdlbs/kandev/pull/159)) +- mobile design ([#160](https://github.com/kdlbs/kandev/pull/160)) +- Add built-in custom prompts for common workflows ([#156](https://github.com/kdlbs/kandev/pull/156)) +- improve kanban board ui ([#155](https://github.com/kdlbs/kandev/pull/155)) +- update deps +- remove outdated docs +- update docs to use mermaid ([#152](https://github.com/kdlbs/kandev/pull/152)) +- update default board settings ([#153](https://github.com/kdlbs/kandev/pull/153)) +- add make start ([#151](https://github.com/kdlbs/kandev/pull/151)) +- Add file editor with diff-based save functionality ([#145](https://github.com/kdlbs/kandev/pull/145)) +- pierre diffs lib ([#147](https://github.com/kdlbs/kandev/pull/147)) +- Implement git discard changes functionality ([#144](https://github.com/kdlbs/kandev/pull/144)) +- Improve task approval workflow and workflow templates ([#141](https://github.com/kdlbs/kandev/pull/141)) +- all tasks page plus search ([#140](https://github.com/kdlbs/kandev/pull/140)) +- improve task deletion ([#139](https://github.com/kdlbs/kandev/pull/139)) +- slash commands from agents ([#136](https://github.com/kdlbs/kandev/pull/136)) +- add GitHub Copilot CLI and Sourcegraph Amp agent support ([#130](https://github.com/kdlbs/kandev/pull/130)) +- improve task creation dialog ([#135](https://github.com/kdlbs/kandev/pull/135)) +- plan comment annotations, Kandev system prompt, and Standard workflow ([#134](https://github.com/kdlbs/kandev/pull/134)) +- improve chat ui messages ([#132](https://github.com/kdlbs/kandev/pull/132)) +- improve make dev shutdown ([#133](https://github.com/kdlbs/kandev/pull/133)) +- implement task plans feature ([#131](https://github.com/kdlbs/kandev/pull/131)) +- add debug toggle button to TaskTopBar ([#128](https://github.com/kdlbs/kandev/pull/128)) +- chat messages normalized ([#121](https://github.com/kdlbs/kandev/pull/121)) +- improve sidebar ([#127](https://github.com/kdlbs/kandev/pull/127)) +- migrate MCP server from backend to agentctl ([#124](https://github.com/kdlbs/kandev/pull/124)) +- Add ask_user_question MCP tool for agent clarifications ([#123](https://github.com/kdlbs/kandev/pull/123)) +- improved chat input ([#120](https://github.com/kdlbs/kandev/pull/120)) +- implement file referencing with @filename autocomplete in chat input ([#118](https://github.com/kdlbs/kandev/pull/118)) +- add thinking/reasoning streaming support ([#113](https://github.com/kdlbs/kandev/pull/113)) +- improve approval flow and step transitions ([#111](https://github.com/kdlbs/kandev/pull/111)) +- Add file unstaging functionality ([#107](https://github.com/kdlbs/kandev/pull/107)) +- implement workflow system with steps terminology ([#102](https://github.com/kdlbs/kandev/pull/102)) +- improve logging ([#105](https://github.com/kdlbs/kandev/pull/105)) +- remove npm warns from terminal + fix terminal render on refresh ([#104](https://github.com/kdlbs/kandev/pull/104)) +- improved chat ux ([#103](https://github.com/kdlbs/kandev/pull/103)) +- git pull before worktree creation ([#100](https://github.com/kdlbs/kandev/pull/100)) +- opencode dynamic model loader ([#99](https://github.com/kdlbs/kandev/pull/99)) +- improve task switch + cli passthrough resume ([#98](https://github.com/kdlbs/kandev/pull/98)) +- cli passthrough state transitions ([#95](https://github.com/kdlbs/kandev/pull/95)) +- cli passthrough setting ([#93](https://github.com/kdlbs/kandev/pull/93)) +- per-task executor selection with multi-runtime support ([#92](https://github.com/kdlbs/kandev/pull/92)) +- diff file multi line comment +- gemini and opencode +- claude code support ([#87](https://github.com/kdlbs/kandev/pull/87)) +- Git status tracking refactor with persistent snapshots and commit history ([#86](https://github.com/kdlbs/kandev/pull/86)) +- improved codex and auggie default permissions +- setup and cleanup script ([#80](https://github.com/kdlbs/kandev/pull/80)) +- improve chat ui paddings ([#83](https://github.com/kdlbs/kandev/pull/83)) +- random port support ([#79](https://github.com/kdlbs/kandev/pull/79)) +- improve preview url loading ([#78](https://github.com/kdlbs/kandev/pull/78)) +- refactor frontend hooks and store ([#76](https://github.com/kdlbs/kandev/pull/76)) +- backend improved comments, logs and agents.md ([#77](https://github.com/kdlbs/kandev/pull/77)) +- process runners ([#71](https://github.com/kdlbs/kandev/pull/71)) +- http logging middleware + mcp random port ([#74](https://github.com/kdlbs/kandev/pull/74)) +- add session turns with duration display and live timer ([#72](https://github.com/kdlbs/kandev/pull/72)) +- refactored chat input panels + pie context ([#70](https://github.com/kdlbs/kandev/pull/70)) +- dynamic model switching and session status fix ([#68](https://github.com/kdlbs/kandev/pull/68)) +- add embedded MCP server with dual transport support ([#67](https://github.com/kdlbs/kandev/pull/67)) +- add context window usage display to task session ([#66](https://github.com/kdlbs/kandev/pull/66)) +- mcp servers + executors ([#60](https://github.com/kdlbs/kandev/pull/60)) +- improve repository list setting ([#65](https://github.com/kdlbs/kandev/pull/65)) +- implement turn cancellation for agent sessions ([#64](https://github.com/kdlbs/kandev/pull/64)) +- custom branch prefix ([#58](https://github.com/kdlbs/kandev/pull/58)) +- add system provider +- Add PR creation via gh CLI and improve git operations ([#57](https://github.com/kdlbs/kandev/pull/57)) +- kanban page refactoring ([#52](https://github.com/kdlbs/kandev/pull/52)) +- settings data loading per page ([#51](https://github.com/kdlbs/kandev/pull/51)) +- preview panel option ([#49](https://github.com/kdlbs/kandev/pull/49)) +- custom prompts ([#48](https://github.com/kdlbs/kandev/pull/48)) +- refactor main, add providers ([#46](https://github.com/kdlbs/kandev/pull/46)) +- remove dev db not used ([#45](https://github.com/kdlbs/kandev/pull/45)) +- refactor sqlite usage ([#44](https://github.com/kdlbs/kandev/pull/44)) +- improve list agents +- multiple editors support ([#41](https://github.com/kdlbs/kandev/pull/41)) +- cli publish ([#40](https://github.com/kdlbs/kandev/pull/40)) +- cli launcher npx kandev ([#39](https://github.com/kdlbs/kandev/pull/39)) +- improve chat UX ([#38](https://github.com/kdlbs/kandev/pull/38)) +- add typed event payloads for event bus messages ([#32](https://github.com/kdlbs/kandev/pull/32)) +- start agents on boot +- improve landing page ([#33](https://github.com/kdlbs/kandev/pull/33)) +- remove premature executor deletion +- session refactor ([#30](https://github.com/kdlbs/kandev/pull/30)) +- updated agents.md ([#28](https://github.com/kdlbs/kandev/pull/28)) +- shell selector ([#27](https://github.com/kdlbs/kandev/pull/27)) +- task switcher column ([#26](https://github.com/kdlbs/kandev/pull/26)) +- inline permission approval for tool calls ([#25](https://github.com/kdlbs/kandev/pull/25)) +- notifications ([#24](https://github.com/kdlbs/kandev/pull/24)) +- improved chat experience ([#22](https://github.com/kdlbs/kandev/pull/22)) +- improve onboarding and task setup ([#21](https://github.com/kdlbs/kandev/pull/21)) +- auto-respawn shell session on unexpected exit +- add graceful shutdown +- task_session_worktrees +- Interactive shell terminal for agent tasks ([#19](https://github.com/kdlbs/kandev/pull/19)) +- flaky resume sessions +- rename agent_sessions to task_sessions +- golang linter +- right panel overlay +- tasks refactor +- improved chat ui topbar +- chat improved renderer and comments pagination +- tanstack virtual +- task comments SSR fetch +- cmd+enter in task chat +- protocol adapter abstraction with agent profile support ([#15](https://github.com/kdlbs/kandev/pull/15)) +- replace agent_type with agent_profile_id ([#14](https://github.com/kdlbs/kandev/pull/14)) +- improve cards +- changed theme to shadcn nova - less paddings +- data fetching refactor ssr -> ws updates +- File browser with syntax-highlighted viewer ([#13](https://github.com/kdlbs/kandev/pull/13)) +- auto-launch agentctl subprocess in standalone mode ([#12](https://github.com/kdlbs/kandev/pull/12)) +- add defaults to workspace: env, executor, agent +- simplify task creation +- display settings in kanban page +- semantic naming and branch cleanup on task deletion ([#11](https://github.com/kdlbs/kandev/pull/11)) +- agents discovery +- add pre-commit hook +- environments and executors +- build agentctl to bin/ and use pre-built binary in Dockerfile ([#9](https://github.com/kdlbs/kandev/pull/9)) +- Real-time Git Status Integration ([#8](https://github.com/kdlbs/kandev/pull/8)) +- improved settings repos, boards +- landing page and pnpm workspaces +- return worktree info in orchestrator.start response +- worktrees at agent session level with random suffix +- cleanup worktree and branch on task deletion +- expose worktree path and branch in task API and UI +- implement Git worktrees for concurrent agent execution +- enhance tool call display with payload details and typing indicator +- make repository_url optional when launching agents +- add persistent agent session tracking +- enhance WebSocket handling and chat panel improvements +- tasks crud +- added repositories support +- enhance comment system and e2e testing +- implement ACP permission request flow +- add bidirectional comment system and agent input request flow +- web app support for boards and columns +- web app support for workspaces +- clean db command +- added workspaces +- ws state +- ws and zustand init +- add http handlers to backend +- implement persistent agent execution logs storage and retrieval +- improve homepage buttons +- settings page +- improved task page +- ui components reset +- task page +- add READY status and multi-turn conversation support +- complete acp-go-sdk integration with task state updates +- multi view kanban +- fix kanban ssr +- init kanban +- web app cleanup ([#2](https://github.com/kdlbs/kandev/pull/2)) +- add build orchestration and architecture documentation ([#1](https://github.com/kdlbs/kandev/pull/1)) +- web app init + +### Fix + +- Handle untracked and new files in git discard operation ([#168](https://github.com/kdlbs/kandev/pull/168)) +- Persist plan notification state across page refreshes ([#150](https://github.com/kdlbs/kandev/pull/150)) +- Approval button not showing when navigating between sessions ([#116](https://github.com/kdlbs/kandev/pull/116)) + +### Refactoring + +- Remove database migrations for clean dev-phase bootstrap ([#176](https://github.com/kdlbs/kandev/pull/176)) +- consolidate system prompts into sysprompt package ([#166](https://github.com/kdlbs/kandev/pull/166)) +- split monolithic sqlite.go into domain-specific files ([#84](https://github.com/kdlbs/kandev/pull/84)) +- remove progress field from TaskSession and AgentExecution ([#75](https://github.com/kdlbs/kandev/pull/75)) +- comprehensive code quality improvements ([#69](https://github.com/kdlbs/kandev/pull/69)) +- remove auggie-specific code and use standard ACP protocol ([#47](https://github.com/kdlbs/kandev/pull/47)) +- unify permission requests into agent event stream ([#31](https://github.com/kdlbs/kandev/pull/31)) +- session resumption cleanup and AgentInstance → AgentExecution rename ([#23](https://github.com/kdlbs/kandev/pull/23)) +- extract manager into focused components ([#20](https://github.com/kdlbs/kandev/pull/20)) +- unify configuration and remove single-instance mode ([#18](https://github.com/kdlbs/kandev/pull/18)) +- unify WebSocket patterns to Pattern A (handlers + controller + dto) +- Replace REST API with WebSocket-only architecture + +### Build + +- improved pre-commit linter config + +### Cleanup + +- remove dead FileListUpdate streaming path ([#204](https://github.com/kdlbs/kandev/pull/204)) + +### Merge + +- resolve conflicts with main + + diff --git a/apps/backend/internal/user/controller/controller.go b/apps/backend/internal/user/controller/controller.go index 5dbe87608f..3cd8fa138b 100644 --- a/apps/backend/internal/user/controller/controller.go +++ b/apps/backend/internal/user/controller/controller.go @@ -44,20 +44,22 @@ func (c *Controller) GetUserSettings(ctx context.Context) (dto.UserSettingsRespo func (c *Controller) UpdateUserSettings(ctx context.Context, req dto.UpdateUserSettingsRequest) (dto.UserSettingsResponse, error) { settings, err := c.svc.UpdateUserSettings(ctx, &service.UpdateUserSettingsRequest{ - WorkspaceID: req.WorkspaceID, - KanbanViewMode: req.KanbanViewMode, - WorkflowFilterID: req.WorkflowFilterID, - RepositoryIDs: req.RepositoryIDs, - InitialSetupComplete: req.InitialSetupComplete, - PreferredShell: req.PreferredShell, - DefaultEditorID: req.DefaultEditorID, - EnablePreviewOnClick: req.EnablePreviewOnClick, - ChatSubmitKey: req.ChatSubmitKey, - ReviewAutoMarkOnScroll: req.ReviewAutoMarkOnScroll, - LspAutoStartLanguages: req.LspAutoStartLanguages, - LspAutoInstallLanguages: req.LspAutoInstallLanguages, - LspServerConfigs: req.LspServerConfigs, - SavedLayouts: req.SavedLayouts, + WorkspaceID: req.WorkspaceID, + KanbanViewMode: req.KanbanViewMode, + WorkflowFilterID: req.WorkflowFilterID, + RepositoryIDs: req.RepositoryIDs, + InitialSetupComplete: req.InitialSetupComplete, + PreferredShell: req.PreferredShell, + DefaultEditorID: req.DefaultEditorID, + EnablePreviewOnClick: req.EnablePreviewOnClick, + ChatSubmitKey: req.ChatSubmitKey, + ReviewAutoMarkOnScroll: req.ReviewAutoMarkOnScroll, + ShowReleaseNotification: req.ShowReleaseNotification, + ReleaseNotesLastSeenVersion: req.ReleaseNotesLastSeenVersion, + LspAutoStartLanguages: req.LspAutoStartLanguages, + LspAutoInstallLanguages: req.LspAutoInstallLanguages, + LspServerConfigs: req.LspServerConfigs, + SavedLayouts: req.SavedLayouts, }) if err != nil { return dto.UserSettingsResponse{}, err diff --git a/apps/backend/internal/user/dto/dto.go b/apps/backend/internal/user/dto/dto.go index 2bd9fa5c82..888805671d 100644 --- a/apps/backend/internal/user/dto/dto.go +++ b/apps/backend/internal/user/dto/dto.go @@ -14,22 +14,24 @@ type UserDTO struct { } type UserSettingsDTO struct { - UserID string `json:"user_id"` - WorkspaceID string `json:"workspace_id"` - KanbanViewMode string `json:"kanban_view_mode"` - WorkflowFilterID string `json:"workflow_filter_id"` - RepositoryIDs []string `json:"repository_ids"` - InitialSetupComplete bool `json:"initial_setup_complete"` - PreferredShell string `json:"preferred_shell"` - DefaultEditorID string `json:"default_editor_id"` - EnablePreviewOnClick bool `json:"enable_preview_on_click"` - ChatSubmitKey string `json:"chat_submit_key"` - ReviewAutoMarkOnScroll bool `json:"review_auto_mark_on_scroll"` - LspAutoStartLanguages []string `json:"lsp_auto_start_languages"` - LspAutoInstallLanguages []string `json:"lsp_auto_install_languages"` - LspServerConfigs map[string]map[string]interface{} `json:"lsp_server_configs,omitempty"` - SavedLayouts []models.SavedLayout `json:"saved_layouts"` - UpdatedAt string `json:"updated_at"` + UserID string `json:"user_id"` + WorkspaceID string `json:"workspace_id"` + KanbanViewMode string `json:"kanban_view_mode"` + WorkflowFilterID string `json:"workflow_filter_id"` + RepositoryIDs []string `json:"repository_ids"` + InitialSetupComplete bool `json:"initial_setup_complete"` + PreferredShell string `json:"preferred_shell"` + DefaultEditorID string `json:"default_editor_id"` + EnablePreviewOnClick bool `json:"enable_preview_on_click"` + ChatSubmitKey string `json:"chat_submit_key"` + ReviewAutoMarkOnScroll bool `json:"review_auto_mark_on_scroll"` + ShowReleaseNotification bool `json:"show_release_notification"` + ReleaseNotesLastSeenVersion string `json:"release_notes_last_seen_version"` + LspAutoStartLanguages []string `json:"lsp_auto_start_languages"` + LspAutoInstallLanguages []string `json:"lsp_auto_install_languages"` + LspServerConfigs map[string]map[string]interface{} `json:"lsp_server_configs,omitempty"` + SavedLayouts []models.SavedLayout `json:"saved_layouts"` + UpdatedAt string `json:"updated_at"` } type UserResponse struct { @@ -48,20 +50,22 @@ type ShellOption struct { } type UpdateUserSettingsRequest struct { - WorkspaceID *string `json:"workspace_id,omitempty"` - KanbanViewMode *string `json:"kanban_view_mode,omitempty"` - WorkflowFilterID *string `json:"workflow_filter_id,omitempty"` - RepositoryIDs *[]string `json:"repository_ids,omitempty"` - InitialSetupComplete *bool `json:"initial_setup_complete,omitempty"` - PreferredShell *string `json:"preferred_shell,omitempty"` - DefaultEditorID *string `json:"default_editor_id,omitempty"` - EnablePreviewOnClick *bool `json:"enable_preview_on_click,omitempty"` - ChatSubmitKey *string `json:"chat_submit_key,omitempty"` - ReviewAutoMarkOnScroll *bool `json:"review_auto_mark_on_scroll,omitempty"` - LspAutoStartLanguages *[]string `json:"lsp_auto_start_languages,omitempty"` - LspAutoInstallLanguages *[]string `json:"lsp_auto_install_languages,omitempty"` - LspServerConfigs *map[string]map[string]interface{} `json:"lsp_server_configs,omitempty"` - SavedLayouts *[]models.SavedLayout `json:"saved_layouts,omitempty"` + WorkspaceID *string `json:"workspace_id,omitempty"` + KanbanViewMode *string `json:"kanban_view_mode,omitempty"` + WorkflowFilterID *string `json:"workflow_filter_id,omitempty"` + RepositoryIDs *[]string `json:"repository_ids,omitempty"` + InitialSetupComplete *bool `json:"initial_setup_complete,omitempty"` + PreferredShell *string `json:"preferred_shell,omitempty"` + DefaultEditorID *string `json:"default_editor_id,omitempty"` + EnablePreviewOnClick *bool `json:"enable_preview_on_click,omitempty"` + ChatSubmitKey *string `json:"chat_submit_key,omitempty"` + ReviewAutoMarkOnScroll *bool `json:"review_auto_mark_on_scroll,omitempty"` + ShowReleaseNotification *bool `json:"show_release_notification,omitempty"` + ReleaseNotesLastSeenVersion *string `json:"release_notes_last_seen_version,omitempty"` + LspAutoStartLanguages *[]string `json:"lsp_auto_start_languages,omitempty"` + LspAutoInstallLanguages *[]string `json:"lsp_auto_install_languages,omitempty"` + LspServerConfigs *map[string]map[string]interface{} `json:"lsp_server_configs,omitempty"` + SavedLayouts *[]models.SavedLayout `json:"saved_layouts,omitempty"` } func FromUser(user *models.User) UserDTO { @@ -75,21 +79,23 @@ func FromUser(user *models.User) UserDTO { func FromUserSettings(settings *models.UserSettings) UserSettingsDTO { return UserSettingsDTO{ - UserID: settings.UserID, - WorkspaceID: settings.WorkspaceID, - KanbanViewMode: settings.KanbanViewMode, - WorkflowFilterID: settings.WorkflowFilterID, - RepositoryIDs: settings.RepositoryIDs, - InitialSetupComplete: settings.InitialSetupComplete, - PreferredShell: settings.PreferredShell, - DefaultEditorID: settings.DefaultEditorID, - EnablePreviewOnClick: settings.EnablePreviewOnClick, - ChatSubmitKey: settings.ChatSubmitKey, - ReviewAutoMarkOnScroll: settings.ReviewAutoMarkOnScroll, - LspAutoStartLanguages: settings.LspAutoStartLanguages, - LspAutoInstallLanguages: settings.LspAutoInstallLanguages, - LspServerConfigs: settings.LspServerConfigs, - SavedLayouts: settings.SavedLayouts, - UpdatedAt: settings.UpdatedAt.Format(time.RFC3339), + UserID: settings.UserID, + WorkspaceID: settings.WorkspaceID, + KanbanViewMode: settings.KanbanViewMode, + WorkflowFilterID: settings.WorkflowFilterID, + RepositoryIDs: settings.RepositoryIDs, + InitialSetupComplete: settings.InitialSetupComplete, + PreferredShell: settings.PreferredShell, + DefaultEditorID: settings.DefaultEditorID, + EnablePreviewOnClick: settings.EnablePreviewOnClick, + ChatSubmitKey: settings.ChatSubmitKey, + ReviewAutoMarkOnScroll: settings.ReviewAutoMarkOnScroll, + ShowReleaseNotification: settings.ShowReleaseNotification, + ReleaseNotesLastSeenVersion: settings.ReleaseNotesLastSeenVersion, + LspAutoStartLanguages: settings.LspAutoStartLanguages, + LspAutoInstallLanguages: settings.LspAutoInstallLanguages, + LspServerConfigs: settings.LspServerConfigs, + SavedLayouts: settings.SavedLayouts, + UpdatedAt: settings.UpdatedAt.Format(time.RFC3339), } } diff --git a/apps/backend/internal/user/models/models.go b/apps/backend/internal/user/models/models.go index 03fa8ca2bd..d3fc6f9f5e 100644 --- a/apps/backend/internal/user/models/models.go +++ b/apps/backend/internal/user/models/models.go @@ -13,23 +13,25 @@ type User struct { } type UserSettings struct { - UserID string `json:"user_id"` - WorkspaceID string `json:"workspace_id"` - KanbanViewMode string `json:"kanban_view_mode"` - WorkflowFilterID string `json:"workflow_filter_id"` - RepositoryIDs []string `json:"repository_ids"` - InitialSetupComplete bool `json:"initial_setup_complete"` - PreferredShell string `json:"preferred_shell"` - DefaultEditorID string `json:"default_editor_id"` - EnablePreviewOnClick bool `json:"enable_preview_on_click"` - ChatSubmitKey string `json:"chat_submit_key"` // "enter" | "cmd_enter" - ReviewAutoMarkOnScroll bool `json:"review_auto_mark_on_scroll"` - LspAutoStartLanguages []string `json:"lsp_auto_start_languages"` - LspAutoInstallLanguages []string `json:"lsp_auto_install_languages"` - LspServerConfigs map[string]map[string]interface{} `json:"lsp_server_configs"` - SavedLayouts []SavedLayout `json:"saved_layouts"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + UserID string `json:"user_id"` + WorkspaceID string `json:"workspace_id"` + KanbanViewMode string `json:"kanban_view_mode"` + WorkflowFilterID string `json:"workflow_filter_id"` + RepositoryIDs []string `json:"repository_ids"` + InitialSetupComplete bool `json:"initial_setup_complete"` + PreferredShell string `json:"preferred_shell"` + DefaultEditorID string `json:"default_editor_id"` + EnablePreviewOnClick bool `json:"enable_preview_on_click"` + ChatSubmitKey string `json:"chat_submit_key"` // "enter" | "cmd_enter" + ReviewAutoMarkOnScroll bool `json:"review_auto_mark_on_scroll"` + ShowReleaseNotification bool `json:"show_release_notification"` + ReleaseNotesLastSeenVersion string `json:"release_notes_last_seen_version"` + LspAutoStartLanguages []string `json:"lsp_auto_start_languages"` + LspAutoInstallLanguages []string `json:"lsp_auto_install_languages"` + LspServerConfigs map[string]map[string]interface{} `json:"lsp_server_configs"` + SavedLayouts []SavedLayout `json:"saved_layouts"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // SavedLayout represents a user-saved dockview layout configuration. diff --git a/apps/backend/internal/user/service/service.go b/apps/backend/internal/user/service/service.go index 20c785d955..8fa4f74edf 100644 --- a/apps/backend/internal/user/service/service.go +++ b/apps/backend/internal/user/service/service.go @@ -26,20 +26,22 @@ type Service struct { } type UpdateUserSettingsRequest struct { - WorkspaceID *string - KanbanViewMode *string - WorkflowFilterID *string - RepositoryIDs *[]string - InitialSetupComplete *bool - PreferredShell *string - DefaultEditorID *string - EnablePreviewOnClick *bool - ChatSubmitKey *string - ReviewAutoMarkOnScroll *bool - LspAutoStartLanguages *[]string - LspAutoInstallLanguages *[]string - LspServerConfigs *map[string]map[string]interface{} - SavedLayouts *[]models.SavedLayout + WorkspaceID *string + KanbanViewMode *string + WorkflowFilterID *string + RepositoryIDs *[]string + InitialSetupComplete *bool + PreferredShell *string + DefaultEditorID *string + EnablePreviewOnClick *bool + ChatSubmitKey *string + ReviewAutoMarkOnScroll *bool + ShowReleaseNotification *bool + ReleaseNotesLastSeenVersion *string + LspAutoStartLanguages *[]string + LspAutoInstallLanguages *[]string + LspServerConfigs *map[string]map[string]interface{} + SavedLayouts *[]models.SavedLayout } func NewService(repo store.Repository, eventBus bus.EventBus, log *logger.Logger) *Service { @@ -129,6 +131,12 @@ func applyBasicSettings(settings *models.UserSettings, req *UpdateUserSettingsRe if req.ReviewAutoMarkOnScroll != nil { settings.ReviewAutoMarkOnScroll = *req.ReviewAutoMarkOnScroll } + if req.ShowReleaseNotification != nil { + settings.ShowReleaseNotification = *req.ShowReleaseNotification + } + if req.ReleaseNotesLastSeenVersion != nil { + settings.ReleaseNotesLastSeenVersion = *req.ReleaseNotesLastSeenVersion + } return nil } @@ -191,22 +199,24 @@ func (s *Service) publishUserSettingsEvent(ctx context.Context, settings *models return } data := map[string]interface{}{ - "user_id": settings.UserID, - "workspace_id": settings.WorkspaceID, - "kanban_view_mode": settings.KanbanViewMode, - "workflow_filter_id": settings.WorkflowFilterID, - "repository_ids": settings.RepositoryIDs, - "initial_setup_complete": settings.InitialSetupComplete, - "preferred_shell": settings.PreferredShell, - "default_editor_id": settings.DefaultEditorID, - "enable_preview_on_click": settings.EnablePreviewOnClick, - "chat_submit_key": settings.ChatSubmitKey, - "review_auto_mark_on_scroll": settings.ReviewAutoMarkOnScroll, - "lsp_auto_start_languages": settings.LspAutoStartLanguages, - "lsp_auto_install_languages": settings.LspAutoInstallLanguages, - "lsp_server_configs": settings.LspServerConfigs, - "saved_layouts": settings.SavedLayouts, - "updated_at": settings.UpdatedAt.Format(time.RFC3339), + "user_id": settings.UserID, + "workspace_id": settings.WorkspaceID, + "kanban_view_mode": settings.KanbanViewMode, + "workflow_filter_id": settings.WorkflowFilterID, + "repository_ids": settings.RepositoryIDs, + "initial_setup_complete": settings.InitialSetupComplete, + "preferred_shell": settings.PreferredShell, + "default_editor_id": settings.DefaultEditorID, + "enable_preview_on_click": settings.EnablePreviewOnClick, + "chat_submit_key": settings.ChatSubmitKey, + "review_auto_mark_on_scroll": settings.ReviewAutoMarkOnScroll, + "show_release_notification": settings.ShowReleaseNotification, + "release_notes_last_seen_version": settings.ReleaseNotesLastSeenVersion, + "lsp_auto_start_languages": settings.LspAutoStartLanguages, + "lsp_auto_install_languages": settings.LspAutoInstallLanguages, + "lsp_server_configs": settings.LspServerConfigs, + "saved_layouts": settings.SavedLayouts, + "updated_at": settings.UpdatedAt.Format(time.RFC3339), } if err := s.eventBus.Publish(ctx, events.UserSettingsUpdated, bus.NewEvent(events.UserSettingsUpdated, "user-service", data)); err != nil { s.logger.Error("failed to publish user settings event", zap.Error(err)) diff --git a/apps/backend/internal/user/service/service_test.go b/apps/backend/internal/user/service/service_test.go index d2f397e878..897996828f 100644 --- a/apps/backend/internal/user/service/service_test.go +++ b/apps/backend/internal/user/service/service_test.go @@ -25,6 +25,69 @@ func makeLayouts(n int) []models.SavedLayout { return layouts } +func TestApplyBasicSettings_ReleaseNotes(t *testing.T) { + t.Run("nil fields leave settings unchanged", func(t *testing.T) { + settings := &models.UserSettings{ + ShowReleaseNotification: true, + ReleaseNotesLastSeenVersion: "1.0.0", + } + req := &UpdateUserSettingsRequest{} + if err := applyBasicSettings(settings, req); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if settings.ShowReleaseNotification != true { + t.Fatalf("expected ShowReleaseNotification=true, got %v", settings.ShowReleaseNotification) + } + if settings.ReleaseNotesLastSeenVersion != "1.0.0" { + t.Fatalf("expected ReleaseNotesLastSeenVersion=1.0.0, got %s", settings.ReleaseNotesLastSeenVersion) + } + }) + + t.Run("ShowReleaseNotification set to false", func(t *testing.T) { + settings := &models.UserSettings{ShowReleaseNotification: true} + req := &UpdateUserSettingsRequest{ShowReleaseNotification: ptr(false)} + if err := applyBasicSettings(settings, req); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if settings.ShowReleaseNotification != false { + t.Fatalf("expected ShowReleaseNotification=false, got %v", settings.ShowReleaseNotification) + } + }) + + t.Run("ShowReleaseNotification set to true", func(t *testing.T) { + settings := &models.UserSettings{ShowReleaseNotification: false} + req := &UpdateUserSettingsRequest{ShowReleaseNotification: ptr(true)} + if err := applyBasicSettings(settings, req); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if settings.ShowReleaseNotification != true { + t.Fatalf("expected ShowReleaseNotification=true, got %v", settings.ShowReleaseNotification) + } + }) + + t.Run("ReleaseNotesLastSeenVersion updated", func(t *testing.T) { + settings := &models.UserSettings{ReleaseNotesLastSeenVersion: "1.0.0"} + req := &UpdateUserSettingsRequest{ReleaseNotesLastSeenVersion: ptr("2.0.0")} + if err := applyBasicSettings(settings, req); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if settings.ReleaseNotesLastSeenVersion != "2.0.0" { + t.Fatalf("expected ReleaseNotesLastSeenVersion=2.0.0, got %s", settings.ReleaseNotesLastSeenVersion) + } + }) + + t.Run("ReleaseNotesLastSeenVersion cleared with empty string", func(t *testing.T) { + settings := &models.UserSettings{ReleaseNotesLastSeenVersion: "1.0.0"} + req := &UpdateUserSettingsRequest{ReleaseNotesLastSeenVersion: ptr("")} + if err := applyBasicSettings(settings, req); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if settings.ReleaseNotesLastSeenVersion != "" { + t.Fatalf("expected empty ReleaseNotesLastSeenVersion, got %s", settings.ReleaseNotesLastSeenVersion) + } + }) +} + func TestApplySavedLayouts(t *testing.T) { tests := []struct { name string diff --git a/apps/backend/internal/user/store/sqlite.go b/apps/backend/internal/user/store/sqlite.go index b22eac02da..fe38fa33e7 100644 --- a/apps/backend/internal/user/store/sqlite.go +++ b/apps/backend/internal/user/store/sqlite.go @@ -130,20 +130,22 @@ func (r *sqliteRepository) UpsertUserSettings(ctx context.Context, settings *mod savedLayouts = []models.SavedLayout{} } settingsPayload, err := json.Marshal(map[string]interface{}{ - "workspace_id": settings.WorkspaceID, - "kanban_view_mode": settings.KanbanViewMode, - "workflow_filter_id": settings.WorkflowFilterID, - "repository_ids": settings.RepositoryIDs, - "initial_setup_complete": settings.InitialSetupComplete, - "preferred_shell": settings.PreferredShell, - "default_editor_id": settings.DefaultEditorID, - "enable_preview_on_click": settings.EnablePreviewOnClick, - "chat_submit_key": settings.ChatSubmitKey, - "review_auto_mark_on_scroll": settings.ReviewAutoMarkOnScroll, - "lsp_auto_start_languages": lspAutoStart, - "lsp_auto_install_languages": lspAutoInstall, - "lsp_server_configs": lspServerConfigs, - "saved_layouts": savedLayouts, + "workspace_id": settings.WorkspaceID, + "kanban_view_mode": settings.KanbanViewMode, + "workflow_filter_id": settings.WorkflowFilterID, + "repository_ids": settings.RepositoryIDs, + "initial_setup_complete": settings.InitialSetupComplete, + "preferred_shell": settings.PreferredShell, + "default_editor_id": settings.DefaultEditorID, + "enable_preview_on_click": settings.EnablePreviewOnClick, + "chat_submit_key": settings.ChatSubmitKey, + "review_auto_mark_on_scroll": settings.ReviewAutoMarkOnScroll, + "show_release_notification": settings.ShowReleaseNotification, + "release_notes_last_seen_version": settings.ReleaseNotesLastSeenVersion, + "lsp_auto_start_languages": lspAutoStart, + "lsp_auto_install_languages": lspAutoInstall, + "lsp_server_configs": lspServerConfigs, + "saved_layouts": savedLayouts, }) if err != nil { return err @@ -186,20 +188,22 @@ func scanUserSettings(scanner interface{ Scan(dest ...any) error }, userID strin return settings, nil } var payload struct { - WorkspaceID string `json:"workspace_id"` - KanbanViewMode string `json:"kanban_view_mode"` - WorkflowFilterID string `json:"workflow_filter_id"` - RepositoryIDs []string `json:"repository_ids"` - InitialSetupComplete bool `json:"initial_setup_complete"` - PreferredShell string `json:"preferred_shell"` - DefaultEditorID string `json:"default_editor_id"` - EnablePreviewOnClick bool `json:"enable_preview_on_click"` - ChatSubmitKey string `json:"chat_submit_key"` - ReviewAutoMarkOnScroll *bool `json:"review_auto_mark_on_scroll"` - LspAutoStartLanguages []string `json:"lsp_auto_start_languages"` - LspAutoInstallLanguages []string `json:"lsp_auto_install_languages"` - LspServerConfigs map[string]map[string]interface{} `json:"lsp_server_configs"` - SavedLayouts []models.SavedLayout `json:"saved_layouts"` + WorkspaceID string `json:"workspace_id"` + KanbanViewMode string `json:"kanban_view_mode"` + WorkflowFilterID string `json:"workflow_filter_id"` + RepositoryIDs []string `json:"repository_ids"` + InitialSetupComplete bool `json:"initial_setup_complete"` + PreferredShell string `json:"preferred_shell"` + DefaultEditorID string `json:"default_editor_id"` + EnablePreviewOnClick bool `json:"enable_preview_on_click"` + ChatSubmitKey string `json:"chat_submit_key"` + ReviewAutoMarkOnScroll *bool `json:"review_auto_mark_on_scroll"` + ShowReleaseNotification *bool `json:"show_release_notification"` + ReleaseNotesLastSeenVersion string `json:"release_notes_last_seen_version"` + LspAutoStartLanguages []string `json:"lsp_auto_start_languages"` + LspAutoInstallLanguages []string `json:"lsp_auto_install_languages"` + LspServerConfigs map[string]map[string]interface{} `json:"lsp_server_configs"` + SavedLayouts []models.SavedLayout `json:"saved_layouts"` } if err := json.Unmarshal([]byte(settingsRaw), &payload); err != nil { return nil, err @@ -223,6 +227,12 @@ func scanUserSettings(scanner interface{ Scan(dest ...any) error }, userID strin } else { settings.ReviewAutoMarkOnScroll = true } + if payload.ShowReleaseNotification != nil { + settings.ShowReleaseNotification = *payload.ShowReleaseNotification + } else { + settings.ShowReleaseNotification = true + } + settings.ReleaseNotesLastSeenVersion = payload.ReleaseNotesLastSeenVersion settings.LspAutoStartLanguages = payload.LspAutoStartLanguages if settings.LspAutoStartLanguages == nil { settings.LspAutoStartLanguages = []string{} diff --git a/apps/commitlint.config.mjs b/apps/commitlint.config.mjs new file mode 100644 index 0000000000..74ba00274a --- /dev/null +++ b/apps/commitlint.config.mjs @@ -0,0 +1,21 @@ +export default { + extends: ["@commitlint/config-conventional"], + rules: { + "type-enum": [ + 2, + "always", + [ + "feat", + "fix", + "perf", + "refactor", + "docs", + "chore", + "ci", + "test", + "release", + ], + ], + "subject-case": [2, "never", ["start-case", "pascal-case", "upper-case"]], + }, +}; diff --git a/apps/package.json b/apps/package.json index 0bea53b65b..ae6409a16f 100644 --- a/apps/package.json +++ b/apps/package.json @@ -13,6 +13,8 @@ "clean": "pnpm -r run clean && rm -rf node_modules" }, "devDependencies": { + "@commitlint/cli": "^20.4.2", + "@commitlint/config-conventional": "^20.4.2", "@types/node": "^20", "prettier": "^3.6.2", "typescript": "^5" diff --git a/apps/pnpm-lock.yaml b/apps/pnpm-lock.yaml index 63221d2440..49e4449f80 100644 --- a/apps/pnpm-lock.yaml +++ b/apps/pnpm-lock.yaml @@ -12,6 +12,12 @@ importers: .: devDependencies: + '@commitlint/cli': + specifier: ^20.4.2 + version: 20.4.2(@types/node@20.19.28)(typescript@5.9.3) + '@commitlint/config-conventional': + specifier: ^20.4.2 + version: 20.4.2 '@types/node': specifier: ^20 version: 20.19.28 @@ -388,7 +394,7 @@ importers: version: 0.55.1 next: specifier: 16.1.1 - version: 16.1.1(@babel/core@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -436,7 +442,7 @@ importers: version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) styled-jsx: specifier: 5.1.6 - version: 5.1.6(@babel/core@7.28.6)(react@19.2.3) + version: 5.1.6(react@19.2.3) tailwind-merge: specifier: ^3.4.0 version: 3.4.0 @@ -738,6 +744,75 @@ packages: '@codemirror/view@6.39.10': resolution: {integrity: sha512-QfT/PXhiiP76PxMnX0RQVPDQrqfRt9wr9QhInNHnEUu4PWoNS8QwwcIDEneXFChJv22y+Yu/Cz5lFMTPz+h16w==} + '@commitlint/cli@20.4.2': + resolution: {integrity: sha512-YjYSX2yj/WsVoxh9mNiymfFS2ADbg2EK4+1WAsMuckwKMCqJ5PDG0CJU/8GvmHWcv4VRB2V02KqSiecRksWqZQ==} + engines: {node: '>=v18'} + hasBin: true + + '@commitlint/config-conventional@20.4.2': + resolution: {integrity: sha512-rwkTF55q7Q+6dpSKUmJoScV0f3EpDlWKw2UPzklkLS4o5krMN1tPWAVOgHRtyUTMneIapLeQwaCjn44Td6OzBQ==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@20.4.0': + resolution: {integrity: sha512-zShmKTF+sqyNOfAE0vKcqnpvVpG0YX8F9G/ZIQHI2CoKyK+PSdladXMSns400aZ5/QZs+0fN75B//3Q5CHw++w==} + engines: {node: '>=v18'} + + '@commitlint/ensure@20.4.1': + resolution: {integrity: sha512-WLQqaFx1pBooiVvBrA1YfJNFqZF8wS/YGOtr5RzApDbV9tQ52qT5VkTsY65hFTnXhW8PcDfZLaknfJTmPejmlw==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@20.0.0': + resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} + engines: {node: '>=v18'} + + '@commitlint/format@20.4.0': + resolution: {integrity: sha512-i3ki3WR0rgolFVX6r64poBHXM1t8qlFel1G1eCBvVgntE3fCJitmzSvH5JD/KVJN/snz6TfaX2CLdON7+s4WVQ==} + engines: {node: '>=v18'} + + '@commitlint/is-ignored@20.4.1': + resolution: {integrity: sha512-In5EO4JR1lNsAv1oOBBO24V9ND1IqdAJDKZiEpdfjDl2HMasAcT7oA+5BKONv1pRoLG380DGPE2W2RIcUwdgLA==} + engines: {node: '>=v18'} + + '@commitlint/lint@20.4.2': + resolution: {integrity: sha512-buquzNRtFng6xjXvBU1abY/WPEEjCgUipNQrNmIWe8QuJ6LWLtei/LDBAzEe5ASm45+Q9L2Xi3/GVvlj50GAug==} + engines: {node: '>=v18'} + + '@commitlint/load@20.4.0': + resolution: {integrity: sha512-Dauup/GfjwffBXRJUdlX/YRKfSVXsXZLnINXKz0VZkXdKDcaEILAi9oflHGbfydonJnJAbXEbF3nXPm9rm3G6A==} + engines: {node: '>=v18'} + + '@commitlint/message@20.4.0': + resolution: {integrity: sha512-B5lGtvHgiLAIsK5nLINzVW0bN5hXv+EW35sKhYHE8F7V9Uz1fR4tx3wt7mobA5UNhZKUNgB/+ldVMQE6IHZRyA==} + engines: {node: '>=v18'} + + '@commitlint/parse@20.4.1': + resolution: {integrity: sha512-XNtZjeRcFuAfUnhYrCY02+mpxwY4OmnvD3ETbVPs25xJFFz1nRo/25nHj+5eM+zTeRFvWFwD4GXWU2JEtoK1/w==} + engines: {node: '>=v18'} + + '@commitlint/read@20.4.0': + resolution: {integrity: sha512-QfpFn6/I240ySEGv7YWqho4vxqtPpx40FS7kZZDjUJ+eHxu3azfhy7fFb5XzfTqVNp1hNoI3tEmiEPbDB44+cg==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@20.4.0': + resolution: {integrity: sha512-ay1KM8q0t+/OnlpqXJ+7gEFQNlUtSU5Gxr8GEwnVf2TPN3+ywc5DzL3JCxmpucqxfHBTFwfRMXxPRRnR5Ki20g==} + engines: {node: '>=v18'} + + '@commitlint/rules@20.4.2': + resolution: {integrity: sha512-oz83pnp5Yq6uwwTAabuVQPNlPfeD2Y5ZjMb7Wx8FSUlu4sLYJjbBWt8031Z0osCFPfHzAwSYrjnfDFKtuSMdKg==} + engines: {node: '>=v18'} + + '@commitlint/to-lines@20.0.0': + resolution: {integrity: sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==} + engines: {node: '>=v18'} + + '@commitlint/top-level@20.4.0': + resolution: {integrity: sha512-NDzq8Q6jmFaIIBC/GG6n1OQEaHdmaAAYdrZRlMgW6glYWGZ+IeuXmiymDvQNXPc82mVxq2KiE3RVpcs+1OeDeA==} + engines: {node: '>=v18'} + + '@commitlint/types@20.4.0': + resolution: {integrity: sha512-aO5l99BQJ0X34ft8b0h7QFkQlqxC6e7ZPVmBKz13xM9O8obDaM1Cld4sQlJDXXU/VFuUzQ30mVtHjVz74TuStw==} + engines: {node: '>=v18'} + '@date-fns/tz@1.4.1': resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} @@ -3335,6 +3410,9 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + array-includes@3.1.9: resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} engines: {node: '>= 0.4'} @@ -3559,6 +3637,9 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -3573,6 +3654,19 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + conventional-changelog-angular@8.1.0: + resolution: {integrity: sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w==} + engines: {node: '>=18'} + + conventional-changelog-conventionalcommits@9.1.0: + resolution: {integrity: sha512-MnbEysR8wWa8dAEvbj5xcBgJKQlX/m0lhS8DsyAAWDHdfs2faDJxTgzRYlRYpXSe7UiKrIIlB4TrBKU9q9DgkA==} + engines: {node: '>=18'} + + conventional-commits-parser@6.2.1: + resolution: {integrity: sha512-20pyHgnO40rvfI0NGF/xiEoFMkXDtkF8FwHvk5BokoFoCuTQRI8vrNCNFWUOfuolKJMm1tPCHc8GgYEtr1XRNA==} + engines: {node: '>=18'} + hasBin: true + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -3598,6 +3692,14 @@ packages: cose-base@2.2.0: resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + cosmiconfig-typescript-loader@6.2.0: + resolution: {integrity: sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' + cosmiconfig@9.0.0: resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} engines: {node: '>=14'} @@ -3786,6 +3888,10 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + dargs@8.1.0: + resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} + engines: {node: '>=12'} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -3926,6 +4032,10 @@ packages: dompurify@3.3.1: resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + dotenv@17.2.3: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} @@ -4391,6 +4501,11 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + git-raw-commits@4.0.0: + resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} + engines: {node: '>=16'} + hasBin: true + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -4399,6 +4514,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -4531,6 +4650,9 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -4538,6 +4660,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -4669,6 +4795,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + is-obj@3.0.0: resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} engines: {node: '>=12'} @@ -4962,9 +5092,27 @@ packages: lodash-es@4.17.23: resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -5073,6 +5221,14 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} + meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -5787,6 +5943,10 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -5962,6 +6122,10 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -6983,6 +7147,115 @@ snapshots: style-mod: 4.1.3 w3c-keyname: 2.2.8 + '@commitlint/cli@20.4.2(@types/node@20.19.28)(typescript@5.9.3)': + dependencies: + '@commitlint/format': 20.4.0 + '@commitlint/lint': 20.4.2 + '@commitlint/load': 20.4.0(@types/node@20.19.28)(typescript@5.9.3) + '@commitlint/read': 20.4.0 + '@commitlint/types': 20.4.0 + tinyexec: 1.0.2 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/config-conventional@20.4.2': + dependencies: + '@commitlint/types': 20.4.0 + conventional-changelog-conventionalcommits: 9.1.0 + + '@commitlint/config-validator@20.4.0': + dependencies: + '@commitlint/types': 20.4.0 + ajv: 8.17.1 + + '@commitlint/ensure@20.4.1': + dependencies: + '@commitlint/types': 20.4.0 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + + '@commitlint/execute-rule@20.0.0': {} + + '@commitlint/format@20.4.0': + dependencies: + '@commitlint/types': 20.4.0 + picocolors: 1.1.1 + + '@commitlint/is-ignored@20.4.1': + dependencies: + '@commitlint/types': 20.4.0 + semver: 7.7.4 + + '@commitlint/lint@20.4.2': + dependencies: + '@commitlint/is-ignored': 20.4.1 + '@commitlint/parse': 20.4.1 + '@commitlint/rules': 20.4.2 + '@commitlint/types': 20.4.0 + + '@commitlint/load@20.4.0(@types/node@20.19.28)(typescript@5.9.3)': + dependencies: + '@commitlint/config-validator': 20.4.0 + '@commitlint/execute-rule': 20.0.0 + '@commitlint/resolve-extends': 20.4.0 + '@commitlint/types': 20.4.0 + cosmiconfig: 9.0.0(typescript@5.9.3) + cosmiconfig-typescript-loader: 6.2.0(@types/node@20.19.28)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3) + is-plain-obj: 4.1.0 + lodash.mergewith: 4.6.2 + picocolors: 1.1.1 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/message@20.4.0': {} + + '@commitlint/parse@20.4.1': + dependencies: + '@commitlint/types': 20.4.0 + conventional-changelog-angular: 8.1.0 + conventional-commits-parser: 6.2.1 + + '@commitlint/read@20.4.0': + dependencies: + '@commitlint/top-level': 20.4.0 + '@commitlint/types': 20.4.0 + git-raw-commits: 4.0.0 + minimist: 1.2.8 + tinyexec: 1.0.2 + + '@commitlint/resolve-extends@20.4.0': + dependencies: + '@commitlint/config-validator': 20.4.0 + '@commitlint/types': 20.4.0 + global-directory: 4.0.1 + import-meta-resolve: 4.2.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + + '@commitlint/rules@20.4.2': + dependencies: + '@commitlint/ensure': 20.4.1 + '@commitlint/message': 20.4.0 + '@commitlint/to-lines': 20.0.0 + '@commitlint/types': 20.4.0 + + '@commitlint/to-lines@20.0.0': {} + + '@commitlint/top-level@20.4.0': + dependencies: + escalade: 3.2.0 + + '@commitlint/types@20.4.0': + dependencies: + conventional-commits-parser: 6.2.1 + picocolors: 1.1.1 + '@date-fns/tz@1.4.1': {} '@dnd-kit/accessibility@3.1.1(react@19.2.3)': @@ -9413,6 +9686,8 @@ snapshots: call-bound: 1.0.4 is-array-buffer: 3.0.5 + array-ify@1.0.0: {} + array-includes@3.1.9: dependencies: call-bind: 1.0.8 @@ -9674,6 +9949,11 @@ snapshots: commander@8.3.0: {} + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + concat-map@0.0.1: {} confbox@0.1.8: {} @@ -9682,6 +9962,18 @@ snapshots: content-type@1.0.5: {} + conventional-changelog-angular@8.1.0: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-conventionalcommits@9.1.0: + dependencies: + compare-func: 2.0.0 + + conventional-commits-parser@6.2.1: + dependencies: + meow: 13.2.0 + convert-source-map@2.0.0: {} cookie-signature@1.2.2: {} @@ -9703,6 +9995,13 @@ snapshots: dependencies: layout-base: 2.0.1 + cosmiconfig-typescript-loader@6.2.0(@types/node@20.19.28)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3): + dependencies: + '@types/node': 20.19.28 + cosmiconfig: 9.0.0(typescript@5.9.3) + jiti: 2.6.1 + typescript: 5.9.3 + cosmiconfig@9.0.0(typescript@5.9.3): dependencies: env-paths: 2.2.1 @@ -9914,6 +10213,8 @@ snapshots: damerau-levenshtein@1.0.8: {} + dargs@8.1.0: {} + data-uri-to-buffer@4.0.1: {} data-view-buffer@1.0.2: @@ -10035,6 +10336,10 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + dotenv@17.2.3: {} dunder-proto@1.0.1: @@ -10256,8 +10561,8 @@ snapshots: '@next/eslint-plugin-next': 16.1.1 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) @@ -10279,7 +10584,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -10290,22 +10595,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -10316,7 +10621,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -10737,6 +11042,12 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + git-raw-commits@4.0.0: + dependencies: + dargs: 8.1.0 + meow: 12.1.1 + split2: 4.2.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -10745,6 +11056,10 @@ snapshots: dependencies: is-glob: 4.0.3 + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + globals@14.0.0: {} globals@16.4.0: {} @@ -10889,10 +11204,14 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-meta-resolve@4.2.0: {} + imurmurhash@0.1.4: {} inherits@2.0.4: {} + ini@4.1.1: {} + inline-style-parser@0.2.7: {} input-otp@1.4.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): @@ -11012,6 +11331,8 @@ snapshots: is-number@7.0.0: {} + is-obj@2.0.0: {} + is-obj@3.0.0: {} is-plain-obj@4.1.0: {} @@ -11248,8 +11569,20 @@ snapshots: lodash-es@4.17.23: {} + lodash.camelcase@4.3.0: {} + + lodash.kebabcase@4.1.1: {} + lodash.merge@4.6.2: {} + lodash.mergewith@4.6.2: {} + + lodash.snakecase@4.1.1: {} + + lodash.startcase@4.4.0: {} + + lodash.upperfirst@4.3.1: {} + lodash@4.17.21: {} log-symbols@6.0.0: @@ -11464,6 +11797,10 @@ snapshots: media-typer@1.1.0: {} + meow@12.1.1: {} + + meow@13.2.0: {} + merge-descriptors@2.0.0: {} merge-stream@2.0.0: {} @@ -11779,7 +12116,7 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - next@16.1.1(@babel/core@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@next/env': 16.1.1 '@swc/helpers': 0.5.15 @@ -11788,7 +12125,7 @@ snapshots: postcss: 8.4.31 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - styled-jsx: 5.1.6(@babel/core@7.28.6)(react@19.2.3) + styled-jsx: 5.1.6(react@19.2.3) optionalDependencies: '@next/swc-darwin-arm64': 16.1.1 '@next/swc-darwin-x64': 16.1.1 @@ -12466,6 +12803,8 @@ snapshots: resolve-from@4.0.0: {} + resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} resolve@1.22.11: @@ -12773,6 +13112,8 @@ snapshots: space-separated-tokens@2.0.2: {} + split2@4.2.0: {} + stable-hash@0.0.5: {} stackback@0.0.2: {} @@ -12897,12 +13238,10 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - styled-jsx@5.1.6(@babel/core@7.28.6)(react@19.2.3): + styled-jsx@5.1.6(react@19.2.3): dependencies: client-only: 0.0.1 react: 19.2.3 - optionalDependencies: - '@babel/core': 7.28.6 stylis@4.3.6: {} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index df2bab2b0b..f272854ba8 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -9,8 +9,9 @@ import { listTaskSessionMessages, } from "@/lib/api"; import { snapshotToState } from "@/lib/ssr/mapper"; +import { mapUserSettingsResponse } from "@/lib/ssr/user-settings"; import type { AppState } from "@/lib/state/store"; -import type { ListWorkspacesResponse, UserSettings, UserSettingsResponse } from "@/lib/types/http"; +import type { ListWorkspacesResponse, UserSettingsResponse } from "@/lib/types/http"; // Server Component: runs on the server for SSR and data hydration. type PageProps = { @@ -36,54 +37,11 @@ function mapWorkspaceItem(ws: WorkspaceItem) { }; } -function buildLspConfig(s: UserSettings) { - return { - lspAutoStartLanguages: s.lsp_auto_start_languages ?? [], - lspAutoInstallLanguages: s.lsp_auto_install_languages ?? [], - lspServerConfigs: s.lsp_server_configs ?? {}, - }; -} - function buildUserSettingsState( resp: UserSettingsResponse | null, workspaceId: string | null, ): AppState["userSettings"] { - const s = resp?.settings; - const shellOptions = resp ? (resp.shell_options ?? []) : []; - if (!s) { - return { - workspaceId, - workflowId: null, - kanbanViewMode: null, - repositoryIds: [], - preferredShell: null, - shellOptions, - defaultEditorId: null, - enablePreviewOnClick: false, - chatSubmitKey: "cmd_enter", - reviewAutoMarkOnScroll: true, - savedLayouts: [], - lspAutoStartLanguages: [], - lspAutoInstallLanguages: [], - lspServerConfigs: {}, - loaded: false, - }; - } - return { - workspaceId, - workflowId: s.workflow_filter_id || null, - kanbanViewMode: s.kanban_view_mode || null, - repositoryIds: Array.from(new Set(s.repository_ids ?? [])).sort(), - preferredShell: s.preferred_shell || null, - shellOptions, - defaultEditorId: s.default_editor_id || null, - enablePreviewOnClick: s.enable_preview_on_click ?? false, - chatSubmitKey: s.chat_submit_key ?? "cmd_enter", - reviewAutoMarkOnScroll: s.review_auto_mark_on_scroll ?? true, - savedLayouts: s.saved_layouts ?? [], - ...buildLspConfig(s), - loaded: true, - }; + return { ...mapUserSettingsResponse(resp), workspaceId }; } function resolveActiveId( diff --git a/apps/web/app/s/[sessionId]/page.tsx b/apps/web/app/s/[sessionId]/page.tsx index 6e647e8c48..6668c1ef43 100644 --- a/apps/web/app/s/[sessionId]/page.tsx +++ b/apps/web/app/s/[sessionId]/page.tsx @@ -23,6 +23,7 @@ import type { } from "@/lib/types/http"; import type { Terminal } from "@/hooks/domains/session/use-terminals"; import { snapshotToState, taskToState } from "@/lib/ssr/mapper"; +import { mapUserSettingsResponse } from "@/lib/ssr/user-settings"; import { TaskPageContent } from "@/components/task/task-page-content"; function buildWorktreeState(allSessions: TaskSession[]) { @@ -50,53 +51,6 @@ function buildWorktreeState(allSessions: TaskSession[]) { }; } -type UserSettingsSettings = NonNullable; - -function buildLspSettings(s: UserSettingsSettings | undefined) { - return { - lspAutoStartLanguages: s?.lsp_auto_start_languages ?? [], - lspAutoInstallLanguages: s?.lsp_auto_install_languages ?? [], - lspServerConfigs: s?.lsp_server_configs ?? {}, - }; -} - -function buildUserSettingsCore(s: UserSettingsSettings) { - return { - workspaceId: s.workspace_id || null, - workflowId: s.workflow_filter_id || null, - kanbanViewMode: s.kanban_view_mode || null, - repositoryIds: Array.from(new Set(s.repository_ids ?? [])).sort(), - preferredShell: s.preferred_shell || null, - defaultEditorId: s.default_editor_id || null, - enablePreviewOnClick: s.enable_preview_on_click ?? false, - chatSubmitKey: s.chat_submit_key ?? "cmd_enter", - reviewAutoMarkOnScroll: s.review_auto_mark_on_scroll ?? true, - savedLayouts: s.saved_layouts ?? [], - }; -} - -function buildUserSettingsFromResponse(userSettingsResponse: UserSettingsResponse | null) { - const s = userSettingsResponse?.settings; - const shellOptions = userSettingsResponse?.shell_options ?? []; - if (!s) - return { - workspaceId: null, - workflowId: null, - kanbanViewMode: null, - repositoryIds: [], - preferredShell: null, - shellOptions, - defaultEditorId: null, - enablePreviewOnClick: false, - chatSubmitKey: "cmd_enter" as const, - reviewAutoMarkOnScroll: true, - savedLayouts: [], - loaded: false, - ...buildLspSettings(undefined), - }; - return { ...buildUserSettingsCore(s), shellOptions, loaded: true, ...buildLspSettings(s) }; -} - type BuildSessionPageStateParams = { task: Task; sessionId: string; @@ -206,7 +160,7 @@ function buildSessionPageState(p: BuildSessionPageStateParams) { agentsLoaded: true, executorsLoaded: false, }, - userSettings: buildUserSettingsFromResponse(userSettingsResponse), + userSettings: mapUserSettingsResponse(userSettingsResponse), }; } diff --git a/apps/web/app/settings/changelog/page.tsx b/apps/web/app/settings/changelog/page.tsx new file mode 100644 index 0000000000..b493d9240e --- /dev/null +++ b/apps/web/app/settings/changelog/page.tsx @@ -0,0 +1,21 @@ +import { ChangelogSettings } from "@/components/settings/changelog-settings"; +import { StateProvider } from "@/components/state-provider"; +import { fetchUserSettings } from "@/lib/api"; +import { mapUserSettingsResponse } from "@/lib/ssr/user-settings"; + +export default async function ChangelogPage() { + let initialState = {}; + try { + const response = await fetchUserSettings({ cache: "no-store" }); + const mapped = mapUserSettingsResponse(response); + initialState = { userSettings: mapped.loaded ? mapped : undefined }; + } catch { + initialState = {}; + } + + return ( + + + + ); +} diff --git a/apps/web/app/settings/general/editors/page.tsx b/apps/web/app/settings/general/editors/page.tsx index 5efd0d4ced..6aefe42b9e 100644 --- a/apps/web/app/settings/general/editors/page.tsx +++ b/apps/web/app/settings/general/editors/page.tsx @@ -1,6 +1,7 @@ import { EditorsSettings } from "@/components/settings/editors-settings"; import { StateProvider } from "@/components/state-provider"; import { fetchUserSettings, listEditors } from "@/lib/api"; +import { mapUserSettingsResponse } from "@/lib/ssr/user-settings"; export default async function GeneralEditorsPage() { let initialState = {}; @@ -9,25 +10,14 @@ export default async function GeneralEditorsPage() { listEditors({ cache: "no-store" }), fetchUserSettings({ cache: "no-store" }), ]); - const settings = settingsResponse.settings; + const mapped = mapUserSettingsResponse(settingsResponse); initialState = { editors: { items: editorsResponse.editors ?? [], loaded: true, loading: false, }, - userSettings: settings - ? { - workspaceId: settings.workspace_id ?? null, - workflowId: settings.workflow_filter_id ?? null, - kanbanViewMode: settings.kanban_view_mode ?? null, - repositoryIds: settings.repository_ids ?? [], - preferredShell: settings.preferred_shell ?? null, - shellOptions: settingsResponse.shell_options ?? [], - defaultEditorId: settings.default_editor_id ?? null, - loaded: true, - } - : undefined, + userSettings: mapped.loaded ? mapped : undefined, }; } catch { initialState = {}; diff --git a/apps/web/app/settings/general/page.tsx b/apps/web/app/settings/general/page.tsx index 2be95c5321..ff252c33b4 100644 --- a/apps/web/app/settings/general/page.tsx +++ b/apps/web/app/settings/general/page.tsx @@ -1,28 +1,14 @@ import { GeneralSettings } from "@/components/settings/general-settings"; import { StateProvider } from "@/components/state-provider"; import { fetchUserSettings } from "@/lib/api"; +import { mapUserSettingsResponse } from "@/lib/ssr/user-settings"; export default async function GeneralSettingsPage() { let initialState = {}; try { const response = await fetchUserSettings({ cache: "no-store" }); - const settings = response.settings; - initialState = { - userSettings: settings - ? { - workspaceId: settings.workspace_id ?? null, - workflowId: settings.workflow_filter_id ?? null, - kanbanViewMode: settings.kanban_view_mode ?? null, - repositoryIds: settings.repository_ids ?? [], - preferredShell: settings.preferred_shell ?? null, - shellOptions: response.shell_options ?? [], - defaultEditorId: settings.default_editor_id ?? null, - enablePreviewOnClick: settings.enable_preview_on_click ?? false, - chatSubmitKey: settings.chat_submit_key ?? "cmd_enter", - loaded: true, - } - : undefined, - }; + const mapped = mapUserSettingsResponse(response); + initialState = { userSettings: mapped.loaded ? mapped : undefined }; } catch { initialState = {}; } diff --git a/apps/web/components/kanban/kanban-header-mobile.tsx b/apps/web/components/kanban/kanban-header-mobile.tsx index 0252198157..a202e29342 100644 --- a/apps/web/components/kanban/kanban-header-mobile.tsx +++ b/apps/web/components/kanban/kanban-header-mobile.tsx @@ -2,7 +2,9 @@ import Link from "next/link"; import { Button } from "@kandev/ui/button"; +import { TooltipProvider } from "@kandev/ui/tooltip"; import { IconMenu2 } from "@tabler/icons-react"; +import { ReleaseNotesButton } from "../release-notes/release-notes-button"; import { MobileMenuSheet } from "./mobile-menu-sheet"; import { useAppStore } from "@/components/state-provider"; @@ -12,6 +14,8 @@ type KanbanHeaderMobileProps = { searchQuery?: string; onSearchChange?: (query: string) => void; isSearchLoading?: boolean; + showReleaseNotesButton: boolean; + onOpenReleaseNotes: () => void; }; export function KanbanHeaderMobile({ @@ -20,6 +24,8 @@ export function KanbanHeaderMobile({ searchQuery = "", onSearchChange, isSearchLoading = false, + showReleaseNotesButton, + onOpenReleaseNotes, }: KanbanHeaderMobileProps) { const isMenuOpen = useAppStore((state) => state.mobileKanban.isMenuOpen); const setMenuOpen = useAppStore((state) => state.setMobileKanbanMenuOpen); @@ -30,15 +36,22 @@ export function KanbanHeaderMobile({ KanDev - +
+ {showReleaseNotesButton && ( + + + + )} + +
void; @@ -98,6 +101,8 @@ function TabletHeader({ toggleValue, handleViewChange, setMenuOpen, + showReleaseNotesButton, + onOpenReleaseNotes, }: { onCreateTask: () => void; searchQuery: string; @@ -106,6 +111,8 @@ function TabletHeader({ toggleValue: string; handleViewChange: (value: string) => void; setMenuOpen: (open: boolean) => void; + showReleaseNotesButton: boolean; + onOpenReleaseNotes: () => void; }) { return (
@@ -133,6 +140,7 @@ function TabletHeader({ className="h-8" itemClassName="h-8 w-8" /> + {showReleaseNotesButton && } + + What's New + + ); +} diff --git a/apps/web/components/release-notes/release-notes-dialog.tsx b/apps/web/components/release-notes/release-notes-dialog.tsx new file mode 100644 index 0000000000..27b14af8f6 --- /dev/null +++ b/apps/web/components/release-notes/release-notes-dialog.tsx @@ -0,0 +1,87 @@ +"use client"; + +import ReactMarkdown from "react-markdown"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@kandev/ui/dialog"; +import { ScrollArea } from "@kandev/ui/scroll-area"; +import { Badge } from "@kandev/ui/badge"; +import { Separator } from "@kandev/ui/separator"; +import { IconExternalLink } from "@tabler/icons-react"; +import { remarkPlugins, markdownComponents } from "@/components/shared/markdown-components"; +import { getReleaseUrl } from "@/lib/release-notes"; +import type { ChangelogEntry } from "@/lib/changelog"; + +type ReleaseNotesDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + entries: ChangelogEntry[]; + latestVersion: string; +}; + +function buildDescription(entries: ChangelogEntry[]): string { + if (entries.length === 1) { + return entries[0].date ? `Released on ${entries[0].date}` : "Latest release notes"; + } + return `${entries.length} new releases`; +} + +export function ReleaseNotesDialog({ + open, + onOpenChange, + entries, + latestVersion, +}: ReleaseNotesDialogProps) { + const releaseUrl = getReleaseUrl(latestVersion); + + return ( + + + + + What's New + v{latestVersion} + + {buildDescription(entries)} + + +
+ {entries.map((entry, index) => ( +
+ {entries.length > 1 && ( +
+ v{entry.version} + {entry.date && ( + {entry.date} + )} +
+ )} +
+ + {entry.notes} + +
+ {index < entries.length - 1 && } +
+ ))} +
+
+ +
+
+ ); +} diff --git a/apps/web/components/settings/changelog-list.tsx b/apps/web/components/settings/changelog-list.tsx new file mode 100644 index 0000000000..5077c5eef7 --- /dev/null +++ b/apps/web/components/settings/changelog-list.tsx @@ -0,0 +1,134 @@ +"use client"; + +import { useMemo, useState } from "react"; +import ReactMarkdown from "react-markdown"; +import { Card, CardContent, CardHeader, CardTitle } from "@kandev/ui/card"; +import { Badge } from "@kandev/ui/badge"; +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, + PaginationEllipsis, +} from "@kandev/ui/pagination"; +import { IconExternalLink } from "@tabler/icons-react"; +import { remarkPlugins, markdownComponents } from "@/components/shared/markdown-components"; +import { getChangelog, type ChangelogEntry } from "@/lib/changelog"; +import { getReleaseUrl } from "@/lib/release-notes"; + +const PAGE_SIZE = 10; + +function ChangelogEntryCard({ entry }: { entry: ChangelogEntry }) { + const releaseUrl = getReleaseUrl(entry.version); + + return ( + + +
+ + v{entry.version} + {entry.date && ( + {entry.date} + )} + + + View on GitHub + + +
+
+ +
+ + {entry.notes} + +
+
+
+ ); +} + +function buildPageNumbers(currentPage: number, totalPages: number): (number | "ellipsis")[] { + if (totalPages <= 5) return Array.from({ length: totalPages }, (_, i) => i + 1); + + const pages: (number | "ellipsis")[] = [1]; + if (currentPage > 3) pages.push("ellipsis"); + + const start = Math.max(2, currentPage - 1); + const end = Math.min(totalPages - 1, currentPage + 1); + for (let i = start; i <= end; i++) pages.push(i); + + if (currentPage < totalPages - 2) pages.push("ellipsis"); + pages.push(totalPages); + + return pages; +} + +export function ChangelogList() { + const changelog = getChangelog(); + const [currentPage, setCurrentPage] = useState(1); + + const totalPages = Math.ceil(changelog.length / PAGE_SIZE); + const pageEntries = useMemo(() => { + const start = (currentPage - 1) * PAGE_SIZE; + return changelog.slice(start, start + PAGE_SIZE); + }, [changelog, currentPage]); + + if (changelog.length === 0) { + return

No changelog entries available.

; + } + + const pageNumbers = buildPageNumbers(currentPage, totalPages); + + return ( +
+ {pageEntries.map((entry) => ( + + ))} + {totalPages > 1 && ( + + + + setCurrentPage((p) => Math.max(1, p - 1))} + className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + {pageNumbers.map((page, i) => + page === "ellipsis" ? ( + + + + ) : ( + + setCurrentPage(page)} + className="cursor-pointer" + > + {page} + + + ), + )} + + setCurrentPage((p) => Math.min(totalPages, p + 1))} + className={ + currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer" + } + /> + + + + )} +
+ ); +} diff --git a/apps/web/components/settings/changelog-notification-card.tsx b/apps/web/components/settings/changelog-notification-card.tsx new file mode 100644 index 0000000000..2415ba41a3 --- /dev/null +++ b/apps/web/components/settings/changelog-notification-card.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@kandev/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@kandev/ui/card"; +import { Label } from "@kandev/ui/label"; +import { Separator } from "@kandev/ui/separator"; +import { Switch } from "@kandev/ui/switch"; +import { useAppStore } from "@/components/state-provider"; +import { updateUserSettings } from "@/lib/api"; + +export function ChangelogNotificationCard() { + const userSettings = useAppStore((state) => state.userSettings); + const setUserSettings = useAppStore((state) => state.setUserSettings); + const [isSaving, setIsSaving] = useState(false); + + const handleToggle = async (checked: boolean) => { + if (isSaving) return; + setIsSaving(true); + const previousValue = userSettings.showReleaseNotification; + try { + setUserSettings({ ...userSettings, showReleaseNotification: checked }); + await updateUserSettings({ show_release_notification: checked }); + } catch { + setUserSettings({ ...userSettings, showReleaseNotification: previousValue }); + } finally { + setIsSaving(false); + } + }; + + const handleResetLastSeen = async () => { + if (isSaving) return; + setIsSaving(true); + try { + setUserSettings({ ...userSettings, releaseNotesLastSeenVersion: null }); + await updateUserSettings({ release_notes_last_seen_version: "" }); + } catch { + // Ignore — worst case the button stays hidden until next release + } finally { + setIsSaving(false); + } + }; + + return ( + + + Topbar Release Notification + + +
+
+ +

+ When enabled, a sparkle icon appears in the topbar when a new version is released +

+
+ +
+ +
+
+ +

+ Clear your last seen version so the topbar notification appears again +

+
+ +
+
+
+ ); +} diff --git a/apps/web/components/settings/changelog-settings.tsx b/apps/web/components/settings/changelog-settings.tsx new file mode 100644 index 0000000000..e6575a8b11 --- /dev/null +++ b/apps/web/components/settings/changelog-settings.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { IconBell } from "@tabler/icons-react"; +import { Separator } from "@kandev/ui/separator"; +import { SettingsSection } from "@/components/settings/settings-section"; +import { ChangelogNotificationCard } from "@/components/settings/changelog-notification-card"; +import { ChangelogList } from "@/components/settings/changelog-list"; + +export function ChangelogSettings() { + return ( +
+
+

Changelog

+

+ View all releases and manage notification preferences +

+
+ + + + } + title="Notifications" + description="Control the release notes notification in the topbar" + > + + + + + + + + +
+ ); +} diff --git a/apps/web/components/settings/editors-settings-state.tsx b/apps/web/components/settings/editors-settings-state.tsx index 0e3c19adc8..9a34fcd868 100644 --- a/apps/web/components/settings/editors-settings-state.tsx +++ b/apps/web/components/settings/editors-settings-state.tsx @@ -212,6 +212,8 @@ function mapEditorSettingsFields( return { chatSubmitKey: s.chat_submit_key ?? "cmd_enter", reviewAutoMarkOnScroll: s.review_auto_mark_on_scroll ?? true, + showReleaseNotification: s.show_release_notification ?? true, + releaseNotesLastSeenVersion: (s.release_notes_last_seen_version as string) || null, lspAutoStartLanguages: s.lsp_auto_start_languages ?? [], lspAutoInstallLanguages: s.lsp_auto_install_languages ?? [], lspServerConfigs: s.lsp_server_configs ?? {}, diff --git a/apps/web/components/settings/settings-app-sidebar.tsx b/apps/web/components/settings/settings-app-sidebar.tsx index 5afa28e31a..7d061d0f64 100644 --- a/apps/web/components/settings/settings-app-sidebar.tsx +++ b/apps/web/components/settings/settings-app-sidebar.tsx @@ -13,6 +13,7 @@ import { IconKey, IconMessageCircle, IconBrandGithub, + IconSparkles, } from "@tabler/icons-react"; import { Sidebar, @@ -299,6 +300,16 @@ export function SettingsAppSidebar() { + + {/* Changelog */} + + + + + Changelog + + + diff --git a/apps/web/generated/.gitignore b/apps/web/generated/.gitignore new file mode 100644 index 0000000000..d6b7ef32c8 --- /dev/null +++ b/apps/web/generated/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/apps/web/hooks/domains/settings/use-editors.ts b/apps/web/hooks/domains/settings/use-editors.ts index 8595c6c476..09ee907ca9 100644 --- a/apps/web/hooks/domains/settings/use-editors.ts +++ b/apps/web/hooks/domains/settings/use-editors.ts @@ -4,29 +4,7 @@ import { useEffect } from "react"; import { fetchUserSettings, listEditors } from "@/lib/api"; import { getWebSocketClient } from "@/lib/ws/connection"; import { useAppStore } from "@/components/state-provider"; -import type { UserSettingsState } from "@/lib/state/slices/settings/types"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function mapUserSettingsResponse(data: any): UserSettingsState { - const s = data.settings; - return { - workspaceId: s.workspace_id || null, - workflowId: s.workflow_filter_id || null, - kanbanViewMode: s.kanban_view_mode || null, - repositoryIds: s.repository_ids ?? [], - preferredShell: s.preferred_shell || null, - shellOptions: data.shell_options ?? [], - defaultEditorId: s.default_editor_id || null, - enablePreviewOnClick: s.enable_preview_on_click ?? false, - chatSubmitKey: s.chat_submit_key ?? "cmd_enter", - reviewAutoMarkOnScroll: s.review_auto_mark_on_scroll ?? true, - lspAutoStartLanguages: s.lsp_auto_start_languages ?? [], - lspAutoInstallLanguages: s.lsp_auto_install_languages ?? [], - lspServerConfigs: s.lsp_server_configs ?? {}, - savedLayouts: s.saved_layouts ?? [], - loaded: true, - }; -} +import { mapUserSettingsResponse } from "@/lib/ssr/user-settings"; export function useEditors() { const editors = useAppStore((state) => state.editors.items); diff --git a/apps/web/hooks/use-release-notes.ts b/apps/web/hooks/use-release-notes.ts new file mode 100644 index 0000000000..a91b98d225 --- /dev/null +++ b/apps/web/hooks/use-release-notes.ts @@ -0,0 +1,102 @@ +"use client"; + +import { useState, useCallback, useMemo, useEffect } from "react"; +import { getReleaseNotes, hasReleaseNotes } from "@/lib/release-notes"; +import { getChangelog, type ChangelogEntry } from "@/lib/changelog"; +import { updateUserSettings } from "@/lib/api"; +import { getWebSocketClient } from "@/lib/ws/connection"; +import { useAppStore, useAppStoreApi } from "@/components/state-provider"; + +const LEGACY_STORAGE_KEY = "kandev.releaseNotes.lastSeenVersion"; + +function isVersionNewer(version: string, baseline: string): boolean { + const a = version.split(".").map(Number); + const b = baseline.split(".").map(Number); + for (let i = 0; i < Math.max(a.length, b.length); i++) { + if ((a[i] ?? 0) > (b[i] ?? 0)) return true; + if ((a[i] ?? 0) < (b[i] ?? 0)) return false; + } + return false; +} + +function getUnseenEntries(changelog: ChangelogEntry[], lastSeen: string | null): ChangelogEntry[] { + if (!hasReleaseNotes()) return []; + if (!lastSeen) return changelog.slice(0, 1); + return changelog.filter((entry) => isVersionNewer(entry.version, lastSeen)); +} + +function persistLastSeenVersion(version: string) { + const payload = { release_notes_last_seen_version: version }; + const client = getWebSocketClient(); + if (client) { + client.request("user.settings.update", payload).catch(() => { + updateUserSettings(payload, { cache: "no-store" }).catch(() => {}); + }); + } else { + updateUserSettings(payload, { cache: "no-store" }).catch(() => {}); + } +} + +export function useReleaseNotes() { + const latestRelease = getReleaseNotes(); + const showReleaseNotification = useAppStore((s) => s.userSettings.showReleaseNotification); + const lastSeenVersion = useAppStore((s) => s.userSettings.releaseNotesLastSeenVersion); + const settingsLoaded = useAppStore((s) => s.userSettings.loaded); + const storeApi = useAppStoreApi(); + + const changelog = useMemo(() => getChangelog(), []); + const unseenEntries = useMemo( + () => getUnseenEntries(changelog, lastSeenVersion), + [changelog, lastSeenVersion], + ); + + const hasUnseen = unseenEntries.length > 0; + const [dialogOpen, setDialogOpen] = useState(false); + // Snapshot entries when dialog opens so markAsSeen doesn't clear them mid-view + const [dialogEntries, setDialogEntries] = useState([]); + + // One-time migration from localStorage to backend + useEffect(() => { + if (!settingsLoaded || lastSeenVersion) return; + try { + const raw = localStorage.getItem(LEGACY_STORAGE_KEY); + if (!raw) return; + const parsed = JSON.parse(raw) as string; + if (!parsed) return; + const { userSettings, setUserSettings } = storeApi.getState(); + setUserSettings({ ...userSettings, releaseNotesLastSeenVersion: parsed }); + persistLastSeenVersion(parsed); + localStorage.removeItem(LEGACY_STORAGE_KEY); + } catch { + // Ignore migration errors + } + }, [settingsLoaded, lastSeenVersion, storeApi]); + + const markAsSeen = useCallback(() => { + const version = latestRelease.version; + const { userSettings, setUserSettings } = storeApi.getState(); + setUserSettings({ ...userSettings, releaseNotesLastSeenVersion: version }); + persistLastSeenVersion(version); + }, [latestRelease.version, storeApi]); + + const openDialog = useCallback(() => { + setDialogEntries(unseenEntries); + setDialogOpen(true); + markAsSeen(); + }, [markAsSeen, unseenEntries]); + + const closeDialog = useCallback(() => { + setDialogOpen(false); + }, []); + + return { + unseenEntries: dialogOpen ? dialogEntries : unseenEntries, + latestVersion: latestRelease.version, + hasUnseen, + dialogOpen, + openDialog, + closeDialog, + hasNotes: hasReleaseNotes(), + showTopbarButton: hasReleaseNotes() && hasUnseen && showReleaseNotification, + }; +} diff --git a/apps/web/hooks/use-user-display-settings.ts b/apps/web/hooks/use-user-display-settings.ts index 8e47dae162..cf44081981 100644 --- a/apps/web/hooks/use-user-display-settings.ts +++ b/apps/web/hooks/use-user-display-settings.ts @@ -4,6 +4,7 @@ import { fetchUserSettings, updateUserSettings } from "@/lib/api"; import { mapSelectedRepositoryIds } from "@/lib/kanban/filters"; import { useAppStore } from "@/components/state-provider"; import { useRepositories } from "@/hooks/domains/workspace/use-repositories"; +import { mapUserSettingsResponse } from "@/lib/ssr/user-settings"; import type { Repository } from "@/lib/types/http"; import type { UserSettingsState } from "@/lib/state/slices/settings/types"; @@ -25,24 +26,31 @@ type CommitPayload = { kanbanViewMode?: string | null; }; +function carryForwardSettings(current: DisplaySettings) { + return { + shellOptions: current.shellOptions ?? [], + defaultEditorId: current.defaultEditorId ?? null, + chatSubmitKey: current.chatSubmitKey ?? "cmd_enter", + reviewAutoMarkOnScroll: current.reviewAutoMarkOnScroll ?? true, + showReleaseNotification: current.showReleaseNotification ?? true, + releaseNotesLastSeenVersion: current.releaseNotesLastSeenVersion ?? null, + lspAutoStartLanguages: current.lspAutoStartLanguages ?? [], + lspAutoInstallLanguages: current.lspAutoInstallLanguages ?? [], + lspServerConfigs: current.lspServerConfigs ?? {}, + savedLayouts: current.savedLayouts ?? [], + }; +} + function buildNormalizedSettings(next: CommitPayload, current: DisplaySettings): DisplaySettings { - const repositoryIds = Array.from(new Set(next.repositoryIds)).sort(); return { workspaceId: next.workspaceId, workflowId: next.workflowId, kanbanViewMode: next.kanbanViewMode !== undefined ? next.kanbanViewMode : (current.kanbanViewMode ?? null), - repositoryIds, + repositoryIds: Array.from(new Set(next.repositoryIds)).sort(), preferredShell: next.preferredShell ?? current.preferredShell ?? null, - shellOptions: current.shellOptions ?? [], - defaultEditorId: current.defaultEditorId ?? null, enablePreviewOnClick: next.enablePreviewOnClick ?? current.enablePreviewOnClick, - chatSubmitKey: current.chatSubmitKey ?? "cmd_enter", - reviewAutoMarkOnScroll: current.reviewAutoMarkOnScroll ?? true, - lspAutoStartLanguages: current.lspAutoStartLanguages ?? [], - lspAutoInstallLanguages: current.lspAutoInstallLanguages ?? [], - lspServerConfigs: current.lspServerConfigs ?? {}, - savedLayouts: current.savedLayouts ?? [], + ...carryForwardSettings(current), loaded: true, }; } @@ -82,29 +90,6 @@ function useUserSettingsRef(userSettings: DisplaySettings) { return userSettingsRef; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function mapFetchedUserSettings(data: any): DisplaySettings { - const s = data.settings; - const repositoryIds = Array.from(new Set(s.repository_ids ?? [])).sort(); - return { - workspaceId: s.workspace_id || null, - workflowId: s.workflow_filter_id || null, - kanbanViewMode: s.kanban_view_mode || null, - repositoryIds, - preferredShell: s.preferred_shell || null, - shellOptions: data.shell_options ?? [], - defaultEditorId: s.default_editor_id || null, - enablePreviewOnClick: s.enable_preview_on_click ?? false, - chatSubmitKey: s.chat_submit_key ?? "cmd_enter", - reviewAutoMarkOnScroll: s.review_auto_mark_on_scroll ?? true, - lspAutoStartLanguages: s.lsp_auto_start_languages ?? [], - lspAutoInstallLanguages: s.lsp_auto_install_languages ?? [], - lspServerConfigs: s.lsp_server_configs ?? {}, - savedLayouts: s.saved_layouts ?? [], - loaded: true, - }; -} - function useLoadUserSettings( loaded: boolean, setUserSettings: (settings: DisplaySettings) => void, @@ -114,7 +99,9 @@ function useLoadUserSettings( fetchUserSettings({ cache: "no-store" }) .then((data) => { if (!data?.settings) return; - setUserSettings(mapFetchedUserSettings(data)); + const mapped = mapUserSettingsResponse(data); + if (!mapped.loaded) return; + setUserSettings(mapped); }) .catch(() => { /* Ignore settings fetch errors for now. */ diff --git a/apps/web/lib/api/domains/settings-api.ts b/apps/web/lib/api/domains/settings-api.ts index 7c5c420085..4571a4b4ee 100644 --- a/apps/web/lib/api/domains/settings-api.ts +++ b/apps/web/lib/api/domains/settings-api.ts @@ -36,6 +36,8 @@ export async function updateUserSettings( enable_preview_on_click?: boolean; chat_submit_key?: "enter" | "cmd_enter"; review_auto_mark_on_scroll?: boolean; + show_release_notification?: boolean; + release_notes_last_seen_version?: string; lsp_auto_start_languages?: string[]; lsp_auto_install_languages?: string[]; lsp_server_configs?: Record>; diff --git a/apps/web/lib/changelog.ts b/apps/web/lib/changelog.ts new file mode 100644 index 0000000000..ae51885a11 --- /dev/null +++ b/apps/web/lib/changelog.ts @@ -0,0 +1,11 @@ +import changelogData from "@/generated/changelog.json"; + +export type ChangelogEntry = { + version: string; + date: string; + notes: string; +}; + +export function getChangelog(): ChangelogEntry[] { + return changelogData; +} diff --git a/apps/web/lib/release-notes.ts b/apps/web/lib/release-notes.ts new file mode 100644 index 0000000000..636583e8a1 --- /dev/null +++ b/apps/web/lib/release-notes.ts @@ -0,0 +1,19 @@ +import releaseNotesData from "@/generated/release-notes.json"; + +export type ReleaseNotes = { + version: string; + date: string; + notes: string; +}; + +export function getReleaseNotes(): ReleaseNotes { + return releaseNotesData; +} + +export function hasReleaseNotes(): boolean { + return !!releaseNotesData.notes && releaseNotesData.version !== "dev"; +} + +export function getReleaseUrl(version: string): string { + return `https://github.com/kdlbs/kandev/releases/tag/v${version}`; +} diff --git a/apps/web/lib/ssr/user-settings.ts b/apps/web/lib/ssr/user-settings.ts new file mode 100644 index 0000000000..56544e2b1b --- /dev/null +++ b/apps/web/lib/ssr/user-settings.ts @@ -0,0 +1,62 @@ +import type { SavedLayout, UserSettingsResponse } from "@/lib/types/http"; + +export type UserSettingsData = NonNullable; + +export function buildCoreFields(s: UserSettingsData) { + return { + workspaceId: s.workspace_id || null, + workflowId: s.workflow_filter_id || null, + kanbanViewMode: s.kanban_view_mode || null, + repositoryIds: s.repository_ids ?? [], + preferredShell: s.preferred_shell || null, + defaultEditorId: s.default_editor_id || null, + enablePreviewOnClick: s.enable_preview_on_click ?? false, + chatSubmitKey: s.chat_submit_key ?? "cmd_enter", + reviewAutoMarkOnScroll: s.review_auto_mark_on_scroll ?? true, + showReleaseNotification: s.show_release_notification ?? true, + releaseNotesLastSeenVersion: s.release_notes_last_seen_version || null, + savedLayouts: s.saved_layouts ?? [], + }; +} + +export function buildLspFields(s: UserSettingsData | undefined) { + return { + lspAutoStartLanguages: s?.lsp_auto_start_languages ?? [], + lspAutoInstallLanguages: s?.lsp_auto_install_languages ?? [], + lspServerConfigs: s?.lsp_server_configs ?? {}, + }; +} + +/** + * Maps a `fetchUserSettings()` API response into the shape expected by `AppState["userSettings"]`. + * Use in SSR pages to build `initialState.userSettings`. + */ +export function mapUserSettingsResponse(response: UserSettingsResponse | null) { + const s = response?.settings; + const shellOptions = response?.shell_options ?? []; + if (!s) { + return { + workspaceId: null, + workflowId: null, + kanbanViewMode: null, + repositoryIds: [] as string[], + preferredShell: null, + shellOptions, + defaultEditorId: null, + enablePreviewOnClick: false, + chatSubmitKey: "cmd_enter" as const, + reviewAutoMarkOnScroll: true, + showReleaseNotification: true, + releaseNotesLastSeenVersion: null, + savedLayouts: [] as SavedLayout[], + ...buildLspFields(undefined), + loaded: false, + }; + } + return { + ...buildCoreFields(s), + shellOptions, + ...buildLspFields(s), + loaded: true, + }; +} diff --git a/apps/web/lib/state/slices/settings/settings-slice.ts b/apps/web/lib/state/slices/settings/settings-slice.ts index 70f563595c..71b428b944 100644 --- a/apps/web/lib/state/slices/settings/settings-slice.ts +++ b/apps/web/lib/state/slices/settings/settings-slice.ts @@ -30,6 +30,8 @@ export const defaultSettingsState: SettingsSliceState = { enablePreviewOnClick: false, chatSubmitKey: "cmd_enter", reviewAutoMarkOnScroll: true, + showReleaseNotification: true, + releaseNotesLastSeenVersion: null, lspAutoStartLanguages: [], lspAutoInstallLanguages: [], lspServerConfigs: {}, diff --git a/apps/web/lib/state/slices/settings/types.ts b/apps/web/lib/state/slices/settings/types.ts index 2956a9bfcf..f60b61fecb 100644 --- a/apps/web/lib/state/slices/settings/types.ts +++ b/apps/web/lib/state/slices/settings/types.ts @@ -108,6 +108,8 @@ export type UserSettingsState = { enablePreviewOnClick: boolean; chatSubmitKey: "enter" | "cmd_enter"; reviewAutoMarkOnScroll: boolean; + showReleaseNotification: boolean; + releaseNotesLastSeenVersion: string | null; lspAutoStartLanguages: string[]; lspAutoInstallLanguages: string[]; lspServerConfigs: Record>; diff --git a/apps/web/lib/types/backend.ts b/apps/web/lib/types/backend.ts index 4d7595177a..012a1f4ff5 100644 --- a/apps/web/lib/types/backend.ts +++ b/apps/web/lib/types/backend.ts @@ -347,6 +347,8 @@ export type UserSettingsUpdatedPayload = { enable_preview_on_click?: boolean; chat_submit_key?: string; review_auto_mark_on_scroll?: boolean; + show_release_notification?: boolean; + release_notes_last_seen_version?: string; lsp_auto_start_languages?: string[]; lsp_auto_install_languages?: string[]; saved_layouts?: SavedLayout[]; diff --git a/apps/web/lib/types/http.ts b/apps/web/lib/types/http.ts index 5110c3cf7b..179fad7b76 100644 --- a/apps/web/lib/types/http.ts +++ b/apps/web/lib/types/http.ts @@ -358,6 +358,8 @@ export type UserSettings = { enable_preview_on_click?: boolean; chat_submit_key?: "enter" | "cmd_enter"; review_auto_mark_on_scroll?: boolean; + show_release_notification?: boolean; + release_notes_last_seen_version?: string; lsp_auto_start_languages?: string[]; lsp_auto_install_languages?: string[]; lsp_server_configs?: Record>; diff --git a/apps/web/lib/ws/handlers/users.ts b/apps/web/lib/ws/handlers/users.ts index 99bdc87d2b..600e11b28d 100644 --- a/apps/web/lib/ws/handlers/users.ts +++ b/apps/web/lib/ws/handlers/users.ts @@ -20,6 +20,9 @@ export function registerUsersHandlers(store: StoreApi): WsHandlers { enablePreviewOnClick: message.payload.enable_preview_on_click ?? false, chatSubmitKey: (message.payload.chat_submit_key as "enter" | "cmd_enter") ?? "cmd_enter", reviewAutoMarkOnScroll: message.payload.review_auto_mark_on_scroll ?? true, + showReleaseNotification: message.payload.show_release_notification ?? true, + releaseNotesLastSeenVersion: + (message.payload.release_notes_last_seen_version as string) || null, lspAutoStartLanguages: message.payload.lsp_auto_start_languages ?? [], lspAutoInstallLanguages: message.payload.lsp_auto_install_languages ?? [], savedLayouts: message.payload.saved_layouts ?? [], diff --git a/apps/web/package.json b/apps/web/package.json index c120c9a9ec..c51c96a50c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,7 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { + "predev": "node scripts/generate-release-notes.mjs && node scripts/generate-changelog.mjs", "dev": "next dev --hostname 0.0.0.0", + "prebuild": "node scripts/generate-release-notes.mjs && node scripts/generate-changelog.mjs", "build": "cross-env NODE_ENV=production next build", "start": "cross-env NODE_ENV=production next start", "lint": "eslint --max-warnings 0", diff --git a/apps/web/scripts/generate-changelog.mjs b/apps/web/scripts/generate-changelog.mjs new file mode 100644 index 0000000000..dc6f9272a5 --- /dev/null +++ b/apps/web/scripts/generate-changelog.mjs @@ -0,0 +1,70 @@ +/** + * Pre-build script that parses CHANGELOG.md into a JSON array of version entries + * for the changelog settings page to import at build time. + * + * Output: apps/web/generated/changelog.json + * Format: [{ version, date, notes }, ...] + */ +import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const WEB_ROOT = join(__dirname, ".."); +const REPO_ROOT = join(WEB_ROOT, "..", ".."); +const OUTPUT_DIR = join(WEB_ROOT, "generated"); +const OUTPUT_FILE = join(OUTPUT_DIR, "changelog.json"); +const CHANGELOG_PATH = join(REPO_ROOT, "CHANGELOG.md"); + +function parseChangelog(content) { + const entries = []; + // Match version headings: ## VERSION - DATE or ## VERSION + const headerPattern = /^## (\S+?)(?:\s*-\s*(\S+))?\s*$/gm; + + let match; + const headers = []; + while ((match = headerPattern.exec(content)) !== null) { + headers.push({ + version: match[1], + date: match[2] || "", + index: match.index, + end: match.index + match[0].length, + }); + } + + for (let i = 0; i < headers.length; i++) { + const header = headers[i]; + const bodyStart = header.end; + const bodyEnd = i + 1 < headers.length ? headers[i + 1].index : content.length; + const notes = content.slice(bodyStart, bodyEnd).trim(); + + if (header.version === "Unreleased") continue; + + entries.push({ + version: header.version, + date: header.date, + notes, + }); + } + + return entries; +} + +function main() { + mkdirSync(OUTPUT_DIR, { recursive: true }); + + let content; + try { + content = readFileSync(CHANGELOG_PATH, "utf-8"); + } catch { + writeFileSync(OUTPUT_FILE, "[]\n"); + console.log("[changelog] CHANGELOG.md not found — wrote empty array"); + return; + } + + const entries = parseChangelog(content); + writeFileSync(OUTPUT_FILE, JSON.stringify(entries, null, 2) + "\n"); + console.log(`[changelog] parsed ${entries.length} version entries`); +} + +main(); diff --git a/apps/web/scripts/generate-release-notes.mjs b/apps/web/scripts/generate-release-notes.mjs new file mode 100644 index 0000000000..f234162f4b --- /dev/null +++ b/apps/web/scripts/generate-release-notes.mjs @@ -0,0 +1,89 @@ +/** + * Pre-build script that extracts the current version's release notes + * from CHANGELOG.md and writes them as JSON for the web app to import. + * + * Version resolution order: + * 1. KANDEV_VERSION env var (set by CI) + * 2. `git describe --tags --abbrev=0` (local dev with tags) + * 3. "dev" fallback + */ +import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { execSync } from "node:child_process"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const WEB_ROOT = join(__dirname, ".."); +const REPO_ROOT = join(WEB_ROOT, "..", ".."); +const OUTPUT_DIR = join(WEB_ROOT, "generated"); +const OUTPUT_FILE = join(OUTPUT_DIR, "release-notes.json"); +const CHANGELOG_PATH = join(REPO_ROOT, "CHANGELOG.md"); + +const FALLBACK = { version: "dev", date: "", notes: "" }; + +function getVersion() { + if (process.env.KANDEV_VERSION) { + return process.env.KANDEV_VERSION.replace(/^v/, ""); + } + try { + const tag = execSync("git describe --tags --abbrev=0", { + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + }).trim(); + return tag.replace(/^v/, ""); + } catch { + return "dev"; + } +} + +function extractVersionNotes(changelog, version) { + // Split on ## headings and find the section for our version + const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerPattern = new RegExp(`^## ${escapedVersion}(?:\\s*-\\s*(\\S+))?\\s*$`, "m"); + const headerMatch = changelog.match(headerPattern); + if (!headerMatch) return null; + + const date = headerMatch[1] || ""; + const sectionStart = headerMatch.index + headerMatch[0].length; + const rest = changelog.slice(sectionStart); + + // Find the next ## heading (or end of string) + const nextHeading = rest.search(/^## /m); + const body = nextHeading === -1 ? rest : rest.slice(0, nextHeading); + const notes = body.trim(); + + return { date, notes }; +} + +function main() { + mkdirSync(OUTPUT_DIR, { recursive: true }); + const version = getVersion(); + + if (version === "dev") { + writeFileSync(OUTPUT_FILE, JSON.stringify(FALLBACK, null, 2) + "\n"); + console.log("[release-notes] dev mode — wrote fallback"); + return; + } + + let changelog; + try { + changelog = readFileSync(CHANGELOG_PATH, "utf-8"); + } catch { + writeFileSync(OUTPUT_FILE, JSON.stringify(FALLBACK, null, 2) + "\n"); + console.log("[release-notes] CHANGELOG.md not found — wrote fallback"); + return; + } + + const extracted = extractVersionNotes(changelog, version); + if (!extracted) { + writeFileSync(OUTPUT_FILE, JSON.stringify({ version, date: "", notes: "" }, null, 2) + "\n"); + console.log(`[release-notes] version ${version} not found in CHANGELOG.md — wrote empty notes`); + return; + } + + const result = { version, date: extracted.date, notes: extracted.notes }; + writeFileSync(OUTPUT_FILE, JSON.stringify(result, null, 2) + "\n"); + console.log(`[release-notes] extracted notes for v${version}`); +} + +main(); diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000000..9a9ca2f1d8 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,45 @@ +# git-cliff configuration +# https://git-cliff.org/docs/configuration + +[changelog] +header = """# Changelog + +All notable changes to Kandev.\n +""" +body = """ +{% if version %}\ +## {{ version | trim_start_matches(pat="v") }} - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ +## Unreleased +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} +### {{ group | upper_first }} +{% for commit in commits %} +- {{ commit.message }}\ +{% endfor %} +{% endfor %}\n +""" +trim = true +footer = "" + +[git] +conventional_commits = true +filter_unconventional = false +split_commits = false +commit_preprocessors = [ + # Convert (#NNN) references into clickable GitHub PR links + { pattern = '\(#(\d+)\)', replace = "([#${1}](https://github.com/kdlbs/kandev/pull/${1}))" }, +] +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^docs", group = "Documentation" }, + { message = "^release", skip = true }, + { message = "^chore", skip = true }, + { message = "^ci", skip = true }, + { message = "^test", skip = true }, +] +tag_pattern = "v[0-9]*" +sort_commits = "newest" diff --git a/scripts/release/release.sh b/scripts/release/release.sh index e7491e6a8e..25be1e133e 100755 --- a/scripts/release/release.sh +++ b/scripts/release/release.sh @@ -85,7 +85,7 @@ Versioning: Prerequisites: - Run from a clean working tree on branch main. - - Required tools: git, node, npm, pnpm. + - Required tools: git, node, npm, pnpm, git-cliff. Examples: # Fully interactive release wizard @@ -320,7 +320,7 @@ compute_next_versions() { ensure_prerequisites() { log_step "1/5" "Checking prerequisites" - for tool in git node npm pnpm; do + for tool in git node npm pnpm git-cliff; do if command_exists "$tool"; then log_ok "$tool" else @@ -419,8 +419,10 @@ print_plan() { fi if [[ "$APP_SELECTED" -eq 1 ]]; then echo " $(dim "Steps for App:")" - echo " $(dim "1.") Create annotated git tag $NEXT_APP_TAG" - echo " $(dim "2.") Push tag to origin (triggers GitHub Actions)" + echo " $(dim "1.") Generate CHANGELOG.md (git-cliff)" + echo " $(dim "2.") Commit and push changelog" + echo " $(dim "3.") Create annotated git tag $NEXT_APP_TAG" + echo " $(dim "4.") Push tag to origin (triggers GitHub Actions)" fi echo } @@ -432,6 +434,8 @@ commit_message() { echo "release: app $NEXT_APP_TAG, cli $NEXT_CLI_VERSION" elif [[ "$CLI_SELECTED" -eq 1 ]]; then echo "release: cli $NEXT_CLI_VERSION" + elif [[ "$APP_SELECTED" -eq 1 ]]; then + echo "release: app $NEXT_APP_TAG" else echo "" fi @@ -467,6 +471,18 @@ apply_cli_release() { log_ok "CLI built" } +generate_changelog() { + [[ "$APP_SELECTED" -eq 1 ]] || return 0 + + log "Generating changelog for $(bold "$NEXT_APP_TAG")..." + if [[ "$DRY_RUN" -eq 1 ]]; then + echo " $(yellow "[dry-run]") git-cliff --tag $NEXT_APP_TAG -o CHANGELOG.md" + return 0 + fi + git-cliff --tag "$NEXT_APP_TAG" -o "$ROOT_DIR/CHANGELOG.md" + log_ok "CHANGELOG.md updated" +} + commit_and_push_if_needed() { local msg msg="$(commit_message)" @@ -474,16 +490,17 @@ commit_and_push_if_needed() { log_step "4/5" "Committing and pushing" - # App-only releases are tag-only and do not require a version commit. if [[ "$DRY_RUN" -eq 1 ]]; then - echo " $(yellow "[dry-run]") git add apps/cli/package.json" + [[ "$CLI_SELECTED" -eq 1 ]] && echo " $(yellow "[dry-run]") git add apps/cli/package.json" + [[ "$APP_SELECTED" -eq 1 ]] && echo " $(yellow "[dry-run]") git add CHANGELOG.md" echo " $(yellow "[dry-run]") git commit -m \"$msg\"" echo " $(yellow "[dry-run]") git push origin $CURRENT_BRANCH" return 0 fi - log "Staging apps/cli/package.json..." - git -C "$ROOT_DIR" add apps/cli/package.json + log "Staging release files..." + [[ "$CLI_SELECTED" -eq 1 ]] && git -C "$ROOT_DIR" add apps/cli/package.json + [[ "$APP_SELECTED" -eq 1 && -f "$ROOT_DIR/CHANGELOG.md" ]] && git -C "$ROOT_DIR" add CHANGELOG.md if git -C "$ROOT_DIR" diff --cached --quiet; then log_skip "No staged changes to commit" return 0 @@ -612,6 +629,7 @@ main() { fi apply_cli_release + generate_changelog commit_and_push_if_needed create_and_push_app_tag publish_cli